Skip to content

Commit b4407c9

Browse files
committed
Quota Notifications
1 parent 1ceb0ff commit b4407c9

File tree

8 files changed

+380
-230
lines changed

8 files changed

+380
-230
lines changed

app/scripts/constants.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,5 +555,10 @@ angular.extend(window.OPENSHIFT_CONSTANTS, {
555555
// href: 'http://example.com/',
556556
// tooltip: 'Open Dashboard'
557557
// }
558-
]
558+
],
559+
QUOTA_NOTIFICATION_MESSAGE: {
560+
// Example quota messages to show in notification drawer
561+
// "pods": "Upgrade to <a href='http://www.openshift.com'>OpenShift Pro</a> if you need additional resources.",
562+
// "limits.memory": "Upgrade to <a href='http://www.openshift.com'>OpenShift Online Pro</a> if you need additional resources."
563+
}
559564
});

app/scripts/controllers/overview.js

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

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

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

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

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

app/scripts/directives/notifications/notificationDrawerWrapper.js

Lines changed: 23 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,12 @@
9495
});
9596
};
9697

98+
var removeNotificationFromGroup = function(notification) {
99+
_.each(drawer.notificationGroups, function(group) {
100+
_.remove(group.notifications, { uid: notification.uid, namespace: notification.namespace });
101+
});
102+
};
103+
97104
var formatAPIEvents = function(apiEvents) {
98105
return _.map(apiEvents, function(event) {
99106
return {
@@ -172,7 +179,7 @@
172179
var id = notification.id || _.uniqueId('notification_') + Date.now();
173180
notificationsMap[project] = notificationsMap[project] || {};
174181
notificationsMap[project][id] = {
175-
actions: null,
182+
actions: notification.actions,
176183
unread: !EventsService.isRead(id),
177184
// using uid to match API events and have one filed to pass
178185
// to EventsService for read/cleared, etc
@@ -183,6 +190,7 @@
183190
// but we sort based on lastTimestamp first.
184191
lastTimestamp: notification.timestamp,
185192
message: notification.message,
193+
isHTML: notification.isHTML,
186194
details: notification.details,
187195
namespace: project,
188196
links: notification.links
@@ -261,7 +269,8 @@
261269
onLinkClick: function(link) {
262270
link.onClick();
263271
drawer.drawerHidden = true;
264-
}
272+
},
273+
countUnreadNotifications: countUnreadNotifications
265274
}
266275
});
267276

@@ -286,6 +295,18 @@
286295
rootScopeWatches.push($rootScope.$on('NotificationDrawerWrapper.toggle', function() {
287296
drawer.drawerHidden = !drawer.drawerHidden;
288297
}));
298+
299+
// event to signal the drawer to close
300+
rootScopeWatches.push($rootScope.$on('NotificationDrawerWrapper.hide', function() {
301+
drawer.drawerHidden = true;
302+
}));
303+
304+
// event to signal the drawer to clear a notification
305+
rootScopeWatches.push($rootScope.$on('NotificationDrawerWrapper.clear', function(event, notification) {
306+
EventsService.markCleared(notification.uid);
307+
removeNotificationFromGroup(notification);
308+
drawer.countUnreadNotifications();
309+
}));
289310
};
290311

