Skip to content

Commit 697843e

Browse files
Initial stab @ integrating internal notifications with events in the notification drawer
1 parent 5709d8a commit 697843e

File tree

4 files changed

+561
-545
lines changed

4 files changed

+561
-545
lines changed

app/scripts/directives/notifications/notificationDrawerWrapper.js

+120-139
Original file line numberDiff line numberDiff line change
@@ -45,25 +45,38 @@
4545
// this one is treated separately from the rootScopeWatches as
4646
// it may need to be updated outside of the lifecycle of init/destroy
4747
var notificationListener;
48-
// our internal notifications
49-
// var clientGeneratedNotifications = [];
50-
5148
var eventsWatcher;
52-
var eventsByNameData = {};
53-
var eventsMap = {};
54-
55-
// TODO:
56-
// include both Notifications & Events,
57-
// rather than destroying the map each time maintain it & add new items
49+
// data
50+
var eventsMap = {
51+
// projName: { events }
52+
};
53+
var notificationsMap = {
54+
// projName: { notifications }
55+
};
5856

59-
// final Processed set of notification groups for UI
60-
// IF POSSIBLE, avoid having to convert back to an array.
61-
// var notificationGroupsMap = {};
62-
var notificationGroups = [];
57+
// internal notifications have different types than API events
58+
var notificationEventTypeMap = {
59+
success: 'Normal',
60+
error: 'Warning'
61+
};
6362

63+
var iconClassByEventSeverity = {
64+
Normal: 'pficon pficon-info',
65+
Warning: 'pficon pficon-warning-triangle-o'
66+
};
6467

6568
var projects = {};
6669

70+
var hideIfNoProject = function(projectName) {
71+
if(!projectName) {
72+
drawer.drawerHidden = true;
73+
}
74+
};
75+
76+
var projectChanged = function(next, current) {
77+
return _.get(next, 'params.project') !== _.get(current, 'params.project');
78+
};
79+
6780
var getProject = function(projectName) {
6881
return DataService
6982
.get('projects', projectName, {}, {errorNotification: false})
@@ -73,98 +86,64 @@
7386
});
7487
};
7588

