@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/src/index.tsx
CHANGED
|
@@ -6,32 +6,50 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Dialog, showDialog } from '@jupyterlab/apputils';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
ITranslator,
|
|
11
|
+
nullTranslator,
|
|
12
|
+
TranslationBundle
|
|
13
|
+
} from '@jupyterlab/translation';
|
|
10
14
|
import {
|
|
11
15
|
caretDownIcon,
|
|
12
16
|
caretRightIcon,
|
|
13
17
|
closeIcon,
|
|
18
|
+
collapseAllIcon,
|
|
19
|
+
expandAllIcon,
|
|
20
|
+
FilterBox,
|
|
21
|
+
IScore,
|
|
14
22
|
LabIcon,
|
|
15
23
|
PanelWithToolbar,
|
|
16
24
|
ReactWidget,
|
|
17
25
|
refreshIcon,
|
|
18
26
|
SidePanel,
|
|
27
|
+
tableRowsIcon,
|
|
28
|
+
Toolbar,
|
|
19
29
|
ToolbarButton,
|
|
20
30
|
ToolbarButtonComponent,
|
|
31
|
+
treeViewIcon,
|
|
21
32
|
UseSignal
|
|
22
33
|
} from '@jupyterlab/ui-components';
|
|
34
|
+
import { IStateDB } from '@jupyterlab/statedb';
|
|
23
35
|
import { Token } from '@lumino/coreutils';
|
|
24
36
|
import { DisposableDelegate, IDisposable } from '@lumino/disposable';
|
|
37
|
+
import { ElementExt } from '@lumino/domutils';
|
|
25
38
|
import { Message } from '@lumino/messaging';
|
|
26
39
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
27
|
-
import { Widget } from '@lumino/widgets';
|
|
28
|
-
import
|
|
40
|
+
import { Panel, Widget } from '@lumino/widgets';
|
|
41
|
+
import React, { isValidElement, ReactNode } from 'react';
|
|
29
42
|
|
|
30
43
|
/**
|
|
31
44
|
* The class name added to a running widget.
|
|
32
45
|
*/
|
|
33
46
|
const RUNNING_CLASS = 'jp-RunningSessions';
|
|
34
47
|
|
|
48
|
+
/**
|
|
49
|
+
* The class name added to a searchable widget.
|
|
50
|
+
*/
|
|
51
|
+
const SEARCHABLE_CLASS = 'jp-SearchableSessions';
|
|
52
|
+
|
|
35
53
|
/**
|
|
36
54
|
* The class name added to the running terminal sessions section.
|
|
37
55
|
*/
|
|
@@ -73,13 +91,51 @@ const SHUTDOWN_BUTTON_CLASS = 'jp-RunningSessions-itemShutdown';
|
|
|
73
91
|
const SHUTDOWN_ALL_BUTTON_CLASS = 'jp-RunningSessions-shutdownAll';
|
|
74
92
|
|
|
75
93
|
/**
|
|
76
|
-
* The
|
|
94
|
+
* The class name added to a collapse/expand carets.
|
|
95
|
+
*/
|
|
96
|
+
const CARET_CLASS = 'jp-RunningSessions-caret';
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The class name added to icons.
|
|
100
|
+
*/
|
|
101
|
+
const ITEM_ICON_CLASS = 'jp-RunningSessions-icon';
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Modifier added to a section when flattened list view is requested.
|
|
105
|
+
*/
|
|
106
|
+
const LIST_VIEW_CLASS = 'jp-mod-running-list-view';
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The class name added to button switching between nested and flat view.
|
|
110
|
+
*/
|
|
111
|
+
const VIEW_BUTTON_CLASS = 'jp-RunningSessions-viewButton';
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The class name added to button switching between nested and flat view.
|
|
115
|
+
*/
|
|
116
|
+
const COLLAPSE_EXPAND_BUTTON_CLASS = 'jp-RunningSessions-collapseButton';
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Identifier used in the state database.
|
|
120
|
+
*/
|
|
121
|
+
const STATE_DB_ID = 'jp-running-sessions';
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The running sessions managers token.
|
|
77
125
|
*/
|
|
78
126
|
export const IRunningSessionManagers = new Token<IRunningSessionManagers>(
|
|
79
127
|
'@jupyterlab/running:IRunningSessionManagers',
|
|
80
128
|
'A service to add running session managers.'
|
|
81
129
|
);
|
|
82
130
|
|
|
131
|
+
/**
|
|
132
|
+
* The running sessions token.
|
|
133
|
+
*/
|
|
134
|
+
export const IRunningSessionSidebar = new Token<IRunningSessionSidebar>(
|
|
135
|
+
'@jupyterlab/running:IRunningSessionsSidebar',
|
|
136
|
+
'A token allowing to modify the running sessions sidebar.'
|
|
137
|
+
);
|
|
138
|
+
|
|
83
139
|
/**
|
|
84
140
|
* The running interface.
|
|
85
141
|
*/
|
|
@@ -143,9 +199,10 @@ export class RunningSessionManagers implements IRunningSessionManagers {
|
|
|
143
199
|
function Item(props: {
|
|
144
200
|
child?: boolean;
|
|
145
201
|
runningItem: IRunningSessions.IRunningItem;
|
|
146
|
-
shutdownLabel?: string;
|
|
202
|
+
shutdownLabel?: string | ((item: IRunningSessions.IRunningItem) => string);
|
|
147
203
|
shutdownItemIcon?: LabIcon;
|
|
148
204
|
translator?: ITranslator;
|
|
205
|
+
collapseToggled: ISignal<Section, boolean>;
|
|
149
206
|
}) {
|
|
150
207
|
const { runningItem } = props;
|
|
151
208
|
const classList = [ITEM_CLASS];
|
|
@@ -158,25 +215,39 @@ function Item(props: {
|
|
|
158
215
|
// Handle shutdown requests.
|
|
159
216
|
let stopPropagation = false;
|
|
160
217
|
const shutdownItemIcon = props.shutdownItemIcon || closeIcon;
|
|
161
|
-
const shutdownLabel =
|
|
218
|
+
const shutdownLabel =
|
|
219
|
+
(typeof props.shutdownLabel === 'function'
|
|
220
|
+
? props.shutdownLabel(runningItem)
|
|
221
|
+
: props.shutdownLabel) ?? trans.__('Shut Down');
|
|
162
222
|
const shutdown = () => {
|
|
163
223
|
stopPropagation = true;
|
|
164
224
|
runningItem.shutdown?.();
|
|
165
225
|
};
|
|
166
226
|
|
|
227
|
+
// Materialise getter to avoid triggering it repeatedly
|
|
228
|
+
const children = runningItem.children;
|
|
229
|
+
|
|
167
230
|
// Manage collapsed state. Use the shutdown flag in lieu of `stopPropagation`.
|
|
168
231
|
const [collapsed, collapse] = React.useState(false);
|
|
169
|
-
const collapsible = !!
|
|
232
|
+
const collapsible = !!children?.length;
|
|
170
233
|
const onClick = collapsible
|
|
171
234
|
? () => !stopPropagation && collapse(!collapsed)
|
|
172
235
|
: undefined;
|
|
173
236
|
|
|
237
|
+
// Listen to signal to collapse from outside
|
|
238
|
+
props.collapseToggled.connect((_emitter, newCollapseState) =>
|
|
239
|
+
collapse(newCollapseState)
|
|
240
|
+
);
|
|
241
|
+
|
|
174
242
|
if (runningItem.className) {
|
|
175
243
|
classList.push(runningItem.className);
|
|
176
244
|
}
|
|
177
245
|
if (props.child) {
|
|
178
246
|
classList.push('jp-mod-running-child');
|
|
179
247
|
}
|
|
248
|
+
if (props.child && !children) {
|
|
249
|
+
classList.push('jp-mod-running-leaf');
|
|
250
|
+
}
|
|
180
251
|
|
|
181
252
|
return (
|
|
182
253
|
<>
|
|
@@ -188,15 +259,15 @@ function Item(props: {
|
|
|
188
259
|
>
|
|
189
260
|
{collapsible &&
|
|
190
261
|
(collapsed ? (
|
|
191
|
-
<caretRightIcon.react tag="span"
|
|
262
|
+
<caretRightIcon.react tag="span" className={CARET_CLASS} />
|
|
192
263
|
) : (
|
|
193
|
-
<caretDownIcon.react tag="span"
|
|
264
|
+
<caretDownIcon.react tag="span" className={CARET_CLASS} />
|
|
194
265
|
))}
|
|
195
266
|
{icon ? (
|
|
196
267
|
typeof icon === 'string' ? (
|
|
197
|
-
<img src={icon} />
|
|
268
|
+
<img src={icon} className={ITEM_ICON_CLASS} />
|
|
198
269
|
) : (
|
|
199
|
-
<icon.react tag="span"
|
|
270
|
+
<icon.react tag="span" className={ITEM_ICON_CLASS} />
|
|
200
271
|
)
|
|
201
272
|
) : undefined}
|
|
202
273
|
<span
|
|
@@ -219,9 +290,10 @@ function Item(props: {
|
|
|
219
290
|
{collapsible && !collapsed && (
|
|
220
291
|
<List
|
|
221
292
|
child={true}
|
|
222
|
-
runningItems={
|
|
293
|
+
runningItems={children!}
|
|
223
294
|
shutdownItemIcon={shutdownItemIcon}
|
|
224
295
|
translator={translator}
|
|
296
|
+
collapseToggled={props.collapseToggled}
|
|
225
297
|
/>
|
|
226
298
|
)}
|
|
227
299
|
</li>
|
|
@@ -232,14 +304,31 @@ function Item(props: {
|
|
|
232
304
|
function List(props: {
|
|
233
305
|
child?: boolean;
|
|
234
306
|
runningItems: IRunningSessions.IRunningItem[];
|
|
235
|
-
shutdownLabel?: string;
|
|
307
|
+
shutdownLabel?: string | ((item: IRunningSessions.IRunningItem) => string);
|
|
236
308
|
shutdownAllLabel?: string;
|
|
237
309
|
shutdownItemIcon?: LabIcon;
|
|
310
|
+
filter?: (item: IRunningSessions.IRunningItem) => Partial<IScore> | null;
|
|
238
311
|
translator?: ITranslator;
|
|
312
|
+
collapseToggled: ISignal<Section, boolean>;
|
|
239
313
|
}) {
|
|
314
|
+
const filter = props.filter;
|
|
315
|
+
const items = filter
|
|
316
|
+
? props.runningItems
|
|
317
|
+
.map(item => {
|
|
318
|
+
return {
|
|
319
|
+
item,
|
|
320
|
+
score: filter(item)
|
|
321
|
+
};
|
|
322
|
+
})
|
|
323
|
+
.filter(({ score }) => score !== null)
|
|
324
|
+
.sort((a, b) => {
|
|
325
|
+
return a.score!.score! - b.score!.score!;
|
|
326
|
+
})
|
|
327
|
+
.map(({ item }) => item)
|
|
328
|
+
: props.runningItems;
|
|
240
329
|
return (
|
|
241
330
|
<ul className={LIST_CLASS}>
|
|
242
|
-
{
|
|
331
|
+
{items.map((item, i) => (
|
|
243
332
|
<Item
|
|
244
333
|
child={props.child}
|
|
245
334
|
key={i}
|
|
@@ -247,27 +336,105 @@ function List(props: {
|
|
|
247
336
|
shutdownLabel={props.shutdownLabel}
|
|
248
337
|
shutdownItemIcon={props.shutdownItemIcon}
|
|
249
338
|
translator={props.translator}
|
|
339
|
+
collapseToggled={props.collapseToggled}
|
|
250
340
|
/>
|
|
251
341
|
))}
|
|
252
342
|
</ul>
|
|
253
343
|
);
|
|
254
344
|
}
|
|
255
345
|
|
|
346
|
+
interface IFilterProvider {
|
|
347
|
+
filter(item: IRunningSessions.IRunningItem): Partial<IScore> | null;
|
|
348
|
+
filterChanged: ISignal<IFilterProvider, void>;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
class FilterWidget extends ReactWidget implements IFilterProvider {
|
|
352
|
+
constructor(translator: ITranslator) {
|
|
353
|
+
super();
|
|
354
|
+
this.filter = this.filter.bind(this);
|
|
355
|
+
this._updateFilter = this._updateFilter.bind(this);
|
|
356
|
+
this._trans = translator.load('jupyterlab');
|
|
357
|
+
this.addClass('jp-SearchableSessions-filter');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
get filterChanged(): ISignal<FilterWidget, void> {
|
|
361
|
+
return this._filterChanged;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
render(): JSX.Element {
|
|
365
|
+
return (
|
|
366
|
+
<FilterBox
|
|
367
|
+
placeholder={this._trans.__('Search')}
|
|
368
|
+
updateFilter={this._updateFilter}
|
|
369
|
+
useFuzzyFilter={false}
|
|
370
|
+
caseSensitive={false}
|
|
371
|
+
/>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
filter(item: IRunningSessions.IRunningItem): Partial<IScore> | null {
|
|
376
|
+
const labels: string[] = [this._getTextContent(item.label())];
|
|
377
|
+
for (const child of item.children ?? []) {
|
|
378
|
+
labels.push(this._getTextContent(child.label()));
|
|
379
|
+
}
|
|
380
|
+
return this._filterFn(labels.join(' '));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private _getTextContent(node: ReactNode): string {
|
|
384
|
+
if (typeof node === 'string') {
|
|
385
|
+
return node;
|
|
386
|
+
}
|
|
387
|
+
if (typeof node === 'number') {
|
|
388
|
+
return '' + node;
|
|
389
|
+
}
|
|
390
|
+
if (typeof node === 'boolean') {
|
|
391
|
+
return '' + node;
|
|
392
|
+
}
|
|
393
|
+
if (Array.isArray(node)) {
|
|
394
|
+
return node.map(n => this._getTextContent(n)).join(' ');
|
|
395
|
+
}
|
|
396
|
+
if (node && isValidElement(node)) {
|
|
397
|
+
return node.props.children
|
|
398
|
+
.map((n: ReactNode) => this._getTextContent(n))
|
|
399
|
+
.join(' ');
|
|
400
|
+
}
|
|
401
|
+
return '';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private _updateFilter(
|
|
405
|
+
filterFn: (item: string) => Partial<IScore> | null
|
|
406
|
+
): void {
|
|
407
|
+
this._filterFn = filterFn;
|
|
408
|
+
this._filterChanged.emit();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private _filterFn: (item: string) => Partial<IScore> | null = (_: string) => {
|
|
412
|
+
return { score: 0 };
|
|
413
|
+
};
|
|
414
|
+
private _filterChanged = new Signal<FilterWidget, void>(this);
|
|
415
|
+
private _trans: TranslationBundle;
|
|
416
|
+
}
|
|
417
|
+
|
|
256
418
|
class ListWidget extends ReactWidget {
|
|
257
419
|
constructor(
|
|
258
420
|
private _options: {
|
|
259
421
|
manager: IRunningSessions.IManager;
|
|
260
422
|
runningItems: IRunningSessions.IRunningItem[];
|
|
261
423
|
shutdownAllLabel: string;
|
|
424
|
+
filterProvider?: IFilterProvider;
|
|
262
425
|
translator?: ITranslator;
|
|
426
|
+
collapseToggled: ISignal<Section, boolean>;
|
|
263
427
|
}
|
|
264
428
|
) {
|
|
265
429
|
super();
|
|
266
430
|
_options.manager.runningChanged.connect(this._emitUpdate, this);
|
|
431
|
+
if (_options.filterProvider) {
|
|
432
|
+
_options.filterProvider.filterChanged.connect(this._emitUpdate, this);
|
|
433
|
+
}
|
|
267
434
|
}
|
|
268
435
|
|
|
269
436
|
dispose() {
|
|
270
|
-
|
|
437
|
+
Signal.clearData(this);
|
|
271
438
|
super.dispose();
|
|
272
439
|
}
|
|
273
440
|
|
|
@@ -282,7 +449,7 @@ class ListWidget extends ReactWidget {
|
|
|
282
449
|
return (
|
|
283
450
|
<UseSignal signal={this._update}>
|
|
284
451
|
{() => {
|
|
285
|
-
// Cache the running items for the
|
|
452
|
+
// Cache the running items for the initial load and request from
|
|
286
453
|
// the service every subsequent load.
|
|
287
454
|
if (cached) {
|
|
288
455
|
cached = false;
|
|
@@ -296,7 +463,9 @@ class ListWidget extends ReactWidget {
|
|
|
296
463
|
shutdownLabel={options.manager.shutdownLabel}
|
|
297
464
|
shutdownAllLabel={options.shutdownAllLabel}
|
|
298
465
|
shutdownItemIcon={options.manager.shutdownItemIcon}
|
|
466
|
+
filter={options.filterProvider?.filter}
|
|
299
467
|
translator={options.translator}
|
|
468
|
+
collapseToggled={options.collapseToggled}
|
|
300
469
|
/>
|
|
301
470
|
</div>
|
|
302
471
|
);
|
|
@@ -345,25 +514,79 @@ class ListWidget extends ReactWidget {
|
|
|
345
514
|
* It is specialized for each based on its props.
|
|
346
515
|
*/
|
|
347
516
|
class Section extends PanelWithToolbar {
|
|
348
|
-
constructor(options: {
|
|
349
|
-
manager: IRunningSessions.IManager;
|
|
350
|
-
translator?: ITranslator;
|
|
351
|
-
}) {
|
|
517
|
+
constructor(options: Section.IOptions) {
|
|
352
518
|
super();
|
|
353
519
|
this._manager = options.manager;
|
|
520
|
+
this._filterProvider = options.filterProvider;
|
|
354
521
|
const translator = options.translator || nullTranslator;
|
|
355
|
-
|
|
356
|
-
const shutdownAllLabel =
|
|
357
|
-
options.manager.shutdownAllLabel || trans.__('Shut Down All');
|
|
358
|
-
const shutdownTitle = `${shutdownAllLabel}?`;
|
|
359
|
-
const shutdownAllConfirmationText =
|
|
360
|
-
options.manager.shutdownAllConfirmationText ||
|
|
361
|
-
`${shutdownAllLabel} ${options.manager.name}`;
|
|
522
|
+
this._trans = translator.load('jupyterlab');
|
|
362
523
|
|
|
363
524
|
this.addClass(SECTION_CLASS);
|
|
364
525
|
this.title.label = options.manager.name;
|
|
365
526
|
|
|
366
|
-
|
|
527
|
+
this._manager.runningChanged.connect(this._onListChanged, this);
|
|
528
|
+
if (options.filterProvider) {
|
|
529
|
+
options.filterProvider.filterChanged.connect(this._onListChanged, this);
|
|
530
|
+
}
|
|
531
|
+
this._updateEmptyClass();
|
|
532
|
+
|
|
533
|
+
let runningItems = options.manager.running();
|
|
534
|
+
|
|
535
|
+
if (options.showToolbar !== false) {
|
|
536
|
+
this._initializeToolbar(runningItems);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
this.addWidget(
|
|
540
|
+
new ListWidget({
|
|
541
|
+
runningItems,
|
|
542
|
+
shutdownAllLabel: this._shutdownAllLabel,
|
|
543
|
+
collapseToggled: this._collapseToggled,
|
|
544
|
+
...options
|
|
545
|
+
})
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Toggle between list and tree view.
|
|
551
|
+
*/
|
|
552
|
+
toggleListView(forceOn?: boolean): void {
|
|
553
|
+
const newState = typeof forceOn !== 'undefined' ? forceOn : !this._listView;
|
|
554
|
+
this._listView = newState;
|
|
555
|
+
if (this._buttons) {
|
|
556
|
+
const switchViewButton = this._buttons['switch-view'];
|
|
557
|
+
switchViewButton.pressed = newState;
|
|
558
|
+
}
|
|
559
|
+
this._collapseToggled.emit(false);
|
|
560
|
+
this.toggleClass(LIST_VIEW_CLASS, newState);
|
|
561
|
+
this._updateButtons();
|
|
562
|
+
this._viewChanged.emit({ mode: newState ? 'list' : 'tree' });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Dispose the resources held by the widget
|
|
567
|
+
*/
|
|
568
|
+
dispose(): void {
|
|
569
|
+
if (this.isDisposed) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
Signal.clearData(this);
|
|
573
|
+
super.dispose();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private get _shutdownAllLabel(): string {
|
|
577
|
+
return this._manager.shutdownAllLabel || this._trans.__('Shut Down All');
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private _initializeToolbar(runningItems: IRunningSessions.IRunningItem[]) {
|
|
581
|
+
const enabled = runningItems.length > 0;
|
|
582
|
+
|
|
583
|
+
const shutdownAllLabel = this._shutdownAllLabel;
|
|
584
|
+
const shutdownTitle = `${shutdownAllLabel}?`;
|
|
585
|
+
const shutdownAllConfirmationText =
|
|
586
|
+
this._manager.shutdownAllConfirmationText ||
|
|
587
|
+
`${shutdownAllLabel} ${this._manager.name}`;
|
|
588
|
+
|
|
589
|
+
const onShutdown = () => {
|
|
367
590
|
void showDialog({
|
|
368
591
|
title: shutdownTitle,
|
|
369
592
|
body: shutdownAllConfirmationText,
|
|
@@ -373,67 +596,168 @@ class Section extends PanelWithToolbar {
|
|
|
373
596
|
]
|
|
374
597
|
}).then(result => {
|
|
375
598
|
if (result.button.accept) {
|
|
376
|
-
|
|
599
|
+
this._manager.shutdownAll();
|
|
377
600
|
}
|
|
378
601
|
});
|
|
379
|
-
}
|
|
602
|
+
};
|
|
380
603
|
|
|
381
|
-
|
|
382
|
-
const enabled = runningItems.length > 0;
|
|
383
|
-
this._button = new ToolbarButton({
|
|
604
|
+
const shutdownAllButton = new ToolbarButton({
|
|
384
605
|
label: shutdownAllLabel,
|
|
385
606
|
className: `${SHUTDOWN_ALL_BUTTON_CLASS}${
|
|
386
607
|
!enabled ? ' jp-mod-disabled' : ''
|
|
387
608
|
}`,
|
|
388
609
|
enabled,
|
|
389
|
-
onClick: onShutdown
|
|
610
|
+
onClick: onShutdown.bind(this)
|
|
611
|
+
});
|
|
612
|
+
const switchViewButton = new ToolbarButton({
|
|
613
|
+
className: VIEW_BUTTON_CLASS,
|
|
614
|
+
enabled,
|
|
615
|
+
icon: tableRowsIcon,
|
|
616
|
+
pressedIcon: treeViewIcon,
|
|
617
|
+
onClick: () => this.toggleListView(),
|
|
618
|
+
tooltip: this._trans.__('Switch to List View'),
|
|
619
|
+
pressedTooltip: this._trans.__('Switch to Tree View')
|
|
620
|
+
});
|
|
621
|
+
const collapseExpandAllButton = new ToolbarButton({
|
|
622
|
+
className: COLLAPSE_EXPAND_BUTTON_CLASS,
|
|
623
|
+
enabled,
|
|
624
|
+
icon: collapseAllIcon,
|
|
625
|
+
pressedIcon: expandAllIcon,
|
|
626
|
+
onClick: () => {
|
|
627
|
+
const newState = !collapseExpandAllButton.pressed;
|
|
628
|
+
this._collapseToggled.emit(newState);
|
|
629
|
+
collapseExpandAllButton.pressed = newState;
|
|
630
|
+
},
|
|
631
|
+
tooltip: this._trans.__('Collapse All'),
|
|
632
|
+
pressedTooltip: this._trans.__('Expand All')
|
|
390
633
|
});
|
|
391
|
-
this._manager.runningChanged.connect(this._updateButton, this);
|
|
392
634
|
|
|
393
|
-
this.
|
|
635
|
+
this._buttons = {
|
|
636
|
+
'switch-view': switchViewButton,
|
|
637
|
+
'collapse-expand': collapseExpandAllButton,
|
|
638
|
+
'shutdown-all': shutdownAllButton
|
|
639
|
+
};
|
|
640
|
+
// Update buttons once defined and before adding to DOM
|
|
641
|
+
this._updateButtons();
|
|
642
|
+
this._manager.runningChanged.connect(this._updateButtons, this);
|
|
643
|
+
|
|
644
|
+
for (const name of ['collapse-expand', 'switch-view', 'shutdown-all']) {
|
|
645
|
+
this.toolbar.addItem(
|
|
646
|
+
name,
|
|
647
|
+
this._buttons[name as keyof typeof this._buttons]
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
this.toolbar.addClass('jp-RunningSessions-toolbar');
|
|
651
|
+
}
|
|
394
652
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
);
|
|
653
|
+
private _onListChanged(): void {
|
|
654
|
+
this._updateButtons();
|
|
655
|
+
this._updateEmptyClass();
|
|
398
656
|
}
|
|
399
657
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
658
|
+
private _updateEmptyClass(): void {
|
|
659
|
+
if (this._filterProvider) {
|
|
660
|
+
const items = this._manager.running().filter(this._filterProvider.filter);
|
|
661
|
+
const empty = items.length === 0;
|
|
662
|
+
if (empty) {
|
|
663
|
+
this.node.classList.toggle('jp-mod-empty', true);
|
|
664
|
+
} else {
|
|
665
|
+
this.node.classList.toggle('jp-mod-empty', false);
|
|
666
|
+
}
|
|
406
667
|
}
|
|
407
|
-
this._manager.runningChanged.disconnect(this._updateButton, this);
|
|
408
|
-
super.dispose();
|
|
409
668
|
}
|
|
410
669
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
} else {
|
|
419
|
-
button.node.querySelector('jp-button')?.classList.add('jp-mod-disabled');
|
|
670
|
+
get viewChanged(): ISignal<Section, Section.IViewState> {
|
|
671
|
+
return this._viewChanged;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
private _updateButtons(): void {
|
|
675
|
+
if (!this._buttons) {
|
|
676
|
+
return;
|
|
420
677
|
}
|
|
678
|
+
let runningItems = this._manager.running();
|
|
679
|
+
const enabled = runningItems.length > 0;
|
|
680
|
+
|
|
681
|
+
const hasNesting = runningItems.filter(item => item.children).length !== 0;
|
|
682
|
+
const inTreeView = hasNesting && !this._buttons['switch-view'].pressed;
|
|
683
|
+
|
|
684
|
+
this._buttons['switch-view'].node.style.display = hasNesting
|
|
685
|
+
? 'block'
|
|
686
|
+
: 'none';
|
|
687
|
+
this._buttons['collapse-expand'].node.style.display = inTreeView
|
|
688
|
+
? 'block'
|
|
689
|
+
: 'none';
|
|
690
|
+
|
|
691
|
+
this._buttons['collapse-expand'].enabled = enabled;
|
|
692
|
+
this._buttons['switch-view'].enabled = enabled;
|
|
693
|
+
this._buttons['shutdown-all'].enabled = enabled;
|
|
421
694
|
}
|
|
422
695
|
|
|
423
|
-
private
|
|
696
|
+
private _buttons: {
|
|
697
|
+
'collapse-expand': ToolbarButton;
|
|
698
|
+
'switch-view': ToolbarButton;
|
|
699
|
+
'shutdown-all': ToolbarButton;
|
|
700
|
+
} | null = null;
|
|
424
701
|
private _manager: IRunningSessions.IManager;
|
|
702
|
+
private _listView: boolean = false;
|
|
703
|
+
private _filterProvider?: IFilterProvider;
|
|
704
|
+
private _collapseToggled = new Signal<Section, boolean>(this);
|
|
705
|
+
private _viewChanged = new Signal<Section, Section.IViewState>(this);
|
|
706
|
+
private _trans: TranslationBundle;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Statics for Section.
|
|
711
|
+
*/
|
|
712
|
+
namespace Section {
|
|
713
|
+
/**
|
|
714
|
+
* Initialisation options for section.
|
|
715
|
+
*/
|
|
716
|
+
export interface IOptions {
|
|
717
|
+
manager: IRunningSessions.IManager;
|
|
718
|
+
showToolbar?: boolean;
|
|
719
|
+
filterProvider?: IFilterProvider;
|
|
720
|
+
translator?: ITranslator;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Information about section view state.
|
|
724
|
+
*/
|
|
725
|
+
export interface IViewState {
|
|
726
|
+
/**
|
|
727
|
+
* View mode
|
|
728
|
+
*/
|
|
729
|
+
mode: 'tree' | 'list';
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* The interface exposing the running sessions sidebar widget properties.
|
|
735
|
+
*/
|
|
736
|
+
export interface IRunningSessionSidebar {
|
|
737
|
+
/**
|
|
738
|
+
* The toolbar of the running sidebar.
|
|
739
|
+
*/
|
|
740
|
+
readonly toolbar: Toolbar;
|
|
425
741
|
}
|
|
426
742
|
|
|
427
743
|
/**
|
|
428
744
|
* A class that exposes the running terminal and kernel sessions.
|
|
429
745
|
*/
|
|
430
|
-
export class RunningSessions
|
|
746
|
+
export class RunningSessions
|
|
747
|
+
extends SidePanel
|
|
748
|
+
implements IRunningSessionSidebar
|
|
749
|
+
{
|
|
431
750
|
/**
|
|
432
751
|
* Construct a new running widget.
|
|
433
752
|
*/
|
|
434
|
-
constructor(
|
|
753
|
+
constructor(
|
|
754
|
+
managers: IRunningSessionManagers,
|
|
755
|
+
translator?: ITranslator,
|
|
756
|
+
stateDB?: IStateDB | null
|
|
757
|
+
) {
|
|
435
758
|
super();
|
|
436
759
|
this.managers = managers;
|
|
760
|
+
this._stateDB = stateDB ?? null;
|
|
437
761
|
this.translator = translator ?? nullTranslator;
|
|
438
762
|
const trans = this.translator.load('jupyterlab');
|
|
439
763
|
|
|
@@ -450,7 +774,6 @@ export class RunningSessions extends SidePanel {
|
|
|
450
774
|
);
|
|
451
775
|
|
|
452
776
|
managers.items().forEach(manager => this.addSection(managers, manager));
|
|
453
|
-
|
|
454
777
|
managers.added.connect(this.addSection, this);
|
|
455
778
|
}
|
|
456
779
|
|
|
@@ -471,12 +794,302 @@ export class RunningSessions extends SidePanel {
|
|
|
471
794
|
* @param managers Managers
|
|
472
795
|
* @param manager New manager
|
|
473
796
|
*/
|
|
474
|
-
protected addSection(_: unknown, manager: IRunningSessions.IManager) {
|
|
475
|
-
|
|
797
|
+
protected async addSection(_: unknown, manager: IRunningSessions.IManager) {
|
|
798
|
+
const section = new Section({ manager, translator: this.translator });
|
|
799
|
+
this.addWidget(section);
|
|
800
|
+
|
|
801
|
+
const state = await this._getState();
|
|
802
|
+
const sectionsInListView = state.listViewSections;
|
|
803
|
+
const sectionId = manager.name;
|
|
804
|
+
|
|
805
|
+
if (sectionsInListView && sectionsInListView.includes(sectionId)) {
|
|
806
|
+
section.toggleListView(true);
|
|
807
|
+
}
|
|
808
|
+
section.viewChanged.connect(
|
|
809
|
+
async (_emitter, viewState: Section.IViewState) => {
|
|
810
|
+
await this._updateState(sectionId, viewState.mode);
|
|
811
|
+
}
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Update state database with the new state of a given section.
|
|
817
|
+
*/
|
|
818
|
+
private async _updateState(sectionId: string, mode: 'list' | 'tree') {
|
|
819
|
+
const state = await this._getState();
|
|
820
|
+
let listViewSections = state.listViewSections ?? [];
|
|
821
|
+
if (mode === 'list' && !listViewSections.includes(sectionId)) {
|
|
822
|
+
listViewSections.push(sectionId);
|
|
823
|
+
} else {
|
|
824
|
+
listViewSections = listViewSections.filter(e => e !== sectionId);
|
|
825
|
+
}
|
|
826
|
+
const newState = { listViewSections };
|
|
827
|
+
if (this._stateDB) {
|
|
828
|
+
await this._stateDB.save(STATE_DB_ID, newState);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Get current state from the state database.
|
|
834
|
+
*/
|
|
835
|
+
private async _getState(): Promise<RunningSessions.IStateDBLayout> {
|
|
836
|
+
if (!this._stateDB) {
|
|
837
|
+
return {};
|
|
838
|
+
}
|
|
839
|
+
return (
|
|
840
|
+
((await this._stateDB.fetch(
|
|
841
|
+
STATE_DB_ID
|
|
842
|
+
)) as RunningSessions.IStateDBLayout) ?? {}
|
|
843
|
+
);
|
|
476
844
|
}
|
|
477
845
|
|
|
478
846
|
protected managers: IRunningSessionManagers;
|
|
479
847
|
protected translator: ITranslator;
|
|
848
|
+
private _stateDB: IStateDB | null;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Interfaces for RunningSessions implementation.
|
|
853
|
+
*/
|
|
854
|
+
namespace RunningSessions {
|
|
855
|
+
/**
|
|
856
|
+
* Layout of the state database.
|
|
857
|
+
*/
|
|
858
|
+
export interface IStateDBLayout {
|
|
859
|
+
/**
|
|
860
|
+
* Names of sections to be presented in the list view.
|
|
861
|
+
*/
|
|
862
|
+
listViewSections?: string[];
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Section but rendering its own title before the content
|
|
868
|
+
*/
|
|
869
|
+
class TitledSection extends Section {
|
|
870
|
+
constructor(options: Section.IOptions) {
|
|
871
|
+
super(options);
|
|
872
|
+
const titleNode = document.createElement('h3');
|
|
873
|
+
titleNode.className = 'jp-SearchableSessions-title';
|
|
874
|
+
const label = titleNode.appendChild(document.createElement('span'));
|
|
875
|
+
label.className = 'jp-SearchableSessions-titleLabel';
|
|
876
|
+
label.textContent = this.title.label;
|
|
877
|
+
this.node.insertAdjacentElement('afterbegin', titleNode);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
class EmptyIndicator extends Widget {
|
|
882
|
+
constructor(translator: ITranslator) {
|
|
883
|
+
super();
|
|
884
|
+
const trans = translator.load('jupyterlab');
|
|
885
|
+
this.addClass('jp-SearchableSessions-emptyIndicator');
|
|
886
|
+
this.node.textContent = trans.__('No matches');
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* A panel intended for use within `Dialog` to allow searching tabs and running sessions.
|
|
892
|
+
*/
|
|
893
|
+
export class SearchableSessions extends Panel {
|
|
894
|
+
constructor(managers: IRunningSessionManagers, translator?: ITranslator) {
|
|
895
|
+
super();
|
|
896
|
+
this._translator = translator ?? nullTranslator;
|
|
897
|
+
|
|
898
|
+
this.addClass(RUNNING_CLASS);
|
|
899
|
+
this.addClass(SEARCHABLE_CLASS);
|
|
900
|
+
this._filterWidget = new FilterWidget(this._translator);
|
|
901
|
+
this.addWidget(this._filterWidget);
|
|
902
|
+
this._list = new SearchableSessionsList(
|
|
903
|
+
managers,
|
|
904
|
+
this._filterWidget,
|
|
905
|
+
translator
|
|
906
|
+
);
|
|
907
|
+
this.addWidget(this._list);
|
|
908
|
+
|
|
909
|
+
this._filterWidget.filterChanged.connect(() => {
|
|
910
|
+
this._activeIndex = 0;
|
|
911
|
+
this._updateActive(0);
|
|
912
|
+
}, this);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Dispose the resources held by the widget
|
|
917
|
+
*/
|
|
918
|
+
dispose(): void {
|
|
919
|
+
if (this.isDisposed) {
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
Signal.clearData(this);
|
|
923
|
+
super.dispose();
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Click active element when the user confirmed the choice in the dialog.
|
|
928
|
+
*/
|
|
929
|
+
getValue() {
|
|
930
|
+
const items = [
|
|
931
|
+
...this.node.querySelectorAll('.' + ITEM_LABEL_CLASS)
|
|
932
|
+
] as HTMLElement[];
|
|
933
|
+
const pos = Math.min(Math.max(this._activeIndex, 0), items.length - 1);
|
|
934
|
+
items[pos].click();
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Handle incoming events.
|
|
939
|
+
*/
|
|
940
|
+
handleEvent(event: Event): void {
|
|
941
|
+
switch (event.type) {
|
|
942
|
+
case 'keydown':
|
|
943
|
+
this._evtKeydown(event as KeyboardEvent);
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* A message handler invoked on an `'after-attach'` message.
|
|
950
|
+
*/
|
|
951
|
+
protected onAfterAttach(_: Message): void {
|
|
952
|
+
this._forceFocusInput();
|
|
953
|
+
this.node.addEventListener('keydown', this);
|
|
954
|
+
setTimeout(() => {
|
|
955
|
+
this._updateActive(0);
|
|
956
|
+
}, 0);
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* A message handler invoked on an `'after-detach'` message.
|
|
960
|
+
*/
|
|
961
|
+
protected onAfterDetach(_: Message): void {
|
|
962
|
+
this.node.removeEventListener('keydown', this);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Force focus on the filter input.
|
|
967
|
+
*
|
|
968
|
+
* Note: forces focus because this widget is intended to be used in `Dialog`,
|
|
969
|
+
* which does not support focusing React widget nested within a non-React
|
|
970
|
+
* widget (a limitation of `focusNodeSelector` option implementation).
|
|
971
|
+
*/
|
|
972
|
+
private _forceFocusInput(): void {
|
|
973
|
+
this._filterWidget.renderPromise
|
|
974
|
+
?.then(() => {
|
|
975
|
+
this._filterWidget.node.querySelector('input')?.focus();
|
|
976
|
+
})
|
|
977
|
+
.catch(console.warn);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Navigate between items using up/down keys by shifting focus.
|
|
982
|
+
*/
|
|
983
|
+
private _evtKeydown(event: KeyboardEvent) {
|
|
984
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
985
|
+
const direction = event.key === 'ArrowDown' ? +1 : -1;
|
|
986
|
+
const wasSet = this._updateActive(direction);
|
|
987
|
+
if (wasSet) {
|
|
988
|
+
event.preventDefault();
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* Set and mark active item relative to the current.
|
|
995
|
+
*
|
|
996
|
+
* Returns whether an active item was set.
|
|
997
|
+
*/
|
|
998
|
+
private _updateActive(direction: -1 | 0 | 1): boolean {
|
|
999
|
+
const items = [...this.node.querySelectorAll('.' + ITEM_CLASS)].filter(e =>
|
|
1000
|
+
e.checkVisibility()
|
|
1001
|
+
) as HTMLElement[];
|
|
1002
|
+
if (!items.length) {
|
|
1003
|
+
return false;
|
|
1004
|
+
}
|
|
1005
|
+
for (const item of items) {
|
|
1006
|
+
if (item.classList.contains('jp-mod-active')) {
|
|
1007
|
+
item.classList.toggle('jp-mod-active', false);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
const currentIndex = this._activeIndex;
|
|
1011
|
+
let newIndex: number | null = null;
|
|
1012
|
+
if (currentIndex === -1) {
|
|
1013
|
+
// First or last
|
|
1014
|
+
newIndex = direction === +1 ? 0 : items.length - 1;
|
|
1015
|
+
} else {
|
|
1016
|
+
newIndex = Math.min(
|
|
1017
|
+
Math.max(currentIndex + direction, 0),
|
|
1018
|
+
items.length - 1
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
if (newIndex !== null) {
|
|
1022
|
+
items[newIndex].classList.add('jp-mod-active');
|
|
1023
|
+
ElementExt.scrollIntoViewIfNeeded(this._list.node, items[newIndex]);
|
|
1024
|
+
this._activeIndex = newIndex;
|
|
1025
|
+
return true;
|
|
1026
|
+
}
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
private _translator: ITranslator;
|
|
1031
|
+
private _filterWidget: FilterWidget;
|
|
1032
|
+
private _activeIndex = 0;
|
|
1033
|
+
private _list: SearchableSessionsList;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* A panel list of searchable sessions.
|
|
1038
|
+
*/
|
|
1039
|
+
export class SearchableSessionsList extends Panel {
|
|
1040
|
+
constructor(
|
|
1041
|
+
managers: IRunningSessionManagers,
|
|
1042
|
+
filterWidget: FilterWidget,
|
|
1043
|
+
translator?: ITranslator
|
|
1044
|
+
) {
|
|
1045
|
+
super();
|
|
1046
|
+
this._managers = managers;
|
|
1047
|
+
this._translator = translator ?? nullTranslator;
|
|
1048
|
+
this._filterWidget = filterWidget;
|
|
1049
|
+
this.addClass('jp-SearchableSessions-list');
|
|
1050
|
+
|
|
1051
|
+
this._emptyIndicator = new EmptyIndicator(this._translator);
|
|
1052
|
+
this.addWidget(this._emptyIndicator);
|
|
1053
|
+
|
|
1054
|
+
managers.items().forEach(manager => this.addSection(managers, manager));
|
|
1055
|
+
managers.added.connect(this.addSection, this);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Dispose the resources held by the widget
|
|
1060
|
+
*/
|
|
1061
|
+
dispose(): void {
|
|
1062
|
+
if (this.isDisposed) {
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
this._managers.added.disconnect(this.addSection, this);
|
|
1066
|
+
super.dispose();
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Add a section for a new manager.
|
|
1071
|
+
*
|
|
1072
|
+
* @param managers Managers
|
|
1073
|
+
* @param manager New manager
|
|
1074
|
+
*/
|
|
1075
|
+
protected addSection(_: unknown, manager: IRunningSessions.IManager) {
|
|
1076
|
+
const section = new TitledSection({
|
|
1077
|
+
manager,
|
|
1078
|
+
translator: this._translator,
|
|
1079
|
+
showToolbar: false,
|
|
1080
|
+
filterProvider: this._filterWidget
|
|
1081
|
+
});
|
|
1082
|
+
// Do not use tree view in searchable list
|
|
1083
|
+
section.toggleListView(true);
|
|
1084
|
+
this.addWidget(section);
|
|
1085
|
+
// Move empty indicator to the end
|
|
1086
|
+
this.addWidget(this._emptyIndicator);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
private _managers: IRunningSessionManagers;
|
|
1090
|
+
private _translator: ITranslator;
|
|
1091
|
+
private _emptyIndicator: EmptyIndicator;
|
|
1092
|
+
private _filterWidget: FilterWidget;
|
|
480
1093
|
}
|
|
481
1094
|
|
|
482
1095
|
/**
|
|
@@ -515,7 +1128,7 @@ export namespace IRunningSessions {
|
|
|
515
1128
|
/**
|
|
516
1129
|
* A string used to describe the shutdown action.
|
|
517
1130
|
*/
|
|
518
|
-
shutdownLabel?: string;
|
|
1131
|
+
shutdownLabel?: string | ((item: IRunningSessions.IRunningItem) => string);
|
|
519
1132
|
|
|
520
1133
|
/**
|
|
521
1134
|
* A string used to describe the shutdown all action.
|
|
@@ -571,7 +1184,7 @@ export namespace IRunningSessions {
|
|
|
571
1184
|
/**
|
|
572
1185
|
* Called to determine the label for each item.
|
|
573
1186
|
*/
|
|
574
|
-
label: () =>
|
|
1187
|
+
label: () => ReactNode;
|
|
575
1188
|
|
|
576
1189
|
/**
|
|
577
1190
|
* Called to determine the `title` attribute for each item, which is
|