Skip to content

Commit

Permalink
Quota Notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
dtaylor113 committed Sep 19, 2017
1 parent 65d843f commit 2985078
Show file tree
Hide file tree
Showing 9 changed files with 280 additions and 141 deletions.
7 changes: 6 additions & 1 deletion app/scripts/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -534,5 +534,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.google.com'>OpenShift Pro</a> if you need additional resources.",
// "limits.memory": "Upgrade to <a href='http://www.google.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 @@ -1040,11 +1040,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 @@ -1304,12 +1303,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
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 @@ -171,7 +172,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 @@ -181,6 +182,7 @@
// but we sort based on lastTimestamp first.
lastTimestamp: notification.timestamp,
message: notification.message,
messageIsHTML: notification.messageIsHTML,
details: notification.details,
namespace: project,
links: notification.links
Expand Down Expand Up @@ -259,7 +261,8 @@
onLinkClick: function(link) {
link.onClick();
drawer.drawerHidden = true;
}
},
countUnreadNotifications: countUnreadNotifications
}
});

Expand Down
9 changes: 9 additions & 0 deletions app/scripts/filters/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -515,4 +515,13 @@ angular.module('openshiftConsole')
return function(feature) {
return _.get(Constants, ['ENABLE_TECH_PREVIEW_FEATURE', feature], false);
};
})
.filter('hasHTML', function() {
var HTML_REGEXP = /(<([^>]+)>)/i;
return function(testString) {
if (!testString) {
return testString;
}
return HTML_REGEXP.test(testString);
};
});
108 changes: 106 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,
$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 hasHTML = $filter('hasHTML');

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,100 @@ 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) {
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, true) + ".";
} else {
msg = msgPrefix + "It is using " + usedValue + " of " + hardValue + " " + humanizeQuotaResource(quotaKey, true) + ".";
}

if (Constants.QUOTA_NOTIFICATION_MESSAGE && Constants.QUOTA_NOTIFICATION_MESSAGE[quotaKey]) {
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) {
var msg = getNotificaitonMessage(used, usedValue, hard, hardValue, quotaKey);
var containsHTML = hasHTML(msg);
notifications.push({
id: "quota-limit-reached-" + quotaKey,
namespace: projectName,
type: (hardValue < usedValue ? 'warning' : 'info'),
message: msg,
messageIsHTML: containsHTML,
skipToast: true,
showInDrawer: true,
actions: [
{
name: 'View Quotas',
title: 'View project quotas',
onClick: function(notification, action, index, group, drawer) {
$location.url("/project/" + $routeParams.project + "/quota");
drawer.drawerHidden = true;
}
},
{
name: "Don't Show Me Again",
title: 'Permenantly hide this notificaiton until quota limit changes',
onClick: function(notification, action, index, group, drawer) {
NotificationsService.permanentlyHideNotification(notification.uid, notification.namespace);
EventsService.markCleared(notification.uid);
group.notifications.splice(index, 1);
drawer.customScope.countUnreadNotifications();
}
},
{
name: "Clear",
title: 'Clear this notificaiton',
onClick: function(notification, action, index, group, drawer) {
EventsService.markCleared(notification.uid);
group.notifications.splice(index, 1);
drawer.customScope.countUnreadNotifications();
}
}
]
});
}
});
};
_.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 +427,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 @@ -4,6 +4,7 @@
ng-click="$ctrl.customScope.markRead(notification)">
<a
class="pull-right"
ng-if="!notification.actions.length"
tabindex="0"
ng-click="$ctrl.customScope.clear(notification, $index, notificationGroup)">
<span class="sr-only">Clear notification</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, $parent.$parent.$index, notificationGroup, $ctrl)">
{{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.messageIsHTML" ng-bind-html="notification.message"></span>
<span ng-if="!notification.messageIsHTML">{{notification.message}}</span>
<span ng-repeat="link in notification.links">
<a
ng-if="!link.href"
Expand Down
Loading

0 comments on commit 2985078

Please sign in to comment.