diff --git a/app/assets/javascripts/angular_modules/module_alerts_center.js b/app/assets/javascripts/angular_modules/module_alerts_center.js new file mode 100644 index 00000000000..a6dfe23a023 --- /dev/null +++ b/app/assets/javascripts/angular_modules/module_alerts_center.js @@ -0,0 +1,6 @@ +miqHttpInject(angular.module('alertsCenter', [ + 'ui.bootstrap', + 'patternfly', + 'miq.util', + 'miq.api' +])); diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 9787b845f12..87c1246b1e9 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -16,6 +16,7 @@ //= require moment //= require moment-strftime/lib/moment-strftime //= require moment-timezone +//= require moment-duration-format //= require sprintf //= require numeral //= require cable diff --git a/app/assets/javascripts/controllers/alerts/alerts_list_controller.js b/app/assets/javascripts/controllers/alerts/alerts_list_controller.js new file mode 100644 index 00000000000..7767e927548 --- /dev/null +++ b/app/assets/javascripts/controllers/alerts/alerts_list_controller.js @@ -0,0 +1,131 @@ +/* global miqHttpInject */ + +angular.module('alertsCenter').controller('alertsListController', + ['$window', 'alertsCenterService', '$interval', '$timeout', + function($window, alertsCenterService, $interval, $timeout) { + var vm = this; + + vm.alerts = []; + vm.alertsList = []; + + function processData(response) { + var updatedAlerts = alertsCenterService.convertToAlertsList(response); + + // update display data for the alerts from the current alert settings + angular.forEach(updatedAlerts, function(nextUpdate) { + matchingAlert = _.find(vm.alerts, function(existingAlert) { + return nextUpdate.id === existingAlert.id; + }); + + if (angular.isDefined(matchingAlert)) { + nextUpdate.isExpanded = matchingAlert.isExpanded; + } + }); + + vm.alerts = updatedAlerts; + vm.loadingDone = true; + vm.filterChange(); + + $timeout(); + } + + function setupConfig() { + vm.severities = alertsCenterService.severities; + vm.acknowledgedTooltip = __("Acknowledged"); + + vm.listConfig = { + showSelectBox: false, + selectItems: false, + useExpandingRows: true, + onClick: expandRow + }; + + vm.menuActions = alertsCenterService.menuActions; + vm.updateMenuActionForItemFn = alertsCenterService.updateMenuActionForItemFn; + + vm.objectTypes = []; + vm.currentFilters = alertsCenterService.getFiltersFromLocation($window.location.search, + alertsCenterService.alertListSortFields); + + vm.filterConfig = { + fields: alertsCenterService.alertListFilterFields, + resultsCount: vm.alertsList.length, + appliedFilters: vm.currentFilters, + onFilterChange: vm.filterChange + }; + + + vm.sortConfig = { + fields: alertsCenterService.alertListSortFields, + onSortChange: sortChange, + isAscending: true + }; + + // Default sort descending by severity + vm.sortConfig.currentField = vm.sortConfig.fields[1]; + vm.sortConfig.isAscending = false; + + vm.toolbarConfig = { + filterConfig: vm.filterConfig, + sortConfig: vm.sortConfig, + actionsConfig: { + actionsInclude: true + } + }; + } + + vm.filterChange = function() { + vm.alertsList = alertsCenterService.filterAlerts(vm.alerts, vm.filterConfig.appliedFilters); + + vm.toolbarConfig.filterConfig.resultsCount = vm.alertsList.length; + + /* Make sure sorting is maintained */ + sortChange(); + }; + + function sortChange() { + if (vm.alertsList) { + vm.alertsList.sort(function(item1, item2) { + return alertsCenterService.compareAlerts(item1, + item2, + vm.toolbarConfig.sortConfig.currentField.id, + vm.toolbarConfig.sortConfig.isAscending); + }); + } + } + + function expandRow(item) { + if (!item.disableRowExpansion) { + item.isExpanded = !item.isExpanded; + } + } + + function getAlerts() { + alertsCenterService.updateAlertsData().then(processData); + + if (alertsCenterService.refreshInterval > 0) { + $interval( + function() { + alertsCenterService.updateAlertsData().then(processData); + }, + alertsCenterService.refreshInterval + ); + } + } + + vm.showHostPage = function(item, event) { + event.stopImmediatePropagation(); + $window.location.href = item.hostLink; + }; + + vm.showObjectPage = function(item, event) { + event.stopImmediatePropagation(); + $window.location.href = item.objectLink; + }; + + alertsCenterService.registerObserverCallback(vm.filterChange); + + setupConfig(); + getAlerts(); + } +]); diff --git a/app/assets/javascripts/controllers/alerts/alerts_most_recent_controller.js b/app/assets/javascripts/controllers/alerts/alerts_most_recent_controller.js new file mode 100644 index 00000000000..b3397f8fa60 --- /dev/null +++ b/app/assets/javascripts/controllers/alerts/alerts_most_recent_controller.js @@ -0,0 +1,134 @@ +/* global miqHttpInject */ + +angular.module('alertsCenter').controller('alertsMostRecentController', + ['$window', 'alertsCenterService', '$interval', '$timeout', + function($window, alertsCenterService, $interval, $timeout) { + var vm = this; + + vm.alertsList = []; + + function processData(response) { + var updatedAlerts = alertsCenterService.convertToAlertsList(response); + + // update display data for the alerts from the current alert settings + angular.forEach(updatedAlerts, function(nextUpdate) { + matchingAlert = _.find(vm.alerts, function(existingAlert) { + return nextUpdate.id === existingAlert.id; + }); + + if (angular.isDefined(matchingAlert)) { + nextUpdate.isExpanded = matchingAlert.isExpanded; + } + }); + + vm.alerts = updatedAlerts; + vm.loadingDone = true; + vm.filterChange(); + + $timeout(); + } + + function setupConfig() { + vm.acknowledgedTooltip = __("Acknowledged"); + + vm.showCount = 25; + vm.showCounts = [25, 50, 100]; + + vm.severities = alertsCenterService.severities; + + vm.listConfig = { + showSelectBox: false, + selectItems: false, + useExpandingRows: true + }; + + vm.menuActions = alertsCenterService.menuActions; + vm.updateMenuActionForItemFn = alertsCenterService.updateMenuActionForItemFn; + + vm.objectTypes = []; + vm.currentFilters = alertsCenterService.getFiltersFromLocation($window.location.search, + alertsCenterService.alertListSortFields); + + vm.filterConfig = { + fields: alertsCenterService.alertListFilterFields, + resultsCount: vm.alertsList.length, + appliedFilters: vm.currentFilters, + onFilterChange: vm.filterChange + }; + + + vm.sortConfig = { + fields: alertsCenterService.alertListSortFields, + onSortChange: sortChange, + isAscending: true + }; + + // Default sort descending by severity + vm.sortConfig.currentField = vm.sortConfig.fields[1]; + vm.sortConfig.isAscending = false; + + vm.toolbarConfig = { + filterConfig: vm.filterConfig, + sortConfig: vm.sortConfig, + actionsConfig: { + actionsInclude: true + } + }; + } + + vm.filterChange = function() { + vm.alertsList = []; + + // Sort by update time descending + vm.alerts.sort(function(alert1, alert2) { + return (alert2.evaluated_on - alert1.evaluated_on); + }); + + vm.alertsList = alertsCenterService.filterAlerts(vm.alerts, vm.filterConfig.appliedFilters); + + vm.toolbarConfig.filterConfig.resultsCount = vm.alertsList.length; + + /* Make sure sorting is maintained */ + sortChange(); + }; + + function sortChange() { + if (vm.alertsList) { + vm.alertsList.sort(function(item1, item2) { + return alertsCenterService.compareAlerts(item1, + item2, + vm.toolbarConfig.sortConfig.currentField.id, + vm.toolbarConfig.sortConfig.isAscending); + }); + } + } + + function getAlerts() { + alertsCenterService.updateAlertsData(vm.showCount, 0, undefined, 'evaluated_on', false).then(processData); + + if (alertsCenterService.refreshInterval > 0) { + $interval( + function() { + alertsCenterService.updateAlertsData(vm.showCount, 0, undefined, 'evaluated_on', false).then(processData); + }, + alertsCenterService.refreshInterval + ); + } + } + + vm.showHostPage = function(item, event) { + event.stopImmediatePropagation(); + $window.location.href = item.hostLink; + }; + + vm.showObjectPage = function(item, event) { + event.stopImmediatePropagation(); + $window.location.href = item.objectLink; + }; + + alertsCenterService.registerObserverCallback(vm.filterChange); + + setupConfig(); + getAlerts(); + } +]); diff --git a/app/assets/javascripts/controllers/alerts/alerts_overview_controller.js b/app/assets/javascripts/controllers/alerts/alerts_overview_controller.js new file mode 100644 index 00000000000..ef8b1ad2ff6 --- /dev/null +++ b/app/assets/javascripts/controllers/alerts/alerts_overview_controller.js @@ -0,0 +1,273 @@ +/* global miqHttpInject */ + +angular.module('alertsCenter').controller('alertsOverviewController', + ['$window', 'alertsCenterService', '$interval', '$timeout', + function($window, alertsCenterService, $interval, $timeout) { + var vm = this; + vm.alertData = []; + vm.loadingDone = false; + + function setupInitialValues() { + document.getElementById("center_div").className += " miq-body"; + + setupConfig(); + + // Default sort ascending by error count + vm.sortConfig.currentField = vm.sortConfig.fields[0]; + vm.sortConfig.isAscending = false; + + // Default to unfiltered + vm.filterConfig.appliedFilters = []; + } + + function setupConfig() { + vm.category = alertsCenterService.categories[0]; + + vm.unGroupedGroup = { + value: __("Ungrouped"), + title: __("Ungrouped"), + itemsList: [], + open: true + }; + + vm.groups = [vm.unGroupedGroup]; + + vm.cardsConfig = { + selectItems: false, + multiSelect: false, + dblClick: false, + selectionMatchProp: 'name', + showSelectBox: false + }; + + vm.filterConfig = { + fields: [ + { + id: 'severityCount', + title: __('Severity'), + placeholder: __('Filter by Severity'), + filterType: 'select', + filterValues: alertsCenterService.severityTitles + }, + { + id: 'name', + title: __('Name'), + placeholder: __('Filter by Name'), + filterType: 'text' + }, + { + id: 'objectType', + title: __('Type'), + placeholder: __('Filter by Type'), + filterType: 'select', + filterValues: alertsCenterService.objectTypes + } + ], + resultsCount: 0, + appliedFilters: [], + onFilterChange: vm.filterChange + }; + + vm.sortConfig = { + fields: [ + { + id: 'errors', + title: __('Error Count'), + sortType: 'numeric' + }, + { + id: 'warnings', + title: __('Warning Count'), + sortType: 'numeric' + }, + { + id: 'infos', + title: __('Information Count'), + sortType: 'numeric' + }, + { + id: 'object_name', + title: __('Object Name'), + sortType: 'alpha' + }, + { + id: 'object_type', + title: __('Object Type'), + sortType: 'alpha' + } + ], + onSortChange: sortChange, + isAscending: true + }; + + vm.toolbarConfig = { + filterConfig: vm.filterConfig, + sortConfig: vm.sortConfig, + actionsConfig: { + actionsInclude: true + } + }; + } + + function filteredOut(item) { + var filtered = true; + if (item.info.length + item.warning.length + item.error.length > 0) { + var filter = _.find(vm.filterConfig.appliedFilters, function (filter) { + if (!alertsCenterService.matchesFilter(item, filter)) { + return true; + } + }); + filtered = filter != undefined; + } + return filtered; + } + + function sortChange() { + angular.forEach(vm.groups, function(group) { + if (group.itemsList) { + group.itemsList.sort(compareItems); + } + }); + } + + function compareItems(item1, item2) { + var compValue = 0; + if (vm.toolbarConfig.sortConfig.currentField.id === 'errors') { + compValue = item1.error.length - item2.error.length; + } else if (vm.toolbarConfig.sortConfig.currentField.id === 'warnings') { + compValue = item1.warning.length - item2.warning.length; + } else if (vm.toolbarConfig.sortConfig.currentField.id === 'infos') { + compValue = item1.info.length - item2.info.length; + } else if (vm.toolbarConfig.sortConfig.currentField.id === 'object_name') { + compValue = item1.name.localeCompare(item2.name); + } else if (vm.toolbarConfig.sortConfig.currentField.id === 'object_type') { + compValue = item1.objectType.localeCompare(item2.objectType); + } + + if (compValue === 0) { + compValue = item1.name.localeCompare(item2.name); + } + + if (!vm.toolbarConfig.sortConfig.isAscending) { + compValue = compValue * -1; + } + + return compValue; + } + + vm.toggleGroupOpen = function(section) { + section.open = !section.open; + }; + + vm.showGroupAlerts = function(item, status) { + var locationRef = "/alerts_list/show?name=" + item.name; + if (angular.isDefined(status)) { + locationRef += "&severity=" + status; + } + $window.location.href = locationRef; + }; + + vm.filterChange = function() { + var totalCount = 0; + + // Clear the existing groups' items + angular.forEach(vm.groups, function(group) { + group.itemsList = []; + group.hasItems = false; + }); + + // Add items to the groups + angular.forEach(vm.alertData, function(item) { + if (item.displayType === vm.displayFilter) { + var group = addGroup(item[vm.category]); + if (!filteredOut(item)) { + group.hasItems = true; + totalCount++; + group.itemsList.push(item); + } + } + }); + + // Sort the groups + vm.groups.sort(function(group1, group2) { + if (!group1.value) { + return 1; + } else if (!group2.value) { + return -1; + } + else { + return group1.value.localeCompare(group2.value); + } + }); + + vm.toolbarConfig.filterConfig.resultsCount = totalCount; + + /* Make sure sorting is maintained */ + sortChange(); + }; + + function addGroup(category) { + var foundGroup; + var groupCategory = category; + + if (angular.isUndefined(category)) { + foundGroup = vm.unGroupedGroup; + } else { + angular.forEach(vm.groups, function(nextGroup) { + if (nextGroup.value === groupCategory) { + foundGroup = nextGroup; + } + }); + } + + if (!foundGroup) { + foundGroup = {value: groupCategory, title: groupCategory, itemsList: [], open: true}; + vm.groups.push(foundGroup); + } + + foundGroup.hasItems = false; + + return foundGroup; + } + + function processData(response) { + vm.alertData = alertsCenterService.convertToAlertsOverview(response); + + // Once we have both providers and hosts from different APIs(?) handle this better + if (alertsCenterService.displayFilters.indexOf(vm.displayFilter) === -1) { + vm.displayFilter = alertsCenterService.displayFilters[0]; + } + + vm.displayFilters = alertsCenterService.displayFilters; + vm.categories = alertsCenterService.categories; + + vm.filterChange(); + vm.loadingDone = true; + + $timeout(); + } + + vm.onHoverAlerts = function(alerts) { + vm.hoverAlerts = alerts; + }; + + vm.processData = processData; + + function initializeAlerts() { + alertsCenterService.updateAlertsData().then(processData); + + if (alertsCenterService.refreshInterval > 0) { + $interval( + function() { + alertsCenterService.updateAlertsData().then(processData); + }, + alertsCenterService.refreshInterval + ); + } + } + + alertsCenterService.registerObserverCallback(vm.filterChange); + setupInitialValues(); + initializeAlerts(); + } +]); diff --git a/app/assets/javascripts/controllers/alerts/edit_alert_dialog_controller.js b/app/assets/javascripts/controllers/alerts/edit_alert_dialog_controller.js new file mode 100644 index 00000000000..5369d293c8d --- /dev/null +++ b/app/assets/javascripts/controllers/alerts/edit_alert_dialog_controller.js @@ -0,0 +1,7 @@ +angular.module('alertsCenter') + .controller('EditAlertDialogController',['editData', + function(editData) { + var vm = this; + vm.editData = editData; + } + ]); diff --git a/app/assets/javascripts/services/alerts_center_service.js b/app/assets/javascripts/services/alerts_center_service.js new file mode 100644 index 00000000000..c76dd1e9628 --- /dev/null +++ b/app/assets/javascripts/services/alerts_center_service.js @@ -0,0 +1,820 @@ +angular.module('alertsCenter').service('alertsCenterService', alertsCenterService); + +alertsCenterService.$inject = ['API', '$q', '$timeout', '$document', '$modal']; + +function alertsCenterService(API, $q, $timeout, $document, $modal) { + var _this = this; + var providersURL = '/api/providers'; + var tagsURL = '/api/tags'; + var alertsURL = '/api/alerts'; + var observerCallbacks = []; + + var notifyObservers = function(){ + angular.forEach(observerCallbacks, function(callback){ + callback(); + }); + }; + + _this.registerObserverCallback = function(callback){ + observerCallbacks.push(callback); + }; + + _this.unregisterObserverCallback = function(callback){ + var index = observerCallbacks.indexOf(callback); + if (index > -1) { + observerCallbacks.splice(index, 1); + } + }; + + _this.refreshInterval = 1000 * 60 * 3; + + _this.objectTypes = []; + + _this.displayFilters = []; + + // Eventually this should be retrieved from smart tags + _this.categories = ["Environment"]; + + _this.severities = { + info: { + title: __("Information"), + value: 1, + severityIconClass: "pficon pficon-info", + severityClass: "alert-info" + }, + warning: { + title: __("Warning"), + value: 2, + severityIconClass: "pficon pficon-warning-triangle-o", + severityClass: "alert-warning" + }, + error: { + title: __("Error"), + value: 3, + severityIconClass: "pficon pficon-error-circle-o", + severityClass: "alert-danger" + } + }; + + function getSeverityTitles() { + var titles = []; + + angular.forEach(_this.severities, function(severity) { + titles.push(severity.title); + }); + + return titles; + } + + _this.severityTitles = getSeverityTitles(); + + _this.alertListFilterFields = [ + { + id: 'severity', + title: __('Severity'), + placeholder: __('Filter by Severity'), + filterType: 'select', + filterValues: _this.severityTitles + }, + { + id: 'host', + title: __('Host Name'), + placeholder: __('Filter by Host Name'), + filterType: 'text' + }, + { + id: 'name', + title: __('Provider Name'), + placeholder: __('Filter by Provider Name'), + filterType: 'text' + }, + { + id: 'objectType', + title: __('Provider Type'), + placeholder: __('Filter by Provider Type'), + filterType: 'select', + filterValues: _this.objectTypes + }, + { + id: 'message', + title: __('Message Text'), + placeholder: __('Filter by Message Text'), + filterType: 'text' + }, + { + id: 'assignee', + title: __('Owner'), + placeholder: __('Filter by Owner'), + filterType: 'text' + }, + { + id: 'acknowledged', + title: __('Acknowledged'), + placeholder: __('Filter by Acknowledged'), + filterType: 'select', + filterValues: [__('Acknowledged'), __('Unacknowledged')] + } + ]; + + _this.getFiltersFromLocation = function(searchString, fields) { + var currentFilters = []; + + if (angular.isString(searchString)) { + var filterString = searchString.slice(1); + var filters = filterString.split('&'); + _.forEach(filters, function(nextFilter) { + var filter = nextFilter.split('='); + var filterId = filter[0].replace(/\[\d*\]/, function(v) { + v.slice(1, -1); + return ''; + }); + + // set parameter value (use 'true' if empty) + var filterValue = angular.isUndefined(filter[1]) ? true : filter[1]; + filterValue = decodeURIComponent(filterValue); + + var filterField = _.find(fields, function(field) { + return field.id === filterId; + }); + if (angular.isDefined(filterField)) { + currentFilters.push({ + id: filterField.id, + value: filterValue, + title: filterField.title + }); + } + }); + } + + return currentFilters; + }; + + function filterStringCompare(value1, value2) { + var match = false; + + if (angular.isString(value1) && angular.isString(value2)) { + match = value1.toLowerCase().indexOf(value2.toLowerCase()) !== -1; + } + + return match; + } + + _this.matchesFilter = function(item, filter) { + var found = false; + + if (filter.id === 'severity') { + found = item.severityInfo.title === filter.value; + } else if (filter.id === 'message') { + found = filterStringCompare(item.message, filter.value); + } else if (filter.id === 'host') { + found = filterStringCompare(item.hostName, filter.value); + } else if (filter.id === 'objectType') { + found = item.objectType === filter.value; + } else if (filter.id === 'name') { + found = filterStringCompare(item.objectName, filter.value); + } else if (filter.id === 'assignee') { + found = item.assignee_name && item.assignee_name.localeCompare(filter.value); + } else if (filter.id === 'acknowledged') { + found = filter.value == __('Acknowledged') ? item.acknowledged : !item.acknowledged; + } else if (filter.id === 'severityCount') { + if (filter.value === _this.severityTitles[0]) { + found = item.info.length > 0; + } else if (filter.value === _this.severityTitles[1]) { + found = item.warning.length > 0; + } else if (filter.value === _this.severityTitles[2]) { + found = item.error.length > 0; + } + } + + return found; + }; + + _this.filterAlerts = function(alertsList, filters) { + var filteredAlerts = []; + + angular.forEach(alertsList, function(nextAlert) { + var doNotAdd = false; + if (filters && filters.length > 0) { + doNotAdd = _.find(filters, function(filter) { + if (!_this.matchesFilter(nextAlert, filter)) { + return true; + } + }); + } + if (!doNotAdd) { + filteredAlerts.push(nextAlert); + } + }); + + return (filteredAlerts) + }; + + _this.compareAlerts = function(item1, item2, sortId, isAscending) { + var compValue = 0; + if (sortId === 'time') { + compValue = item1.evaluated_on - item2.evaluated_on; + } else if (sortId === 'severity') { + compValue = item1.severityInfo.value - item2.severityInfo.value; + } else if (sortId === 'host') { + compValue = item1.hostName.localeCompare(item2.hostName); + } else if (sortId === 'name') { + compValue = item1.objectName.localeCompare(item2.objectName); + } else if (sortId === 'objectType') { + compValue = item1.objectType.localeCompare(item2.objectType); + } else if (sortId === 'assignee') { + compValue = item1.assignee_name.localeCompare(item2.assignee_name); + } else if (sortId === 'acknowledged') { + compValue = item1.acknowledged ? (item2.acknowledged ? 0 : -1) : (item2.acknowledged ? 1 : 0); + } + + if (compValue === 0) { + compValue = item1.severityInfo.value - item2.severityInfo.value; + if (compValue === 0) { + compValue = item1.evaluated_on - item2.evaluated_on; + } + } + + if (!isAscending) { + compValue = compValue * -1; + } + + return compValue; + }; + + _this.alertListSortFields = [ + { + id: 'time', + title: __('Time'), + sortType: 'numeric' + }, + { + id: 'severity', + title: __('Severity'), + sortType: 'numeric' + }, + { + id: 'host', + title: __('Host Name'), + sortType: 'alpha' + }, + { + id: 'name', + title: __('Provider Name'), + sortType: 'alpha' + }, + { + id: 'objectType', + title: __('Provider Type'), + sortType: 'alpha' + }, + { + id: 'assignee', + title: __('Owner'), + sortType: 'alpha' + }, + { + id: 'acknowledged', + title: __('Acknowledged'), + sortType: 'numeric' + } + ]; + + _this.menuActions = [ + { + id: 'acknowledge', + name: __('Acknowledge'), + isVisible: true, + actionFn: handleMenuAction + }, + { + id: 'addcomment', + name: __('Add Note'), + isVisible: true, + actionFn: handleMenuAction + }, + { + id: 'assign', + name: __('Assign'), + isVisible: true, + actionFn: handleMenuAction + }, + { + id: 'unacknowledge', + name: __('Unacknowledge'), + isVisible: true, + actionFn: handleMenuAction + }, + { + id: 'unassign', + name: __('Unassign'), + isVisible: true, + actionFn: handleMenuAction + } + ]; + + _this.updateMenuActionForItemFn = function(action, item) { + if (action.id === 'unassign') { + action.isVisible = item.assigned; + } else if (action.id === 'acknowledge') { + action.isVisible = (item.assignee_id == _this.currentUser.id) && item.acknowledged !== true; + } else if (action.id === 'unacknowledge') { + action.isVisible = (item.assignee_id == _this.currentUser.id) && item.acknowledged === true; + } else { + action.isVisbile = true; + } + }; + + _this.getUserByIdOrUserId = function(id) { + var foundUser; + for (var i = 0; i < _this.existingUsers.length && !foundUser; i++) { + if (_this.existingUsers[i].id === id || _this.existingUsers[i].userid === id) { + foundUser = _this.existingUsers[i]; + } + } + + return foundUser; + }; + + _this.getCurrentUser = function() { + var deferred = $q.defer(); + + // Get the current user + API.get('/api').then(function(response) { + _this.currentUser = response.identity; + deferred.resolve(); + }); + + return deferred.promise; + }; + + _this.updateExistingUsers = function() { + var deferred = $q.defer(); + + // Get the existing users + API.get('/api/users?expand=resources').then(function (response) { + // update the existing users list and current user + _this.existingUsers = response.resources; + _this.existingUsers.sort(function(user1, user2) { + return user1.name.localeCompare(user2.name); + }); + _this.currentUser = _this.getUserByIdOrUserId(_this.currentUser.userid); + + deferred.resolve(); + }); + + return deferred.promise; + }; + + _this.updateProviders = function() { + var deferred = $q.defer(); + + API.get(providersURL + '?expand=resources&attributes=tags').then(function(response) { + _this.providers = response.resources; + deferred.resolve(); + }); + + return deferred.promise; + }; + + _this.updateTags = function() { + var deferred = $q.defer(); + + API.get(tagsURL + '?expand=resources&attributes=category,categorization').then(function(response) { + _this.tags = response.resources; + deferred.resolve(); + }); + + return deferred.promise; + }; + + _this.updateAlertsData = function(limit, offset, filters, sortField, sortAscending) { + // Update data then get the alerts data + return _this.getCurrentUser() + .then(_this.updateExistingUsers) + .then(_this.updateProviders) + .then(_this.updateTags) + .then(function() { + return _this.getAlertsData(limit, offset, filters, sortField, sortAscending); + }); + }; + + _this.getAlertsData = function(limit, offset, filters, sortField, sortAscending) { + var deferred = $q.defer(); + var resourceOptions = '?expand=resources,alert_actions&attributes=assignee,resource'; + var limitOptions = ''; + var offsetOptions = ''; + var sortOptions = ''; + + if (sortField) { + sortOptions = '&sort_by=' + sortField + '&sort_order=' + (sortAscending ? 'asc' : 'desc'); + } + + if (limit) { + limitOptions = '&limit=' + limit; + } + + if (offset) { + offsetOptions = '&offset=' + offset; + } + + // Get the alert data + API.get(alertsURL + resourceOptions + limitOptions + offsetOptions + sortOptions).then(function(response) { + deferred.resolve(response); + }); + + return deferred.promise; + }; + + function getObjectType(item) { + var objectType = item.type; + var descriptors = item.type.split("::"); + + if (descriptors.length >= 3) { + objectType = descriptors[2]; + } + + return objectType; + } + + function convertApiTime(apiTimestamp) { + var apiDate = new Date(apiTimestamp); + return apiDate.getTime(); + } + + function updateAlertStatus(updateAlert) { + if (updateAlert && updateAlert.alert_actions && updateAlert.alert_actions.length > 0) { + var i; + var actionUser; + + for (i = 0; i < updateAlert.alert_actions.length; i++) { + updateAlert.alert_actions[i].created_at = convertApiTime(updateAlert.alert_actions[i].created_at); + updateAlert.alert_actions[i].updated_at = convertApiTime(updateAlert.alert_actions[i].updated_at); + } + + // Sort from newest to oldest + updateAlert.alert_actions.sort(function(state1, state2) { + return state2.updated_at - state1.updated_at; + }); + + // Set the lastUpdate to the time of the newest state change + updateAlert.lastUpdate = updateAlert.alert_actions[0].updated_at; + + // update each state + updateAlert.numComments = 0; + for (i = 0; i < updateAlert.alert_actions.length; i++) { + actionUser = _this.getUserByIdOrUserId(updateAlert.alert_actions[i].user_id); + updateAlert.alert_actions[i].username = angular.isDefined(actionUser) ? actionUser.name : ''; + + // Bump the comments count if a comment was made + if (updateAlert.alert_actions[i].comment) { + updateAlert.numComments++; + } + } + + if (updateAlert.numComments === 1) { + updateAlert.commentsTooltip = sprintf(__("%d Note"), 1); + } else { + updateAlert.commentsTooltip = sprintf(__("%d Notes"), updateAlert.numComments); + } + } + } + + function convertAlert(alertData, key, objectName, objectType, retrievalTime) { + var path = '/assets/svg/'; + var suffix = '.svg'; + var prefix = ''; + var imageName = objectType.replace(/([a-z\d])([A-Z]+)/g, '$1_$2').replace(/[-\s]+/g, '_').toLowerCase(); + + if (key === 'providers') { + prefix = 'vendor-'; + } else { + prefix = 'os-'; + } + + var typeImage = path + prefix + imageName + suffix; + + var newAlert = { + id: alertData.id, + description: alertData.description, + assignee: alertData.assignee, + acknowledged: angular.isDefined(alertData.acknowledged) ? alertData.acknowledged : false, + hostName: alertData.resource.name, + hostType: alertData.resource.type, + hostLink: '/container_node/show/' + alertData.resource.id, + objectName: objectName, + objectType: objectType, + objectTypeImg: typeImage, + objectLink: '/ems_container/' + alertData.ems_id, + sopLink: alertData.url, + evaluated_on: convertApiTime(alertData.evaluated_on), + severity: alertData.severity, + alert_actions: alertData.alert_actions + }; + + if (newAlert.severity == 'error') { + newAlert.severityInfo = _this.severities.error; + } else if (newAlert.severity == 'warning') { + newAlert.severityInfo = _this.severities.warning; + } else { + newAlert.severityInfo = _this.severities.info; + } + + newAlert.age = moment.duration(retrievalTime - newAlert.evaluated_on).format("dd[d] hh[h] mm[m] ss[s]"); + newAlert.rowClass = "alert " + newAlert.severityInfo.severityClass; + newAlert.lastUpdate = newAlert.evaluated_on; + newAlert.numComments = 0; + + if (angular.isDefined(alertData.assignee)) { + newAlert.assigned = true; + newAlert.assignee_name = alertData.assignee.name; + newAlert.assignee_id = alertData.assignee.id; + } else { + newAlert.assigned = false; + newAlert.assignee_name = __('Unassigned'); + } + + updateAlertStatus(newAlert); + + return newAlert; + } + + _this.convertToAlertsList = function(response) { + var alertData = response; + var alerts = []; + _this.objectTypes.splice(0, _this.objectTypes.length); + var newTypes = []; + var retrievalTime = (new Date()).getTime(); + var alertProvider; + var key; + var objectType; + var objectName; + + angular.forEach(alertData.resources, function(item) { + + alertProvider = _.find(_this.providers, function(provider) { + return provider.id === item.ems_id; + }); + + if (angular.isDefined(alertProvider)) { + key = 'providers'; + objectType = getObjectType(alertProvider); + objectName = alertProvider.name; + } + + // Add filter for this object type + if (newTypes.indexOf(objectType) == -1) { + newTypes.push(objectType); + } + + alerts.push(convertAlert(item, key, objectName, objectType, retrievalTime)); + }); + + newTypes.sort(); + angular.forEach(newTypes, function(type) { + _this.objectTypes.push(type); + }); + + return alerts; + }; + + _this.convertToAlertsOverview = function(responseData) { + var alertData = []; + var path = '/assets/svg/'; + var suffix = '.svg'; + + _this.objectTypes.splice(0, _this.objectTypes.length); + + // Add each alert in the appropriate group + angular.forEach(responseData.resources, function(item) { + var objectType; + var providerType; + var foundType; + var descriptors; + var summaryItem; + var matchingTag; + var foundTag; + + // Set the provider object for the alert (we only support provider alerts at this time) + summaryItem = _.find(alertData, function(nextSummaryItem) { + return nextSummaryItem.id === item.ems_id; + }); + + if (!summaryItem) { + angular.forEach(_this.providers, function(provider) { + if (provider.id === item.ems_id) { + summaryItem = { + id: provider.id, + name: provider.name, + objectName: provider.name, + displayType: 'providers', + tags: [], + error: [], + warning: [], + info: [] + }; + + providerType = getObjectType(provider); + descriptors = provider.type.toLowerCase().split("::"); + if (descriptors.length >= 3) { + summaryItem.displayType = descriptors[1]; + objectType = descriptors[2]; + } + + summaryItem.objectType = objectType.replace(/([a-z\d])([A-Z]+)/g, '$1_$2').replace(/[-\s]+/g, '_').toLowerCase(); + summaryItem.objectTypeImg = path + 'vendor-' + summaryItem.objectType + suffix; + + foundType = _.find(_this.objectTypes, function(nextType) { + return nextType === summaryItem.objectType; + }); + + if (!foundType) { + _this.objectTypes.push(summaryItem.objectType); + } + + // Determine the tag values for this object + if (provider.tags) { + angular.forEach(provider.tags, function(providerTag) { + matchingTag = _.find(_this.tags, function(nextTag) { + return nextTag.id === providerTag.id; + }); + if (angular.isDefined(matchingTag)) { + summaryItem.tags.push({ + id: providerTag.id, + categoryName: matchingTag.categorization.category.name, + categoryTitle: matchingTag.categorization.category.description, + value: matchingTag.categorization.name, + title: matchingTag.categorization.description + }); + } + }); + + // Determine the categories for this object + angular.forEach(_this.categories, function(nextCategory) { + foundTag = _.find(summaryItem.tags, function(nextTag) { + return nextTag.categoryTitle === nextCategory; + }); + if (angular.isDefined(foundTag)) { + summaryItem[nextCategory] = foundTag.title; + } + }); + } + + alertData.push(summaryItem); + } + }); + } + + if (summaryItem) { + if (_this.displayFilters.indexOf(summaryItem.displayType) === -1) { + _this.displayFilters.push(summaryItem.displayType); + } + + if (angular.isUndefined(item.severity)) { + item.severity = 'info'; + } + summaryItem[item.severity].push(item); + } + }); + + return alertData; + }; + + function processState(response) { + var newState; + + if (response.results && response.results.length > 0) { + newState = response.results[0]; + + if (angular.isUndefined(_this.editItem.alert_actions)) { + _this.editItem.alert_actions = []; + } + _this.editItem.alert_actions.push(newState); + if (newState.action_type === 'assign') { + _this.editItem.assigned = true; + _this.editItem.assignee_id = newState.assignee_id; + _this.editItem.assignee_name = _this.getUserByIdOrUserId(newState.assignee_id).name; + } else if (newState.action_type === 'unassign') { + _this.editItem.assigned = false; + _this.editItem.assignee_id = undefined; + _this.editItem.assignee_name = __('Unassigned'); + _this.editItem.acknowledged = false; + } else if (newState.action_type === 'acknowledge') { + _this.editItem.acknowledged = true; + } else if (newState.action_type === 'unacknowledge') { + _this.editItem.acknowledged = false; + } + + updateAlertStatus(_this.editItem); + + notifyObservers(); + + if (angular.isDefined(_this.doAfterStateChange)) { + _this.newComment = ''; + _this.doAfterStateChange(); + _this.doAfterStateChange = undefined; + } + } + } + + function doAddState(action) { + var state = { + action_type: action, + comment: _this.newComment, + user_id: _this.currentUser.id + }; + if (action === 'assign') { + state.assignee_id = _this.owner.id; + } + + var stateURL = alertsURL + '/' + _this.editItem.id + '/alert_actions'; + API.post(stateURL, state).then(processState); + } + + function doAcknowledge() { + doAddState('acknowledge'); + } + + function doUnacknowledge() { + doAddState('unacknowledge'); + } + + function doAssign() { + if (_this.editItem.assignee_id != _this.owner.id) { + if (_this.owner) { + if (_this.currentAcknowledged !== _this.editItem.acknowledged) { + _this.doAfterStateChange = _this.currentAcknowledged ? doAcknowledge : doUnacknowledge; + } + + doAddState('assign'); + + } else { + doUnassign(); + } + } + } + + function doUnassign() { + doAddState('unassign'); + } + + function doAddComment() { + if (_this.newComment) { + doAddState("comment"); + } + } + + var modalOptions = { + animation: true, + backdrop: 'static', + templateUrl: '/static/edit_alert_dialog.html', + controller: 'EditAlertDialogController as vm', + resolve: { + editData: function() { + return _this; + } + } + }; + + function showEditDialog(item, title, showAssign, doneCallback, querySelector) { + _this.editItem = item; + _this.editTitle = title; + _this.showAssign = showAssign; + _this.owner = undefined; + _this.currentAcknowledged = _this.editItem.acknowledged; + for (var i = 0; i < _this.existingUsers.length; i++) { + if (item.assigned && _this.existingUsers[i].id === item.assignee_id) { + _this.owner = _this.existingUsers[i]; + } + } + _this.newComment = ''; + var modalInstance = $modal.open(modalOptions); + modalInstance.result.then(doneCallback); + + $timeout(function() { + var queryResult = $document[0].querySelector(querySelector); + if (queryResult) { + queryResult.focus(); + } + }, 200); + } + + function handleMenuAction(action, item) { + switch (action.id) { + case 'acknowledge': + showEditDialog(item, __("Acknowledge Alert"), false, doAcknowledge, '#edit-alert-ok'); + break; + case 'unacknowledge': + showEditDialog(item, __("Uncknowledge Alert"), false, doUnacknowledge, '#edit-alert-ok'); + break; + case 'assign': + showEditDialog(item, __("Assign Alert"), true, doAssign, '[data-id="assign-select"]'); + break; + case 'unassign': + showEditDialog(item, __("Unassign Alert"), false, doUnassign, '#edit-alert-ok-button'); + break; + case 'addcomment': + showEditDialog(item, __("Add Note"), false, doAddComment, '#comment-text-area'); + break; + } + } +} diff --git a/app/assets/stylesheets/alerts.scss b/app/assets/stylesheets/alerts.scss new file mode 100644 index 00000000000..f905a1159f0 --- /dev/null +++ b/app/assets/stylesheets/alerts.scss @@ -0,0 +1,270 @@ +.miq-alerts-center { + margin: -20px; + .breadcrumb { + background-color: #f5f5f5; + border-bottom: 1px solid #d1d1d1; + margin: 0 -20px; + padding-left: 20px; + padding-right: 20px; + } + .init-hidden { + display: none; + + &.initialized { + display: block; + } + } + .toolbar-pf .form-group { + padding-left: 25px; + .btn { + margin-left: 0; + } + .btn.btn-link { + margin-left: 10px; + } + .btn-group { + margin-left: 0; + } + } + .list-view-pf-view { + margin-top: -1px; + margin-left: -20px; + margin-right: -20px; + } + .list-view-container, .row-tile-pf { + height: calc(100vh - 196px); + overflow-y: auto; + position: static; + top: 98px; + } + .list-view-pf { + .list-view-pf-body > .row { + width: 100%; + } + .list-view-pf-left { + padding-right: 20px; + } + [class^="col-"] { + padding-left: 10px; + padding-right: 10px; + } + .inner-row { + margin-left: -10px; + margin-right: -10px; + } + } + .list-group-item.list-view-pf-expand-active { + padding-bottom: 0; + .list-group-item-header { + padding-bottom: 7px; + } + .list-group-item-container { + margin-right: -14px; + [class^="col-"] { + padding-left: 20px; + padding-right: 20px; + } + .row { + margin-bottom: 10px; + &:last-of-type { + margin-bottom: 0; + } + } + .table { + table-layout: fixed; + width: 100%; + .fixed-col { + height: 26px; + @media (min-width: $screen-sm) { + width: 20%; + } + @media (min-width: $screen-md) { + width: 160px; + } + } + .grow-col { + height: 26px; + } + } + } + .header { + margin-right: 10px; + font-size: 14px; + } + } + .list-view-severity { + width: 25px; + height: 25px; + position: relative; + top: 3px; + } + .list-view-type-icon { + font-size: 16px; + vertical-align: middle; + } + .list-view-type-img { + width: 16px; + height: 16px; + } + .list-group-item { + &.alert { + padding-left: 15px; + margin-bottom: -1px; + &.alert-danger { + background-color: $alert-danger-bg; + } + &.alert-info { + background-color: #fff; + } + &.alert-success { + background-color: $alert-success-bg; + } + &.alert-warning { + background-color: $alert-warning-bg; + } + &:hover { + background-color: #ededed; + } + } + .list-view-pf-expand .fa-angle-right { + margin-top: 12px; + } + } + .column-label { + display: inline-block; + font-weight: 700; + text-align: right; + width: 60px; + } + .assignee-column { + } + .description-column { + display: inline-block; + line-height: 20px; + max-height: calc(2 * 20px); + overflow: hidden; + text-overflow: ellipsis; + word-wrap: break-word; + } + .ack-icon { + font-size: 14px; + margin-top: -12px; + vertical-align: middle; + width: 14px; + &::before { + color: #363636; + } + &.unacknowledged { + visibility: hidden; + } + } + .no-wrap.assignee { + display: inline-block; + width: calc(100% - 50px); + margin: 0 5px; + } + .comment-icon { + font-size: 18px; + margin-top: -12px; + vertical-align: middle; + width: 18px; + } + + .group-toolbar-actions { + .bootstrap-select:not([class*="col-"]):not([class*="form-control"]):not(.input-group-btn) { + margin: 0 10px 0 5px; + width: 135px; + } + .bootstrap-select:last-of-type { + margin-right: 0; + } + } + .recent-toolbar-actions { + .bootstrap-select:not([class*="col-"]):not([class*="form-control"]):not(.input-group-btn) { + width: 60px; + } + } + .alerts-group-item { + background-color: transparent; + > a { + color: #030303; + cursor: pointer; + font-size: 13px; + font-weight: 700; + padding-left: 30px; + padding-right: 5px; + text-decoration: none; + transition: 250ms; + &:before { + content: "\f107"; + display: block; + font-family: FontAwesome; + font-size: 16px; + font-weight: 500; + left: 20px; + position: absolute; + top: 7px; + } + &.collapsed { + &:before { + content: "\f105"; + } + } + } + } + .card-view-pf { + margin-left: 20px; + .card-pf { + height: inherit; + display: block; + float: left; + margin: 0 20px 20px 0; + padding: 10px; + position: relative; + text-align: center; + width: 260px; + } + .card-view-type-img { + position: relative; + top: -2px; + width: 22px; + height: 22px; + } + } + + .tooltip-inner { + @media (min-width: $screen-sm) { + max-width: 450px; + } + @media (min-width: $screen-md) { + max-width: 600px; + } + .tip-title { + margin: 0 -10px; + padding: 0 10px; + font-weight: 700; + margin-bottom: 10px; + border-bottom: 1px solid $color-pf-black; + } + .tip-row { + width: 100%; + } + .tip-col { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + } + .host-col { + width: 100px; + } + .description-col { + width: calc(100% - 225px); + padding: 0 5px; + } + .date-col { + width: 118px; + } + } +} + diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss index 8a79c36e956..06df7aecdb3 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -37,7 +37,7 @@ $login-container-details-border-color-rgba: rgba(0, 0, 0, 0.5); @import "about_modal_background"; // sets a custom background image in the 'About' modal @import "timeline"; // used for the patternfly-timeline styles @import "metrics"; - +@import "alerts"; .login-pf #brand img { // sets size of brand.svg on login screen (upstream only) height: 38px; diff --git a/app/controllers/alerts_list_controller.rb b/app/controllers/alerts_list_controller.rb new file mode 100644 index 00000000000..cd1966c25d1 --- /dev/null +++ b/app/controllers/alerts_list_controller.rb @@ -0,0 +1,28 @@ +class AlertsListController < ApplicationController + extend ActiveSupport::Concern + + before_action :check_privileges + before_action :session_data + after_action :cleanup_action + after_action :set_session_data + + def show + if params[:id].nil? + @breadcrumbs.clear + end + end + + def index + redirect_to :action => 'show' + end + + private + + def session_data + @layout = "monitor_alerts_list" + end + + def set_session_data + session[:layout] = @layout + end +end diff --git a/app/controllers/alerts_most_recent_controller.rb b/app/controllers/alerts_most_recent_controller.rb new file mode 100644 index 00000000000..2946f8d9f47 --- /dev/null +++ b/app/controllers/alerts_most_recent_controller.rb @@ -0,0 +1,28 @@ +class AlertsMostRecentController < ApplicationController + extend ActiveSupport::Concern + + before_action :check_privileges + before_action :session_data + after_action :cleanup_action + after_action :set_session_data + + def show + if params[:id].nil? + @breadcrumbs.clear + end + end + + def index + redirect_to :action => 'show' + end + + private + + def session_data + @layout = "monitor_alerts_most_recent" + end + + def set_session_data + session[:layout] = @layout + end +end diff --git a/app/controllers/alerts_overview_controller.rb b/app/controllers/alerts_overview_controller.rb new file mode 100644 index 00000000000..1156ecdd0a1 --- /dev/null +++ b/app/controllers/alerts_overview_controller.rb @@ -0,0 +1,28 @@ +class AlertsOverviewController < ApplicationController + extend ActiveSupport::Concern + + before_action :check_privileges + before_action :session_data + after_action :cleanup_action + after_action :set_session_data + + def show + if params[:id].nil? + @breadcrumbs.clear + end + end + + def index + redirect_to :action => 'show' + end + + private + + def session_data + @layout = "monitor_alerts_overview" + end + + def set_session_data + session[:layout] = @layout + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a28aef9cdfb..225a2a61770 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,3 +1,4 @@ +# rubocop:disable Metrics/LineLength, Lint/EmptyWhen require 'open-uri' require 'simple-rss' @@ -2127,7 +2128,7 @@ def set_global_session_data case controller_name # These controllers don't use breadcrumbs, see above get method to store URL - when "dashboard", "report", "support", "alert", "jobs", "ui_jobs", "miq_ae_tools", "miq_policy", "miq_action", "miq_capacity", "chargeback", "service" + when "dashboard", "report", "support", "alert", "alert_center", "jobs", "ui_jobs", "miq_ae_tools", "miq_policy", "miq_action", "miq_capacity", "chargeback", "service" when "ontap_storage_system", "ontap_logical_disk", "cim_base_storage_extent", "ontap_storage_volume", "ontap_file_share", "snia_local_file_system", "storage_manager" session[:tab_bc][:sto] = @breadcrumbs.dup if ["show", "show_list", "index"].include?(action_name) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de6fd09b481..1e1446b8066 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -647,6 +647,9 @@ def taskbar_in_header? miq_policy miq_policy_export miq_policy_rsop + monitor_alerts_overview + monitor_alerts_list + monitor_alerts_most_recent network_topology ops pxe @@ -1270,6 +1273,9 @@ def pdf_page_size_style middleware_topology miq_schedule miq_template + monitor_alerts_overview + monitor_alerts_list + monitor_alerts_most_recent network_port network_router network_topology diff --git a/app/helpers/application_helper/page_layouts.rb b/app/helpers/application_helper/page_layouts.rb index ccbb3710867..7b55ef86e3a 100644 --- a/app/helpers/application_helper/page_layouts.rb +++ b/app/helpers/application_helper/page_layouts.rb @@ -24,6 +24,9 @@ def layout_uses_listnav? miq_policy miq_policy_export miq_policy_logs + monitor_alerts_overview + monitor_alerts_list + monitor_alerts_most_recent my_tasks my_ui_tasks ops @@ -76,6 +79,10 @@ def layout_uses_tabs? %w(dashboard topology).include?(@showtype) # Dashboard tabs are located in taskbar because they are otherwise hidden behind the taskbar regardless of z-index return false + elsif @layout == "monitor_alerts_overview" || + @layout == "monitor_alerts_list" || + @layout == "monitor_alerts_most_recent" + return false end true end diff --git a/app/presenters/menu/default_menu.rb b/app/presenters/menu/default_menu.rb index 6a07bcb7fa0..55139acf016 100644 --- a/app/presenters/menu/default_menu.rb +++ b/app/presenters/menu/default_menu.rb @@ -1,3 +1,4 @@ +# rubocop:disable Metrics/LineLength module Menu class DefaultMenu class << self @@ -245,6 +246,18 @@ def optimize_menu_section ]) end + def alerts_menu_section + Menu::Section.new(:monitor_alerts, N_("Alerts"), 'fa fa-bullhorn-o fa-2x', [ + Menu::Item.new('monitor_alerts_overview', N_('Overview'), 'monitor_alerts_overview', {:feature => 'monitor_alerts_overview', :any => true}, '/alerts_overview'), + Menu::Item.new('monitor_alerts_list', N_('All Alerts'), 'monitor_alerts_list', {:feature => 'monitor_alerts_list', :any => true}, '/alerts_list'), + Menu::Item.new('monitor_alerts_most_recent', N_('Most Recent Alerts'), 'monitor_alerts_most_recent', {:feature => 'monitor_alerts_most_recent', :any => true}, '/alerts_most_recent') + ]) + end + + def monitor_menu_section + Menu::Section.new(:monitor, N_("Monitor"), 'fa fa-heartbeat fa-2x', [alerts_menu_section]) + end + def settings_menu_section Menu::Section.new(:set, N_("Settings"), 'pficon pficon-settings fa-2x', [ Menu::Item.new('configuration', N_('My Settings'), 'my_settings', {:feature => 'my_settings', :any => true}, '/configuration/index'), @@ -256,7 +269,8 @@ def settings_menu_section def default_menu [cloud_inteligence_menu_section, services_menu_section, compute_menu_section, configuration_menu_section, network_menu_section, middleware_menu_section, datawarehouse_menu_section, storage_menu_section, - control_menu_section, automation_menu_section, optimize_menu_section, settings_menu_section].compact + control_menu_section, automation_menu_section, optimize_menu_section, monitor_menu_section, + settings_menu_section].compact end end end diff --git a/app/services/alerts_service.rb b/app/services/alerts_service.rb new file mode 100644 index 00000000000..639601031b9 --- /dev/null +++ b/app/services/alerts_service.rb @@ -0,0 +1,7 @@ +class AlertsService + include UiServiceMixin + + def initialize(controller) + @controller = controller + end +end diff --git a/app/views/alerts_list/show.html.haml b/app/views/alerts_list/show.html.haml new file mode 100644 index 00000000000..81283931538 --- /dev/null +++ b/app/views/alerts_list/show.html.haml @@ -0,0 +1 @@ += render :partial => "shared/views/show_alerts_list" diff --git a/app/views/alerts_most_recent/show.html.haml b/app/views/alerts_most_recent/show.html.haml new file mode 100644 index 00000000000..b37d5a07a76 --- /dev/null +++ b/app/views/alerts_most_recent/show.html.haml @@ -0,0 +1 @@ += render :partial => "shared/views/show_alerts_most_recent" diff --git a/app/views/alerts_overview/show.html.haml b/app/views/alerts_overview/show.html.haml new file mode 100644 index 00000000000..272fa2389f2 --- /dev/null +++ b/app/views/alerts_overview/show.html.haml @@ -0,0 +1 @@ += render :partial => "shared/views/show_alerts_overview" diff --git a/app/views/shared/views/_alerts_list.html.haml b/app/views/shared/views/_alerts_list.html.haml new file mode 100644 index 00000000000..8e33a78beee --- /dev/null +++ b/app/views/shared/views/_alerts_list.html.haml @@ -0,0 +1,110 @@ +.col-md-12.list-view-container.list-view-compact + .list-view-pf-view{"pf-list-view" => "", + "config" => "vm.listConfig", + "items" => "vm.alertsList", + "menu-actions" => "vm.menuActions", + "update-menu-action-for-item-fn" => "vm.updateMenuActionForItemFn", + "custom-scope" => "vm"} + .list-view-pf-left + %span{:class=> "list-view-severity {{item.severityInfo.severityIconClass}}", + "tooltip" => "{{item.severityInfo.title}}", + "tooltip-placement" => "bottom", + "tooltip-popup-delay" => "1000"} + .list-view-pf-body + .row + .col-md-3.col-sm-4.col-xs-6 + %div + %a{"href" => "#", "ng-click" => "customScope.showHostPage(item, $event)"} + %span.no-wrap{"tooltip" => "{{item.hostName}}", + "tooltip-placement" => "bottom", + "tooltip-popup-delay" => "1000"} + %img.list-view-type-img{"ng-src" => "/assets/100/container_node.png"} + {{item.hostName}} + %div + %a{"href" => "#", "ng-click" => "customScope.showObjectPage(item, $event)"} + %span.no-wrap{"tooltip" => "{{item.objectName}}", + "tooltip-placement" => "bottom", + "tooltip-popup-delay" => "1000"} + %img.list-view-type-img{"ng-src" => "{{item.objectTypeImg}}"} + {{item.objectName}} + .col-lg-3.col-lg-push-4.col-md-4.col-md-push-3.col-sm-3.col-sm-push-3.col-xs-6.container-fluid + %div + %span.no-wrap{"tooltip" => "{{item.lastUpdate| date:'yyyy-MM-dd hh:mm:ss'}}", + "tooltip-placement" => "bottom", + "tooltip-popup-delay" => "1000"} + %span.column-label + = _("Updated") + {{item.lastUpdate| date:'yyyy-MM-dd hh:mm:ss'}} + %div + %span.no-wrap{"tooltip" => "{{item.age}}", + "tooltip-placement" => "bottom", + "tooltip-popup-delay" => "1000"} + %span.column-label + = _("Age") + {{item.age}} + .col-lg-4.col-lg-pull-3.col-md-3.col-md-pull-4.col-sm-3.col-sm-pull-3.col-xs-6.col + %span.description-column{"tooltip" => "{{item.description}}", + "tooltip-placement" => "bottom", + "tooltip-popup-delay" => "1000"} + {{item.description}} + .col-md-2.col-sm-2.col-xs-6.assignee-column + %span.ack-icon.pficon.pficon-ok{"ng-class" => "{unacknowledged: !item.acknowledged}", + "tooltip" => "{{customScope.acknowledgedTooltip}}", + "tooltip-placement" => "bottom", + "tooltip-popup-delay" => "1000"} + %span.assignee.no-wrap{"tooltip" => "{{item.assignee_name}}", + "tooltip-placement" => "bottom", + "tooltip-popup-delay" => "1000"} + {{item.assignee_name}} + %span.comment-icon.fa.fa-comments-o{"ng-if" => "item.numComments > 0", + "tooltip" => "{{item.commentsTooltip}}", + "tooltip-placement" => "bottom", + "tooltip-popup-delay" => "1000"} + %list-expanded-content + .row + .col-md-12 + %a{"href" => "{{$parent.item.sopLink}}", "target" => "_blank", "ng-if" => "$parent.item.sopLink"} + = _("View SOP") + .row + .col-md-12 + %span.header + = _("History") + .col-md-12 + %table.table.table-striped.table-bordered.table-hover + %colgroup + %col.fixed-col + %col.fixed-col + %col.fixed-col + %col.grow-col + %thead + %tr + %th + = _("Time") + %th + = _("Action") + %th + = _("User") + %th + = _("Note") + %tbody + %tr{"ng-repeat" => "state in $parent.item.alert_actions"} + %td + .no-wrap{"tooltip" => "{{state.updated_at | date:'yyyy-MM-dd hh:mm:ss'}}", + "tooltip-placement" => "top", + "tooltip-popup-delay" => "1000"} + {{state.updated_at | date:'yyyy-MM-dd hh:mm:ss'}} + %td + .no-wrap{"tooltip" => "{{state.action_type}}", + "tooltip-placement" => "top", + "tooltip-popup-delay" => "1000"} + {{state.action_type}} + %td + .no-wrap{"tooltip" => "{{state.username}}", + "tooltip-placement" => "top", + "tooltip-popup-delay" => "1000"} + {{state.username}} + %td + .no-wrap{"tooltip" => "{{state.comment}}", + "tooltip-placement" => "top", + "tooltip-popup-delay" => "1000"} + {{state.comment}} diff --git a/app/views/shared/views/_show_alerts_list.html.haml b/app/views/shared/views/_show_alerts_list.html.haml new file mode 100644 index 00000000000..eccc228d0a2 --- /dev/null +++ b/app/views/shared/views/_show_alerts_list.html.haml @@ -0,0 +1,17 @@ +.row.miq-alerts-center + .container-fluid{"ng-controller" => "alertsListController as vm"} + %span + %ol.breadcrumb + %li + = _("Monitor") + %li + = _("Alerts") + %li.active + %strong + = _("All Alerts") + .row.init-hidden{"ng-class" => "{'initialized': vm.loadingDone}"} + %div{"pf-toolbar" => "", "config" => "vm.toolbarConfig"} + = render :partial => "shared/views/alerts_list" + +:javascript + miq_bootstrap('.miq-alerts-center', 'alertsCenter'); diff --git a/app/views/shared/views/_show_alerts_most_recent.html.haml b/app/views/shared/views/_show_alerts_most_recent.html.haml new file mode 100644 index 00000000000..73906866a5e --- /dev/null +++ b/app/views/shared/views/_show_alerts_most_recent.html.haml @@ -0,0 +1,26 @@ +.row.miq-alerts-center + .container-fluid{"ng-controller" => "alertsMostRecentController as vm"} + %span + %ol.breadcrumb + %li + = _("Monitor") + %li + = _("Alerts") + %li.active + %strong + = _("Most Recent Alerts") + .row.init-hidden{"ng-class" => "{'initialized': vm.loadingDone}"} + %div{"pf-toolbar" => "", "config" => "vm.toolbarConfig"} + %actions.recent-toolbar-actions + %span + = _("Show") + %select{"pf-select" => "true", + "ng-model" => "vm.showCount", + "ng-options" => "o as o for o in vm.showCounts", + "ng-change" => "vm.filterChange()"} + %span + = _("alerts") + = render :partial => "shared/views/alerts_list" + +:javascript + miq_bootstrap('.miq-alerts-center', 'alertsCenter'); diff --git a/app/views/shared/views/_show_alerts_overview.html.haml b/app/views/shared/views/_show_alerts_overview.html.haml new file mode 100644 index 00000000000..516b4568cae --- /dev/null +++ b/app/views/shared/views/_show_alerts_overview.html.haml @@ -0,0 +1,71 @@ +.container-fluid.container-tiles-pf.containers-dashboard.miq-dashboard-view.miq-alerts-center{"ng-controller" => "alertsOverviewController as vm"} + %span + %ol.breadcrumb + %li + = _("Monitor") + %li + = _("Alerts") + %li.active + %strong + = _("Overview") + .row.init-hidden{"ng-class" => "{'initialized': vm.loadingDone}"} + %div{"pf-toolbar" => "", "config" => "vm.toolbarConfig"} + %actions.group-toolbar-actions + %span + = _("Group By") + %select{"pf-select" => "true", + "ng-model" => "vm.category", + "ng-options" => "o as o for o in vm.categories", + "ng-change" => "vm.filterChange()"} + %span + = _("Display") + %select{"pf-select" => "true", + "ng-model" => "vm.displayFilter", + "ng-options" => "o as o for o in vm.displayFilters", + "ng-change" => "vm.filterChange()"} + .row.row-tile-pf.init-hidden{"ng-class" => "{'initialized': vm.loadingDone}"} + %ul.list-group + %li.list-group-item.alerts-group-item{"ng-repeat" => "group in vm.groups track by $index"} + %a.miq-alert-group-title{"ng-if" => "group.hasItems", + "ng-class" => "{'collapsed': !group.open}", + "ng-click" => "vm.toggleGroupOpen(group)"} + {{group.title}} ({{group.itemsList.length}}) + .card-view-pf{"ng-if" => "group.hasItems", "ng-class" => "{'collapse': !group.open}"} + .card-pf.card-pf-aggregate-status{"ng-repeat" => "item in group.itemsList track by item.name"} + %a{"href" => "#", "ng-click" => "vm.showGroupAlerts(item)"} + %span.h2.card-pf-title + %img.card-view-type-img{"ng-if" => "item.objectTypeImg", + "ng-src" => "{{item.objectTypeImg}}"} + {{item.name}} + .card-pf-body + %p.card-pf-aggregate-status-notifications + %span.card-pf-aggregate-status-notification{"ng-if" => "item.danger.length > 0", + "ng-mouseover" => "vm.onHoverAlerts(item.danger)", + "tooltip-template" => "'/static/alert-overview-popover.html'", + "tooltip-append-to-body" => "true", + "tooltip-class" => "miq-alerts-center", + "tooltip-popup-delay" => "1000"} + %a{"href" => "#", "ng-click" => "vm.showGroupAlerts(item, 'Error')"} + %span.pficon.pficon-error-circle-o + {{item.danger.length}} + %span.card-pf-aggregate-status-notification{"ng-if" => "item.warning.length > 0", + "ng-mouseover" => "vm.onHoverAlerts(item.warning)", + "tooltip-template" => "'/static/alert-overview-popover.html'", + "tooltip-append-to-body" => "true", + "tooltip-class" => "miq-alerts-center", + "tooltip-popup-delay" => "1000"} + %a{"href" => "#", "ng-click" => "vm.showGroupAlerts(item, 'Warning')"} + %span.pficon.pficon-warning-triangle-o + {{item.warning.length}} + %span.card-pf-aggregate-status-notification{"ng-if" => "item.info.length > 0", + "ng-mouseover" => "vm.onHoverAlerts(item.info)", + "tooltip-append-to-body" => "true", + "tooltip-class" => "miq-alerts-center", + "tooltip-template" => "'/static/alert-overview-popover.html'", + "tooltip-popup-delay" => "1000"} + %a{"href" => "#", "ng-click" => "vm.showGroupAlerts(item, 'Information')"} + %span.pficon.pficon-info + {{item.info.length}} + +:javascript + miq_bootstrap('.miq-alerts-center', 'alertsCenter'); diff --git a/app/views/static/alert-overview-popover.html.haml b/app/views/static/alert-overview-popover.html.haml new file mode 100644 index 00000000000..05a942a9780 --- /dev/null +++ b/app/views/static/alert-overview-popover.html.haml @@ -0,0 +1,10 @@ +%div + .tip-title + {{item.name}} + .tip-row{"ng-repeat" => "alert in vm.hoverAlerts track by $index"} + .tip-col.host-col{"ng-if" => "$index < 5"} + {{alert.resource.name}} + .tip-col.description-col{"ng-if" => "$index < 5"} + {{alert.description}} + .tip-col.date-col{"ng-if" => "$index < 5"} + {{alert.evaluated_on | date:"yyyy-MM-dd hh:mm:ss"}} diff --git a/app/views/static/edit_alert_dialog.html.haml b/app/views/static/edit_alert_dialog.html.haml new file mode 100644 index 00000000000..ceb7a90a93b --- /dev/null +++ b/app/views/static/edit_alert_dialog.html.haml @@ -0,0 +1,41 @@ +%div + .modal-header + %button.close{"type" => "button", "ng-click" => "$dismiss()", "aria-hidden" => "true"} + %span.pficon.pficon-close + %h4.modal-title + {{vm.editData.editTitle}} + .modal-body + %form.form-horizontal + .form-group + %label.col-sm-2.control-label + = _("Message") + .col-sm-10 + %label + {{vm.editData.editItem.description}} + .form-group + %label.col-sm-2.control-label + = _("Owner") + .col-sm-5 + %select#assign-select{"ng-if" => "vm.editData.showAssign", + "ng-model" => "vm.editData.owner", + "ng-options" => "user as user.name for user in vm.editData.existingUsers", + "pf-select" => ""} + %label{"ng-if" => "!vm.editData.showAssign && vm.editData.owner"} + {{vm.editData.owner.name}} + %label{"ng-if" => "!vm.editData.showAssign && !vm.editData.owner"} + = _("Unassigned") + .col-sm-4{"ng-if" => "vm.editData.showAssign && vm.editData.owner.id === vm.editData.currentUser.id"} + %label.control-label.checkbox-inline + %input{"type" => "checkbox", "ng-model" => "vm.editData.currentAcknowledged"} + = _("Acknowledge") + .form-group + %label.col-sm-2.control-label + = _("Note") + .col-sm-10 + %textarea.form-control#comment-text-area{"rows" => "4", "ng-model" => "vm.editData.newComment"} + .modal-footer + .confirmation__buttons + %button.confirmation__button.btn.btn-default{"type" => "button", "ng-click" => "$dismiss()"} + = _("Cancel") + %button.confirmation__button.btn.btn-rounded.btn-primary#edit-alert-ok{"type" => "button", "ng-click" => "$close()"} + = _("OK") diff --git a/bower.json b/bower.json index d1e0827c672..d6c30645203 100644 --- a/bower.json +++ b/bower.json @@ -44,6 +44,7 @@ "manageiq-ui-components": "0.0.12", "moment-strftime": "~0.2.0", "moment-timezone": "~0.4.1", + "moment-duration-format": "~1.3.0", "numeral": "~1.5.5", "patternfly-bootstrap-treeview": "~2.1.1", "patternfly-timeline": "~1.0.3", diff --git a/config/routes.rb b/config/routes.rb index 8e0c13eb12b..230c4e7755d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,5 @@ Rails.application.routes.draw do # rubocop:disable AlignHash - # rubocop:disable MultilineOperationIndentation # grouped routes adv_search_post = %w( adv_search_button @@ -975,6 +974,24 @@ ) }, + :alerts_overview => { + :get => %w( + show + ) + }, + + :alerts_list => { + :get => %w( + show + ) + }, + + :alerts_most_recent => { + :get => %w( + show + ) + }, + :dashboard => { :get => %w( auth_error diff --git a/spec/javascripts/controllers/alerts_center/alerts_list_controller_spec.js b/spec/javascripts/controllers/alerts_center/alerts_list_controller_spec.js new file mode 100644 index 00000000000..713c2e13af2 --- /dev/null +++ b/spec/javascripts/controllers/alerts_center/alerts_list_controller_spec.js @@ -0,0 +1,240 @@ +describe('alertsListController', function() { + var $scope, $controller, alertsCenterService; + var adminResponse, operatorResponse, existingUsersResponse, providersResponse, tagsResponse, alertsResponse; + + beforeEach(module('alertsCenter')); + + beforeEach(function() { + var $window = {location: { pathname: '/alerts_overview/show' }}; + + module(function($provide) { + $provide.value('$window', $window); + }); + }); + + beforeEach(inject(function(_$rootScope_, _$controller_, _alertsCenterService_) { + $scope = _$rootScope_.$new(); + alertsCenterService = _alertsCenterService_; + + alertsCenterService.refreshInterval = -1; + adminResponse = getJSONFixture('alerts_center/admin_user_response.json'); + operatorResponse = getJSONFixture('alerts_center/operator_user_response.json'); + existingUsersResponse = getJSONFixture('alerts_center/existing_users_response.json'); + providersResponse = getJSONFixture('alerts_center/providers_response.json'); + tagsResponse = getJSONFixture('alerts_center/tags_response.json'); + alertsResponse = getJSONFixture('alerts_center/alerts_response.json'); + + fakeGetAlerts = function() { + alertsCenterService.currentUser = adminResponse.identity; + alertsCenterService.currentUser.id = 1; + alertsCenterService.existingUsers = existingUsersResponse.resources; + alertsCenterService.providers = providersResponse.resources; + alertsCenterService.tags = tagsResponse.resources; + + return Promise.resolve(alertsResponse); + }; + + spyOn(alertsCenterService, 'updateAlertsData').and.callFake(fakeGetAlerts); + + $controller = _$controller_('alertsListController', + { + $scope: $scope, + alertsCenterService: alertsCenterService + } + ); + })); + + describe('data loads successfully and', function() { + it('shows the correct alerts', function(done) { + expect($controller.loadingDone).toBeFalsy(); + + setTimeout(function () { + expect($controller.loadingDone).toBeTruthy(); + expect($controller.alerts.length).toBe(4); + expect($controller.alertsList.length).toBe(4); + done(); + }); + }); + + it('filters out items appropriately', function(done) { + setTimeout(function () { + $controller.filterConfig.appliedFilters = [{id: 'severity', value: 'Error'}]; + $controller.filterChange(); + expect($controller.alerts.length).toBe(4); + expect($controller.alertsList.length).toBe(2); + + done(); + }); + }); + + it('shows the appropriate actions for items', function(done) { + setTimeout(function () { + var acknowledgeAction = alertsCenterService.menuActions[0]; + var commentAction = alertsCenterService.menuActions[1]; + var assignAction = alertsCenterService.menuActions[2]; + var unacknowledgeAction = alertsCenterService.menuActions[3]; + var unassignAction = alertsCenterService.menuActions[4]; + + + expect(acknowledgeAction.id).toBe('acknowledge'); + expect(commentAction.id).toBe('addcomment'); + expect(assignAction.id).toBe('assign'); + expect(unacknowledgeAction.id).toBe('unacknowledge'); + expect(unassignAction.id).toBe('unassign'); + + expect($controller.alertsList[0].assigned).toBeTruthy(); + expect($controller.alertsList[0].assignee_id).toBe(2); + expect($controller.alertsList[0].acknowledged).toBeFalsy(); + + alertsCenterService.updateMenuActionForItemFn (acknowledgeAction, $controller.alertsList[0]); + expect(acknowledgeAction.isVisible).toBe(false); + + alertsCenterService.updateMenuActionForItemFn (commentAction, $controller.alertsList[0]); + expect(commentAction.isVisible).toBe(true); + + alertsCenterService.updateMenuActionForItemFn (assignAction, $controller.alertsList[0]); + expect(assignAction.isVisible).toBe(true); + + alertsCenterService.updateMenuActionForItemFn (unacknowledgeAction, $controller.alertsList[0]); + expect(unacknowledgeAction.isVisible).toBe(false); + + alertsCenterService.updateMenuActionForItemFn (unassignAction, $controller.alertsList[0]); + expect(unassignAction.isVisible).toBe(true); + + expect($controller.alertsList[1].assigned).toBeFalsy(); + expect($controller.alertsList[1].assignee_id).toBeUndefined(); + expect($controller.alertsList[1].acknowledged).toBeFalsy(); + + + alertsCenterService.updateMenuActionForItemFn (acknowledgeAction, $controller.alertsList[1]); + expect(acknowledgeAction.isVisible).toBe(false); + + alertsCenterService.updateMenuActionForItemFn (commentAction, $controller.alertsList[1]); + expect(commentAction.isVisible).toBe(true); + + alertsCenterService.updateMenuActionForItemFn (assignAction, $controller.alertsList[1]); + expect(assignAction.isVisible).toBe(true); + + alertsCenterService.updateMenuActionForItemFn (unacknowledgeAction, $controller.alertsList[1]); + expect(unacknowledgeAction.isVisible).toBe(false); + + alertsCenterService.updateMenuActionForItemFn (unassignAction, $controller.alertsList[1]); + expect(unassignAction.isVisible).toBe(false); + + expect($controller.alertsList[2].assigned).toBeFalsy(); + expect($controller.alertsList[2].assignee_id).toBeUndefined(2); + expect($controller.alertsList[2].acknowledged).toBeFalsy(); + + + alertsCenterService.updateMenuActionForItemFn (acknowledgeAction, $controller.alertsList[2]); + expect(acknowledgeAction.isVisible).toBe(false); + + alertsCenterService.updateMenuActionForItemFn (commentAction, $controller.alertsList[2]); + expect(commentAction.isVisible).toBe(true); + + alertsCenterService.updateMenuActionForItemFn (assignAction, $controller.alertsList[2]); + expect(assignAction.isVisible).toBe(true); + + alertsCenterService.updateMenuActionForItemFn (unacknowledgeAction, $controller.alertsList[2]); + expect(unacknowledgeAction.isVisible).toBe(false); + + alertsCenterService.updateMenuActionForItemFn (unassignAction, $controller.alertsList[2]); + expect(unassignAction.isVisible).toBe(false); + + expect($controller.alertsList[3].assigned).toBeTruthy(); + expect($controller.alertsList[3].assignee_id).toBe(1); + expect($controller.alertsList[3].acknowledged).toBeTruthy(); + + + alertsCenterService.updateMenuActionForItemFn (acknowledgeAction, $controller.alertsList[3]); + expect(acknowledgeAction.isVisible).toBe(false); + + alertsCenterService.updateMenuActionForItemFn (commentAction, $controller.alertsList[3]); + expect(commentAction.isVisible).toBe(true); + + alertsCenterService.updateMenuActionForItemFn (assignAction, $controller.alertsList[3]); + expect(assignAction.isVisible).toBe(true); + + alertsCenterService.updateMenuActionForItemFn (unacknowledgeAction, $controller.alertsList[3]); + expect(unacknowledgeAction.isVisible).toBe(true); + + alertsCenterService.updateMenuActionForItemFn (unassignAction, $controller.alertsList[3]); + expect(unassignAction.isVisible).toBe(true); + + alertsCenterService.currentUser.id = 2; + + alertsCenterService.updateMenuActionForItemFn (unacknowledgeAction, $controller.alertsList[3]); + expect(unacknowledgeAction.isVisible).toBe(false); + + done(); + }); + }); + + it('sorts items appropriately', function(done) { + setTimeout(function () { + expect($controller.alerts.length).toBe(4); + expect($controller.alertsList.length).toBe(4); + + expect($controller.alertsList[0].severityInfo.severityClass).toBe('alert-danger'); + expect($controller.alertsList[0].objectName).toBe('Provider 2'); + + expect($controller.alertsList[1].severityInfo.severityClass).toBe('alert-danger'); + expect($controller.alertsList[1].objectName).toBe('Provider 4'); + + expect($controller.alertsList[2].severityInfo.severityClass).toBe('alert-warning'); + expect($controller.alertsList[2].objectName).toBe('Provider 1'); + + expect($controller.alertsList[3].severityInfo.severityClass).toBe('alert-info'); + expect($controller.alertsList[3].objectName).toBe('Provider 1'); + + $controller.sortConfig.isAscending = true; + $controller.filterChange(); + + expect($controller.alertsList[0].severityInfo.severityClass).toBe('alert-info'); + expect($controller.alertsList[0].objectName).toBe('Provider 1'); + + expect($controller.alertsList[1].severityInfo.severityClass).toBe('alert-warning'); + expect($controller.alertsList[1].objectName).toBe('Provider 1'); + + expect($controller.alertsList[2].severityInfo.severityClass).toBe('alert-danger'); + expect($controller.alertsList[2].objectName).toBe('Provider 2'); + + expect($controller.alertsList[3].severityInfo.severityClass).toBe('alert-danger'); + expect($controller.alertsList[3].objectName).toBe('Provider 4'); + + // Sort by Name + $controller.sortConfig.currentField = alertsCenterService.alertListSortFields[2]; + $controller.filterChange(); + + expect($controller.alertsList[0].severityInfo.severityClass).toBe('alert-info'); + expect($controller.alertsList[0].objectName).toBe('Provider 1'); + + expect($controller.alertsList[1].severityInfo.severityClass).toBe('alert-warning'); + expect($controller.alertsList[1].objectName).toBe('Provider 1'); + + expect($controller.alertsList[2].severityInfo.severityClass).toBe('alert-danger'); + expect($controller.alertsList[2].objectName).toBe('Provider 2'); + + expect($controller.alertsList[3].severityInfo.severityClass).toBe('alert-danger'); + expect($controller.alertsList[3].objectName).toBe('Provider 4'); + + $controller.sortConfig.isAscending = false; + $controller.filterChange(); + + expect($controller.alertsList[0].severityInfo.severityClass).toBe('alert-danger'); + expect($controller.alertsList[0].objectName).toBe('Provider 4'); + + expect($controller.alertsList[1].severityInfo.severityClass).toBe('alert-danger'); + expect($controller.alertsList[1].objectName).toBe('Provider 2'); + + expect($controller.alertsList[2].severityInfo.severityClass).toBe('alert-warning'); + expect($controller.alertsList[2].objectName).toBe('Provider 1'); + + expect($controller.alertsList[3].severityInfo.severityClass).toBe('alert-info'); + expect($controller.alertsList[3].objectName).toBe('Provider 1'); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/controllers/alerts_center/alerts_overview_controller_spec.js b/spec/javascripts/controllers/alerts_center/alerts_overview_controller_spec.js new file mode 100644 index 00000000000..6db0bf80963 --- /dev/null +++ b/spec/javascripts/controllers/alerts_center/alerts_overview_controller_spec.js @@ -0,0 +1,101 @@ +describe('alertsOverviewController', function() { + var $scope, $controller, alertsCenterService; + var adminResponse, operatorResponse, existingUsersResponse, providersResponse, tagsResponse, alertsResponse; + + beforeEach(module('alertsCenter')); + + beforeEach(inject(function(_$rootScope_, _$controller_, _alertsCenterService_) { + $scope = _$rootScope_.$new(); + alertsCenterService = _alertsCenterService_; + + var dummyDocument = document.createElement('div'); + spyOn(document, 'getElementById').and.returnValue(dummyDocument); + + alertsCenterService.refreshInterval = -1; + adminResponse = getJSONFixture('alerts_center/admin_user_response.json'); + operatorResponse = getJSONFixture('alerts_center/operator_user_response.json'); + existingUsersResponse = getJSONFixture('alerts_center/existing_users_response.json'); + providersResponse = getJSONFixture('alerts_center/providers_response.json'); + tagsResponse = getJSONFixture('alerts_center/tags_response.json'); + alertsResponse = getJSONFixture('alerts_center/alerts_response.json'); + + fakeGetAlerts = function() { + alertsCenterService.currentUser = adminResponse.identity; + alertsCenterService.existingUsers = existingUsersResponse.resources; + alertsCenterService.providers = providersResponse.resources; + alertsCenterService.tags = tagsResponse.resources; + + return Promise.resolve(alertsResponse); + }; + + spyOn(alertsCenterService, 'updateAlertsData').and.callFake(fakeGetAlerts); + + $controller = _$controller_('alertsOverviewController', + { + $scope: $scope, + alertsCenterService: alertsCenterService + } + ); + })); + + describe('data loads successfully and', function() { + it('shows the correct summary item cards', function(done) { + expect($controller.loadingDone).toBeFalsy(); + + setTimeout(function () { + expect($controller.loadingDone).toBeTruthy(); + expect($controller.groups.length).toBe(3); + expect($controller.displayFilters.length).toBe(1); + expect($controller.categories.length).toBe(1); + done(); + }); + }); + + it('shows the correct items in the summary cards', function(done) { + setTimeout(function () { + expect($controller.groups.length).toBe(3); + expect($controller.groups[0].hasItems).toBeTruthy(); + expect($controller.groups[0].itemsList.length).toBe(1); + expect($controller.groups[0].itemsList[0].error.length).toBe(1); + expect($controller.groups[0].itemsList[0].info.length).toBe(0); + expect($controller.groups[0].itemsList[0].warning.length).toBe(0); + + expect($controller.groups[1].hasItems).toBeTruthy(); + expect($controller.groups[1].itemsList.length).toBe(1); + expect($controller.groups[1].itemsList[0].error.length).toBe(0); + expect($controller.groups[1].itemsList[0].info.length).toBe(1); + expect($controller.groups[1].itemsList[0].warning.length).toBe(1); + + expect($controller.groups[2].hasItems).toBeTruthy(); + expect($controller.groups[2].itemsList.length).toBe(1); + expect($controller.groups[2].itemsList[0].error.length).toBe(1); + expect($controller.groups[2].itemsList[0].info.length).toBe(0); + expect($controller.groups[2].itemsList[0].warning.length).toBe(0); + + done(); + }); + }); + + it('filters out cards appropriately', function(done) { + setTimeout(function () { + $controller.filterConfig.appliedFilters = [{id: 'severityCount', value: 'Error'}]; + $controller.filterChange(); + + expect($controller.groups.length).toBe(3); + expect($controller.groups[0].hasItems).toBeTruthy(); + expect($controller.groups[1].hasItems).toBeFalsy(); + expect($controller.groups[2].hasItems).toBeTruthy(); + + $controller.filterConfig.appliedFilters = [{id: 'name', value: '1'}]; + $controller.filterChange(); + + expect($controller.groups.length).toBe(3); + expect($controller.groups[0].hasItems).toBeFalsy(); + expect($controller.groups[1].hasItems).toBeTruthy(); + expect($controller.groups[2].hasItems).toBeFalsy(); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/fixtures/json/alerts_center/admin_user_response.json b/spec/javascripts/fixtures/json/alerts_center/admin_user_response.json new file mode 100644 index 00000000000..7391eaa21e3 --- /dev/null +++ b/spec/javascripts/fixtures/json/alerts_center/admin_user_response.json @@ -0,0 +1,6 @@ +{ + "identity": { + "userid": "admin", + "name": "Administrator" + } +} diff --git a/spec/javascripts/fixtures/json/alerts_center/alerts_response.json b/spec/javascripts/fixtures/json/alerts_center/alerts_response.json new file mode 100644 index 00000000000..c33c9fc3215 --- /dev/null +++ b/spec/javascripts/fixtures/json/alerts_center/alerts_response.json @@ -0,0 +1,109 @@ +{ + "name": "alerts", + "count": 3, + "subcount": 3, + "resources": [ + { + "id": 1, + "acknowledged": true, + "assignee": { + "id": 1, + "name": "Administrator", + "userid": "admin", + "created_on": "2017-01-26T17:27:26Z", + "updated_on": "2017-01-26T17:27:26Z" + }, + "resource": { + "id": 1, + "name": "host 1", + "type": "ManageIQ::Providers::Kubernetes::ContainerManager::ContainerNode" + }, + "severity": "info", + "miq_alert_id": 1, + "resource_id": 1, + "resource_type": "ContainerNode", + "evaluated_on": "2017-01-22T13:03:46Z", + "ems_id": 1, + "alert_actions": [ + { + "id": 1, + "action_type": "comment", + "user_id": 1, + "comment": "Hello there", + "miq_alert_status_id": 1, + "created_at": "2017-01-30T12:33:51Z", + "updated_at": "2017-01-30T12:33:51Z" + }, + { + "id": 2, + "action_type": "acknowledge", + "user_id": 1, + "comment": "Acknowledged", + "miq_alert_status_id": 1, + "created_at": "2017-01-30T12:33:51Z", + "updated_at": "2017-01-30T12:33:51Z" + } + ] + }, + { + "id": 2, + "severity": "warning", + "miq_alert_id": 1, + "resource_id": 1, + "resource_type": "ContainerNode", + "evaluated_on": "2017-01-22T13:03:46Z", + "ems_id": 1, + "resource": { + "id": 1, + "name": "host 1", + "type": "ManageIQ::Providers::Kubernetes::ContainerManager::ContainerNode" + } + }, + { + "id": 3, + "assignee": { + "id": 2, + "name": "Operator", + "userid": "operator", + "created_on": "2017-01-26T17:27:26Z", + "updated_on": "2017-01-26T17:27:26Z" + }, + "severity": "error", + "miq_alert_id": 3, + "resource_id": 2, + "resource_type": "ContainerNode", + "evaluated_on": "2017-01-22T13:03:46Z", + "ems_id": 2, + "resource": { + "id": 2, + "name": "host 2", + "type": "ManageIQ::Providers::Kubernetes::ContainerManager::ContainerNode" + }, + "alert_actions": [ + { + "id": 2, + "action_type": "comment", + "user_id": 2, + "comment": "Hello there", + "miq_alert_status_id": 1, + "created_at": "2017-01-30T12:33:51Z", + "updated_at": "2017-01-30T12:33:51Z" + } + ] + }, + { + "id": 4, + "severity": "error", + "miq_alert_id": 1, + "resource_id": 4, + "resource_type": "ContainerNode", + "evaluated_on": "2017-01-22T13:03:46Z", + "ems_id": 4, + "resource": { + "id": 4, + "name": "host 4", + "type": "ManageIQ::Providers::Kubernetes::ContainerManager::ContainerNode" + } + } + ] +} diff --git a/spec/javascripts/fixtures/json/alerts_center/existing_users_response.json b/spec/javascripts/fixtures/json/alerts_center/existing_users_response.json new file mode 100644 index 00000000000..f55ae3f837b --- /dev/null +++ b/spec/javascripts/fixtures/json/alerts_center/existing_users_response.json @@ -0,0 +1,17 @@ +{ + "name":"users", + "count":2, + "subcount":2, + "resources":[ + { + "id": 1, + "name": "Administrator", + "userid": "admin" + }, + { + "id": 2, + "name": "Operator", + "userid": "operator" + } + ] +} diff --git a/spec/javascripts/fixtures/json/alerts_center/operator_user_response.json b/spec/javascripts/fixtures/json/alerts_center/operator_user_response.json new file mode 100644 index 00000000000..f09a62dc88f --- /dev/null +++ b/spec/javascripts/fixtures/json/alerts_center/operator_user_response.json @@ -0,0 +1,6 @@ +{ + "identity": { + "userid": "operator", + "name": "Operator" + } +} diff --git a/spec/javascripts/fixtures/json/alerts_center/providers_response.json b/spec/javascripts/fixtures/json/alerts_center/providers_response.json new file mode 100644 index 00000000000..f776f2be1bd --- /dev/null +++ b/spec/javascripts/fixtures/json/alerts_center/providers_response.json @@ -0,0 +1,43 @@ +{ + "name": "providers", + "count": 2, + "subcount": 2, + "resources": [ + { + "id": 1, + "name": "Provider 1", + "type": "ManageIQ::Providers::Hawkular::DatawarehouseManager", + "tags": [ + { + "id": 1 + } + ] + }, + { + "id": 2, + "name": "Provider 2", + "type": "ManageIQ::Providers::Openshift::ContainerManager", + "tags": [ + { + "id": 2 + } + ] + }, + { + "id": 3, + "name": "Provider 3", + "type": "ManageIQ::Providers::Openshift::ContainerManager", + "tags": [ + { + "id": 2 + } + ] + }, + { + "id": 4, + "name": "Provider 4", + "type": "ManageIQ::Providers::Openshift::ContainerManager", + "tags": [] + } + ] +} diff --git a/spec/javascripts/fixtures/json/alerts_center/tags_response.json b/spec/javascripts/fixtures/json/alerts_center/tags_response.json new file mode 100644 index 00000000000..1d793b74ace --- /dev/null +++ b/spec/javascripts/fixtures/json/alerts_center/tags_response.json @@ -0,0 +1,51 @@ +{ + "name": "tags", + "count": 4, + "subcount": 4, + "resources": [ + { + "id": 1, + "categorization" : { + "name": "qa", + "description": "Quality Assurance", + "category": { + "name": "environment", + "description": "Environment" + } + } + }, + { + "id": 2, + "categorization" : { + "name": "prod", + "description": "Production", + "category": { + "name": "environment", + "description": "Environment" + } + } + }, + { + "id": 3, + "categorization" : { + "name": "dev", + "description": "Development", + "category": { + "name": "environment", + "description": "Environment" + } + } + }, + { + "id": 4, + "categorization" : { + "name": "8192", + "description": "8GB", + "category": { + "name": "quota_max_memory", + "description": "Quota - Max Memory" + } + } + } + ] +} diff --git a/spec/javascripts/services/alerts_center_service_spec.js b/spec/javascripts/services/alerts_center_service_spec.js new file mode 100644 index 00000000000..16953a1e5fa --- /dev/null +++ b/spec/javascripts/services/alerts_center_service_spec.js @@ -0,0 +1,186 @@ +describe('alertsCenterService', function() { + var testService, $timeout, API, $q, $rootScope; + var adminResponse, operatorResponse, existingUsersResponse, providersResponse, tagsResponse, alertsResponse; + var deferred; + + beforeEach(module('alertsCenter')); + + beforeEach(inject(function(_$timeout_, _alertsCenterService_, _API_, _$q_, _$rootScope_) { + testService = _alertsCenterService_; + $timeout = _$timeout_; + API = _API_; + $q = _$q_; + $rootScope = _$rootScope_; + + adminResponse = getJSONFixture('alerts_center/admin_user_response.json'); + operatorResponse = getJSONFixture('alerts_center/operator_user_response.json'); + existingUsersResponse = getJSONFixture('alerts_center/existing_users_response.json'); + providersResponse = getJSONFixture('alerts_center/providers_response.json'); + tagsResponse = getJSONFixture('alerts_center/tags_response.json'); + alertsResponse = getJSONFixture('alerts_center/alerts_response.json'); + })); + + describe('static values', function() { + it('should give the correct severity titles', function() { + var titles = testService.severityTitles; + expect(titles.length).toBe(3); + expect(titles[0]).toBe(__("Information")); + expect(titles[1]).toBe(__("Warning")); + expect(titles[2]).toBe(__("Error")); + }); + + it('should give the correct filter choices', function() { + var filters = testService.alertListFilterFields; + expect(filters.length).toBe(7); + + expect(filters[0].title).toBe(__("Severity")); + expect(filters[0].filterValues).toBe(testService.severityTitles); + expect(filters[1].title).toBe(__("Host Name")); + expect(filters[2].title).toBe(__("Provider Name")); + expect(filters[3].title).toBe(__("Provider Type")); + expect(filters[3].filterValues).toBe(testService.objectTypes); + expect(filters[4].title).toBe(__("Message Text")); + expect(filters[5].title).toBe(__("Owner")); + expect(filters[6].title).toBe(__("Acknowledged")); + expect(filters[6].filterValues.length).toBe(2); + expect(filters[6].filterValues[0]).toBe(__('Acknowledged')); + expect(filters[6].filterValues[1]).toBe(__('Unacknowledged')); + }); + + it('should give the correct sort options', function() { + var sortFields = testService.alertListSortFields; + expect(sortFields.length).toBe(7); + + expect(sortFields[0].title).toBe(__("Time")); + expect(sortFields[1].title).toBe(__("Severity")); + expect(sortFields[2].title).toBe(__("Host Name")); + expect(sortFields[3].title).toBe(__("Provider Name")); + expect(sortFields[4].title).toBe(__("Provider Type")); + expect(sortFields[5].title).toBe(__("Owner")); + expect(sortFields[6].title).toBe(__("Acknowledged")); + }); + + it('should give the correct menu actions', function() { + var menuActions = testService.menuActions; + expect(menuActions.length).toBe(5); + + expect(menuActions[0].name).toBe(__("Acknowledge")); + expect(menuActions[1].name).toBe(__("Add Note")); + expect(menuActions[2].name).toBe(__("Assign")); + expect(menuActions[3].name).toBe(__("Unacknowledge")); + expect(menuActions[4].name).toBe(__("Unassign")); + }); + }); + + describe('update commands', function() { + + beforeEach(function() { + deferred = $q.defer(); + + spyOn(API, 'get').and.callFake(function() {return deferred.promise;}); + }); + + it('should get the current user correctly', function() { + testService.getCurrentUser(); + + deferred.resolve(adminResponse); + $rootScope.$apply(); + + expect(testService.currentUser.userid).toBe('admin'); + }); + + it('should update existing user correctly', function() { + testService.currentUser = adminResponse.identity; + + testService.updateExistingUsers(); + + deferred.resolve(existingUsersResponse); + $rootScope.$apply(); + + expect(testService.existingUsers.length).toBe(2); + }); + + it('should update existing providers correctly', function() { + testService.updateProviders(); + + deferred.resolve(providersResponse); + $rootScope.$apply(); + + expect(testService.providers.length).toBe(4); + }); + + it('should update existing tags correctly', function() { + + testService.updateTags(); + + deferred.resolve(tagsResponse); + $rootScope.$apply(); + + expect(testService.tags.length).toBe(4); + }); + + it('should retrieve existing alerts correctly', function() { + var alerts = []; + + testService.getAlertsData().then(function(response) { + alerts = response.resources; + return true; + }); + + deferred.resolve(alertsResponse); + $rootScope.$apply(); + + expect(alerts.length).toBe(4); + }); + }); + + describe('convert functions', function() { + beforeEach(function() { + testService.currentUser = adminResponse.identity; + testService.existingUsers = existingUsersResponse.resources; + testService.providers = providersResponse.resources; + testService.tags = tagsResponse.resources; + }); + + it('should convert into an alert list correctly', function() { + var alertsList = testService.convertToAlertsList(alertsResponse); + + expect(alertsList.length).toBe(4); + + expect(alertsList[0].objectType).toBe("Hawkular"); + expect(alertsList[0].objectTypeImg).toMatch(/svg\/vendor-hawkular/); + expect(alertsList[0].assigned).toBe(true); + + expect(alertsList[1].objectType).toBe("Hawkular"); + expect(alertsList[1].objectTypeImg).toMatch(/svg\/vendor-hawkular/); + expect(alertsList[1].assigned).toBe(false); + + expect(alertsList[2].objectType).toBe("Openshift"); + expect(alertsList[2].objectTypeImg).toMatch(/svg\/vendor-openshift/); + expect(alertsList[2].assigned).toBe(true); + }); + + it('should convert into an alert overview items correctly', function() { + var overviewItems = testService.convertToAlertsOverview(alertsResponse); + + expect(overviewItems.length).toBe(3); + + expect(overviewItems[0].id).toBe(1); + expect(overviewItems[0].name).toBe('Provider 1'); + expect(overviewItems[0].displayType).toBe('providers'); + expect(overviewItems[0].tags.length).toBe(1); + expect(overviewItems[0].error.length).toBe(0); + expect(overviewItems[0].warning.length).toBe(1); + expect(overviewItems[0].info.length).toBe(1); + + expect(overviewItems[1].id).toBe(2); + expect(overviewItems[1].name).toBe('Provider 2'); + expect(overviewItems[1].displayType).toBe('providers'); + expect(overviewItems[1].tags.length).toBe(1); + expect(overviewItems[1].error.length).toBe(1); + expect(overviewItems[1].warning.length).toBe(0); + expect(overviewItems[1].info.length).toBe(0); + + }); + }); +});