@jupyterlab/logconsole 4.0.0-alpha.2 → 4.0.0-alpha.20

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/src/widget.ts ADDED
@@ -0,0 +1,489 @@
1
+ // Copyright (c) Jupyter Development Team.
2
+ // Distributed under the terms of the Modified BSD License.
3
+
4
+ import { IChangedArgs } from '@jupyterlab/coreutils';
5
+ import * as nbformat from '@jupyterlab/nbformat';
6
+ import { IOutputPrompt, OutputArea } from '@jupyterlab/outputarea';
7
+ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
8
+ import { Kernel, KernelMessage } from '@jupyterlab/services';
9
+ import {
10
+ ITranslator,
11
+ nullTranslator,
12
+ TranslationBundle
13
+ } from '@jupyterlab/translation';
14
+ import { Message } from '@lumino/messaging';
15
+ import { ISignal, Signal } from '@lumino/signaling';
16
+ import { Panel, PanelLayout, StackedPanel, Widget } from '@lumino/widgets';
17
+ import { LoggerOutputAreaModel, LogOutputModel } from './logger';
18
+ import {
19
+ IContentChange,
20
+ ILogger,
21
+ ILoggerRegistry,
22
+ ILoggerRegistryChange,
23
+ IStateChange,
24
+ LogLevel
25
+ } from './tokens';
26
+
27
+ function toTitleCase(value: string) {
28
+ return value.length === 0 ? value : value[0].toUpperCase() + value.slice(1);
29
+ }
30
+
31
+ /**
32
+ * All severity levels, including an internal one for metadata.
33
+ */
34
+ type FullLogLevel = LogLevel | 'metadata';
35
+
36
+ /**
37
+ * Log console output prompt implementation
38
+ */
39
+ class LogConsoleOutputPrompt extends Widget implements IOutputPrompt {
40
+ constructor() {
41
+ super();
42
+ this._timestampNode = document.createElement('div');
43
+ this.node.append(this._timestampNode);
44
+ }
45
+
46
+ /**
47
+ * Date & time when output is logged.
48
+ */
49
+ set timestamp(value: Date) {
50
+ this._timestamp = value;
51
+ this._timestampNode.innerHTML = this._timestamp.toLocaleTimeString();
52
+ this.update();
53
+ }
54
+
55
+ /**
56
+ * Log level
57
+ */
58
+ set level(value: FullLogLevel) {
59
+ this._level = value;
60
+ this.node.dataset.logLevel = value;
61
+ this.update();
62
+ }
63
+
64
+ update() {
65
+ if (this._level !== undefined && this._timestamp !== undefined) {
66
+ this.node.title = `${this._timestamp.toLocaleString()}; ${toTitleCase(
67
+ this._level
68
+ )} level`;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * The execution count for the prompt.
74
+ */
75
+ executionCount: nbformat.ExecutionCount;
76
+
77
+ private _timestamp: Date;
78
+ private _level: FullLogLevel;
79
+ private _timestampNode: HTMLDivElement;
80
+ }
81
+
82
+ /**
83
+ * Output Area implementation displaying log outputs
84
+ * with prompts showing log timestamps.
85
+ */
86
+ class LogConsoleOutputArea extends OutputArea {
87
+ /**
88
+ * Output area model used by the widget.
89
+ */
90
+ readonly model: LoggerOutputAreaModel;
91
+
92
+ /**
93
+ * Create an output item with a prompt and actual output
94
+ */
95
+ protected createOutputItem(model: LogOutputModel): Widget | null {
96
+ const panel = super.createOutputItem(model) as Panel;
97
+ if (panel === null) {
98
+ // Could not render model
99
+ return null;
100
+ }
101
+
102
+ // first widget in panel is prompt of type LoggerOutputPrompt
103
+ const prompt = panel.widgets[0] as LogConsoleOutputPrompt;
104
+ prompt.timestamp = model.timestamp;
105
+ prompt.level = model.level;
106
+ return panel;
107
+ }
108
+
109
+ /**
110
+ * Handle an input request from a kernel by doing nothing.
111
+ */
112
+ protected onInputRequest(
113
+ msg: KernelMessage.IInputRequestMsg,
114
+ future: Kernel.IShellFuture
115
+ ): void {
116
+ return;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Implementation of `IContentFactory` for Output Area
122
+ * which creates custom output prompts.
123
+ */
124
+ class LogConsoleContentFactory extends OutputArea.ContentFactory {
125
+ /**
126
+ * Create the output prompt for the widget.
127
+ */
128
+ createOutputPrompt(): LogConsoleOutputPrompt {
129
+ return new LogConsoleOutputPrompt();
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Implements a panel which supports pinning the position to the end if it is
135
+ * scrolled to the end.
136
+ *
137
+ * #### Notes
138
+ * This is useful for log viewing components or chat components that append
139
+ * elements at the end. We would like to automatically scroll when the user
140
+ * has scrolled to the bottom, but not change the scrolling when the user has
141
+ * changed the scroll position.
142
+ */
143
+ export class ScrollingWidget<T extends Widget> extends Widget {
144
+ constructor({ content, ...options }: ScrollingWidget.IOptions<T>) {
145
+ super(options);
146
+ this.addClass('jp-Scrolling');
147
+ const layout = (this.layout = new PanelLayout());
148
+ layout.addWidget(content);
149
+
150
+ this._content = content;
151
+ this._sentinel = document.createElement('div');
152
+ this.node.appendChild(this._sentinel);
153
+ }
154
+
155
+ /**
156
+ * The content widget.
157
+ */
158
+ get content(): T {
159
+ return this._content;
160
+ }
161
+
162
+ protected onAfterAttach(msg: Message): void {
163
+ super.onAfterAttach(msg);
164
+ // defer so content gets a chance to attach first
165
+ requestAnimationFrame(() => {
166
+ this._sentinel.scrollIntoView();
167
+ this._scrollHeight = this.node.scrollHeight;
168
+ });
169
+
170
+ // Set up intersection observer for the sentinel
171
+ if (typeof IntersectionObserver !== 'undefined') {
172
+ this._observer = new IntersectionObserver(
173
+ args => {
174
+ this._handleScroll(args);
175
+ },
176
+ { root: this.node, threshold: 1 }
177
+ );
178
+ this._observer.observe(this._sentinel);
179
+ }
180
+ }
181
+
182
+ protected onBeforeDetach(msg: Message): void {
183
+ if (this._observer) {
184
+ this._observer.disconnect();
185
+ }
186
+ }
187
+
188
+ protected onAfterShow(msg: Message): void {
189
+ if (this._tracking) {
190
+ this._sentinel.scrollIntoView();
191
+ }
192
+ }
193
+
194
+ private _handleScroll([entry]: IntersectionObserverEntry[]) {
195
+ if (entry.isIntersecting) {
196
+ this._tracking = true;
197
+ } else if (this.isVisible) {
198
+ const currentHeight = this.node.scrollHeight;
199
+ if (currentHeight === this._scrollHeight) {
200
+ // Likely the user scrolled manually
201
+ this._tracking = false;
202
+ } else {
203
+ // We assume we scrolled because our size changed, so scroll to the end.
204
+ this._sentinel.scrollIntoView();
205
+ this._scrollHeight = currentHeight;
206
+ this._tracking = true;
207
+ }
208
+ }
209
+ }
210
+
211
+ private _content: T;
212
+ private _observer: IntersectionObserver | null = null;
213
+ private _scrollHeight: number;
214
+ private _sentinel: HTMLDivElement;
215
+ private _tracking: boolean;
216
+ }
217
+
218
+ export namespace ScrollingWidget {
219
+ export interface IOptions<T extends Widget> extends Widget.IOptions {
220
+ content: T;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * A StackedPanel implementation that creates Output Areas
226
+ * for each log source and activates as source is switched.
227
+ */
228
+ export class LogConsolePanel extends StackedPanel {
229
+ /**
230
+ * Construct a LogConsolePanel instance.
231
+ *
232
+ * @param loggerRegistry - The logger registry that provides
233
+ * logs to be displayed.
234
+ */
235
+ constructor(loggerRegistry: ILoggerRegistry, translator?: ITranslator) {
236
+ super();
237
+ this.translator = translator || nullTranslator;
238
+ this._trans = this.translator.load('jupyterlab');
239
+ this._loggerRegistry = loggerRegistry;
240
+ this.addClass('jp-LogConsolePanel');
241
+
242
+ loggerRegistry.registryChanged.connect(
243
+ (sender: ILoggerRegistry, args: ILoggerRegistryChange) => {
244
+ this._bindLoggerSignals();
245
+ },
246
+ this
247
+ );
248
+
249
+ this._bindLoggerSignals();
250
+
251
+ this._placeholder = new Widget();
252
+ this._placeholder.addClass('jp-LogConsoleListPlaceholder');
253
+ this.addWidget(this._placeholder);
254
+ }
255
+
256
+ /**
257
+ * The logger registry providing the logs.
258
+ */
259
+ get loggerRegistry(): ILoggerRegistry {
260
+ return this._loggerRegistry;
261
+ }
262
+
263
+ /**
264
+ * The current logger.
265
+ */
266
+ get logger(): ILogger | null {
267
+ if (this.source === null) {
268
+ return null;
269
+ }
270
+ return this.loggerRegistry.getLogger(this.source);
271
+ }
272
+
273
+ /**
274
+ * The log source displayed
275
+ */
276
+ get source(): string | null {
277
+ return this._source;
278
+ }
279
+ set source(name: string | null) {
280
+ if (name === this._source) {
281
+ return;
282
+ }
283
+ const oldValue = this._source;
284
+ const newValue = (this._source = name);
285
+ this._showOutputFromSource(newValue);
286
+ this._handlePlaceholder();
287
+ this._sourceChanged.emit({ oldValue, newValue, name: 'source' });
288
+ }
289
+
290
+ /**
291
+ * The source version displayed.
292
+ */
293
+ get sourceVersion(): number | null {
294
+ const source = this.source;
295
+ return source !== null
296
+ ? this._loggerRegistry.getLogger(source).version
297
+ : null;
298
+ }
299
+
300
+ /**
301
+ * Signal for source changes
302
+ */
303
+ get sourceChanged(): ISignal<
304
+ this,
305
+ IChangedArgs<string | null, string | null, 'source'>
306
+ > {
307
+ return this._sourceChanged;
308
+ }
309
+
310
+ /**
311
+ * Signal for source changes
312
+ */
313
+ get sourceDisplayed(): ISignal<this, ISourceDisplayed> {
314
+ return this._sourceDisplayed;
315
+ }
316
+
317
+ protected onAfterAttach(msg: Message): void {
318
+ super.onAfterAttach(msg);
319
+ this._updateOutputAreas();
320
+ this._showOutputFromSource(this._source);
321
+ this._handlePlaceholder();
322
+ }
323
+
324
+ protected onAfterShow(msg: Message): void {
325
+ super.onAfterShow(msg);
326
+ if (this.source !== null) {
327
+ this._sourceDisplayed.emit({
328
+ source: this.source,
329
+ version: this.sourceVersion
330
+ });
331
+ }
332
+ }
333
+
334
+ private _bindLoggerSignals() {
335
+ const loggers = this._loggerRegistry.getLoggers();
336
+ for (const logger of loggers) {
337
+ if (this._loggersWatched.has(logger.source)) {
338
+ continue;
339
+ }
340
+
341
+ logger.contentChanged.connect((sender: ILogger, args: IContentChange) => {
342
+ this._updateOutputAreas();
343
+ this._handlePlaceholder();
344
+ }, this);
345
+
346
+ logger.stateChanged.connect((sender: ILogger, change: IStateChange) => {
347
+ if (change.name !== 'rendermime') {
348
+ return;
349
+ }
350
+ const viewId = `source:${sender.source}`;
351
+ const outputArea = this._outputAreas.get(viewId);
352
+ if (outputArea) {
353
+ if (change.newValue) {
354
+ // cast away readonly
355
+ (outputArea.rendermime as IRenderMimeRegistry) = change.newValue;
356
+ } else {
357
+ outputArea.dispose();
358
+ }
359
+ }
360
+ }, this);
361
+
362
+ this._loggersWatched.add(logger.source);
363
+ }
364
+ }
365
+
366
+ private _showOutputFromSource(source: string | null) {
367
+ // If the source is null, pick a unique name so all output areas hide.
368
+ const viewId = source === null ? 'null source' : `source:${source}`;
369
+
370
+ this._outputAreas.forEach(
371
+ (outputArea: LogConsoleOutputArea, name: string) => {
372
+ // Show/hide the output area parents, the scrolling windows.
373
+ if (outputArea.id === viewId) {
374
+ outputArea.parent?.show();
375
+ if (outputArea.isVisible) {
376
+ this._sourceDisplayed.emit({
377
+ source: this.source,
378
+ version: this.sourceVersion
379
+ });
380
+ }
381
+ } else {
382
+ outputArea.parent?.hide();
383
+ }
384
+ }
385
+ );
386
+
387
+ const title =
388
+ source === null
389
+ ? this._trans.__('Log Console')
390
+ : this._trans.__('Log: %1', source);
391
+ this.title.label = title;
392
+ this.title.caption = title;
393
+ }
394
+
395
+ private _handlePlaceholder() {
396
+ if (this.source === null) {
397
+ this._placeholder.node.textContent = this._trans.__(
398
+ 'No source selected.'
399
+ );
400
+ this._placeholder.show();
401
+ } else if (this._loggerRegistry.getLogger(this.source).length === 0) {
402
+ this._placeholder.node.textContent = this._trans.__('No log messages.');
403
+ this._placeholder.show();
404
+ } else {
405
+ this._placeholder.hide();
406
+ this._placeholder.node.textContent = '';
407
+ }
408
+ }
409
+
410
+ private _updateOutputAreas() {
411
+ const loggerIds = new Set<string>();
412
+ const loggers = this._loggerRegistry.getLoggers();
413
+
414
+ for (const logger of loggers) {
415
+ const source = logger.source;
416
+ const viewId = `source:${source}`;
417
+ loggerIds.add(viewId);
418
+
419
+ // add view for logger if not exist
420
+ if (!this._outputAreas.has(viewId)) {
421
+ const outputArea = new LogConsoleOutputArea({
422
+ rendermime: logger.rendermime!,
423
+ contentFactory: new LogConsoleContentFactory(),
424
+ model: logger.outputAreaModel
425
+ });
426
+ outputArea.id = viewId;
427
+
428
+ // Attach the output area so it is visible, so the accounting
429
+ // functions below record the outputs actually displayed.
430
+ const w = new ScrollingWidget({
431
+ content: outputArea
432
+ });
433
+ this.addWidget(w);
434
+ this._outputAreas.set(viewId, outputArea);
435
+
436
+ // This is where the source object is associated with the output area.
437
+ // We capture the source from this environment in the closure.
438
+ const outputUpdate = (sender: LogConsoleOutputArea) => {
439
+ // If the current log console panel source is the source associated
440
+ // with this output area, and the output area is visible, then emit
441
+ // the logConsolePanel source displayed signal.
442
+ if (this.source === source && sender.isVisible) {
443
+ // We assume that the output area has been updated to the current
444
+ // version of the source.
445
+ this._sourceDisplayed.emit({
446
+ source: this.source,
447
+ version: this.sourceVersion
448
+ });
449
+ }
450
+ };
451
+ // Notify messages were displayed any time the output area is updated
452
+ // and update for any outputs rendered on construction.
453
+ outputArea.outputLengthChanged.connect(outputUpdate, this);
454
+ // Since the output area was attached above, we can rely on its
455
+ // visibility to account for the messages displayed.
456
+ outputUpdate(outputArea);
457
+ }
458
+ }
459
+
460
+ // remove output areas that do not have corresponding loggers anymore
461
+ const viewIds = this._outputAreas.keys();
462
+
463
+ for (const viewId of viewIds) {
464
+ if (!loggerIds.has(viewId)) {
465
+ const outputArea = this._outputAreas.get(viewId);
466
+ outputArea?.dispose();
467
+ this._outputAreas.delete(viewId);
468
+ }
469
+ }
470
+ }
471
+
472
+ protected translator: ITranslator;
473
+ private _trans: TranslationBundle;
474
+ private _loggerRegistry: ILoggerRegistry;
475
+ private _outputAreas = new Map<string, LogConsoleOutputArea>();
476
+ private _source: string | null = null;
477
+ private _sourceChanged = new Signal<
478
+ this,
479
+ IChangedArgs<string | null, string | null, 'source'>
480
+ >(this);
481
+ private _sourceDisplayed = new Signal<this, ISourceDisplayed>(this);
482
+ private _placeholder: Widget;
483
+ private _loggersWatched: Set<string> = new Set();
484
+ }
485
+
486
+ export interface ISourceDisplayed {
487
+ source: string | null;
488
+ version: number | null;
489
+ }