mirror of
https://github.com/nasa/openmct.git
synced 2025-05-31 22:50:49 +00:00
427 lines
14 KiB
JavaScript
427 lines
14 KiB
JavaScript
/*****************************************************************************
|
|
* Open MCT, Copyright (c) 2014-2024, United States Government
|
|
* as represented by the Administrator of the National Aeronautics and Space
|
|
* Administration. All rights reserved.
|
|
*
|
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
* License for the specific language governing permissions and limitations
|
|
* under the License.
|
|
*
|
|
* Open MCT includes source code licensed under additional open source
|
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
* this source code distribution or the Licensing information page available
|
|
* at runtime from the About dialog for additional information.
|
|
*****************************************************************************/
|
|
|
|
/**
|
|
* This bundle implements the notification service, which can be used to
|
|
* show banner notifications to the user. Banner notifications
|
|
* are used to inform users of events in a non-intrusive way. As
|
|
* much as possible, notifications share a model with blocking
|
|
* dialogs so that the same information can be provided in a dialog
|
|
* and then minimized to a banner notification if needed.
|
|
*
|
|
* @namespace platform/api/notifications
|
|
*/
|
|
import EventEmitter from 'eventemitter3';
|
|
import moment from 'moment';
|
|
|
|
/**
|
|
* @typedef {object} NotificationProperties
|
|
* @property {function} dismiss Dismiss the notification
|
|
* @property {NotificationModel} model The Notification model
|
|
* @property {(progressPerc: number, progressText: string) => void} [progress] Update the progress of the notification
|
|
*/
|
|
|
|
/**
|
|
* @typedef {EventEmitter & NotificationProperties} Notification
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} NotificationLink
|
|
* @property {function} onClick The function to be called when the link is clicked
|
|
* @property {string} cssClass A CSS class name to style the link
|
|
* @property {string} text The text to be displayed for the link
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} NotificationOptions
|
|
* @property {number} [autoDismissTimeout] Milliseconds to wait before automatically dismissing the notification
|
|
* @property {boolean} [minimized] Allows for a notification to be minimized into the indicator by default
|
|
* @property {NotificationLink} [link] A link for the notification
|
|
*/
|
|
|
|
/**
|
|
* A representation of a banner notification. Banner notifications
|
|
* are used to inform users of events in a non-intrusive way. As
|
|
* much as possible, notifications share a model with blocking
|
|
* dialogs so that the same information can be provided in a dialog
|
|
* and then minimized to a banner notification if needed, or vice-versa.
|
|
*
|
|
* @see DialogModel
|
|
* @typedef {object} NotificationModel
|
|
* @property {string} message The message to be displayed by the notification
|
|
* @property {number | 'unknown'} [progress] The progress of some ongoing task. Should be a number between 0 and 100, or
|
|
* with the string literal 'unknown'.
|
|
* @property {string} [progressText] A message conveying progress of some ongoing task.
|
|
* @property {string} [severity] The severity of the notification. Should be one of 'info', 'alert', or 'error'.
|
|
* @property {string} [timestamp] The time at which the notification was created. Should be a string in ISO 8601 format.
|
|
* @property {boolean} [minimized] Whether or not the notification has been minimized
|
|
* @property {boolean} [autoDismiss] Whether the notification should be automatically dismissed after a short period of time.
|
|
* @property {NotificationOptions} options The notification options
|
|
*/
|
|
|
|
const DEFAULT_AUTO_DISMISS_TIMEOUT = 3000;
|
|
const MINIMIZE_ANIMATION_TIMEOUT = 300;
|
|
|
|
/**
|
|
* The notification service is responsible for informing the user of
|
|
* events via the use of banner notifications.
|
|
*/
|
|
export default class NotificationAPI extends EventEmitter {
|
|
constructor() {
|
|
super();
|
|
/** @type {Notification[]} */
|
|
this.notifications = [];
|
|
/** @type {{severity: "info" | "alert" | "error"}} */
|
|
this.highest = { severity: 'info' };
|
|
|
|
/**
|
|
* A context in which to hold the active notification and a
|
|
* handle to its timeout.
|
|
* @type {Notification | undefined}
|
|
*/
|
|
this.activeNotification = undefined;
|
|
}
|
|
|
|
/**
|
|
* Info notifications are low priority informational messages for the user. They will be auto-destroy after a brief
|
|
* period of time.
|
|
* @param {string} message The message to display to the user
|
|
* @param {NotificationOptions} [options] The notification options
|
|
* @returns {Notification}
|
|
*/
|
|
info(message, options = {}) {
|
|
/** @type {NotificationModel} */
|
|
const notificationModel = {
|
|
message: message,
|
|
autoDismiss: true,
|
|
severity: 'info',
|
|
options
|
|
};
|
|
|
|
return this._notify(notificationModel);
|
|
}
|
|
|
|
/**
|
|
* Present an alert to the user.
|
|
* @param {string} message The message to display to the user.
|
|
* @param {NotificationOptions} [options] object with following properties
|
|
* autoDismissTimeout: {number} in milliseconds to automatically dismisses notification
|
|
* link: {Object} Add a link to notifications for navigation
|
|
* onClick: callback function
|
|
* cssClass: css class name to add style on link
|
|
* text: text to display for link
|
|
* @returns {Notification}
|
|
*/
|
|
alert(message, options = {}) {
|
|
const notificationModel = {
|
|
message: message,
|
|
severity: 'alert',
|
|
options
|
|
};
|
|
|
|
return this._notify(notificationModel);
|
|
}
|
|
|
|
/**
|
|
* Present an error message to the user
|
|
* @param {string} message
|
|
* @param {Object} [options] object with following properties
|
|
* autoDismissTimeout: {number} in milliseconds to automatically dismisses notification
|
|
* link: {Object} Add a link to notifications for navigation
|
|
* onClick: callback function
|
|
* cssClass: css class name to add style on link
|
|
* text: text to display for link
|
|
* @returns {Notification}
|
|
*/
|
|
error(message, options = {}) {
|
|
let notificationModel = {
|
|
message: message,
|
|
severity: 'error',
|
|
options
|
|
};
|
|
|
|
return this._notify(notificationModel);
|
|
}
|
|
|
|
/**
|
|
* Create a new progress notification. These notifications will contain a progress bar.
|
|
* @param {string} message
|
|
* @param {number | null} progressPerc A value between 0 and 100, or null.
|
|
* @param {string} [progressText] Text description of progress (eg. "10 of 20 objects copied").
|
|
*/
|
|
progress(message, progressPerc, progressText) {
|
|
let notificationModel = {
|
|
message: message,
|
|
progressPerc: progressPerc,
|
|
progressText: progressText,
|
|
severity: 'info',
|
|
options: {}
|
|
};
|
|
|
|
return this._notify(notificationModel);
|
|
}
|
|
|
|
dismissAllNotifications() {
|
|
this.notifications = [];
|
|
this.emit('dismiss-all');
|
|
}
|
|
|
|
/**
|
|
* Minimize a notification. The notification will still be available
|
|
* from the notification list. Typically notifications with a
|
|
* severity of 'info' should not be minimized, but rather
|
|
* dismissed.
|
|
*
|
|
* @private
|
|
* @param {Notification | undefined} notification
|
|
*/
|
|
_minimize(notification) {
|
|
if (!notification) {
|
|
return;
|
|
}
|
|
|
|
//Check this is a known notification
|
|
let index = this.notifications.indexOf(notification);
|
|
|
|
if (this.activeTimeout) {
|
|
/*
|
|
Method can be called manually (clicking dismiss) or
|
|
automatically from an auto-timeout. this.activeTimeout
|
|
acts as a semaphore to prevent race conditions. Cancel any
|
|
timeout in progress (for the case where a manual dismiss
|
|
has shortcut an active auto-dismiss), and clear the
|
|
semaphore.
|
|
*/
|
|
clearTimeout(this.activeTimeout);
|
|
delete this.activeTimeout;
|
|
}
|
|
|
|
if (index >= 0) {
|
|
notification.model.minimized = true;
|
|
notification.emit('minimized');
|
|
//Add a brief timeout before showing the next notification
|
|
// in order to allow the minimize animation to run through.
|
|
setTimeout(() => {
|
|
notification.emit('destroy');
|
|
this._setActiveNotification(this._selectNextNotification());
|
|
}, MINIMIZE_ANIMATION_TIMEOUT);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Completely removes a notification. This will dismiss it from the
|
|
* message banner and remove it from the list of notifications.
|
|
* Typically only notifications with a severity of info should be
|
|
* dismissed. If you're not sure whether to dismiss or minimize a
|
|
* notification, use {@link Notification#dismissOrMinimize}.
|
|
* dismiss
|
|
*
|
|
* @private
|
|
* @param {Notification | undefined} notification
|
|
*/
|
|
_dismiss(notification) {
|
|
if (!notification) {
|
|
return;
|
|
}
|
|
|
|
//Check this is a known notification
|
|
let index = this.notifications.indexOf(notification);
|
|
|
|
if (this.activeTimeout) {
|
|
/* Method can be called manually (clicking dismiss) or
|
|
* automatically from an auto-timeout. this.activeTimeout
|
|
* acts as a semaphore to prevent race conditions. Cancel any
|
|
* timeout in progress (for the case where a manual dismiss
|
|
* has shortcut an active auto-dismiss), and clear the
|
|
* semaphore.
|
|
*/
|
|
|
|
clearTimeout(this.activeTimeout);
|
|
delete this.activeTimeout;
|
|
}
|
|
|
|
if (index >= 0) {
|
|
this.notifications.splice(index, 1);
|
|
}
|
|
|
|
this._setActiveNotification(this._selectNextNotification());
|
|
this._setHighestSeverity();
|
|
notification.emit('destroy');
|
|
}
|
|
|
|
/**
|
|
* Depending on the severity of the notification will selectively
|
|
* dismiss or minimize where appropriate.
|
|
*
|
|
* @private
|
|
* @param {Notification | undefined} notification
|
|
*/
|
|
_dismissOrMinimize(notification) {
|
|
let model = notification?.model;
|
|
if (model?.severity === 'info') {
|
|
this._dismiss(notification);
|
|
} else {
|
|
this._minimize(notification);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_setHighestSeverity() {
|
|
let severity = {
|
|
info: 1,
|
|
alert: 2,
|
|
error: 3
|
|
};
|
|
|
|
this.highest.severity = this.notifications.reduce((previous, notification) => {
|
|
if (severity[notification.model.severity] > severity[previous]) {
|
|
return notification.model.severity;
|
|
} else {
|
|
return previous;
|
|
}
|
|
}, 'info');
|
|
}
|
|
|
|
/**
|
|
* Notifies the user of an event. If there is a banner notification
|
|
* already active, then it will be dismissed or minimized automatically,
|
|
* and the provided notification displayed in its place.
|
|
*
|
|
* @param {NotificationModel} notificationModel The notification to
|
|
* display
|
|
* @returns {Notification} the provided notification decorated with
|
|
* functions to {@link Notification#dismiss} or {@link Notification#minimize}
|
|
*/
|
|
_notify(notificationModel) {
|
|
let notification;
|
|
let activeNotification = this.activeNotification;
|
|
|
|
notificationModel.severity = notificationModel.severity || 'info';
|
|
notificationModel.timestamp = moment.utc().format('YYYY-MM-DD hh:mm:ss.ms');
|
|
|
|
notification = this._createNotification(notificationModel);
|
|
|
|
this.notifications.push(notification);
|
|
this._setHighestSeverity();
|
|
|
|
/*
|
|
Check if there is already an active (ie. visible) notification
|
|
*/
|
|
if (!this.activeNotification && !notification?.model?.options?.minimized) {
|
|
this._setActiveNotification(notification);
|
|
} else if (!this.activeTimeout) {
|
|
/*
|
|
If there is already an active notification, time it out. If it's
|
|
already got a timeout in progress (either because it has had
|
|
timeout forced because of a queue of messages, or it had an
|
|
autodismiss specified), leave it to run. Otherwise force a
|
|
timeout.
|
|
|
|
This notification has been added to queue and will be
|
|
serviced as soon as possible.
|
|
*/
|
|
this.activeTimeout = setTimeout(() => {
|
|
this._dismissOrMinimize(activeNotification);
|
|
}, DEFAULT_AUTO_DISMISS_TIMEOUT);
|
|
}
|
|
|
|
return notification;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {NotificationModel} notificationModel
|
|
* @returns {Notification}
|
|
*/
|
|
_createNotification(notificationModel) {
|
|
/** @type {Notification} */
|
|
let notification = new EventEmitter();
|
|
notification.model = notificationModel;
|
|
notification.dismiss = () => {
|
|
this._dismiss(notification);
|
|
};
|
|
|
|
if (Object.prototype.hasOwnProperty.call(notificationModel, 'progressPerc')) {
|
|
notification.progress = (progressPerc, progressText) => {
|
|
notification.model.progressPerc = progressPerc;
|
|
notification.model.progressText = progressText;
|
|
notification.emit('progress', progressPerc, progressText);
|
|
};
|
|
}
|
|
|
|
return notification;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {Notification | undefined} notification
|
|
*/
|
|
_setActiveNotification(notification) {
|
|
this.activeNotification = notification;
|
|
|
|
if (!notification) {
|
|
delete this.activeTimeout;
|
|
|
|
return;
|
|
}
|
|
|
|
this.emit('notification', notification);
|
|
|
|
if (notification.model.autoDismiss || this._selectNextNotification()) {
|
|
const autoDismissTimeout =
|
|
notification.model.options.autoDismissTimeout || DEFAULT_AUTO_DISMISS_TIMEOUT;
|
|
this.activeTimeout = setTimeout(() => {
|
|
this._dismissOrMinimize(notification);
|
|
}, autoDismissTimeout);
|
|
} else {
|
|
delete this.activeTimeout;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Used internally by the NotificationService
|
|
*
|
|
* @private
|
|
*/
|
|
_selectNextNotification() {
|
|
let notification;
|
|
let i = 0;
|
|
|
|
/*
|
|
Loop through the notifications queue and find the first one that
|
|
has not already been minimized (manually or otherwise).
|
|
*/
|
|
for (; i < this.notifications.length; i++) {
|
|
notification = this.notifications[i];
|
|
|
|
const isNotificationMinimized =
|
|
notification.model.minimized || notification?.model?.options?.minimized;
|
|
|
|
if (!isNotificationMinimized && notification !== this.activeNotification) {
|
|
return notification;
|
|
}
|
|
}
|
|
}
|
|
}
|