UNPKG

14.1 kBPlain TextView Raw
1/*
2 * Copyright (c) Jupyter Development Team.
3 * Distributed under the terms of the Modified BSD License.
4 */
5
6import { ReadonlyJSONValue, UUID } from '@lumino/coreutils';
7import { IDisposable } from '@lumino/disposable';
8import { ISignal, Signal } from '@lumino/signaling';
9
10/**
11 * Notification manager
12 */
13export 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 */
198export 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}