From 4c0fa5513b1c0e9584dee17129f3040413298d1b Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 18 Sep 2017 14:02:28 -0400 Subject: [PATCH] Quota Notifications --- app/scripts/constants.js | 7 +- app/scripts/controllers/overview.js | 13 +- .../notificationDrawerWrapper.js | 28 +- app/scripts/services/quota.js | 104 +++++++- app/scripts/services/resourceAlerts.js | 40 +-- .../notifications/notification-body.html | 6 +- dist/scripts/scripts.js | 245 ++++++++++-------- dist/scripts/templates.js | 7 +- 8 files changed, 300 insertions(+), 150 deletions(-) diff --git a/app/scripts/constants.js b/app/scripts/constants.js index b980fcebd1..fcc7435e88 100644 --- a/app/scripts/constants.js +++ b/app/scripts/constants.js @@ -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 OpenShift Pro if you need additional resources.", + // "limits.memory": "Upgrade to OpenShift Online Pro if you need additional resources." + } }); diff --git a/app/scripts/controllers/overview.js b/app/scripts/controllers/overview.js index d8a886dd36..076aad7c07 100644 --- a/app/scripts/controllers/overview.js +++ b/app/scripts/controllers/overview.js @@ -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() { @@ -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'); diff --git a/app/scripts/directives/notifications/notificationDrawerWrapper.js b/app/scripts/directives/notifications/notificationDrawerWrapper.js index df75e02dbd..b092a26b95 100644 --- a/app/scripts/directives/notifications/notificationDrawerWrapper.js +++ b/app/scripts/directives/notifications/notificationDrawerWrapper.js @@ -17,6 +17,7 @@ 'Constants', 'DataService', 'EventsService', + 'NotificationsService', NotificationDrawerWrapper ] }); @@ -94,6 +95,14 @@ }); }; + var removeNotificationFromGroup = function(notification) { + _.each(drawer.notificationGroups, function(group) { + _.remove(group.notifications, function(grpNotification) { + return grpNotification.uid === notification.uid && grpNotification.namespace === notification.namespace; + }); + }); + }; + var formatAPIEvents = function(apiEvents) { return _.map(apiEvents, function(event) { return { @@ -171,7 +180,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 @@ -181,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 @@ -217,6 +227,7 @@ hasUnread: false, showClearAll: true, showMarkAllRead: true, + removeNotificationFromGroup: removeNotificationFromGroup, onClose: function() { drawer.drawerHidden = true; }, @@ -259,7 +270,8 @@ onLinkClick: function(link) { link.onClick(); drawer.drawerHidden = true; - } + }, + countUnreadNotifications: countUnreadNotifications } }); @@ -284,6 +296,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); + drawer.removeNotificationFromGroup(notification); + drawer.countUnreadNotifications(); + })); }; drawer.$onInit = function() { diff --git a/app/scripts/services/quota.js b/app/scripts/services/quota.js index 0e24fbeca8..bc75e22ed2 100644 --- a/app/scripts/services/quota.js +++ b/app/scripts/services/quota.js @@ -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 @@ -254,6 +263,96 @@ 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) + "."; + } + + msg = _.escape(msg); + + 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) { + 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. @@ -324,6 +423,7 @@ angular.module("openshiftConsole") getLatestQuotaAlerts: getLatestQuotaAlerts, isAnyQuotaExceeded: isAnyQuotaExceeded, isAnyStorageQuotaExceeded: isAnyStorageQuotaExceeded, - willRequestExceedQuota: willRequestExceedQuota + willRequestExceedQuota: willRequestExceedQuota, + getQuotaNotifications: getQuotaNotifications }; }); diff --git a/app/scripts/services/resourceAlerts.js b/app/scripts/services/resourceAlerts.js index 4f9313ac8c..5d95c8f1bc 100644 --- a/app/scripts/services/resourceAlerts.js +++ b/app/scripts/services/resourceAlerts.js @@ -6,6 +6,7 @@ angular.module("openshiftConsole") AlertMessageService, DeploymentsService, Navigate, + NotificationsService, QuotaService) { var annotation = $filter('annotation'); var humanizeKind = $filter('humanizeKind'); @@ -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 @@ -207,9 +185,9 @@ angular.module("openshiftConsole") return { getPodAlerts: getPodAlerts, - setGenericQuotaWarning: setGenericQuotaWarning, getDeploymentStatusAlerts: getDeploymentStatusAlerts, getPausedDeploymentAlerts: getPausedDeploymentAlerts, - getServiceInstanceAlerts: getServiceInstanceAlerts + getServiceInstanceAlerts: getServiceInstanceAlerts, + setQuotaNotifications: setQuotaNotifications }; }); diff --git a/app/views/directives/notifications/notification-body.html b/app/views/directives/notifications/notification-body.html index 8f6e30ab79..1b8f0a186c 100644 --- a/app/views/directives/notifications/notification-body.html +++ b/app/views/directives/notifications/notification-body.html @@ -4,6 +4,7 @@ ng-click="$ctrl.customScope.markRead(notification)"> Clear notification @@ -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}} @@ -71,7 +72,8 @@ - {{notification.message}} + + {{notification.message}} \n" + - "\n" + + "\n" + "Clear notification\n" + "\n" + "\n" + @@ -7566,7 +7566,7 @@ angular.module('openshiftConsoleTemplates', []).run(['$templateCache', function( "\n" + "
    \n" + "
  • \n" + - "\n" + + "\n" + "{{action.name}}\n" + "\n" + "
  • \n" + @@ -7587,7 +7587,8 @@ angular.module('openshiftConsoleTemplates', []).run(['$templateCache', function( "{{notification.event.involvedObject.name}}\n" + "\n" + "\n" + - "{{notification.message}}\n" + + "\n" + + "{{notification.message}}\n" + "\n" + "{{link.label}}\n" + "{{link.label}}\n" +