Skip to content

Commit

Permalink
Initial stab @ integrating internal notifications with events in the …
Browse files Browse the repository at this point in the history
…notification drawer
  • Loading branch information
benjaminapetersen committed Sep 6, 2017
1 parent 5709d8a commit 8316f01
Show file tree
Hide file tree
Showing 6 changed files with 591 additions and 635 deletions.
266 changes: 115 additions & 151 deletions app/scripts/directives/notifications/notificationDrawerWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,26 @@
// this one is treated separately from the rootScopeWatches as
// it may need to be updated outside of the lifecycle of init/destroy
var notificationListener;
// our internal notifications
// var clientGeneratedNotifications = [];

var eventsWatcher;
var eventsByNameData = {};
var eventsMap = {};

// TODO:
// include both Notifications & Events,
// rather than destroying the map each time maintain it & add new items
// data
var eventsMap = {
// projName: { events }
};
var notificationsMap = {
// projName: { notifications }
};

// final Processed set of notification groups for UI
// IF POSSIBLE, avoid having to convert back to an array.
// var notificationGroupsMap = {};
var notificationGroups = [];
var projects = {};

var hideIfNoProject = function(projectName) {
if(!projectName) {
drawer.drawerHidden = true;
}
};

var projects = {};
var projectChanged = function(next, current) {
return _.get(next, 'params.project') !== _.get(current, 'params.project');
};

var getProject = function(projectName) {
return DataService
Expand All @@ -73,98 +75,67 @@
});
};

var ensureProjectGroupExists = function(groups, projectName) {
if(projectName && !groups[projectName]) {
groups[projectName] = {
heading: $filter('displayName')(projects[projectName]) || projectName,
project: projects[projectName],
notifications: []
};
}
};

var deregisterEventsWatch = function() {
if(eventsWatcher) {
DataService.unwatch(eventsWatcher);
}
};

var watchEvents = function(projectName, cb) {
deregisterEventsWatch();
if(projectName) {
eventsWatcher = DataService.watch('events', {namespace: projectName}, _.debounce(cb, 400), { skipDigest: true });
}
};

// NotificationService notifications are minimal, they do no necessarily contain projectName info.
// ATM tacking this on via watching the current project.
// var watchNotifications = function(projectName, cb) {
// deregisterNotificationListener();
// if(!projectName) {
// return;
// }
// notificationListener = $rootScope.$on('NotificationsService.onNotificationAdded', cb);
// };

