Skip to content

Commit

Permalink
Quota Notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
dtaylor113 committed Sep 20, 2017
1 parent 1ceb0ff commit b4407c9
Show file tree
Hide file tree
Showing 8 changed files with 380 additions and 230 deletions.
7 changes: 6 additions & 1 deletion app/scripts/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -555,5 +555,10 @@ angular.extend(window.OPENSHIFT_CONSTANTS, {
// href: 'http://example.com/',
// tooltip: 'Open Dashboard'
// }
]
],
QUOTA_NOTIFICATION_MESSAGE: {
// Example quota messages to show in notification drawer
// "pods": "Upgrade to <a href='http://www.openshift.com'>OpenShift Pro</a> if you need additional resources.",
// "limits.memory": "Upgrade to <a href='http://www.openshift.com'>OpenShift Online Pro</a> if you need additional resources."
}
});
13 changes: 6 additions & 7 deletions app/scripts/controllers/overview.js
Original file line number Diff line number Diff line change
Expand Up @@ -1041,11 +1041,10 @@ function OverviewController($scope,
groupRecentBuildsByDeploymentConfig();
};

var updateQuotaWarnings = function() {
ResourceAlertsService.setGenericQuotaWarning(state.quotas,
state.clusterQuotas,
$routeParams.project,
state.alerts);
var setQuotaNotifications = function() {
ResourceAlertsService.setQuotaNotifications(state.quotas,
state.clusterQuotas,
$routeParams.project);
};

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

watches.push(DataService.watch('appliedclusterresourcequotas', context, function(clusterQuotaData) {
state.clusterQuotas = clusterQuotaData.by("metadata.name");
updateQuotaWarnings();
setQuotaNotifications();
}, {poll: true, pollInterval: DEFAULT_POLL_INTERVAL}));

var canI = $filter('canI');
Expand Down
25 changes: 23 additions & 2 deletions app/scripts/directives/notifications/notificationDrawerWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
'Constants',
'DataService',
'EventsService',
'NotificationsService',
NotificationDrawerWrapper
]
});
Expand Down Expand Up @@ -94,6 +95,12 @@
});
};

var removeNotificationFromGroup = function(notification) {
_.each(drawer.notificationGroups, function(group) {
_.remove(group.notifications, { uid: notification.uid, namespace: notification.namespace });
});
};

var formatAPIEvents = function(apiEvents) {
return _.map(apiEvents, function(event) {
return {
Expand Down Expand Up @@ -172,7 +179,7 @@
var id = notification.id || _.uniqueId('notification_') + Date.now();
notificationsMap[project] = notificationsMap[project] || {};
notificationsMap[project][id] = {
actions: null,
actions: notification.actions,
unread: !EventsService.isRead(id),
// using uid to match API events and have one filed to pass
// to EventsService for read/cleared, etc
Expand All @@ -183,6 +190,7 @@
// but we sort based on lastTimestamp first.
lastTimestamp: notification.timestamp,
message: notification.message,
isHTML: notification.isHTML,
details: notification.details,
namespace: project,
links: notification.links
Expand Down Expand Up @@ -261,7 +269,8 @@
onLinkClick: function(link) {
link.onClick();
drawer.drawerHidden = true;
}
},
countUnreadNotifications: countUnreadNotifications
}
});

Expand All @@ -286,6 +295,18 @@
rootScopeWatches.push($rootScope.$on('NotificationDrawerWrapper.toggle', function() {
drawer.drawerHidden = !drawer.drawerHidden;
}));

// event to signal the drawer to close
rootScopeWatches.push($rootScope.$on('NotificationDrawerWrapper.hide', function() {
drawer.drawerHidden = true;
}));

// event to signal the drawer to clear a notification
rootScopeWatches.push($rootScope.$on('NotificationDrawerWrapper.clear', function(event, notification) {
EventsService.markCleared(notification.uid);
removeNotificationFromGroup(notification);
drawer.countUnreadNotifications();
}));
};

