Skip to content

Commit 4c0fa55

Browse files
committed
Quota Notifications
1 parent 65d843f commit 4c0fa55

File tree

8 files changed

+300
-150
lines changed

8 files changed

+300
-150
lines changed

app/scripts/constants.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,5 +534,10 @@ angular.extend(window.OPENSHIFT_CONSTANTS, {
534534
// href: 'http://example.com/',
535535
// tooltip: 'Open Dashboard'
536536
// }
537-
]
537+
],
538+
QUOTA_NOTIFICATION_MESSAGE: {
539+
// Example quota messages to show in notification drawer
540+
// "pods": "Upgrade to <a href='http://www.openshift.com'>OpenShift Pro</a> if you need additional resources.",
541+
// "limits.memory": "Upgrade to <a href='http://www.openshift.com'>OpenShift Online Pro</a> if you need additional resources."
542+
}
538543
});

app/scripts/controllers/overview.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,11 +1040,10 @@ function OverviewController($scope,
10401040
groupRecentBuildsByDeploymentConfig();
10411041
};
10421042

1043-
var updateQuotaWarnings = function() {
1044-
ResourceAlertsService.setGenericQuotaWarning(state.quotas,
1045-
state.clusterQuotas,
1046-
$routeParams.project,
1047-
state.alerts);
1043+
var setQuotaNotifications = function() {
1044+
ResourceAlertsService.setQuotaNotifications(state.quotas,
1045+
state.clusterQuotas,
1046+
$routeParams.project);
10481047
};
10491048

10501049
overview.clearFilter = function() {
@@ -1304,12 +1303,12 @@ function OverviewController($scope,
13041303
// Always poll quotas instead of watching, its not worth the overhead of maintaining websocket connections
13051304
watches.push(DataService.watch('resourcequotas', context, function(quotaData) {
13061305
state.quotas = quotaData.by("metadata.name");
1307-
updateQuotaWarnings();
1306+
setQuotaNotifications();
13081307
}, {poll: true, pollInterval: DEFAULT_POLL_INTERVAL}));
13091308

13101309
watches.push(DataService.watch('appliedclusterresourcequotas', context, function(clusterQuotaData) {
13111310
state.clusterQuotas = clusterQuotaData.by("metadata.name");
1312-
updateQuotaWarnings();
1311+
setQuotaNotifications();
13131312
}, {poll: true, pollInterval: DEFAULT_POLL_INTERVAL}));
13141313

13151314
var canI = $filter('canI');

app/scripts/directives/notifications/notificationDrawerWrapper.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
'Constants',
1818
'DataService',
1919
'EventsService',
20+
'NotificationsService',
2021
NotificationDrawerWrapper
2122
]
2223
});
@@ -94,6 +95,14 @@
9495
});
9596
};
9697

98+
var removeNotificationFromGroup = function(notification) {
99+
_.each(drawer.notificationGroups, function(group) {
100+
_.remove(group.notifications, function(grpNotification) {
101+
return grpNotification.uid === notification.uid && grpNotification.namespace === notification.namespace;
102+
});
103+
});
104+
};
105+
97106
var formatAPIEvents = function(apiEvents) {
98107
return _.map(apiEvents, function(event) {
99108
return {
@@ -171,7 +180,7 @@
171180
var id = notification.id || _.uniqueId('notification_') + Date.now();
172181
notificationsMap[project] = notificationsMap[project] || {};
173182
notificationsMap[project][id] = {
174-
actions: null,
183+
actions: notification.actions,
175184
unread: !EventsService.isRead(id),
176185
// using uid to match API events and have one filed to pass
177186
// to EventsService for read/cleared, etc
@@ -181,6 +190,7 @@
181190
// but we sort based on lastTimestamp first.
182191
lastTimestamp: notification.timestamp,
183192
message: notification.message,
193+
isHTML: notification.isHTML,
184194
details: notification.details,
185195
namespace: project,
186196
links: notification.links
@@ -217,6 +227,7 @@
217227
hasUnread: false,
218228
showClearAll: true,
219229
showMarkAllRead: true,
230+
removeNotificationFromGroup: removeNotificationFromGroup,
220231
onClose: function() {
221232
drawer.drawerHidden = true;
222233
},
@@ -259,7 +270,8 @@
259270
onLinkClick: function(link) {
260271
link.onClick();
261272
drawer.drawerHidden = true;
262-
}
273+
},
274+
countUnreadNotifications: countUnreadNotifications
263275
}
264276
});
265277

@@ -284,6 +296,18 @@
284296
rootScopeWatches.push($rootScope.$on('NotificationDrawerWrapper.toggle', function() {
285297
drawer.drawerHidden = !drawer.drawerHidden;
286298
}));
299+
300+
// event to signal the drawer to close
301+
rootScopeWatches.push($rootScope.$on('NotificationDrawerWrapper.hide', function() {
302+
drawer.drawerHidden = true;
303+
}));
304+
305+
// event to signal the drawer to clear a notification
306+
rootScopeWatches.push($rootScope.$on('NotificationDrawerWrapper.clear', function(event, notification) {
307+
EventsService.markCleared(notification.uid);
308+
drawer.removeNotificationFromGroup(notification);
309+
drawer.countUnreadNotifications();
310+
}));
287311
};
288312

