diff --git a/awx/ui/client/assets/i_severity_critical.svg b/awx/ui/client/assets/i_severity_critical.svg new file mode 100644 index 0000000000..996df323a1 --- /dev/null +++ b/awx/ui/client/assets/i_severity_critical.svg @@ -0,0 +1,61 @@ + + + +image/svg+xml diff --git a/awx/ui/client/assets/i_severity_high.svg b/awx/ui/client/assets/i_severity_high.svg new file mode 100644 index 0000000000..7bd2ba55c8 --- /dev/null +++ b/awx/ui/client/assets/i_severity_high.svg @@ -0,0 +1,61 @@ + + + +image/svg+xml diff --git a/awx/ui/client/assets/i_severity_low.svg b/awx/ui/client/assets/i_severity_low.svg new file mode 100644 index 0000000000..539664987d --- /dev/null +++ b/awx/ui/client/assets/i_severity_low.svg @@ -0,0 +1,62 @@ + + + +image/svg+xml diff --git a/awx/ui/client/assets/i_severity_med.svg b/awx/ui/client/assets/i_severity_med.svg new file mode 100644 index 0000000000..33e3c19c30 --- /dev/null +++ b/awx/ui/client/assets/i_severity_med.svg @@ -0,0 +1,62 @@ + + + +image/svg+xml diff --git a/awx/ui/client/src/inventories/hosts/host.form.js b/awx/ui/client/src/inventories/hosts/host.form.js index 6f45b7b338..9cfec370d2 100644 --- a/awx/ui/client/src/inventories/hosts/host.form.js +++ b/awx/ui/client/src/inventories/hosts/host.form.js @@ -125,7 +125,8 @@ function(i18n) { awToolTip: i18n._('Please save before viewing Insights'), dataPlacement: 'top', title: i18n._('Insights'), - skipGenerator: true + skipGenerator: true, + ngIf: 'host.insights_system_id!==null' } } }; diff --git a/awx/ui/client/src/inventories/hosts/hosts.partial.html b/awx/ui/client/src/inventories/hosts/hosts.partial.html index bdc9a030c8..1f11db98bb 100644 --- a/awx/ui/client/src/inventories/hosts/hosts.partial.html +++ b/awx/ui/client/src/inventories/hosts/hosts.partial.html @@ -74,6 +74,9 @@
+ diff --git a/awx/ui/client/src/inventories/hosts/list/host-list.controller.js b/awx/ui/client/src/inventories/hosts/list/host-list.controller.js index e906052a79..d88bfadeb3 100644 --- a/awx/ui/client/src/inventories/hosts/list/host-list.controller.js +++ b/awx/ui/client/src/inventories/hosts/list/host-list.controller.js @@ -65,6 +65,9 @@ function HostsList($scope, HostsList, $rootScope, GetBasePath, $scope.editHost = function(id){ $state.go('hosts.edit', {host_id: id}); }; + $scope.goToInsights = function(id){ + $state.go('hosts.edit.insights', {host_id:id}); + }; $scope.deleteHost = function(id, name){ var body = '
Are you sure you want to permanently delete the host below from the inventory?
' + $filter('sanitize')(name) + '
'; var action = function(){ diff --git a/awx/ui/client/src/inventories/insights/insights.block.less b/awx/ui/client/src/inventories/insights/insights.block.less index 38df774da8..a353bd76f4 100644 --- a/awx/ui/client/src/inventories/insights/insights.block.less +++ b/awx/ui/client/src/inventories/insights/insights.block.less @@ -1,5 +1,12 @@ @import "../../shared/branding/colors.default.less"; +.InsightsLastCheck{ + display: flex; + justify-content: flex-end; + padding-bottom: 20px; + align-items: baseline; +} + .InsightsNav{ width: 100%; display: flex; @@ -8,7 +15,6 @@ flex-wrap: wrap; font-size: 14px; font-weight: bold; - } .InsightsNav-rightSide{ @@ -16,7 +22,8 @@ display: flex; flex: 1 0 auto; flex-wrap: wrap; - padding: 10px 0px 10px 0px + max-width: 100%; + padding-left: 10px; } .InsightsNav-leftSide{ @@ -26,6 +33,45 @@ justify-content: flex-end; flex-wrap: wrap; max-width: 100%; + padding-right: 10px; +} + +.InsightsNav-badgeTitle{ + color: #707070; + font-size: 14px; + margin-right: 10px; + font-weight: normal; + text-transform: uppercase; + margin-left: 10px; +} + +.InsightsIcon{ + height: 30px; + width:30px; +} + +.InsightsIcon-warning{ + color:@default-warning; + padding-right: 7px; +} + +.InsightsNav-anchor{ + display:flex; + align-items: center; + cursor:pointer; + height: 40px; + padding-right:10px; +} + +.InsightsNav-anchor.is-currentFilter{ + padding-top: 5px; + border-bottom: 5px solid @menu-link-btm-hov; +} + +.InsightsNav-anchor:hover{ + background-color: @menu-link-bg-hov; + padding-top: 5px; + border-bottom: 5px solid @menu-link-btm-hov; } .InsightsNav-totalIssues{ @@ -42,7 +88,7 @@ } .InsightsNav-mediumIssues{ - background-color: @default-succ; + background-color: @insights-yellow; } .InsightsNav-lowIssues{ @@ -52,6 +98,29 @@ .InsightsNav-solvableBadge{ background-color: @b7grey; } -.InsightsNav-solvableBadge:last-of-type{ - margin-right: 20px; + +.InsightsRow{ + margin-top:10px; +} +.InsightsRow-title{ + display: flex; + align-items: center; +} + +.InsightsRow-description{ + font-size:14px; + font-weight: bold; + padding-left: 5px; +} + +.InsightsRow-category{ + margin-left: 10px; +} + +.InsightsRow-body{ + padding-left: 35px; +} + +.InsightsRow-plan{ + padding-left: 35px; } diff --git a/awx/ui/client/src/inventories/insights/insights.controller.js b/awx/ui/client/src/inventories/insights/insights.controller.js index e892b62387..786df445a1 100644 --- a/awx/ui/client/src/inventories/insights/insights.controller.js +++ b/awx/ui/client/src/inventories/insights/insights.controller.js @@ -4,13 +4,71 @@ * All Rights Reserved *************************************************/ -export default [ -function () { +export default [ 'InsightsData', '$scope', 'moment', '$state', 'resourceData', +function (data, $scope, moment, $state, resourceData) { function init() { - // $scope.insights + + $scope.reports = data.reports; + $scope.reports_dataset = data; + $scope.currentFilter = "total"; + $scope.solvable_count = _.filter($scope.reports_dataset.reports, (report) => {return report.maintenance_actions.length > 0;}).length; + $scope.not_solvable_count = _.filter($scope.reports_dataset.reports, (report) => {return report.maintenance_actions.length === 0; }).length; + $scope.critical_count = 0 || _.filter($scope.reports_dataset.reports, (report) => {return report.rule.severity === "CRITICAL"; }).length; + $scope.high_count = _.filter($scope.reports_dataset.reports, (report) => {return report.rule.severity === "ERROR"; }).length; + $scope.med_count = _.filter($scope.reports_dataset.reports, (report) => {return report.rule.severity === "WARN"; }).length; + $scope.low_count = _.filter($scope.reports_dataset.reports, (report) => {return report.rule.severity === "INFO"; }).length; + let a = moment(), b = moment($scope.reports_dataset.last_check_in); + $scope.last_check_in = a.diff(b, 'hours'); + $scope.inventory = resourceData.data; + $scope.insights_credential = resourceData.data.summary_fields.insights_credential.id; } init(); + $scope.filter = function(filter){ + $scope.currentFilter = filter; + if(filter === "total"){ + $scope.reports = $scope.reports_dataset.reports; + } + if(filter === "solvable"){ + $scope.reports = _.filter($scope.reports_dataset.reports, function(report){ + return (report.maintenance_actions.length > 0); + }); + } + if(filter === "not_solvable"){ + $scope.reports = _.filter($scope.reports_dataset.reports, function(report){ + return (report.maintenance_actions.length === 0); + }); + } + if(filter === "critical"){ + $scope.reports = _.filter($scope.reports_dataset.reports, function(report){ + return (report.rule.severity === 'CRITICAL'); + }); + } + if(filter === "high"){ + $scope.reports = _.filter($scope.reports_dataset.reports, function(report){ + return (report.rule.severity === 'ERROR'); + }); + } + if(filter === "medium"){ + $scope.reports = _.filter($scope.reports_dataset.reports, function(report){ + return (report.rule.severity === 'WARN'); + }); + } + if(filter === "low"){ + $scope.reports = _.filter($scope.reports_dataset.reports, function(report){ + return (report.rule.severity === 'INFO'); + }); + } + }; + $scope.viewDataInInsights = function(){ + window.open(`https://access.redhat.com/insights/inventory?machine=${$scope.$parent.host.insights_system_id}`, '_blank'); + }; + $scope.remediateInventory = function(inv_id, inv_name, insights_credential){ + $state.go('templates.addJobTemplate', {inventory_id: inv_id, inventory_name:inv_name, credential_id: insights_credential}); + }; + $scope.formCancel = function(){ + $state.go('inventories', null, {reload: true}); + }; }]; diff --git a/awx/ui/client/src/inventories/insights/insights.partial.html b/awx/ui/client/src/inventories/insights/insights.partial.html index 30268578bc..d0c98ae65e 100644 --- a/awx/ui/client/src/inventories/insights/insights.partial.html +++ b/awx/ui/client/src/inventories/insights/insights.partial.html @@ -1,20 +1,81 @@ +
+ + This machine has not checked in with Insights in {{last_check_in}} hours +
-
Total Issues
- 4 -
Critical
- 1 -
High
- 1 -
Medium
- 1 -
Low
- 1 +
+
Total Issues
+ {{reports_dataset.reports.length}} +
+ +
+
Critical
+ {{critical_count}} +
+
+
High
+ {{high_count}} +
+
+
Medium
+ {{med_count}} +
+
+
Low
+ {{low_count}} +
-
Solvable With Playbook
- 4 -
Not Solvable With Playbook
- 1 + +
+
Solvable With Playbook
+ {{solvable_count}} +
+
+
Not Solvable With Playbook
+ {{not_solvable_count}} +
+ +
+
+
+ + + + +
ISSUE: {{report.rule.description}}
+ {{report.rule.category}} +
+
{{report.rule.summary}}
+
+
+
+
+
+ +
+ + + +
diff --git a/awx/ui/client/src/inventories/insights/insights.route.js b/awx/ui/client/src/inventories/insights/insights.route.js index 1bba477d18..3d7cf2c664 100644 --- a/awx/ui/client/src/inventories/insights/insights.route.js +++ b/awx/ui/client/src/inventories/insights/insights.route.js @@ -13,15 +13,21 @@ export default { } }, resolve: { - Facts: ['$stateParams', 'GetBasePath', 'Rest', - function($stateParams, GetBasePath, Rest) { - let ansibleFactsUrl = GetBasePath('hosts') + $stateParams.host_id + '/ansible_facts'; - Rest.setUrl(ansibleFactsUrl); + InsightsData: ['Rest', '$stateParams', 'GetBasePath', 'ProcessErrors', + (Rest, $stateParams, GetBasePath, ProcessErrors) => { + var path = `${GetBasePath('hosts')}${$stateParams.host_id}/insights`; + Rest.setUrl(path); return Rest.get() - .success(function(data) { - return data; + .then(function(data) { + return (data.data.insights_content); + }).catch(function(response) { + ProcessErrors(null, response.data, response.status, null, { + hdr: 'Error!', + msg: 'Failed to get insights info. GET returned status: ' + + response.status + }); }); } - ] + ], } }; diff --git a/awx/ui/client/src/inventories/insights/main.js b/awx/ui/client/src/inventories/insights/main.js index 75e05fd1eb..44573e6c9a 100644 --- a/awx/ui/client/src/inventories/insights/main.js +++ b/awx/ui/client/src/inventories/insights/main.js @@ -5,7 +5,9 @@ *************************************************/ import controller from './insights.controller'; +import planFilter from './plan-filter'; export default angular.module('insightsDashboard', []) + .filter('planFilter', planFilter) .controller('InsightsController', controller); diff --git a/awx/ui/client/src/inventories/insights/plan-filter.js b/awx/ui/client/src/inventories/insights/plan-filter.js new file mode 100644 index 0000000000..40916cd5ec --- /dev/null +++ b/awx/ui/client/src/inventories/insights/plan-filter.js @@ -0,0 +1,16 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + export default function(){ + return function(plan) { + if(plan === null || plan === undefined){ + return "PLAN: Not Available CREATE A NEW PLAN IN INSIGHTS"; + } else { + let name = (plan.maintenance_plan.name === null) ? "Unnamed Plan" : plan.maintenance_plan.name; + return `${name} (${plan.maintenance_plan.maintenance_id})`; + } + }; + } diff --git a/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js b/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js index b02c9cbf43..3af8c0c6eb 100644 --- a/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js +++ b/awx/ui/client/src/inventories/related-hosts/list/host-list.controller.js @@ -86,6 +86,9 @@ export default ['$scope', 'ListDefinition', '$rootScope', 'GetBasePath', $scope.editHost = function(id){ $state.go('inventories.edit.hosts.edit', {host_id: id}); }; + $scope.goToInsights = function(id){ + $state.go('inventories.edit.hosts.edit.insights', {host_id:id}); + }; $scope.deleteHost = function(id, name){ var body = '
Are you sure you want to permanently delete the host below from the inventory?
' + $filter('sanitize')(name) + '
'; var action = function(){ diff --git a/awx/ui/client/src/inventories/related-hosts/related-host.form.js b/awx/ui/client/src/inventories/related-hosts/related-host.form.js index 941f98d995..0ad88d8835 100644 --- a/awx/ui/client/src/inventories/related-hosts/related-host.form.js +++ b/awx/ui/client/src/inventories/related-hosts/related-host.form.js @@ -125,7 +125,8 @@ function(i18n) { awToolTip: i18n._('Please save before viewing Insights'), dataPlacement: 'top', title: i18n._('Insights'), - skipGenerator: true + skipGenerator: true, + ngIf: 'host.insights_system_id!==null' } } }; diff --git a/awx/ui/client/src/inventories/related-hosts/related-host.list.js b/awx/ui/client/src/inventories/related-hosts/related-host.list.js index c806fbaa58..7e1e114bad 100644 --- a/awx/ui/client/src/inventories/related-hosts/related-host.list.js +++ b/awx/ui/client/src/inventories/related-hosts/related-host.list.js @@ -50,6 +50,13 @@ export default { fieldActions: { columnClass: 'col-lg-6 col-md-4 col-sm-4 col-xs-5 text-right', + insights: { + ngClick: "goToInsights(host.id)", + icon: 'fa-info', + awToolTip: 'View Insights Data', + dataPlacement: 'top', + ngShow: 'host.insights_system_id' + }, copy: { mode: 'all', ngClick: "copyMoveHost(host.id)", diff --git a/awx/ui/client/src/inventories/standard/edit/inventory-edit.controller.js b/awx/ui/client/src/inventories/standard/edit/inventory-edit.controller.js index 205b483779..70500bac10 100644 --- a/awx/ui/client/src/inventories/standard/edit/inventory-edit.controller.js +++ b/awx/ui/client/src/inventories/standard/edit/inventory-edit.controller.js @@ -29,6 +29,7 @@ function InventoriesEdit($scope, $location, $scope = angular.extend($scope, inventoryData); + $scope.credential_name = (inventoryData.summary_fields.insights_credential && inventoryData.summary_fields.insights_credential.name) ? inventoryData.summary_fields.insights_credential.name : null; $scope.organization_name = inventoryData.summary_fields.organization.name; $scope.inventory_variables = inventoryData.variables === null || inventoryData.variables === '' ? '---' : ParseVariableString(inventoryData.variables); $scope.parseType = 'yaml'; @@ -90,6 +91,10 @@ function InventoriesEdit($scope, $location, $state.go('inventories'); }; + $scope.remediateInventory = function(inv_id, inv_name, insights_credential){ + $state.go('templates.addJobTemplate', {inventory_id: inv_id, inventory_name:inv_name, credential_id: insights_credential}); + }; + } export default ['$scope', '$location', diff --git a/awx/ui/client/src/inventories/standard/inventory.form.js b/awx/ui/client/src/inventories/standard/inventory.form.js index 08e3d8df48..468ab1112f 100644 --- a/awx/ui/client/src/inventories/standard/inventory.form.js +++ b/awx/ui/client/src/inventories/standard/inventory.form.js @@ -68,6 +68,17 @@ function(i18n, InventoryCompletedJobsList) { ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd) || !canEditOrg', awLookupWhen: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd) && canEditOrg' }, + insights_credential: { + label: i18n._('Insights Credential'), + type: 'lookup', + list: 'CredentialList', + basePath: 'credentials', + sourceModel: 'credential', + sourceField: 'name', + search: { + credential_type: 13 //insights + } + }, inventory_variables: { realName: 'variables', label: i18n._('Variables'), @@ -177,6 +188,14 @@ function(i18n, InventoryCompletedJobsList) { skipGenerator: true }, completed_jobs: completed_jobs_object + }, + relatedButtons: { + remediate_inventory: { + ngClick: 'remediateInventory(id, name, insights_credential)', + ngShow: 'insights_credential!==null', + label: i18n._('Remediate Inventory'), + class: 'Form-primaryButton' + } } };}]; diff --git a/awx/ui/client/src/projects/add/projects-add.controller.js b/awx/ui/client/src/projects/add/projects-add.controller.js index 2c90d6448e..a458e1c51e 100644 --- a/awx/ui/client/src/projects/add/projects-add.controller.js +++ b/awx/ui/client/src/projects/add/projects-add.controller.js @@ -7,9 +7,10 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm', 'ProjectsForm', 'Rest', 'Alert', 'ProcessErrors', 'GetBasePath', 'GetProjectPath', 'GetChoices', 'Wait', '$state', 'CreateSelect2', 'i18n', + 'CredentialTypes', function($scope, $location, $stateParams, GenerateForm, ProjectsForm, Rest, Alert, ProcessErrors, GetBasePath, GetProjectPath, GetChoices, Wait, $state, - CreateSelect2, i18n) { + CreateSelect2, i18n, CredentialTypes) { var form = ProjectsForm(), base = $location.path().replace(/^\//, '').split('/')[0], @@ -121,6 +122,7 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm', if ($scope.scm_type.value) { switch ($scope.scm_type.value) { case 'git': + $scope.credentialLabel = "SCM Credential"; $scope.urlPopover = '

' + i18n._('Example URLs for GIT SCM include:') + '