drawer.$onInit = function() {
Expand Down
107 changes: 105 additions & 2 deletions app/scripts/services/quota.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@
angular.module("openshiftConsole")
.factory("QuotaService", function(APIService,
$filter,
$location,
$rootScope,
$routeParams,
$q,
Constants,
DataService,
Logger) {
EventsService,
Logger,
NotificationsService) {

var isNil = $filter('isNil');
var usageValue = $filter('usageValue');
var usageWithUnits = $filter('usageWithUnits');
var percent = $filter('percent');

var isBestEffortPod = function(pod) {
// To be best effort a pod must not have any containers that have non-zero requests or limits
// Break out as soon as we find any pod with a non-zero request or limit
Expand Down Expand Up @@ -254,6 +263,99 @@ angular.module("openshiftConsole")
});
};

var COMPUTE_RESOURCE_QUOTAS = [
"cpu",
"requests.cpu",
"memory",
"requests.memory",
"limits.cpu",
"limits.memory"
];

var getNotificaitonMessage = function(used, usedValue, hard, hardValue, quotaKey) {
// Note: This function returns HTML markup, not plain text

var msgPrefix = "Your project is " + (hardValue < usedValue ? 'over' : 'at') + " quota. ";
var msg;
if (_.includes(COMPUTE_RESOURCE_QUOTAS, quotaKey)) {
msg = msgPrefix + "It is using " + percent((usedValue/hardValue), 0) + " of " + usageWithUnits(hard, quotaKey) + " " + humanizeQuotaResource(quotaKey) + ".";
} else {
msg = msgPrefix + "It is using " + usedValue + " of " + hardValue + " " + humanizeQuotaResource(quotaKey) + ".";
}

msg = _.escape(msg);

if (Constants.QUOTA_NOTIFICATION_MESSAGE && Constants.QUOTA_NOTIFICATION_MESSAGE[quotaKey]) {
// QUOTA_NOTICIATION_MESSAGE can contain HTML and shouldn't be escaped.
msg += " " + Constants.QUOTA_NOTIFICATION_MESSAGE[quotaKey];
}

return msg;
};

// Return notifications if you are at quota or over any quota for any resource. Do *not*
// warn about quota for 'resourcequotas' or resources whose hard limit is
// 0, however.
var getQuotaNotifications = function(quotas, clusterQuotas, projectName) {
var notifications = [];

var notificationsForQuota = function(quota) {
var q = quota.status.total || quota.status;
_.each(q.hard, function(hard, quotaKey) {
var hardValue = usageValue(hard);
var used = _.get(q, ['used', quotaKey]);
var usedValue = usageValue(used);

// We always ignore quota warnings about being out of
// resourcequotas since end users cant do anything about it
if (quotaKey === 'resourcequotas' || !hardValue || !usedValue) {
return;
}

if(hardValue <= usedValue) {
notifications.push({
id: "quota-limit-reached-" + quotaKey,
namespace: projectName,
type: (hardValue < usedValue ? 'warning' : 'info'),
message: getNotificaitonMessage(used, usedValue, hard, hardValue, quotaKey),
isHTML: true,
skipToast: true,
showInDrawer: true,
actions: [
{
name: 'View Quotas',
title: 'View project quotas',
onClick: function() {
$location.url("/project/" + $routeParams.project + "/quota");
$rootScope.$emit('NotificationDrawerWrapper.hide');
}
},
{
name: "Don't Show Me Again",
title: 'Permenantly hide this notificaiton until quota limit changes',
onClick: function(notification) {
NotificationsService.permanentlyHideNotification(notification.uid, notification.namespace);
$rootScope.$emit('NotificationDrawerWrapper.clear', notification);
}
},
{
name: "Clear",
title: 'Clear this notificaiton',
onClick: function(notification) {
$rootScope.$emit('NotificationDrawerWrapper.clear', notification);
}
}
]
});
}
});
};
_.each(quotas, notificationsForQuota);
_.each(clusterQuotas, notificationsForQuota);

return notifications;
};

// Warn if you are at quota or over any quota for any resource. Do *not*
// warn about quota for 'resourcequotas' or resources whose hard limit is
// 0, however.
Expand Down Expand Up @@ -324,6 +426,7 @@ angular.module("openshiftConsole")
getLatestQuotaAlerts: getLatestQuotaAlerts,
isAnyQuotaExceeded: isAnyQuotaExceeded,
isAnyStorageQuotaExceeded: isAnyStorageQuotaExceeded,
willRequestExceedQuota: willRequestExceedQuota
willRequestExceedQuota: willRequestExceedQuota,
getQuotaNotifications: getQuotaNotifications
};
});
40 changes: 9 additions & 31 deletions app/scripts/services/resourceAlerts.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ angular.module("openshiftConsole")
AlertMessageService,
DeploymentsService,
Navigate,
NotificationsService,
QuotaService) {
var annotation = $filter('annotation');
var humanizeKind = $filter('humanizeKind');
Expand Down Expand Up @@ -70,36 +71,13 @@ angular.module("openshiftConsole")
return alerts;
};

