Notifications progress method (#2193)

* Added progress method to notifications so no longer dependent on reactive properties

* Updated notification launch controller to use new progress method

* Added progress function to Notifications API. Introduced NotificationService compatibility layer for legacy code
This commit is contained in:
Andrew Henry 2018-10-15 10:00:05 -07:00 committed by Deep Tailor
parent c3b7e7869e
commit 6f1b5b4ae3
10 changed files with 246 additions and 211 deletions

View File

@ -79,30 +79,34 @@ define(
* periodically, tracking an ongoing process.
*/
$scope.newProgress = function () {
let progress = 0;
var notificationModel = {
title: "Progress notification example",
severity: "info",
progress: 0,
progress: progress,
actionText: getExampleActionText()
};
let notification;
/**
* Simulate an ongoing process and update the progress bar.
* @param notification
*/
function incrementProgress() {
notificationModel.progress = Math.min(100, Math.floor(notificationModel.progress + Math.random() * 30));
notificationModel.progressText = ["Estimated time" +
progress = Math.min(100, Math.floor(progress + Math.random() * 30))
let progressText = ["Estimated time" +
" remaining:" +
" about ", 60 - Math.floor((notificationModel.progress / 100) * 60), " seconds"].join(" ");
if (notificationModel.progress < 100) {
" about ", 60 - Math.floor((progress / 100) * 60), " seconds"].join(" ");
notification.progress(progress, progressText);
if (progress < 100) {
$timeout(function () {
incrementProgress(notificationModel);
}, 1000);
}
}
notificationService.notify(notificationModel);
notification = notificationService.notify(notificationModel);
incrementProgress();
};

View File

@ -2,31 +2,14 @@
ng-class="'message-severity-' + ngModel.severity">
<div class="w-message-contents">
<div class="top-bar">
<div class="title">{{ngModel.title}}</div>
</div>
<div class="hint" ng-hide="ngModel.hint === undefined">
{{ngModel.hint}}
<span ng-if="ngModel.timestamp !== undefined">[{{ngModel.timestamp}}]</span>
<div class="title">{{ngModel.message}}</div>
</div>
<div class="message-body">
<div class="message-action">
{{ngModel.actionText}}
</div>
<mct-include key="'progress-bar'"
ng-model="ngModel"
ng-show="ngModel.progress !== undefined || ngModel.unknownProgress"></mct-include>
ng-show="ngModel.progressPerc !== undefined"></mct-include>
</div>
<div class="bottom-bar">
<a ng-repeat="dialogOption in ngModel.options"
class="s-button"
ng-click="dialogOption.callback()">
{{dialogOption.label}}
</a>
<a class="s-button major"
ng-if="ngModel.primaryOption"
ng-click="ngModel.primaryOption.callback()">
{{ngModel.primaryOption.label}}
</a>
</div>
</div>
</div>

View File

@ -1,10 +1,10 @@
<span class="l-progress-bar s-progress-bar"
ng-class="{ indeterminate:ngModel.unknownProgress }">
ng-class="{ indeterminate:ngModel.progressPerc === 'unknown' }">
<span class="progress-amt-holder">
<span class="progress-amt" style="width: {{ngModel.progress}}%"></span>
<span class="progress-amt" style="width: {{ngModel.progressPerc === 'unknown' ? 100 : ngModel.progressPerc}}%"></span>
</span>
</span>
<div class="progress-info hint" ng-hide="ngModel.progressText === undefined">
<span class="progress-amt-text" ng-show="ngModel.progress > 0">{{ngModel.progress}}% complete. </span>
<span class="progress-amt-text" ng-show="ngModel.progressPerc !== 'unknown' && ngModel.progressPerc > 0">{{ngModel.progressPerc}}% complete. </span>
{{ngModel.progressText}}
</div>

View File

@ -50,7 +50,7 @@ define(
};
$scope.dismiss = function (notification, $event) {
$event.stopPropagation();
notification.dismissOrMinimize();
notification.dismiss();
};
$scope.maximize = function (notification) {
if (notification.model.severity !== "info") {

View File

@ -23,11 +23,13 @@
define([
"./src/NotificationIndicatorController",
"./src/NotificationIndicator",
"./src/NotificationService",
"./res/notification-indicator.html",
'legacyRegistry'
], function (
NotificationIndicatorController,
NotificationIndicator,
NotificationService,
notificationIndicatorTemplate,
legacyRegistry
) {
@ -46,7 +48,7 @@ define([
"implementation": NotificationIndicatorController,
"depends": [
"$scope",
"notificationService",
"openmct",
"dialogService"
]
}
@ -61,7 +63,7 @@ define([
{
"key": "notificationService",
"implementation": function (openmct) {
return openmct.notifications;
return new NotificationService.default(openmct);
},
"depends": [
"openmct"

View File

@ -35,9 +35,9 @@ define(
* @param dialogService
* @constructor
*/
function NotificationIndicatorController($scope, notificationService, dialogService) {
$scope.notifications = notificationService.notifications;
$scope.highest = notificationService.highest;
function NotificationIndicatorController($scope, openmct, dialogService) {
$scope.notifications = openmct.notifications.notifications;
$scope.highest = openmct.notifications.highest;
/**
* Launch a dialog showing a list of current notifications.
@ -48,7 +48,7 @@ define(
title: "Messages",
//Launch the message list dialog with the models
// from the notifications
messages: notificationService.notifications
messages: openmct.notifications.notifications
}
});

View File

@ -0,0 +1,64 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, 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.
*****************************************************************************/
export default class NotificationService {
constructor(openmct) {
this.openmct = openmct;
}
info(message) {
if (typeof message === 'string') {
return this.openmct.notifications.info(message);
} else {
if (message.hasOwnProperty('progress')) {
return this.openmct.notifications.progress(message.title, message.progress, message.progressText);
} else {
return this.openmct.notifications.info(message.title);
}
}
}
alert(message) {
if (typeof message === 'string') {
return this.openmct.notifications.alert(message);
} else {
return this.openmct.notifications.alert(message.title);
}
}
error(message) {
if (typeof message === 'string') {
return this.openmct.notifications.error(message);
} else {
return this.openmct.notifications.error(message.title);
}
}
notify(options) {
switch (options.severity) {
case 'info':
return this.info(options);
case 'alert':
return this.alert(options);
case 'error':
return this.error(options);
}
}
getAllNotifications() {
return this.openmct.notifications.notifications;
}
}

View File

@ -1,48 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, 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.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
export default class MCTNotification extends EventEmitter {
constructor(notificationModel, notificationAPI) {
super();
this.notifications = notificationAPI;
this.model = notificationModel;
this.initializeModel();
}
minimize() {
this.notifications.minimize(this);
}
dismiss() {
this.notifications.dismiss(this)
}
dismissOrMinimize() {
this.notifications.dismissOrMinimize(this);
}
initializeModel() {
this.model.minimized = this.model.minimized || false;
}
}

View File

@ -32,7 +32,6 @@
*/
import moment from 'moment';
import EventEmitter from 'EventEmitter';
import MCTNotification from './MCTNotification.js';
/**
* A representation of a banner notification. Banner notifications
@ -42,20 +41,11 @@ import MCTNotification from './MCTNotification.js';
* and then minimized to a banner notification if needed, or vice-versa.
*
* @typedef {object} NotificationModel
* @property {string} title The title of the message
* @property {string} severity The importance of the message (one of
* 'info', 'alert', or 'error' where info < alert <error)
* @property {number} [progress] The completion status of a task
* represented numerically
* @property {boolean} [unknownProgress] a boolean indicating that the
* progress of the underlying task is unknown. This will result in a
* visually distinct progress bar.
* @property {boolean} [autoDismiss] If truthy, dialog will
* be automatically minimized or dismissed (depending on severity).
* Additionally, if the provided value is a number, it will be used
* as the delay period before being dismissed.
* @property {boolean} [dismissable=true] If true, notification will
* include an option to dismiss it completely.
* @property {string} message The message to be displayed by the notification
* @property {number | 'unknown'} [progress] The progres 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.
* @see DialogModel
*/
@ -65,14 +55,9 @@ const MINIMIZE_ANIMATION_TIMEOUT = 300;
/**
* The notification service is responsible for informing the user of
* events via the use of banner notifications.
* @memberof platform/commonUI/notification
* @constructor
* @param defaultAutoDismissTimeout The period of time that an
* auto-dismissed message will be displayed for.
* @param minimizeAnimationTimeout When notifications are minimized, a brief
* animation is shown. This animation requires some time to execute,
* so a timeout is required before the notification is hidden
*/
* @memberof ui/notification
* @constructor */
export default class NotificationAPI extends EventEmitter {
constructor() {
super();
@ -86,16 +71,72 @@ export default class NotificationAPI extends EventEmitter {
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
* @returns {InfoNotification}
*/
info(message) {
let notificationModel = {
message: message,
autoDismiss: true,
severity: "info"
}
return this._notify(notificationModel);
}
/**
* Present an alert to the user.
* @param {string} message The message to display to the user.
* @returns {Notification}
*/
alert(message) {
let notificationModel = {
message: message,
severity: "alert"
}
return this._notify(notificationModel);
}
/**
* Present an error message to the user
* @param {string} message
* @returns {Notification}
*/
error(message) {
let notificationModel = {
message: message,
severity: "error"
}
return this._notify(notificationModel);
}
/**
* Create a new progress notification. These notifications will contain a progress bar.
* @param {string} message
* @param {number | 'unknown'} progressPerc A value between 0 and 100, or the string 'unknown'.
* @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"
}
return this._notify(notificationModel);
}
/**
* 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. If you're not sure which is appropriate,
* use {@link Notification#dismissOrMinimize}
* dismissed.
*
* @private
*/
minimize(notification) {
_minimize(notification) {
//Check this is a known notification
let index = this.notifications.indexOf(notification);
@ -114,11 +155,12 @@ export default class NotificationAPI extends EventEmitter {
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());
this._setActiveNotification(this._selectNextNotification());
}, MINIMIZE_ANIMATION_TIMEOUT);
}
}
@ -133,7 +175,7 @@ export default class NotificationAPI extends EventEmitter {
*
* @private
*/
dismiss(notification) {
_dismiss(notification) {
//Check this is a known notification
let index = this.notifications.indexOf(notification);
@ -153,8 +195,8 @@ export default class NotificationAPI extends EventEmitter {
if (index >= 0) {
this.notifications.splice(index, 1);
}
this.setActiveNotification(this.selectNextNotification());
this.setHighestSeverity();
this._setActiveNotification(this._selectNextNotification());
this._setHighestSeverity();
notification.emit('destroy');
}
@ -164,81 +206,19 @@ export default class NotificationAPI extends EventEmitter {
*
* @private
*/
dismissOrMinimize(notification) {
_dismissOrMinimize(notification) {
let model = notification.model;
if (model.severity === "info") {
if (model.autoDismiss === false) {
this.minimize(notification);
} else {
this.dismiss(notification);
}
this._dismiss(notification);
} else {
this.minimize(notification);
this._minimize(notification);
}
}
/**
* Returns the notification that is currently visible in the banner area
* @returns {Notification}
*/
getActiveNotification() {
return this.activeNotification;
}
/**
* A convenience method for info notifications. Notifications
* created via this method will be auto-destroy after a default
* wait period unless explicitly forbidden by the caller through
* the {autoDismiss} property on the {NotificationModel}, in which
* case the notification will be minimized after the wait.
* @param {NotificationModel | string} message either a string for
* the title of the notification message, or a {@link NotificationModel}
* defining the options notification to display
* @returns {Notification} the provided notification decorated with
* functions to dismiss or minimize
*/
info(message) {
let notificationModel = typeof message === "string" ? {title: message} : message;
notificationModel.severity = "info";
return this.notify(notificationModel);
}
/**
* A convenience method for alert notifications. Notifications
* created via this method will will have severity of "alert" enforced
* @param {NotificationModel | string} message either a string for
* the title of the alert message with default options, or a
* {@link NotificationModel} defining the options notification to
* display
* @returns {Notification} the provided notification decorated with
* functions to dismiss or minimize
*/
alert(message) {
let notificationModel = typeof message === "string" ? {title: message} : message;
notificationModel.severity = "alert";
return this.notify(notificationModel);
}
/**
* A convenience method for error notifications. Notifications
* created via this method will will have severity of "error" enforced
* @param {NotificationModel | string} message either a string for
* the title of the error message with default options, or a
* {@link NotificationModel} defining the options of the notification to
* display
* @returns {Notification} the provided notification decorated with
* functions to dismiss or minimize
*/
error(message) {
let notificationModel = typeof message === "string" ? {title: message} : message;
notificationModel.severity = "error";
return this.notify(notificationModel);
}
/**
* @private
*/
setHighestSeverity() {
_setHighestSeverity() {
let severity = {
"info": 1,
"alert": 2,
@ -263,23 +243,23 @@ export default class NotificationAPI extends EventEmitter {
* @returns {Notification} the provided notification decorated with
* functions to {@link Notification#dismiss} or {@link Notification#minimize}
*/
notify(notificationModel) {
_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 = new MCTNotification(notificationModel, this);
notification = this._createNotification(notificationModel);
this.notifications.push(notification);
this.setHighestSeverity();
this._setHighestSeverity();
/*
Check if there is already an active (ie. visible) notification
*/
if (!this.activeNotification) {
this.setActiveNotification(notification);
this._setActiveNotification(notification);
} else if (!this.activeTimeout) {
/*
If there is already an active notification, time it out. If it's
@ -292,19 +272,38 @@ export default class NotificationAPI extends EventEmitter {
serviced as soon as possible.
*/
this.activeTimeout = setTimeout(() => {
this.dismissOrMinimize(activeNotification);
this._dismissOrMinimize(activeNotification);
}, DEFAULT_AUTO_DISMISS_TIMEOUT);
}
return notification;
}
/**
* Used internally by the NotificationService
* @private
*/
setActiveNotification(notification) {
let shouldAutoDismiss;
_createNotification(notificationModel) {
let notification = new EventEmitter();
notification.model = notificationModel;
notification.dismiss = () => {
this._dismiss(notification);
};
if (notificationModel.hasOwnProperty('progressPerc')) {
notification.progress = (progressPerc, progressText) => {
notification.model.progressPerc = progressPerc;
notification.model.progressText = progressText;
notification.emit('progress', progressPerc, progressText);
}
}
return notification;
}
/**
* @private
*/
_setActiveNotification(notification) {
this.activeNotification = notification;
if (!notification) {
@ -313,15 +312,9 @@ export default class NotificationAPI extends EventEmitter {
}
this.emit('notification', notification);
if (notification.model.severity === "info") {
shouldAutoDismiss = true;
} else {
shouldAutoDismiss = notification.model.autoDismiss;
}
if (shouldAutoDismiss || this.selectNextNotification()) {
if (notification.model.autoDismiss || this._selectNextNotification()) {
this.activeTimeout = setTimeout(() => {
this.dismissOrMinimize(notification);
this._dismissOrMinimize(notification);
}, DEFAULT_AUTO_DISMISS_TIMEOUT);
} else {
delete this.activeTimeout;
@ -333,7 +326,7 @@ export default class NotificationAPI extends EventEmitter {
*
* @private
*/
selectNextNotification() {
_selectNextNotification() {
let notification;
let i = 0;

View File

@ -24,18 +24,18 @@
'minimized': activeModel.minimized,
'new': !activeModel.minimized
}]"
v-if="activeModel">
<span @click="maximize()" class="banner-elem label">{{activeModel.title}}</span>
<span @click="maximize()" v-if="activeModel.progress !== undefined || activeModel.unknownProgress">
v-if="activeModel.message">
<span @click="maximize()" class="banner-elem label">{{activeModel.message}}</span>
<span @click="maximize()" v-if="activeModel.progressPerc !== undefined">
<div class="banner-elem"><!-- was mct-include -->
<span class="l-progress-bar s-progress-bar"
:class="{'indeterminate': activeModel.unknownProgress }">
:class="{'indeterminate': activeModel.progressPerc === 'unknown' }">
<span class="progress-amt-holder">
<span class="progress-amt" :style="progressWidth"></span>
</span>
</span>
<div class="progress-info hint" v-if="activeModel.progressText !== undefined">
<span class="progress-amt-text" v-if="activeModel.progress > 0">{{activeModel.progress}}% complete. </span>
<span class="progress-amt-text" v-if="activeModel.progressPerc > 0">{{activeModel.progressPerc}}% complete. </span>
{{activeModel.progressText}}
</div>
</div>
@ -62,22 +62,59 @@
inject: ['openmct'],
data() {
return {
activeModel: undefined
activeModel: {
message: undefined,
progressPerc: undefined,
progressText: undefined,
minimized: undefined
}
}
},
methods: {
showNotification(notification) {
if (activeNotification) {
activeNotification.off('progress', this.updateProgress);
activeNotification.off('minimized', this.minimized);
activeNotification.off('destroy', this.destroyActiveNotification);
}
activeNotification = notification;
this.activeModel = notification.model;
activeNotification.once('destroy', () => {
if (this.activeModel === notification.model){
this.activeModel = undefined;
activeNotification = undefined;
}
});
this.clearModel();
this.applyModel(notification.model);
activeNotification.once('destroy', this.destroyActiveNotification);
activeNotification.on('progress', this.updateProgress);
activeNotification.on('minimized', this.minimized);
},
isEqual(modelA, modelB) {
return modelA.message === modelB.message &&
modelA.timestamp === modelB.timestamp;
},
applyModel(model) {
Object.keys(model).forEach((key) => this.activeModel[key] = model[key]);
},
clearModel() {
Object.keys(this.activeModel).forEach((key) => this.activeModel[key] = undefined);
},
updateProgress(progressPerc, progressText) {
this.activeModel.progressPerc = progressPerc;
this.activeModel.progressText = progressText;
},
destroyActiveNotification() {
this.clearModel();
activeNotification = undefined;
},
dismiss() {
activeNotification.dismissOrMinimize();
if (activeNotification.model.severity === 'info') {
activeNotification.dismiss();
} else {
this.openmct.notifications._minimize(activeNotification);
}
},
minimized() {
this.activeModel.minimized = true;
activeNotification.off('progress', this.updateProgress);
activeNotification.off('minimized', this.minimized);
activeNotification.off('destroy', this.destroyActiveNotification);
},
maximize() {
//Not implemented yet.