var deregisterNotificationListener = function() {
notificationListener && notificationListener();
notificationListener = null;
var makeProjectGroup = function(projectName, notifications) {
return {
heading: $filter('displayName')(projects[projectName]),
project: projects[projectName],
notifications: notifications
};
};

var unread = function(notifications) {
return _.filter(notifications, 'unread');
};

// returns a count for each type of notification, example:
// {Normal: 1, Warning: 5}
// TODO: eliminate this $rootScope.$applyAsync,
// there is a quirk here where the values are not picked up the
// first time the function runs, despite the same $applyAsync
// in the render() function
var countUnreadNotificationsForGroup = function(group) {
$rootScope.$applyAsync(function() {

// currently we only show 1 at a time anyway
var countUnreadNotifications = function() {
_.each(drawer.notificationGroups, function(group) {
group.totalUnread = unread(group.notifications).length;
group.hasUnread = !!group.totalUnread;
$rootScope.$emit('NotificationDrawerWrapper.count', group.totalUnread);
});
};

// currently we only show 1 at a time anyway
var countUnreadNotificationsForAllGroups = function() {
_.each(notificationGroups, countUnreadNotificationsForGroup);
// returns a map of filtered events ONLY.
// will worry about unread, actions, etc in render.
var formatAndFilterAPIEvents = function(events) {
return _.reduce(events, function(result, event) {
if(EventsService.isImportantAPIEvent(event) && !EventsService.isCleared(event.id)) {
result[event.metadata.uid] = {
actions: null,
uid: event.metadata.uid,
unread: !EventsService.isRead(event.uid),
type: event.type,
timestamp: event.lastTimestamp,
event: event
};
}
return result;
}, {});
};

var sortNotifications = function(notifications) {
return _.orderBy(notifications, ['event.lastTimestamp', 'event.firstTimestamp'], ['desc', 'desc']);
// we have to keep notifications & events separate as
// notifications are ephemerial, but events have a time to live
// set by the server. we can merge them right before we update
// the UI.
var mergeMaps = function(firstMap, secondMap) {
var proj = $routeParams.project;
return _.assign({}, firstMap[proj], secondMap[proj]);
};

var sortNotificationGroups = function(groupsMap) {
// convert the map into a sorted array
var sortedGroups = _.sortBy(groupsMap, function(group) {
return group.heading;
});
// and sort the notifications under each one
_.each(sortedGroups, function(group) {
group.notifications = sortNotifications(group.notifications);
group.counts = countUnreadNotificationsForGroup(group);
});
return sortedGroups;
//
var sortMap = function(map) {
return _.orderBy(map, ['event.lastTimestamp', 'event.firstTimestamp'], ['desc', 'desc']);
};

var formatAndFilterEvents = function(eventMap) {
var filtered = {};
ensureProjectGroupExists(filtered, $routeParams.project);
_.each(eventMap, function(event) {
if(EventsService.isImportantEvent(event) && !EventsService.isCleared(event)) {
ensureProjectGroupExists(filtered, event.metadata.namespace);
filtered[event.metadata.namespace].notifications.push({
unread: !EventsService.isRead(event),
event: event,
actions: null
});
}
var render = function() {
$rootScope.$evalAsync(function() {
drawer.notificationGroups = [
makeProjectGroup($routeParams.project, sortMap( mergeMaps(eventsMap, notificationsMap )))
];
countUnreadNotifications();
});
return filtered;
};

var deregisterRootScopeWatches = function() {
Expand All @@ -174,51 +145,61 @@
rootScopeWatches = [];
};

var hideIfNoProject = function(projectName) {
if(!projectName) {
drawer.drawerHidden = true;
var deregisterEventsWatch = function() {
if(eventsWatcher) {
DataService.unwatch(eventsWatcher);
eventsWatcher = null;
}
};

var render = function() {
$rootScope.$evalAsync(function () {
countUnreadNotificationsForAllGroups();
// NOTE: we are currently only showing one project in the drawer at a
// time. If we go back to multiple projects, we can eliminate the filter here
// and just pass the whole array as notificationGroups.
// if we do, we will have to handle group.open to keep track of what the
// user is viewing at the time & indicate to the user that the non-active
// project is "asleep"/not being watched.
drawer.notificationGroups = _.filter(notificationGroups, function(group) {
return group.project.metadata.name === $routeParams.project;
});
});
var deregisterNotificationListener = function() {
notificationListener && notificationListener();
notificationListener = null;
};

// TODO: follow-on PR to decide which of these events to toast,
// via config in constants.js
var eventWatchCallback = function(eventData) {
eventsByNameData = eventData.by('metadata.name');
eventsMap = formatAndFilterEvents(eventsByNameData);
// TODO: Update to an intermediate map, so that we can then combine both
// events + notifications into the final notificationGroups output
notificationGroups = sortNotificationGroups(eventsMap);
eventsMap[$routeParams.project] = formatAndFilterAPIEvents(eventData.by('metadata.name'));
render();
};

var notificationWatchCallback = function(event, notification) {
var project = notification.namespace || $routeParams.project;
notificationsMap[project] = notificationsMap[project] || {};

notificationsMap[project][notification.id] = {
actions: null,
unread: !EventsService.isRead(notification.id),
// using uid to match API events and have one filed to pass
// to EventsService for read/cleared, etc
uid: notification.id,
type: notification.type,
timestamp: notification.timestamp,
message: notification.message,
namespace: project,
links: notification.links
};
render();
};

// TODO: Follow-on PR to update & add the internal notifications to the
// var notificationWatchCallback = function(event, notification) {
// // will need to add .event = {} and immitate structure
// if(!notification.lastTimestamp) {
// // creates a timestamp that matches event format: 2017-08-09T19:55:35Z
// notification.lastTimestamp = moment.parseZone(new Date()).utc().format();
// }
// clientGeneratedNotifications.push(notification);
// };

var iconClassByEventSeverity = {
Normal: 'pficon pficon-info',
Warning: 'pficon pficon-warning-triangle-o'
var watchEvents = function(projectName, cb) {
deregisterEventsWatch();
if(projectName) {
eventsWatcher = DataService.watch('events', {namespace: projectName}, _.debounce(cb, 400), { skipDigest: true });
}
};

var watchNotifications = _.once(function(projectName, cb) {
deregisterNotificationListener();
notificationListener = $rootScope.$on('NotificationsService.onNotificationAdded', cb);
});

var reset = function() {
getProject($routeParams.project).then(function() {
watchEvents($routeParams.project, eventWatchCallback);
watchNotifications($routeParams.project, notificationWatchCallback);
hideIfNoProject($routeParams.project);
render();
});
};

angular.extend(drawer, {
Expand All @@ -235,58 +216,42 @@
onMarkAllRead: function(group) {
_.each(group.notifications, function(notification) {
notification.unread = false;
EventsService.markRead(notification.event);
EventsService.markRead(notification.uid);
});
render();
$rootScope.$emit('NotificationDrawerWrapper.onMarkAllRead');
},
onClearAll: function(group) {
_.each(group.notifications, function(notification) {
EventsService.markRead(notification.event);
EventsService.markCleared(notification.event);
notification.unread = false;
EventsService.markRead(notification.uid);
EventsService.markCleared(notification.uid);
});
group.notifications = [];
eventsMap[$routeParams.project] = {};
notificationsMap[$routeParams.project] = {};
render();
$rootScope.$emit('NotificationDrawerWrapper.onMarkAllRead');
},
notificationGroups: notificationGroups,
notificationGroups: [],
headingInclude: 'views/directives/notifications/header.html',
notificationBodyInclude: 'views/directives/notifications/notification-body.html',
customScope: {
clear: function(notification, index, group) {
EventsService.markCleared(notification.event);
EventsService.markCleared(notification.uid);
group.notifications.splice(index, 1);
countUnreadNotificationsForAllGroups();
countUnreadNotifications();
},
markRead: function(notification) {
notification.unread = false;
EventsService.markRead(notification.event);
countUnreadNotificationsForAllGroups();
},
getNotficationStatusIconClass: function(event) {
return iconClassByEventSeverity[event.type] || iconClassByEventSeverity.info;
},
getStatusForCount: function(countKey) {
return iconClassByEventSeverity[countKey] || iconClassByEventSeverity.info;
EventsService.markRead(notification.uid);
countUnreadNotifications();
},
close: function() {
drawer.drawerHidden = true;
}
}
});

var projectChanged = function(next, current) {
return _.get(next, 'params.project') !== _.get(current, 'params.project');
};

var reset = function() {
getProject($routeParams.project).then(function() {
watchEvents($routeParams.project, eventWatchCallback);
//watchNotifications($routeParams.project, notificationWatchCallback);
hideIfNoProject($routeParams.project);
render();
});
};

var initWatches = function() {
if($routeParams.project) {
Expand Down Expand Up @@ -322,7 +287,6 @@
deregisterEventsWatch();
deregisterRootScopeWatches();
};

}

})();
Loading

0 comments on commit 8316f01

Please sign in to comment.