289313
drawer.$onInit = function() {

app/scripts/services/quota.js

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,21 @@
33
angular.module("openshiftConsole")
44
.factory("QuotaService", function(APIService,
55
$filter,
6+
$location,
7+
$rootScope,
8+
$routeParams,
69
$q,
10+
Constants,
711
DataService,
8-
Logger) {
12+
EventsService,
13+
Logger,
14+
NotificationsService) {
915

1016
var isNil = $filter('isNil');
1117
var usageValue = $filter('usageValue');
18+
var usageWithUnits = $filter('usageWithUnits');
19+
var percent = $filter('percent');
20+
1221
var isBestEffortPod = function(pod) {
1322
// To be best effort a pod must not have any containers that have non-zero requests or limits
1423
// Break out as soon as we find any pod with a non-zero request or limit
@@ -254,6 +263,96 @@ angular.module("openshiftConsole")
254263
});
255264
};
256265

266+
var COMPUTE_RESOURCE_QUOTAS = [
267+
"cpu",
268+
"requests.cpu",
269+
"memory",
270+
"requests.memory",
271+
"limits.cpu",
272+
"limits.memory"
273+
];
274+
275+
var getNotificaitonMessage = function(used, usedValue, hard, hardValue, quotaKey) {
276+
var msgPrefix = "Your project is " + (hardValue < usedValue ? 'over' : 'at') + " quota. ";
277+
var msg;
278+
if (_.includes(COMPUTE_RESOURCE_QUOTAS, quotaKey)) {
279+
msg = msgPrefix + "It is using " + percent((usedValue/hardValue), 0) + " of " + usageWithUnits(hard, quotaKey) + " " + humanizeQuotaResource(quotaKey, true) + ".";
280+
} else {
281+
msg = msgPrefix + "It is using " + usedValue + " of " + hardValue + " " + humanizeQuotaResource(quotaKey, true) + ".";
282+
}
283+
284+
msg = _.escape(msg);
285+
286+
if (Constants.QUOTA_NOTIFICATION_MESSAGE && Constants.QUOTA_NOTIFICATION_MESSAGE[quotaKey]) {
287+
msg += " " + Constants.QUOTA_NOTIFICATION_MESSAGE[quotaKey];
288+
}
289+
290+
return msg;
291+
};
292+
293+
// Return notifications if you are at quota or over any quota for any resource. Do *not*
294+
// warn about quota for 'resourcequotas' or resources whose hard limit is
295+
// 0, however.
296+
var getQuotaNotifications = function(quotas, clusterQuotas, projectName) {
297+
var notifications = [];
298+
299+
var notificationsForQuota = function(quota) {
300+
var q = quota.status.total || quota.status;
301+
_.each(q.hard, function(hard, quotaKey) {
302+
var hardValue = usageValue(hard);
303+
var used = _.get(q, ['used', quotaKey]);
304+
var usedValue = usageValue(used);
305+
306+
// We always ignore quota warnings about being out of
307+
// resourcequotas since end users cant do anything about it
308+
if (quotaKey === 'resourcequotas' || !hardValue || !usedValue) {
309+
return;
310+
}
311+
312+
if(hardValue <= usedValue) {
313+
notifications.push({
314+
id: "quota-limit-reached-" + quotaKey,
315+
namespace: projectName,
316+
type: (hardValue < usedValue ? 'warning' : 'info'),
317+
message: getNotificaitonMessage(used, usedValue, hard, hardValue, quotaKey),
318+
isHTML: true,
319+
skipToast: true,
320+
showInDrawer: true,
321+
actions: [
322+
{
323+
name: 'View Quotas',
324+
title: 'View project quotas',
325+
onClick: function() {
326+
$location.url("/project/" + $routeParams.project + "/quota");
327+
$rootScope.$emit('NotificationDrawerWrapper.hide');
328+
}
329+
},
330+
{
331+
name: "Don't Show Me Again",
332+
title: 'Permenantly hide this notificaiton until quota limit changes',
333+
onClick: function(notification) {
334+
NotificationsService.permanentlyHideNotification(notification.uid, notification.namespace);
335+
$rootScope.$emit('NotificationDrawerWrapper.clear', notification);
336+
}
337+
},
338+
{
339+
name: "Clear",
340+
title: 'Clear this notificaiton',
341+
onClick: function(notification) {
342+
$rootScope.$emit('NotificationDrawerWrapper.clear', notification);
343+
}
344+
}
345+
]
346+
});
347+
}
348+
});
349+
};
350+
_.each(quotas, notificationsForQuota);
351+
_.each(clusterQuotas, notificationsForQuota);
352+
353+
return notifications;
354+
};
355+
257356
// Warn if you are at quota or over any quota for any resource. Do *not*
258357
// warn about quota for 'resourcequotas' or resources whose hard limit is
259358
// 0, however.
@@ -324,6 +423,7 @@ angular.module("openshiftConsole")
324423
getLatestQuotaAlerts: getLatestQuotaAlerts,
325424
isAnyQuotaExceeded: isAnyQuotaExceeded,
326425
isAnyStorageQuotaExceeded: isAnyStorageQuotaExceeded,
327-
willRequestExceedQuota: willRequestExceedQuota
426+
willRequestExceedQuota: willRequestExceedQuota,
427+
getQuotaNotifications: getQuotaNotifications
328428
};
329429
});