var setGenericQuotaWarning = function(quotas, clusterQuotas, projectName, alerts) {
var isHidden = AlertMessageService.isAlertPermanentlyHidden("overview-quota-limit-reached", projectName);
if (!isHidden && QuotaService.isAnyQuotaExceeded(quotas, clusterQuotas)) {
if (alerts['quotaExceeded']) {
// Don't recreate the alert or it will reset the temporary hidden state
return;
var setQuotaNotifications = function(quotas, clusterQuotas, projectName) {
var notifications = QuotaService.getQuotaNotifications(quotas, clusterQuotas, projectName);
_.each(notifications, function(notification) {
if(!NotificationsService.isNotificationPermanentlyHidden(notification)) {
NotificationsService.addNotification(notification);
}

alerts['quotaExceeded'] = {
type: 'warning',
message: 'Quota limit has been reached.',
links: [{
href: Navigate.quotaURL(projectName),
label: "View Quota"
},{
href: "",
label: "Don't Show Me Again",
onClick: function() {
// Hide the alert on future page loads.
AlertMessageService.permanentlyHideAlert("overview-quota-limit-reached", projectName);

// Return true close the existing alert.
return true;
}
}]
};
}
else {
delete alerts['quotaExceeded'];
}
});
};

// deploymentConfig, k8s deployment
Expand Down Expand Up @@ -207,9 +185,9 @@ angular.module("openshiftConsole")

return {
getPodAlerts: getPodAlerts,
setGenericQuotaWarning: setGenericQuotaWarning,
getDeploymentStatusAlerts: getDeploymentStatusAlerts,
getPausedDeploymentAlerts: getPausedDeploymentAlerts,
getServiceInstanceAlerts: getServiceInstanceAlerts
getServiceInstanceAlerts: getServiceInstanceAlerts,
setQuotaNotifications: setQuotaNotifications
};
});
6 changes: 4 additions & 2 deletions app/views/directives/notifications/notification-body.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<a
class="pull-right"
href=""
ng-if="!notification.actions.length"
ng-click="$ctrl.customScope.clear(notification, $index, notificationGroup)">
<span class="sr-only">Clear notification</span>
<span aria-hidden="true" class="pull-left pficon pficon-close"></span>
Expand Down Expand Up @@ -35,7 +36,7 @@
href=""
class="secondary-action"
title="{{action.title}}"
ng-click="$ctrl.customScope.handleAction(notification, action)">
ng-click="action.onClick(notification)">
{{action.name}}
</a>
</li>
Expand Down Expand Up @@ -71,7 +72,8 @@
</span>

<span ng-if="!(notification.event.involvedObject)">
{{notification.message}}
<span ng-if="notification.isHTML" ng-bind-html="notification.message"></span>
<span ng-if="!notification.isHTML">{{notification.message}}</span>
<span ng-repeat="link in notification.links">
<a
ng-if="!link.href"
Expand Down
Loading

0 comments on commit b4407c9

Please sign in to comment.