76-
var ensureProjectGroupExists = function(groups, projectName) {
77-
if(projectName && !groups[projectName]) {
78-
groups[projectName] = {
79-
heading: $filter('displayName')(projects[projectName]) || projectName,
80-
project: projects[projectName],
81-
notifications: []
82-
};
83-
}
84-
};
85-
86-
var deregisterEventsWatch = function() {
87-
if(eventsWatcher) {
88-
DataService.unwatch(eventsWatcher);
89-
}
90-
};
91-
92-
var watchEvents = function(projectName, cb) {
93-
deregisterEventsWatch();
94-
if(projectName) {
95-
eventsWatcher = DataService.watch('events', {namespace: projectName}, _.debounce(cb, 400), { skipDigest: true });
96-
}
97-
};
98-
99-
// NotificationService notifications are minimal, they do no necessarily contain projectName info.
100-
// ATM tacking this on via watching the current project.
101-
// var watchNotifications = function(projectName, cb) {
102-
// deregisterNotificationListener();
103-
// if(!projectName) {
104-
// return;
105-
// }
106-
// notificationListener = $rootScope.$on('NotificationsService.onNotificationAdded', cb);
107-
// };
108-
109-
var deregisterNotificationListener = function() {
110-
notificationListener && notificationListener();
111-
notificationListener = null;
89+
var makeProjectGroup = function(projectName, notifications) {
90+
return {
91+
heading: $filter('displayName')(projects[projectName]) || projectName,
92+
project: projects[projectName],
93+
notifications: notifications
94+
};
11295
};
11396

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

118-
// returns a count for each type of notification, example:
119-
// {Normal: 1, Warning: 5}
120-
// TODO: eliminate this $rootScope.$applyAsync,
121-
// there is a quirk here where the values are not picked up the
122-
// first time the function runs, despite the same $applyAsync
123-
// in the render() function
124-
var countUnreadNotificationsForGroup = function(group) {
125-
$rootScope.$applyAsync(function() {
101+
102+
// currently we only show 1 at a time anyway
103+
var countUnreadNotifications = function() {
104+
_.each(drawer.notificationGroups, function(group) {
126105
group.totalUnread = unread(group.notifications).length;
127106
group.hasUnread = !!group.totalUnread;
128107
$rootScope.$emit('NotificationDrawerWrapper.count', group.totalUnread);
129108
});
130109
};
131110

132-
// currently we only show 1 at a time anyway
133-
var countUnreadNotificationsForAllGroups = function() {
134-
_.each(notificationGroups, countUnreadNotificationsForGroup);
135-
};
136-
137-
var sortNotifications = function(notifications) {
138-
return _.orderBy(notifications, ['event.lastTimestamp', 'event.firstTimestamp'], ['desc', 'desc']);
111+
// returns a map of filtered events ONLY.
112+
// will worry about unread, actions, etc in render.
113+
var formatAndFilterEvents = function(events) {
114+
return _.reduce(
115+
events,
116+
function(result, event) {
117+
if(EventsService.isImportantEvent(event) && !EventsService.isCleared(event)) {
118+
result[event.metadata.uid] = {
119+
actions: null,
120+
uid: event.metadata.uid,
121+
unread: !EventsService.isRead(event),
122+
event: event
123+
};
124+
}
125+
return result;
126+
}, {});
139127
};
140128

141-
var sortNotificationGroups = function(groupsMap) {
142-
// convert the map into a sorted array
143-
var sortedGroups = _.sortBy(groupsMap, function(group) {
144-
return group.heading;
145-
});
146-
// and sort the notifications under each one
147-
_.each(sortedGroups, function(group) {
148-
group.notifications = sortNotifications(group.notifications);
149-
group.counts = countUnreadNotificationsForGroup(group);
150-
});
151-
return sortedGroups;
129+
// Since the events map & internal notifications map are separate
130+
// data and updated asyncronously, they need to be copied/merged
131+
// at some point before being sent to the UI to render.
132+
var mergeAndSortMaps = function(firstMap, secondMap) {
133+
var proj = $routeParams.project;
134+
return _.orderBy(
135+
_.assign({}, firstMap[proj], secondMap[proj]),
136+
['event.lastTimestamp', 'event.firstTimestamp'],
137+
['desc', 'desc']);
152138
};
153139

154-
var formatAndFilterEvents = function(eventMap) {
155-
var filtered = {};
156-
ensureProjectGroupExists(filtered, $routeParams.project);
157-
_.each(eventMap, function(event) {
158-
if(EventsService.isImportantEvent(event) && !EventsService.isCleared(event)) {
159-
ensureProjectGroupExists(filtered, event.metadata.namespace);
160-
filtered[event.metadata.namespace].notifications.push({
161-
unread: !EventsService.isRead(event),
162-
event: event,
163-
actions: null
164-
});
165-
}
140+
var render = function() {
141+
$rootScope.$evalAsync(function() {
142+
drawer.notificationGroups = [
143+
makeProjectGroup($routeParams.project, mergeAndSortMaps(eventsMap, notificationsMap))
144+
];
145+
countUnreadNotifications();
166146
});
167-
return filtered;
168147
};
169148

170149
var deregisterRootScopeWatches = function() {
@@ -174,51 +153,66 @@
174153
rootScopeWatches = [];
175154
};
176155

177-
var hideIfNoProject = function(projectName) {
178-
if(!projectName) {
179-
drawer.drawerHidden = true;
156+
var deregisterEventsWatch = function() {
157+
if(eventsWatcher) {
158+
DataService.unwatch(eventsWatcher);
180159
}
181160
};
182161

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

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

209-
// TODO: Follow-on PR to update & add the internal notifications to the
210-
// var notificationWatchCallback = function(event, notification) {
211-
// // will need to add .event = {} and immitate structure
212-
// if(!notification.lastTimestamp) {
213-
// // creates a timestamp that matches event format: 2017-08-09T19:55:35Z
214-
// notification.lastTimestamp = moment.parseZone(new Date()).utc().format();
215-
// }
216-
// clientGeneratedNotifications.push(notification);
217-
// };
172+
var notificationWatchCallback = function(event, notification) {
173+
var uid = notification.id || _.uniqueId('notification-');
174+
var project = notification.namespace || $routeParams.project;
175+
notificationsMap[project] = notificationsMap[project] || {};
176+
notificationsMap[project][uid] = {
177+
actions: null,
178+
unread: true,
179+
uid: uid,
180+
// emulating an API event to simplify the template
181+
event: {
182+
lastTimestamp: notification.lastTimestamp || moment.parseZone(new Date()).utc().format(),
183+
message: notification.message,
184+
namespace: project,
185+
type: notificationEventTypeMap[notification.type] || 'Normal',
186+
links: notification.links
187+
}
188+
};
189+
render();
190+
};
218191

219-
var iconClassByEventSeverity = {
220-
Normal: 'pficon pficon-info',
221-
Warning: 'pficon pficon-warning-triangle-o'
192+
var watchEvents = function(projectName, cb) {
193+
deregisterEventsWatch();
194+
if(projectName) {
195+
eventsWatcher = DataService.watch('events', {namespace: projectName}, _.debounce(cb, 400), { skipDigest: true });
196+
}
197+
};
198+
199+
// NotificationsService notifications are minimal, they do no necessarily contain projectName info.
200+
// ATM tacking this on via watching the current project.
201+
var watchNotifications = function(projectName, cb) {
202+
deregisterNotificationListener();
203+
if(!projectName) {
204+
return;
205+
}
206+
notificationListener = $rootScope.$on('NotificationsService.onNotificationAdded', cb);
207+
};
208+
209+
var reset = function() {
210+
getProject($routeParams.project).then(function() {
211+
watchEvents($routeParams.project, eventWatchCallback);
212+
watchNotifications($routeParams.project, notificationWatchCallback);
213+
hideIfNoProject($routeParams.project);
214+
render();
215+
});
222216
};
223217

224218
angular.extend(drawer, {
@@ -249,19 +243,19 @@
249243
render();
250244
$rootScope.$emit('NotificationDrawerWrapper.onMarkAllRead');
251245
},
252-
notificationGroups: notificationGroups,
246+
notificationGroups: [],
253247
headingInclude: 'views/directives/notifications/header.html',
254248
notificationBodyInclude: 'views/directives/notifications/notification-body.html',
255249
customScope: {
256250
clear: function(notification, index, group) {
257251
EventsService.markCleared(notification.event);
258252
group.notifications.splice(index, 1);
259-
countUnreadNotificationsForAllGroups();
253+
countUnreadNotifications();
260254
},
261255
markRead: function(notification) {
262256
notification.unread = false;
263257
EventsService.markRead(notification.event);
264-
countUnreadNotificationsForAllGroups();
258+
countUnreadNotifications();
265259
},
266260
getNotficationStatusIconClass: function(event) {
267261
return iconClassByEventSeverity[event.type] || iconClassByEventSeverity.info;
@@ -275,18 +269,6 @@
275269
}
276270
});
277271

278-
var projectChanged = function(next, current) {
279-
return _.get(next, 'params.project') !== _.get(current, 'params.project');
280-
};
281-
282-
var reset = function() {
283-
getProject($routeParams.project).then(function() {
284-
watchEvents($routeParams.project, eventWatchCallback);
285-
//watchNotifications($routeParams.project, notificationWatchCallback);
286-
hideIfNoProject($routeParams.project);
287-
render();
288-
});
289-
};
290272

291273
var initWatches = function() {
292274
if($routeParams.project) {
@@ -322,7 +304,6 @@
322304
deregisterEventsWatch();
323305
deregisterRootScopeWatches();
324306
};
325-
326307
}
327308

328309
})();

app/views/directives/notifications/notification-body.html

+22-4
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,18 @@
5252
ng-class="$ctrl.customScope.getNotficationStatusIconClass(notification.event)"></span>
5353
<span class="sr-only">{{notification.event.type}}</span>
5454
<div class="drawer-pf-notification-content">
55+
5556
<div
57+
ng-if="notification.event"
5658
class="drawer-pf-notification-message"
5759
ng-attr-title="{{notification.event.message}}">
5860
<div>
59-
<span>
60-
{{notification.event.reason | humanize}} &mdash; {{notification.event.involvedObject.kind | humanize}}
61+
<span ng-if="notification.event.reason">
62+
{{notification.event.reason | humanize}} &mdash; <span ng-if="notification.event.involvedObject">{{notification.event.involvedObject.kind | humanize}}</span>
6163
</span>
62-
<span ng-init="eventObjUrl = (notification.event | navigateEventInvolvedObjectURL)">
64+
<span
65+
ng-if="notification.event.involvedObject"
66+
ng-init="eventObjUrl = (notification.event | navigateEventInvolvedObjectURL)">
6367
<a
6468
ng-if="eventObjUrl"
6569
ng-attr-title="Navigate to {{notification.event.involvedObject.name}}"
@@ -69,12 +73,26 @@
6973
</a>
7074
<span ng-if="!(eventObjUrl)">{{notification.event.involvedObject.name}}</span>
7175
</span>
76+
<span ng-if="!(notification.event.involvedObject)">
77+
{{notification.event.message}}
78+
<span ng-if="notification.event.links">
79+
<a
80+
ng-repeat="link in notification.event.links"
81+
href=""
82+
ng-click="link.onClick()">
83+
{{link.label}}
84+
</a>
85+
</span>
86+
</span>
7287
</div>
7388
<div
7489
ng-if="notification.event.count > 1"
7590
class="text-muted small">
7691
{{notification.event.count}} times in the last
77-
<duration-until-now timestamp="notification.event.firstTimestamp" omit-single="true" precision="1"></duration-until-now>
92+
<duration-until-now
93+
timestamp="notification.event.firstTimestamp"
94+
omit-single="true"
95+
precision="1"></duration-until-now>
7896
</div>
7997
</div>
8098

0 commit comments

Comments
 (0)