1 | /*
|
2 | * Copyright (c) Jupyter Development Team.
|
3 | * Distributed under the terms of the Modified BSD License.
|
4 | */
|
5 |
|
6 | import { ReadonlyJSONValue, UUID } from '@lumino/coreutils';
|
7 | import { IDisposable } from '@lumino/disposable';
|
8 | import { ISignal, Signal } from '@lumino/signaling';
|
9 |
|
10 | /**
|
11 | * Notification manager
|
12 | */
|
13 | export class NotificationManager implements IDisposable {
|
14 | constructor() {
|
15 | this._isDisposed = false;
|
16 | this._queue = [];
|
17 | this._changed = new Signal<NotificationManager, Notification.IChange>(this);
|
18 | }
|
19 |
|
20 | /**
|
21 | * Signal emitted whenever a notification changes.
|
22 | */
|
23 | get changed(): ISignal<NotificationManager, Notification.IChange> {
|
24 | return this._changed;
|
25 | }
|
26 |
|
27 | /**
|
28 | * Total number of notifications.
|
29 | */
|
30 | get count(): number {
|
31 | return this._queue.length;
|
32 | }
|
33 |
|
34 | /**
|
35 | * Whether the manager is disposed or not.
|
36 | */
|
37 | get isDisposed(): boolean {
|
38 | return this._isDisposed;
|
39 | }
|
40 |
|
41 | /**
|
42 | * The list of notifications.
|
43 | */
|
44 | get notifications(): Notification.INotification[] {
|
45 | return this._queue.slice();
|
46 | }
|
47 |
|
48 | /**
|
49 | * Dismiss one notification (specified by its id) or all if no id provided.
|
50 | *
|
51 | * @param id Notification id
|
52 | */
|
53 | dismiss(id?: string): void {
|
54 | if (typeof id === 'undefined') {
|
55 | const q = this._queue.slice();
|
56 | this._queue.length = 0;
|
57 | for (const notification of q) {
|
58 | this._changed.emit({
|
59 | type: 'removed',
|
60 | notification
|
61 | });
|
62 | }
|
63 | } else {
|
64 | const notificationIndex = this._queue.findIndex(n => n.id === id);
|
65 | if (notificationIndex > -1) {
|
66 | const notification = this._queue.splice(notificationIndex, 1)[0];
|
67 | this._changed.emit({
|
68 | type: 'removed',
|
69 | notification
|
70 | });
|
71 | }
|
72 | }
|
73 | }
|
74 |
|
75 | /**
|
76 | * Dispose the manager.
|
77 | */
|
78 | dispose(): void {
|
79 | if (this._isDisposed) {
|
80 | return;
|
81 | }
|
82 |
|
83 | this._isDisposed = true;
|
84 | Signal.clearData(this);
|
85 | }
|
86 |
|
87 | /**
|
88 | * Test whether a notification exists or not.
|
89 | *
|
90 | * @param id Notification id
|
91 | * @returns Notification status
|
92 | */
|
93 | has(id: string): boolean {
|
94 | return this._queue.findIndex(n => n.id === id) > -1;
|
95 | }
|
96 |
|
97 | /**
|
98 | * Add a new notification.
|
99 | *
|
100 | * This will trigger the `changed` signal with an `added` event.
|
101 | *
|
102 | * @param message Notification message
|
103 | * @param type Notification type
|
104 | * @param options Notification option
|
105 | * @returns Notification unique id
|
106 | */
|
107 | notify<T extends ReadonlyJSONValue = ReadonlyJSONValue>(
|
108 | message: string,
|
109 | type: Notification.TypeOptions,
|
110 | options: Notification.IOptions<T>
|
111 | ): string {
|
112 | const now = Date.now();
|
113 | const { progress, ...othersOptions } = options;
|
114 | const notification: Notification.INotification = Object.freeze({
|
115 | id: UUID.uuid4(),
|
116 | createdAt: now,
|
117 | modifiedAt: now,
|
118 | message,
|
119 | type,
|
120 | options: {
|
121 | // By default notification will be silent
|
122 | autoClose: 0,
|
123 | progress:
|
124 | typeof progress === 'number'
|
125 | ? Math.min(Math.max(0, progress), 1)
|
126 | : progress,
|
127 | ...othersOptions
|
128 | }
|
129 | });
|
130 |
|
131 | this._queue.unshift(notification);
|
132 |
|
133 | this._changed.emit({
|
134 | type: 'added',
|
135 | notification
|
136 | });
|
137 |
|
138 | return notification.id;
|
139 | }
|
140 |
|
141 | /**
|
142 | * Update an existing notification.
|
143 | *
|
144 | * If the notification does not exists this won't do anything.
|
145 | *
|
146 | * Once updated the notification will be moved at the begin
|
147 | * of the notification stack.
|
148 | *
|
149 | * @param args Update options
|
150 | * @returns Whether the update was successful or not.
|
151 | */
|
152 | update<T extends ReadonlyJSONValue = ReadonlyJSONValue>(
|
153 | args: Notification.IUpdate<T>
|
154 | ): boolean {
|
155 | const { id, message, actions, autoClose, data, progress, type } = args;
|
156 | const newProgress =
|
157 | typeof progress === 'number'
|
158 | ? Math.min(Math.max(0, progress), 1)
|
159 | : progress;
|
160 | const notificationIndex = this._queue.findIndex(n => n.id === id);
|
161 | if (notificationIndex > -1) {
|
162 | const oldNotification = this._queue[notificationIndex];
|
163 | // We need to create a new object as notification are frozen; i.e. cannot be edited
|
164 | const notification = Object.freeze({
|
165 | ...oldNotification,
|
166 | message: message ?? oldNotification.message,
|
167 | type: type ?? oldNotification.type,
|
168 | options: {
|
169 | actions: actions ?? oldNotification.options.actions,
|
170 | autoClose: autoClose ?? oldNotification.options.autoClose,
|
171 | data: data ?? oldNotification.options.data,
|
172 | progress: newProgress ?? oldNotification.options.progress
|
173 | },
|
174 | modifiedAt: Date.now()
|
175 | });
|
176 |
|
177 | this._queue.splice(notificationIndex, 1);
|
178 | this._queue.unshift(notification);
|
179 |
|
180 | this._changed.emit({
|
181 | type: 'updated',
|
182 | notification
|
183 | });
|
184 | return true;
|
185 | }
|
186 |
|
187 | return false;
|
188 | }
|
189 |
|
190 | private _isDisposed: boolean;
|
191 | private _changed: Signal<NotificationManager, Notification.IChange>;
|
192 | private _queue: Notification.INotification[];
|
193 | }
|
194 |
|
195 | /**
|
196 | * Notification namespace
|
197 | */
|
198 | export namespace Notification {
|
199 | /**
|
200 | * Enumeration of available action display type.
|
201 | */
|
202 | export type ActionDisplayType = 'default' | 'accent' | 'warn' | 'link';
|
203 |
|
204 | /**
|
205 | * Interface describing an action linked to a notification.
|
206 | */
|
207 | export interface IAction {
|
208 | /**
|
209 | * The action label.
|
210 | *
|
211 | * This should be a short description.
|
212 | */
|
213 | label: string;
|
214 |
|
215 | /**
|
216 | * Callback function to trigger
|
217 | *
|
218 | * ### Notes
|
219 | * By default execution of the callback will close the toast
|
220 | * and dismiss the notification. You can prevent this by calling
|
221 | * `event.preventDefault()` in the callback.
|
222 | */
|
223 | callback: (event: MouseEvent) => void;
|
224 |
|
225 | /**
|
226 | * The action caption.
|
227 | *
|
228 | * This can be a longer description of the action.
|
229 | */
|
230 | caption?: string;
|
231 |
|
232 | /**
|
233 | * The action display type.
|
234 | *
|
235 | * This will be used to modify the action button style.
|
236 | */
|
237 | displayType?: ActionDisplayType;
|
238 | }
|
239 |
|
240 | /**
|
241 | * Notification interface
|
242 | */
|
243 | export interface INotification<
|
244 | T extends ReadonlyJSONValue = ReadonlyJSONValue
|
245 | > {
|
246 | /**
|
247 | * Notification unique identifier
|
248 | */
|
249 | id: string;
|
250 | /**
|
251 | * Notification message
|
252 | *
|
253 | * #### Notes
|
254 | * The message will be truncated if longer than 140 characters.
|
255 | */
|
256 | message: string;
|
257 | /**
|
258 | * Notification creation date
|
259 | */
|
260 | createdAt: number;
|
261 | /**
|
262 | * Notification modification date
|
263 | */
|
264 | modifiedAt: number;
|
265 | /**
|
266 | * Notification type
|
267 | */
|
268 | type: TypeOptions;
|
269 | /**
|
270 | * Notification options
|
271 | */
|
272 | options: IOptions<T>;
|
273 | }
|
274 |
|
275 | /**
|
276 | * Notification change interface
|
277 | */
|
278 | export interface IChange {
|
279 | /**
|
280 | * Change type
|
281 | */
|
282 | type: 'added' | 'removed' | 'updated';
|
283 | /**
|
284 | * Notification that changed
|
285 | */
|
286 | notification: INotification;
|
287 | }
|
288 |
|
289 | /**
|
290 | * Notification options
|
291 | */
|
292 | export interface IOptions<T extends ReadonlyJSONValue> {
|
293 | /**
|
294 | * Autoclosing behavior - false (not closing automatically)
|
295 | * or number (time in milliseconds before hiding the notification)
|
296 | *
|
297 | * Set to zero if you want the notification to be retained in the notification
|
298 | * center but not displayed as toast. This is the default behavior.
|
299 | */
|
300 | autoClose?: number | false;
|
301 |
|
302 | /**
|
303 | * List of associated actions
|
304 | */
|
305 | actions?: Array<IAction>;
|
306 |
|
307 | /**
|
308 | * Data associated with a notification
|
309 | */
|
310 | data?: T;
|
311 |
|
312 | /**
|
313 | * Task progression
|
314 | *
|
315 | * ### Notes
|
316 | * This should be a number between 0 (not started) and 1 (completed).
|
317 | */
|
318 | progress?: number;
|
319 | }
|
320 |
|
321 | /**
|
322 | * Parameters for notification depending on a promise.
|
323 | */
|
324 | export interface IPromiseOptions<
|
325 | Pending extends ReadonlyJSONValue,
|
326 | Success extends ReadonlyJSONValue = Pending,
|
327 | Error extends ReadonlyJSONValue = Pending
|
328 | > {
|
329 | /**
|
330 | * Promise pending message and options
|
331 | *
|
332 | * #### Notes
|
333 | * The message will be truncated if longer than 140 characters.
|
334 | */
|
335 | pending: { message: string; options?: IOptions<Pending> };
|
336 | /**
|
337 | * Message when promise resolves and options
|
338 | *
|
339 | * The message factory receives as first argument the result
|
340 | * of the promise and as second the success `options.data`.
|
341 | *
|
342 | * #### Notes
|
343 | * The message will be truncated if longer than 140 characters.
|
344 | */
|
345 | success: {
|
346 | message: (result: unknown, data?: Success) => string;
|
347 | options?: IOptions<Success>;
|
348 | };
|
349 | /**
|
350 | * Message when promise rejects and options
|
351 | *
|
352 | * The message factory receives as first argument the error
|
353 | * of the promise and as second the error `options.data`.
|
354 | *
|
355 | * #### Notes
|
356 | * The message will be truncated if longer than 140 characters.
|
357 | */
|
358 | error: {
|
359 | message: (reason: unknown, data?: Error) => string;
|
360 | options?: IOptions<Error>;
|
361 | };
|
362 | }
|
363 |
|
364 | /**
|
365 | * Type of notifications
|
366 | */
|
367 | export type TypeOptions =
|
368 | | 'info'
|
369 | | 'in-progress'
|
370 | | 'success'
|
371 | | 'warning'
|
372 | | 'error'
|
373 | | 'default';
|
374 |
|
375 | /**
|
376 | * Options for updating a notification
|
377 | */
|
378 | export interface IUpdate<T extends ReadonlyJSONValue> extends IOptions<T> {
|
379 | /**
|
380 | * Notification unique id
|
381 | */
|
382 | id: string;
|
383 | /**
|
384 | * New notification message
|
385 | */
|
386 | message?: string;
|
387 | /**
|
388 | * New notification type
|
389 | */
|
390 | type?: TypeOptions;
|
391 | }
|
392 |
|
393 | /**
|
394 | * The global notification manager.
|
395 | */
|
396 | export const manager = new NotificationManager();
|
397 |
|
398 | /**
|
399 | * Dismiss one notification (specified by its id) or all if no id provided
|
400 | *
|
401 | * @param id notification id
|
402 | */
|
403 | export function dismiss(id?: string): void {
|
404 | manager.dismiss(id);
|
405 | }
|
406 |
|
407 | /**
|
408 | * Helper function to emit a notification.
|
409 | *
|
410 | * #### Notes
|
411 | * The message will be truncated if longer than 140 characters.
|
412 | *
|
413 | * @param message Notification message
|
414 | * @param type Notification type
|
415 | * @param options Options for the error notification
|
416 | * @returns Notification unique id
|
417 | */
|
418 | export function emit<T extends ReadonlyJSONValue = ReadonlyJSONValue>(
|
419 | message: string,
|
420 | type: TypeOptions = 'default',
|
421 | options: IOptions<T> = {}
|
422 | ): string {
|
423 | return manager.notify<T>(message, type, options);
|
424 | }
|
425 |
|
426 | /**
|
427 | * Helper function to emit an error notification.
|
428 | *
|
429 | * #### Notes
|
430 | * The message will be truncated if longer than 140 characters.
|
431 | *
|
432 | * @param message Notification message
|
433 | * @param options Options for the error notification
|
434 | * @returns Notification unique id
|
435 | */
|
436 | export function error<T extends ReadonlyJSONValue = ReadonlyJSONValue>(
|
437 | message: string,
|
438 | options: IOptions<T> = {}
|
439 | ): string {
|
440 | return manager.notify<T>(message, 'error', options);
|
441 | }
|
442 |
|
443 | /**
|
444 | * Helper function to emit an info notification.
|
445 | *
|
446 | * #### Notes
|
447 | * The message will be truncated if longer than 140 characters.
|
448 | *
|
449 | * @param message Notification message
|
450 | * @param options Options for the info notification
|
451 | * @returns Notification unique id
|
452 | */
|
453 | export function info<T extends ReadonlyJSONValue = ReadonlyJSONValue>(
|
454 | message: string,
|
455 | options: IOptions<T> = {}
|
456 | ): string {
|
457 | return manager.notify<T>(message, 'info', options);
|
458 | }
|
459 |
|
460 | /**
|
461 | * Helper function to show an in-progress notification.
|
462 | *
|
463 | * #### Notes
|
464 | * The message will be truncated if longer than 140 characters.
|
465 | *
|
466 | * @param promise Promise to wait for
|
467 | * @param options Options for the in-progress notification
|
468 | * @returns Notification unique id
|
469 | */
|
470 | export function promise<
|
471 | Pending extends ReadonlyJSONValue = ReadonlyJSONValue,
|
472 | Success extends ReadonlyJSONValue = Pending,
|
473 | Error extends ReadonlyJSONValue = Pending
|
474 | >(
|
475 | promise: Promise<Success>,
|
476 | options: IPromiseOptions<Pending, Success, Error>
|
477 | ): string {
|
478 | const { pending, error, success } = options;
|
479 | const id = manager.notify<Pending>(
|
480 | pending.message,
|
481 | 'in-progress',
|
482 | pending.options ?? {}
|
483 | );
|
484 | promise
|
485 | .then(result => {
|
486 | manager.update<Success>({
|
487 | id,
|
488 | message: success.message(result, success.options?.data),
|
489 | type: 'success',
|
490 | ...success.options,
|
491 | data: success.options?.data ?? result
|
492 | });
|
493 | })
|
494 | .catch(reason => {
|
495 | manager.update<Error>({
|
496 | id,
|
497 | message: error.message(reason, error.options?.data),
|
498 | type: 'error',
|
499 | ...error.options,
|
500 | data: error.options?.data ?? reason
|
501 | });
|
502 | });
|
503 | return id;
|
504 | }
|
505 |
|
506 | /**
|
507 | * Helper function to emit a success notification.
|
508 | *
|
509 | * #### Notes
|
510 | * The message will be truncated if longer than 140 characters.
|
511 | *
|
512 | * @param message Notification message
|
513 | * @param options Options for the success notification
|
514 | * @returns Notification unique id
|
515 | */
|
516 | export function success<T extends ReadonlyJSONValue = ReadonlyJSONValue>(
|
517 | message: string,
|
518 | options: IOptions<T> = {}
|
519 | ): string {
|
520 | return manager.notify<T>(message, 'success', options);
|
521 | }
|
522 |
|
523 | /**
|
524 | * Helper function to update a notification.
|
525 | *
|
526 | * If the notification does not exists, nothing will happen.
|
527 | *
|
528 | * Once updated the notification will be moved at the begin
|
529 | * of the notification stack.
|
530 | *
|
531 | * #### Notes
|
532 | * The message will be truncated if longer than 140 characters.
|
533 | *
|
534 | * @param args Update options
|
535 | * @returns Whether the update was successful or not.
|
536 | */
|
537 | export function update<T extends ReadonlyJSONValue = ReadonlyJSONValue>(
|
538 | args: IUpdate<T>
|
539 | ): boolean {
|
540 | return manager.update(args);
|
541 | }
|
542 |
|
543 | /**
|
544 | * Helper function to emit a warning notification.
|
545 | *
|
546 | * #### Notes
|
547 | * The message will be truncated if longer than 140 characters.
|
548 | *
|
549 | * @param message Notification message
|
550 | * @param options Options for the warning notification
|
551 | * @returns Notification unique id
|
552 | */
|
553 | export function warning<T extends ReadonlyJSONValue = ReadonlyJSONValue>(
|
554 | message: string,
|
555 | options: IOptions<T> = {}
|
556 | ): string {
|
557 | return manager.notify<T>(message, 'warning', options);
|
558 | }
|
559 | }
|