@jupyterlab/logconsole 4.0.0-alpha.19 → 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/lib/logger.d.ts +2 -2
- package/lib/tokens.d.ts +5 -5
- package/package.json +16 -15
- package/src/index.ts +11 -0
- package/src/logger.ts +436 -0
- package/src/registry.ts +107 -0
- package/src/tokens.ts +189 -0
- package/src/widget.ts +489 -0
package/lib/logger.d.ts
CHANGED
|
@@ -6,11 +6,11 @@ import { IContentChange, ILogger, ILoggerOutputAreaModel, ILogPayload, IStateCha
|
|
|
6
6
|
/**
|
|
7
7
|
* All severity levels, including an internal one for metadata.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
type FullLogLevel = LogLevel | 'metadata';
|
|
10
10
|
/**
|
|
11
11
|
* Custom Notebook Output with log info.
|
|
12
12
|
*/
|
|
13
|
-
|
|
13
|
+
type ILogOutput = nbformat.IOutput & {
|
|
14
14
|
/**
|
|
15
15
|
* Date & time when output is logged in integer representation.
|
|
16
16
|
*/
|
package/lib/tokens.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { ISignal } from '@lumino/signaling';
|
|
|
9
9
|
* The Logger Registry token.
|
|
10
10
|
*/
|
|
11
11
|
export declare const ILoggerRegistry: Token<ILoggerRegistry>;
|
|
12
|
-
export
|
|
12
|
+
export type ILoggerRegistryChange = 'append';
|
|
13
13
|
/**
|
|
14
14
|
* A Logger Registry that registers and provides loggers by source.
|
|
15
15
|
*/
|
|
@@ -36,7 +36,7 @@ export interface ILoggerRegistry extends IDisposable {
|
|
|
36
36
|
/**
|
|
37
37
|
* Log severity level
|
|
38
38
|
*/
|
|
39
|
-
export
|
|
39
|
+
export type LogLevel = 'critical' | 'error' | 'warning' | 'info' | 'debug';
|
|
40
40
|
/**
|
|
41
41
|
* The base log payload type.
|
|
42
42
|
*/
|
|
@@ -96,9 +96,9 @@ export interface IOutputLog extends ILogPayloadBase {
|
|
|
96
96
|
/**
|
|
97
97
|
* Log payload union type.
|
|
98
98
|
*/
|
|
99
|
-
export
|
|
100
|
-
export
|
|
101
|
-
export
|
|
99
|
+
export type ILogPayload = ITextLog | IHtmlLog | IOutputLog;
|
|
100
|
+
export type IContentChange = 'append' | 'clear';
|
|
101
|
+
export type IStateChange = IChangedArgs<IRenderMimeRegistry | null, IRenderMimeRegistry | null, 'rendermime'> | IChangedArgs<LogLevel, LogLevel, 'level'>;
|
|
102
102
|
export interface ILoggerOutputAreaModel extends IOutputAreaModel {
|
|
103
103
|
/**
|
|
104
104
|
* The maximum number of outputs to store.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyterlab/logconsole",
|
|
3
|
-
"version": "4.0.0-alpha.
|
|
3
|
+
"version": "4.0.0-alpha.20",
|
|
4
4
|
"description": "JupyterLab - Log Console",
|
|
5
5
|
"homepage": "https://github.com/jupyterlab/jupyterlab",
|
|
6
6
|
"bugs": {
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"files": [
|
|
25
25
|
"lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
|
|
26
26
|
"style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
|
|
27
|
-
"style/index.js"
|
|
27
|
+
"style/index.js",
|
|
28
|
+
"src/**/*.{ts,tsx}"
|
|
28
29
|
],
|
|
29
30
|
"scripts": {
|
|
30
31
|
"build": "tsc -b",
|
|
@@ -37,23 +38,23 @@
|
|
|
37
38
|
"watch": "tsc -b --watch"
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|
|
40
|
-
"@jupyterlab/coreutils": "^6.0.0-alpha.
|
|
41
|
-
"@jupyterlab/nbformat": "^4.0.0-alpha.
|
|
42
|
-
"@jupyterlab/outputarea": "^4.0.0-alpha.
|
|
43
|
-
"@jupyterlab/rendermime": "^4.0.0-alpha.
|
|
44
|
-
"@jupyterlab/services": "^7.0.0-alpha.
|
|
45
|
-
"@jupyterlab/translation": "^4.0.0-alpha.
|
|
46
|
-
"@lumino/coreutils": "^2.0.0-
|
|
47
|
-
"@lumino/disposable": "^2.0.0-
|
|
48
|
-
"@lumino/messaging": "^2.0.0-
|
|
49
|
-
"@lumino/signaling": "^2.0.0-
|
|
50
|
-
"@lumino/widgets": "^2.0.0-
|
|
41
|
+
"@jupyterlab/coreutils": "^6.0.0-alpha.20",
|
|
42
|
+
"@jupyterlab/nbformat": "^4.0.0-alpha.20",
|
|
43
|
+
"@jupyterlab/outputarea": "^4.0.0-alpha.20",
|
|
44
|
+
"@jupyterlab/rendermime": "^4.0.0-alpha.20",
|
|
45
|
+
"@jupyterlab/services": "^7.0.0-alpha.20",
|
|
46
|
+
"@jupyterlab/translation": "^4.0.0-alpha.20",
|
|
47
|
+
"@lumino/coreutils": "^2.0.0-rc.0",
|
|
48
|
+
"@lumino/disposable": "^2.0.0-rc.0",
|
|
49
|
+
"@lumino/messaging": "^2.0.0-rc.0",
|
|
50
|
+
"@lumino/signaling": "^2.0.0-rc.0",
|
|
51
|
+
"@lumino/widgets": "^2.0.0-rc.0"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
|
-
"@jupyterlab/testing": "^4.0.0-alpha.
|
|
54
|
+
"@jupyterlab/testing": "^4.0.0-alpha.20",
|
|
54
55
|
"@types/jest": "^29.2.0",
|
|
55
56
|
"rimraf": "~3.0.0",
|
|
56
|
-
"typescript": "~
|
|
57
|
+
"typescript": "~5.0.0-beta"
|
|
57
58
|
},
|
|
58
59
|
"publishConfig": {
|
|
59
60
|
"access": "public"
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
/**
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
* @module logconsole
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export * from './logger';
|
|
9
|
+
export * from './registry';
|
|
10
|
+
export * from './tokens';
|
|
11
|
+
export * from './widget';
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
|
|
4
|
+
import * as nbformat from '@jupyterlab/nbformat';
|
|
5
|
+
import { IOutputAreaModel, OutputAreaModel } from '@jupyterlab/outputarea';
|
|
6
|
+
import {
|
|
7
|
+
IOutputModel,
|
|
8
|
+
IRenderMimeRegistry,
|
|
9
|
+
OutputModel
|
|
10
|
+
} from '@jupyterlab/rendermime';
|
|
11
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
12
|
+
import {
|
|
13
|
+
IContentChange,
|
|
14
|
+
ILogger,
|
|
15
|
+
ILoggerOutputAreaModel,
|
|
16
|
+
ILogPayload,
|
|
17
|
+
IStateChange,
|
|
18
|
+
LogLevel
|
|
19
|
+
} from './tokens';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* All severity levels, including an internal one for metadata.
|
|
23
|
+
*/
|
|
24
|
+
type FullLogLevel = LogLevel | 'metadata';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Custom Notebook Output with log info.
|
|
28
|
+
*/
|
|
29
|
+
type ILogOutput = nbformat.IOutput & {
|
|
30
|
+
/**
|
|
31
|
+
* Date & time when output is logged in integer representation.
|
|
32
|
+
*/
|
|
33
|
+
timestamp: number;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Log level
|
|
37
|
+
*/
|
|
38
|
+
level: FullLogLevel;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export interface ILogOutputModel extends IOutputModel {
|
|
42
|
+
/**
|
|
43
|
+
* Date & time when output is logged.
|
|
44
|
+
*/
|
|
45
|
+
readonly timestamp: Date;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Log level
|
|
49
|
+
*/
|
|
50
|
+
readonly level: FullLogLevel;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Log Output Model with timestamp which provides
|
|
55
|
+
* item information for Output Area Model.
|
|
56
|
+
*/
|
|
57
|
+
export class LogOutputModel extends OutputModel implements ILogOutputModel {
|
|
58
|
+
/**
|
|
59
|
+
* Construct a LogOutputModel.
|
|
60
|
+
*
|
|
61
|
+
* @param options - The model initialization options.
|
|
62
|
+
*/
|
|
63
|
+
constructor(options: LogOutputModel.IOptions) {
|
|
64
|
+
super(options);
|
|
65
|
+
|
|
66
|
+
this.timestamp = new Date(options.value.timestamp);
|
|
67
|
+
this.level = options.value.level;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Date & time when output is logged.
|
|
72
|
+
*/
|
|
73
|
+
readonly timestamp: Date;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Log level
|
|
77
|
+
*/
|
|
78
|
+
readonly level: FullLogLevel;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Log Output Model namespace that defines initialization options.
|
|
83
|
+
*/
|
|
84
|
+
namespace LogOutputModel {
|
|
85
|
+
export interface IOptions extends IOutputModel.IOptions {
|
|
86
|
+
value: ILogOutput;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Implementation of `IContentFactory` for Output Area Model
|
|
92
|
+
* which creates LogOutputModel instances.
|
|
93
|
+
*/
|
|
94
|
+
class LogConsoleModelContentFactory extends OutputAreaModel.ContentFactory {
|
|
95
|
+
/**
|
|
96
|
+
* Create a rendermime output model from notebook output.
|
|
97
|
+
*/
|
|
98
|
+
createOutputModel(options: LogOutputModel.IOptions): LogOutputModel {
|
|
99
|
+
return new LogOutputModel(options);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Output Area Model implementation which is able to
|
|
105
|
+
* limit number of outputs stored.
|
|
106
|
+
*/
|
|
107
|
+
export class LoggerOutputAreaModel
|
|
108
|
+
extends OutputAreaModel
|
|
109
|
+
implements ILoggerOutputAreaModel
|
|
110
|
+
{
|
|
111
|
+
constructor({ maxLength, ...options }: LoggerOutputAreaModel.IOptions) {
|
|
112
|
+
super(options);
|
|
113
|
+
this.maxLength = maxLength;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Add an output, which may be combined with previous output.
|
|
118
|
+
*
|
|
119
|
+
* @returns The total number of outputs.
|
|
120
|
+
*
|
|
121
|
+
* #### Notes
|
|
122
|
+
* The output bundle is copied. Contiguous stream outputs of the same `name`
|
|
123
|
+
* are combined. The oldest outputs are possibly removed to ensure the total
|
|
124
|
+
* number of outputs is at most `.maxLength`.
|
|
125
|
+
*/
|
|
126
|
+
add(output: ILogOutput): number {
|
|
127
|
+
super.add(output);
|
|
128
|
+
this._applyMaxLength();
|
|
129
|
+
return this.length;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Whether an output should combine with the previous output.
|
|
134
|
+
*
|
|
135
|
+
* We combine if the two outputs are in the same second, which is the
|
|
136
|
+
* resolution for our time display.
|
|
137
|
+
*/
|
|
138
|
+
protected shouldCombine(options: {
|
|
139
|
+
value: ILogOutput;
|
|
140
|
+
lastModel: ILogOutputModel;
|
|
141
|
+
}): boolean {
|
|
142
|
+
const { value, lastModel } = options;
|
|
143
|
+
|
|
144
|
+
const oldSeconds = Math.trunc(lastModel.timestamp.getTime() / 1000);
|
|
145
|
+
const newSeconds = Math.trunc(value.timestamp / 1000);
|
|
146
|
+
|
|
147
|
+
return oldSeconds === newSeconds;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get an item at the specified index.
|
|
152
|
+
*/
|
|
153
|
+
get(index: number): ILogOutputModel {
|
|
154
|
+
return super.get(index) as ILogOutputModel;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Maximum number of outputs to store in the model.
|
|
159
|
+
*/
|
|
160
|
+
get maxLength(): number {
|
|
161
|
+
return this._maxLength;
|
|
162
|
+
}
|
|
163
|
+
set maxLength(value: number) {
|
|
164
|
+
this._maxLength = value;
|
|
165
|
+
this._applyMaxLength();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Manually apply length limit.
|
|
170
|
+
*/
|
|
171
|
+
private _applyMaxLength() {
|
|
172
|
+
if (this.list.length > this._maxLength) {
|
|
173
|
+
this.list.removeRange(0, this.list.length - this._maxLength);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private _maxLength: number;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export namespace LoggerOutputAreaModel {
|
|
181
|
+
export interface IOptions extends IOutputAreaModel.IOptions {
|
|
182
|
+
/**
|
|
183
|
+
* The maximum number of messages stored.
|
|
184
|
+
*/
|
|
185
|
+
maxLength: number;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* A concrete implementation of ILogger.
|
|
191
|
+
*/
|
|
192
|
+
export class Logger implements ILogger {
|
|
193
|
+
/**
|
|
194
|
+
* Construct a Logger.
|
|
195
|
+
*
|
|
196
|
+
* @param source - The name of the log source.
|
|
197
|
+
*/
|
|
198
|
+
constructor(options: Logger.IOptions) {
|
|
199
|
+
this.source = options.source;
|
|
200
|
+
this.outputAreaModel = new LoggerOutputAreaModel({
|
|
201
|
+
contentFactory: new LogConsoleModelContentFactory(),
|
|
202
|
+
maxLength: options.maxLength
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The maximum number of outputs stored.
|
|
208
|
+
*
|
|
209
|
+
* #### Notes
|
|
210
|
+
* Oldest entries will be trimmed to ensure the length is at most
|
|
211
|
+
* `.maxLength`.
|
|
212
|
+
*/
|
|
213
|
+
get maxLength(): number {
|
|
214
|
+
return this.outputAreaModel.maxLength;
|
|
215
|
+
}
|
|
216
|
+
set maxLength(value: number) {
|
|
217
|
+
this.outputAreaModel.maxLength = value;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The level of outputs logged
|
|
222
|
+
*/
|
|
223
|
+
get level(): LogLevel {
|
|
224
|
+
return this._level;
|
|
225
|
+
}
|
|
226
|
+
set level(newValue: LogLevel) {
|
|
227
|
+
const oldValue = this._level;
|
|
228
|
+
if (oldValue === newValue) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
this._level = newValue;
|
|
232
|
+
this._log({
|
|
233
|
+
output: {
|
|
234
|
+
output_type: 'display_data',
|
|
235
|
+
data: {
|
|
236
|
+
'text/plain': `Log level set to ${newValue}`
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
level: 'metadata'
|
|
240
|
+
});
|
|
241
|
+
this._stateChanged.emit({ name: 'level', oldValue, newValue });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Number of outputs logged.
|
|
246
|
+
*/
|
|
247
|
+
get length(): number {
|
|
248
|
+
return this.outputAreaModel.length;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* A signal emitted when the list of log messages changes.
|
|
253
|
+
*/
|
|
254
|
+
get contentChanged(): ISignal<this, IContentChange> {
|
|
255
|
+
return this._contentChanged;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* A signal emitted when the log state changes.
|
|
260
|
+
*/
|
|
261
|
+
get stateChanged(): ISignal<this, IStateChange> {
|
|
262
|
+
return this._stateChanged;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Rendermime to use when rendering outputs logged.
|
|
267
|
+
*/
|
|
268
|
+
get rendermime(): IRenderMimeRegistry | null {
|
|
269
|
+
return this._rendermime;
|
|
270
|
+
}
|
|
271
|
+
set rendermime(value: IRenderMimeRegistry | null) {
|
|
272
|
+
if (value !== this._rendermime) {
|
|
273
|
+
const oldValue = this._rendermime;
|
|
274
|
+
const newValue = (this._rendermime = value);
|
|
275
|
+
this._stateChanged.emit({ name: 'rendermime', oldValue, newValue });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The number of messages that have ever been stored.
|
|
281
|
+
*/
|
|
282
|
+
get version(): number {
|
|
283
|
+
return this._version;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The source for the logger.
|
|
288
|
+
*/
|
|
289
|
+
readonly source: string;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The output area model used for the logger.
|
|
293
|
+
*
|
|
294
|
+
* #### Notes
|
|
295
|
+
* This will usually not be accessed directly. It is a public attribute so
|
|
296
|
+
* that the renderer can access it.
|
|
297
|
+
*/
|
|
298
|
+
readonly outputAreaModel: LoggerOutputAreaModel;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Log an output to logger.
|
|
302
|
+
*
|
|
303
|
+
* @param log - The output to be logged.
|
|
304
|
+
*/
|
|
305
|
+
log(log: ILogPayload): void {
|
|
306
|
+
// Filter by our current log level
|
|
307
|
+
if (
|
|
308
|
+
Private.LogLevel[log.level as keyof typeof Private.LogLevel] <
|
|
309
|
+
Private.LogLevel[this._level as keyof typeof Private.LogLevel]
|
|
310
|
+
) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
let output: nbformat.IOutput | null = null;
|
|
314
|
+
switch (log.type) {
|
|
315
|
+
case 'text':
|
|
316
|
+
output = {
|
|
317
|
+
output_type: 'display_data',
|
|
318
|
+
data: {
|
|
319
|
+
'text/plain': log.data
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
break;
|
|
323
|
+
case 'html':
|
|
324
|
+
output = {
|
|
325
|
+
output_type: 'display_data',
|
|
326
|
+
data: {
|
|
327
|
+
'text/html': log.data
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
break;
|
|
331
|
+
case 'output':
|
|
332
|
+
output = log.data;
|
|
333
|
+
break;
|
|
334
|
+
default:
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (output) {
|
|
339
|
+
this._log({
|
|
340
|
+
output,
|
|
341
|
+
level: log.level
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Clear all outputs logged.
|
|
348
|
+
*/
|
|
349
|
+
clear(): void {
|
|
350
|
+
this.outputAreaModel.clear(false);
|
|
351
|
+
this._contentChanged.emit('clear');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Add a checkpoint to the log.
|
|
356
|
+
*/
|
|
357
|
+
checkpoint(): void {
|
|
358
|
+
this._log({
|
|
359
|
+
output: {
|
|
360
|
+
output_type: 'display_data',
|
|
361
|
+
data: {
|
|
362
|
+
'text/html': '<hr/>'
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
level: 'metadata'
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Whether the logger is disposed.
|
|
371
|
+
*/
|
|
372
|
+
get isDisposed(): boolean {
|
|
373
|
+
return this._isDisposed;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Dispose the logger.
|
|
378
|
+
*/
|
|
379
|
+
dispose(): void {
|
|
380
|
+
if (this.isDisposed) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
this._isDisposed = true;
|
|
384
|
+
this.clear();
|
|
385
|
+
this._rendermime = null!;
|
|
386
|
+
Signal.clearData(this);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private _log(options: { output: nbformat.IOutput; level: FullLogLevel }) {
|
|
390
|
+
// First, make sure our version reflects the new message so things
|
|
391
|
+
// triggering from the signals below have the correct version.
|
|
392
|
+
this._version++;
|
|
393
|
+
|
|
394
|
+
// Next, trigger any displays of the message
|
|
395
|
+
this.outputAreaModel.add({
|
|
396
|
+
...options.output,
|
|
397
|
+
timestamp: Date.now(),
|
|
398
|
+
level: options.level
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Finally, tell people that the message was appended (and possibly
|
|
402
|
+
// already displayed).
|
|
403
|
+
this._contentChanged.emit('append');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private _isDisposed = false;
|
|
407
|
+
private _contentChanged = new Signal<this, IContentChange>(this);
|
|
408
|
+
private _stateChanged = new Signal<this, IStateChange>(this);
|
|
409
|
+
private _rendermime: IRenderMimeRegistry | null = null;
|
|
410
|
+
private _version = 0;
|
|
411
|
+
private _level: LogLevel = 'warning';
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export namespace Logger {
|
|
415
|
+
export interface IOptions {
|
|
416
|
+
/**
|
|
417
|
+
* The log source identifier.
|
|
418
|
+
*/
|
|
419
|
+
source: string;
|
|
420
|
+
/**
|
|
421
|
+
* The maximum number of messages to store.
|
|
422
|
+
*/
|
|
423
|
+
maxLength: number;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
namespace Private {
|
|
428
|
+
export enum LogLevel {
|
|
429
|
+
debug,
|
|
430
|
+
info,
|
|
431
|
+
warning,
|
|
432
|
+
error,
|
|
433
|
+
critical,
|
|
434
|
+
metadata
|
|
435
|
+
}
|
|
436
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
|
|
4
|
+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
5
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
6
|
+
import { Logger } from './logger';
|
|
7
|
+
import { ILogger, ILoggerRegistry, ILoggerRegistryChange } from './tokens';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A concrete implementation of ILoggerRegistry.
|
|
11
|
+
*/
|
|
12
|
+
export class LoggerRegistry implements ILoggerRegistry {
|
|
13
|
+
/**
|
|
14
|
+
* Construct a LoggerRegistry.
|
|
15
|
+
*
|
|
16
|
+
* @param defaultRendermime - Default rendermime to render outputs
|
|
17
|
+
* with when logger is not supplied with one.
|
|
18
|
+
*/
|
|
19
|
+
constructor(options: LoggerRegistry.IOptions) {
|
|
20
|
+
this._defaultRendermime = options.defaultRendermime;
|
|
21
|
+
this._maxLength = options.maxLength;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the logger for the specified source.
|
|
26
|
+
*
|
|
27
|
+
* @param source - The name of the log source.
|
|
28
|
+
*
|
|
29
|
+
* @returns The logger for the specified source.
|
|
30
|
+
*/
|
|
31
|
+
getLogger(source: string): ILogger {
|
|
32
|
+
const loggers = this._loggers;
|
|
33
|
+
let logger = loggers.get(source);
|
|
34
|
+
if (logger) {
|
|
35
|
+
return logger;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
logger = new Logger({ source, maxLength: this.maxLength });
|
|
39
|
+
logger.rendermime = this._defaultRendermime;
|
|
40
|
+
loggers.set(source, logger);
|
|
41
|
+
|
|
42
|
+
this._registryChanged.emit('append');
|
|
43
|
+
|
|
44
|
+
return logger;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get all loggers registered.
|
|
49
|
+
*
|
|
50
|
+
* @returns The array containing all registered loggers.
|
|
51
|
+
*/
|
|
52
|
+
getLoggers(): ILogger[] {
|
|
53
|
+
return Array.from(this._loggers.values());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A signal emitted when the logger registry changes.
|
|
58
|
+
*/
|
|
59
|
+
get registryChanged(): ISignal<this, ILoggerRegistryChange> {
|
|
60
|
+
return this._registryChanged;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The max length for loggers.
|
|
65
|
+
*/
|
|
66
|
+
get maxLength(): number {
|
|
67
|
+
return this._maxLength;
|
|
68
|
+
}
|
|
69
|
+
set maxLength(value: number) {
|
|
70
|
+
this._maxLength = value;
|
|
71
|
+
this._loggers.forEach(logger => {
|
|
72
|
+
logger.maxLength = value;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether the register is disposed.
|
|
78
|
+
*/
|
|
79
|
+
get isDisposed(): boolean {
|
|
80
|
+
return this._isDisposed;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Dispose the registry and all loggers.
|
|
85
|
+
*/
|
|
86
|
+
dispose(): void {
|
|
87
|
+
if (this.isDisposed) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this._isDisposed = true;
|
|
91
|
+
this._loggers.forEach(x => x.dispose());
|
|
92
|
+
Signal.clearData(this);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private _defaultRendermime: IRenderMimeRegistry;
|
|
96
|
+
private _loggers = new Map<string, ILogger>();
|
|
97
|
+
private _maxLength: number;
|
|
98
|
+
private _registryChanged = new Signal<this, ILoggerRegistryChange>(this);
|
|
99
|
+
private _isDisposed = false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export namespace LoggerRegistry {
|
|
103
|
+
export interface IOptions {
|
|
104
|
+
defaultRendermime: IRenderMimeRegistry;
|
|
105
|
+
maxLength: number;
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/tokens.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
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 { IOutputAreaModel } from '@jupyterlab/outputarea';
|
|
7
|
+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
8
|
+
import { Token } from '@lumino/coreutils';
|
|
9
|
+
import { IDisposable } from '@lumino/disposable';
|
|
10
|
+
import { ISignal } from '@lumino/signaling';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The Logger Registry token.
|
|
14
|
+
*/
|
|
15
|
+
export const ILoggerRegistry = new Token<ILoggerRegistry>(
|
|
16
|
+
'@jupyterlab/logconsole:ILoggerRegistry'
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export type ILoggerRegistryChange = 'append';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A Logger Registry that registers and provides loggers by source.
|
|
23
|
+
*/
|
|
24
|
+
export interface ILoggerRegistry extends IDisposable {
|
|
25
|
+
/**
|
|
26
|
+
* Get the logger for the specified source.
|
|
27
|
+
*
|
|
28
|
+
* @param source - The name of the log source.
|
|
29
|
+
*
|
|
30
|
+
* @returns The logger for the specified source.
|
|
31
|
+
*/
|
|
32
|
+
getLogger(source: string): ILogger;
|
|
33
|
+
/**
|
|
34
|
+
* Get all loggers registered.
|
|
35
|
+
*
|
|
36
|
+
* @returns The array containing all registered loggers.
|
|
37
|
+
*/
|
|
38
|
+
getLoggers(): ILogger[];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A signal emitted when the logger registry changes.
|
|
42
|
+
*/
|
|
43
|
+
readonly registryChanged: ISignal<this, ILoggerRegistryChange>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Log severity level
|
|
48
|
+
*/
|
|
49
|
+
export type LogLevel = 'critical' | 'error' | 'warning' | 'info' | 'debug';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The base log payload type.
|
|
53
|
+
*/
|
|
54
|
+
export interface ILogPayloadBase {
|
|
55
|
+
/**
|
|
56
|
+
* Type of log data.
|
|
57
|
+
*/
|
|
58
|
+
type: string;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Log level
|
|
62
|
+
*/
|
|
63
|
+
level: LogLevel;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Data
|
|
67
|
+
*/
|
|
68
|
+
data: any;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Plain text log payload.
|
|
73
|
+
*/
|
|
74
|
+
export interface ITextLog extends ILogPayloadBase {
|
|
75
|
+
/**
|
|
76
|
+
* Type of log data.
|
|
77
|
+
*/
|
|
78
|
+
type: 'text';
|
|
79
|
+
/**
|
|
80
|
+
* Log data as plain text.
|
|
81
|
+
*/
|
|
82
|
+
data: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* HTML log payload.
|
|
87
|
+
*/
|
|
88
|
+
export interface IHtmlLog extends ILogPayloadBase {
|
|
89
|
+
/**
|
|
90
|
+
* Type of log data.
|
|
91
|
+
*/
|
|
92
|
+
type: 'html';
|
|
93
|
+
/**
|
|
94
|
+
* Log data as HTML string.
|
|
95
|
+
*/
|
|
96
|
+
data: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Notebook kernel output log payload.
|
|
101
|
+
*/
|
|
102
|
+
export interface IOutputLog extends ILogPayloadBase {
|
|
103
|
+
/**
|
|
104
|
+
* Type of log data.
|
|
105
|
+
*/
|
|
106
|
+
type: 'output';
|
|
107
|
+
/**
|
|
108
|
+
* Log data as Notebook kernel output.
|
|
109
|
+
*/
|
|
110
|
+
data: nbformat.IOutput;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Log payload union type.
|
|
115
|
+
*/
|
|
116
|
+
export type ILogPayload = ITextLog | IHtmlLog | IOutputLog;
|
|
117
|
+
|
|
118
|
+
export type IContentChange = 'append' | 'clear';
|
|
119
|
+
|
|
120
|
+
export type IStateChange =
|
|
121
|
+
| IChangedArgs<
|
|
122
|
+
IRenderMimeRegistry | null,
|
|
123
|
+
IRenderMimeRegistry | null,
|
|
124
|
+
'rendermime'
|
|
125
|
+
>
|
|
126
|
+
| IChangedArgs<LogLevel, LogLevel, 'level'>;
|
|
127
|
+
|
|
128
|
+
export interface ILoggerOutputAreaModel extends IOutputAreaModel {
|
|
129
|
+
/**
|
|
130
|
+
* The maximum number of outputs to store.
|
|
131
|
+
*/
|
|
132
|
+
maxLength: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A Logger that manages logs from a particular source.
|
|
137
|
+
*/
|
|
138
|
+
export interface ILogger extends IDisposable {
|
|
139
|
+
/**
|
|
140
|
+
* Number of outputs logged.
|
|
141
|
+
*/
|
|
142
|
+
readonly length: number;
|
|
143
|
+
/**
|
|
144
|
+
* Max number of messages.
|
|
145
|
+
*/
|
|
146
|
+
maxLength: number;
|
|
147
|
+
/**
|
|
148
|
+
* Log level.
|
|
149
|
+
*/
|
|
150
|
+
level: LogLevel;
|
|
151
|
+
/**
|
|
152
|
+
* Rendermime to use when rendering outputs logged.
|
|
153
|
+
*/
|
|
154
|
+
rendermime: IRenderMimeRegistry | null;
|
|
155
|
+
/**
|
|
156
|
+
* A signal emitted when the log model changes.
|
|
157
|
+
*/
|
|
158
|
+
readonly contentChanged: ISignal<this, IContentChange>;
|
|
159
|
+
/**
|
|
160
|
+
* A signal emitted when the rendermime changes.
|
|
161
|
+
*/
|
|
162
|
+
readonly stateChanged: ISignal<this, IStateChange>;
|
|
163
|
+
/**
|
|
164
|
+
* The name of the log source.
|
|
165
|
+
*/
|
|
166
|
+
readonly source: string;
|
|
167
|
+
/**
|
|
168
|
+
* Output Area Model used to manage log storage in memory.
|
|
169
|
+
*/
|
|
170
|
+
readonly outputAreaModel: ILoggerOutputAreaModel;
|
|
171
|
+
/**
|
|
172
|
+
* The cumulative number of messages the log has stored.
|
|
173
|
+
*/
|
|
174
|
+
readonly version: number;
|
|
175
|
+
/**
|
|
176
|
+
* Log an output to logger.
|
|
177
|
+
*
|
|
178
|
+
* @param log - The output to be logged.
|
|
179
|
+
*/
|
|
180
|
+
log(log: ILogPayload): void;
|
|
181
|
+
/**
|
|
182
|
+
* Add a checkpoint in the log.
|
|
183
|
+
*/
|
|
184
|
+
checkpoint(): void;
|
|
185
|
+
/**
|
|
186
|
+
* Clear all outputs logged.
|
|
187
|
+
*/
|
|
188
|
+
clear(): void;
|
|
189
|
+
}
|
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
|
+
}
|