app/scripts/services/resourceAlerts.js

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ angular.module("openshiftConsole")
66
AlertMessageService,
77
DeploymentsService,
88
Navigate,
9+
NotificationsService,
910
QuotaService) {
1011
var annotation = $filter('annotation');
1112
var humanizeKind = $filter('humanizeKind');
@@ -70,36 +71,13 @@ angular.module("openshiftConsole")
7071
return alerts;
7172
};
7273

73-
var setGenericQuotaWarning = function(quotas, clusterQuotas, projectName, alerts) {
74-
var isHidden = AlertMessageService.isAlertPermanentlyHidden("overview-quota-limit-reached", projectName);
75-
if (!isHidden && QuotaService.isAnyQuotaExceeded(quotas, clusterQuotas)) {
76-
if (alerts['quotaExceeded']) {
77-
// Don't recreate the alert or it will reset the temporary hidden state
78-
return;
74+
var setQuotaNotifications = function(quotas, clusterQuotas, projectName) {
75+
var notifications = QuotaService.getQuotaNotifications(quotas, clusterQuotas, projectName);
76+
_.each(notifications, function(notification) {
77+
if(!NotificationsService.isNotificationPermanentlyHidden(notification)) {
78+
NotificationsService.addNotification(notification);
7979
}
80-
81-
alerts['quotaExceeded'] = {
82-
type: 'warning',
83-
message: 'Quota limit has been reached.',
84-
links: [{
85-
href: Navigate.quotaURL(projectName),
86-
label: "View Quota"
87-
},{
88-
href: "",
89-
label: "Don't Show Me Again",
90-
onClick: function() {
91-
// Hide the alert on future page loads.
92-
AlertMessageService.permanentlyHideAlert("overview-quota-limit-reached", projectName);
93-
94-
// Return true close the existing alert.
95-
return true;
96-
}
97-
}]
98-
};
99-
}
100-
else {
101-
delete alerts['quotaExceeded'];
102-
}
80+
});
10381
};
10482

10583
// deploymentConfig, k8s deployment
@@ -207,9 +185,9 @@ angular.module("openshiftConsole")
207185

208186
return {
209187
getPodAlerts: getPodAlerts,
210-
setGenericQuotaWarning: setGenericQuotaWarning,
211188
getDeploymentStatusAlerts: getDeploymentStatusAlerts,
212189
getPausedDeploymentAlerts: getPausedDeploymentAlerts,
213-
getServiceInstanceAlerts: getServiceInstanceAlerts
190+
getServiceInstanceAlerts: getServiceInstanceAlerts,
191+
setQuotaNotifications: setQuotaNotifications
214192
};
215193
});

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
ng-click="$ctrl.customScope.markRead(notification)">
55
<a
66
class="pull-right"
7+
ng-if="!notification.actions.length"
78
tabindex="0"
89
ng-click="$ctrl.customScope.clear(notification, $index, notificationGroup)">
910
<span class="sr-only">Clear notification</span>
@@ -35,7 +36,7 @@
3536
href=""
3637
class="secondary-action"
3738
title="{{action.title}}"
38-
ng-click="$ctrl.customScope.handleAction(notification, action)">
39+
ng-click="action.onClick(notification)">
3940
{{action.name}}
4041
</a>
4142
</li>
@@ -71,7 +72,8 @@
7172
</span>
7273

7374
<span ng-if="!(notification.event.involvedObject)">
74-
{{notification.message}}
75+
<span ng-if="notification.isHTML" ng-bind-html="notification.message"></span>
76+
<span ng-if="!notification.isHTML">{{notification.message}}</span>
7577
<span ng-repeat="link in notification.links">
7678
<a
7779
ng-if="!link.href"

0 commit comments

Comments
 (0)