291312
drawer.$onInit = function() {

app/scripts/services/quota.js

Lines changed: 105 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,99 @@ 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+
// Note: This function returns HTML markup, not plain text
277+
278+
var msgPrefix = "Your project is " + (hardValue < usedValue ? 'over' : 'at') + " quota. ";
279+
var msg;
280+
if (_.includes(COMPUTE_RESOURCE_QUOTAS, quotaKey)) {
281+
msg = msgPrefix + "It is using " + percent((usedValue/hardValue), 0) + " of " + usageWithUnits(hard, quotaKey) + " " + humanizeQuotaResource(quotaKey) + ".";
282+
} else {
283+
msg = msgPrefix + "It is using " + usedValue + " of " + hardValue + " " + humanizeQuotaResource(quotaKey) + ".";
284+
}
285+
286+
msg = _.escape(msg);
287+
288+
if (Constants.QUOTA_NOTIFICATION_MESSAGE && Constants.QUOTA_NOTIFICATION_MESSAGE[quotaKey]) {
289+
// QUOTA_NOTICIATION_MESSAGE can contain HTML and shouldn't be escaped.
290+
msg += " " + Constants.QUOTA_NOTIFICATION_MESSAGE[quotaKey];
291+
}
292+
293+
return msg;
294+
};
295+
296+
// Return notifications if you are at quota or over any quota for any resource. Do *not*
297+
// warn about quota for 'resourcequotas' or resources whose hard limit is
298+
// 0, however.
299+
var getQuotaNotifications = function(quotas, clusterQuotas, projectName) {
300+
var notifications = [];
301+
302+
var notificationsForQuota = function(quota) {
303+
var q = quota.status.total || quota.status;
304+
_.each(q.hard, function(hard, quotaKey) {
305+
var hardValue = usageValue(hard);
306+
var used = _.get(q, ['used', quotaKey]);
307+
var usedValue = usageValue(used);
308+
309+
// We always ignore quota warnings about being out of
310+
// resourcequotas since end users cant do anything about it
311+
if (quotaKey === 'resourcequotas' || !hardValue || !usedValue) {
312+
return;
313+
}
314+
315+
if(hardValue <= usedValue) {
316+
notifications.push({
317+
id: "quota-limit-reached-" + quotaKey,
318+
namespace: projectName,
319+
type: (hardValue < usedValue ? 'warning' : 'info'),
320+
message: getNotificaitonMessage(used, usedValue, hard, hardValue, quotaKey),
321+
isHTML: true,
322+
skipToast: true,
323+
showInDrawer: true,
324+
actions: [
325+
{
326+
name: 'View Quotas',
327+
title: 'View project quotas',
328+
onClick: function() {
329+
$location.url("/project/" + $routeParams.project + "/quota");
330+
$rootScope.$emit('NotificationDrawerWrapper.hide');
331+
}
332+
},
333+
{
334+
name: "Don't Show Me Again",
335+
title: 'Permenantly hide this notificaiton until quota limit changes',
336+
onClick: function(notification) {
337+
NotificationsService.permanentlyHideNotification(notification.uid, notification.namespace);
338+
$rootScope.$emit('NotificationDrawerWrapper.clear', notification);
339+
}
340+
},
341+
{
342+
name: "Clear",
343+
title: 'Clear this notificaiton',
344+
onClick: function(notification) {
345+
$rootScope.$emit('NotificationDrawerWrapper.clear', notification);
346+
}
347+
}
348+
]
349+
});
350+
}
351+
});
352+
};
353+
_.each(quotas, notificationsForQuota);
354+
_.each(clusterQuotas, notificationsForQuota);
355+
356+
return notifications;
357+
};
358+
257359
// Warn if you are at quota or over any quota for any resource. Do *not*
258360
// warn about quota for 'resourcequotas' or resources whose hard limit is
259361
// 0, however.
@@ -324,6 +426,7 @@ angular.module("openshiftConsole")
324426
getLatestQuotaAlerts: getLatestQuotaAlerts,
325427
isAnyQuotaExceeded: isAnyQuotaExceeded,
326428
isAnyStorageQuotaExceeded: isAnyStorageQuotaExceeded,
327-
willRequestExceedQuota: willRequestExceedQuota
429+
willRequestExceedQuota: willRequestExceedQuota,
430+
getQuotaNotifications: getQuotaNotifications
328431
};
329432
});

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
@@ -5,6 +5,7 @@
55
<a
66
class="pull-right"
77
href=""
8+
ng-if="!notification.actions.length"
89
ng-click="$ctrl.customScope.clear(notification, $index, notificationGroup)">
910
<span class="sr-only">Clear notification</span>
1011
<span aria-hidden="true" class="pull-left pficon pficon-close"></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)