diff --git a/awx/ui/static/js/shared/breadcrumbs/breadcrumbs.controller.js b/awx/ui/static/js/shared/breadcrumbs/breadcrumbs.controller.js index 62e892b8e3..bc6bd7e661 100644 --- a/awx/ui/static/js/shared/breadcrumbs/breadcrumbs.controller.js +++ b/awx/ui/static/js/shared/breadcrumbs/breadcrumbs.controller.js @@ -11,10 +11,11 @@ export default $scope.breadcrumbs = []; - this.addBreadcrumb = function(title, path) { + this.addBreadcrumb = function(title, path, isCurrent) { var breadcrumb = { title: title, - path: path + path: path, + isCurrent: isCurrent }; if ($rootScope.enteredPath === path) { diff --git a/awx/ui/static/js/shared/lodash-as-promised.js b/awx/ui/static/js/shared/lodash-as-promised.js index ef0f77029a..7316c0e021 100644 --- a/awx/ui/static/js/shared/lodash-as-promised.js +++ b/awx/ui/static/js/shared/lodash-as-promised.js @@ -35,7 +35,13 @@ function lodashAsPromised($q) { } function _then(promise, fn) { - return promise.then(fn); + return promise.then(function(value) { + if (_.isFunction(value.then)) { + return _then(value, fn); + } else { + return fn(value); + } + }); } function _catch(promise, fn) { diff --git a/awx/ui/static/js/shared/route-extensions/model-listener.config.js b/awx/ui/static/js/shared/route-extensions/model-listener.config.js index 78feebb8f9..bf4bb5feba 100644 --- a/awx/ui/static/js/shared/route-extensions/model-listener.config.js +++ b/awx/ui/static/js/shared/route-extensions/model-listener.config.js @@ -4,10 +4,16 @@ * All Rights Reserved *************************************************/ +import {wrapDelegate} from './route-params.decorator'; + export default [ '$rootScope', '$routeParams', function($rootScope, $routeParams) { + $rootScope.$on('$routeChangeStart', function(e, newRoute) { + wrapDelegate(newRoute.params); + }); + $rootScope.$on('$routeChangeSuccess', function(e, newRoute) { if (angular.isUndefined(newRoute.model)) { var keys = Object.keys(newRoute.params); diff --git a/awx/ui/static/js/system-tracking/compare-facts/flat.js b/awx/ui/static/js/system-tracking/compare-facts/flat.js index 449db66e7c..5354d94c32 100644 --- a/awx/ui/static/js/system-tracking/compare-facts/flat.js +++ b/awx/ui/static/js/system-tracking/compare-facts/flat.js @@ -7,23 +7,23 @@ export default function flatCompare(facts, nameKey, compareKeys) { - var leftFacts = facts[0]; - var rightFacts = facts[1]; + var comparatorFacts = facts[0]; + var basisFacts = facts[1]; - return rightFacts.reduce(function(arr, rightFact) { + return basisFacts.reduce(function(arr, basisFact) { var searcher = {}; - searcher[nameKey] = rightFact[nameKey]; + searcher[nameKey] = basisFact[nameKey]; var isNewFactValue = false; - var matchingFact = _.where(leftFacts, searcher); + var matchingFact = _.where(comparatorFacts, searcher); var diffs; if (_.isEmpty(matchingFact)) { isNewFactValue = true; diffs = - _.map(rightFact, function(value, key) { + _.map(basisFact, function(value, key) { return { keyName: key, value1: value, value2: '' @@ -34,8 +34,18 @@ export default diffs = _(compareKeys) .map(function(key) { - var leftValue = rightFact[key]; - var rightValue = matchingFact[key]; + var basisValue = basisFact[key]; + var comparatorValue = matchingFact[key]; + var leftValue, rightValue; + + if (basisFacts.position === 'left') { + leftValue = basisValue; + rightValue = comparatorValue; + } else { + rightValue = basisValue; + leftValue = comparatorValue; + } + if (leftValue !== rightValue) { return { keyName: key, @@ -49,7 +59,7 @@ export default } var descriptor = - { displayKeyPath: rightFact[nameKey], + { displayKeyPath: basisFact[nameKey], isNew: isNewFactValue, nestingLevel: 0, facts: diffs diff --git a/awx/ui/static/js/system-tracking/data-services/fact-scan-data.service.js b/awx/ui/static/js/system-tracking/data-services/fact-scan-data.service.js index 01d582680f..7187a4078d 100644 --- a/awx/ui/static/js/system-tracking/data-services/fact-scan-data.service.js +++ b/awx/ui/static/js/system-tracking/data-services/fact-scan-data.service.js @@ -15,7 +15,7 @@ function (Rest, GetBasePath, ProcessErrors, _) { return version .then(function(versionData) { if (_.isEmpty(versionData)) { - return []; + return { fact: [] }; } else { return getFacts(versionData); } @@ -26,8 +26,8 @@ function (Rest, GetBasePath, ProcessErrors, _) { var promise; Rest.setUrl(version.related.fact_view); promise = Rest.get(); - return promise.then(function (data) { - return data.data.fact; + return promise.then(function (response) { + return response.data; }).catch(function (response) { ProcessErrors(null, response.data, response.status, null, { hdr: 'Error!', diff --git a/awx/ui/static/js/system-tracking/data-services/get-data-for-comparison.factory.js b/awx/ui/static/js/system-tracking/data-services/get-data-for-comparison.factory.js index 1eea1edc54..42c8796181 100644 --- a/awx/ui/static/js/system-tracking/data-services/get-data-for-comparison.factory.js +++ b/awx/ui/static/js/system-tracking/data-services/get-data-for-comparison.factory.js @@ -6,17 +6,24 @@ export default [ 'factScanDataService', + 'getModuleOptions', 'lodashAsPromised', - function(factScanDataService, _) { + function(factScanDataService, getModuleOptions, _) { return function(hostIds, moduleName, leftDate, rightDate) { + var moduleOptions; + if (hostIds.length === 1) { hostIds = hostIds.concat(hostIds[0]); } - return _(hostIds) - .promise() - .thenMap(function(hostId, index) { + return getModuleOptions(hostIds[0]) + .then(function(modules) { + moduleOptions = modules; + return modules; + }).then(function() { + return hostIds; + }).thenMap(function(hostId, index) { var date = leftDate; var fetchScanNumber; @@ -42,6 +49,8 @@ export default .bind(factScanDataService); return getHostFacts(params); + }).then(function(hostFacts) { + return [moduleOptions, hostFacts]; }); }; } diff --git a/awx/ui/static/js/system-tracking/data-services/get-module-options.factory.js b/awx/ui/static/js/system-tracking/data-services/get-module-options.factory.js new file mode 100644 index 0000000000..5ea39f2480 --- /dev/null +++ b/awx/ui/static/js/system-tracking/data-services/get-module-options.factory.js @@ -0,0 +1,70 @@ +var moduleConfig = + { 'packages': + { compareKey: ['release', 'version'], + nameKey: 'name', + displayType: 'flat', + sortKey: 1 + }, + 'services': + { compareKey: ['state', 'source'], + nameKey: 'name', + displayType: 'flat', + sortKey: 2 + }, + 'files': + { compareKey: ['size', 'mode', 'md5', 'mtime', 'gid', 'uid'], + nameKey: 'path', + displayType: 'flat', + sortKey: 3 + }, + 'ansible': + { displayType: 'nested', + sortKey: 4 + }, + 'custom': + { displayType: 'nested' + } + }; + +function makeModule(option, index) { + var name = option[0]; + var displayName = option[1]; + var config = moduleConfig.hasOwnProperty(name) ? + moduleConfig[name] : moduleConfig.custom; + var modulesCount = _.keys(moduleConfig).length - 1; + + config.name = name; + config.displayName = displayName; + + // Use index to sort custom modules, + // offset by built-in modules since + // they have a hardcoded sort key + // + if (_.isUndefined(config.sortKey)) { + config.sortKey = (index - 1) + modulesCount; + } + + return config; +} + +function factory(hostId, rest, getBasePath, _) { + var url = [ getBasePath('hosts') + hostId, + 'fact_versions' + ].join('/'); + + rest.setUrl(url); + return _(rest.options()) + .then(function(response) { + var choices = response.data.actions.GET.module.choices; + return _.sortBy(choices, '1'); + }).thenMap(makeModule); +} + +export default + [ 'Rest', + 'GetBasePath', + 'lodashAsPromised', + function(rest, getBasePath, lodash) { + return _.partialRight(factory, rest, getBasePath, lodash); + } + ]; diff --git a/awx/ui/static/js/system-tracking/fact-module-filter.block.less b/awx/ui/static/js/system-tracking/fact-module-filter.block.less index b8cef91cdc..6545cf4245 100644 --- a/awx/ui/static/js/system-tracking/fact-module-filter.block.less +++ b/awx/ui/static/js/system-tracking/fact-module-filter.block.less @@ -3,9 +3,13 @@ .FactModuleFilter { width: 100%; display: flex; + flex-flow: row wrap; margin-bottom: 2.8rem; &-module { - flex: 1; + flex: 1 1 20%; + @media screen and (max-width: 750px) { + flex-basis: 50%; + } &--isActive { // copied from bootstrap's .btn:focus background-color: #ebebeb; diff --git a/awx/ui/static/js/system-tracking/main.js b/awx/ui/static/js/system-tracking/main.js index 7f20e86d89..be08358455 100644 --- a/awx/ui/static/js/system-tracking/main.js +++ b/awx/ui/static/js/system-tracking/main.js @@ -7,6 +7,7 @@ import route from './system-tracking.route'; import factScanDataService from './data-services/fact-scan-data.service'; import getDataForComparison from './data-services/get-data-for-comparison.factory'; +import getModuleOptions from './data-services/get-module-options.factory'; import controller from './system-tracking.controller'; import stringOrDateFilter from './string-or-date.filter'; import shared from 'tower/shared/main'; @@ -22,6 +23,7 @@ export default ]) .service('factScanDataService', factScanDataService) .factory('getDataForComparison', getDataForComparison) + .factory('getModuleOptions', getModuleOptions) .filter('stringOrDate', stringOrDateFilter) .controller('systemTracking', controller) .config(['$routeProvider', function($routeProvider) { diff --git a/awx/ui/static/js/system-tracking/search-date-range.js b/awx/ui/static/js/system-tracking/search-date-range.js index 6fe497b18b..c2b5141a2a 100644 --- a/awx/ui/static/js/system-tracking/search-date-range.js +++ b/awx/ui/static/js/system-tracking/search-date-range.js @@ -4,9 +4,23 @@ * All Rights Reserved *************************************************/ -export function searchDateRange(date) { +export function searchDateRange(dateString) { + var date; + + switch(dateString) { + case 'yesterday': + date = moment().subtract(1, 'day'); + break; + case 'tomorrow': + date = moment().add(1, 'day'); + break; + default: + date = moment(dateString); + } + + return { - from: moment(date).startOf('day'), - to: moment(date).endOf('day') + from: date.clone().startOf('day'), + to: date.clone().endOf('day') }; } diff --git a/awx/ui/static/js/system-tracking/system-tracking.controller.js b/awx/ui/static/js/system-tracking/system-tracking.controller.js index 493bdc5118..da1a249664 100644 --- a/awx/ui/static/js/system-tracking/system-tracking.controller.js +++ b/awx/ui/static/js/system-tracking/system-tracking.controller.js @@ -19,90 +19,124 @@ function controller($rootScope, _) { // var inventoryId = $routeParams.id; var hostIds = $routeParams.hosts.split(','); + var hosts = $routeParams.model.hosts; + + $scope.hostIds = $routeParams.hosts; + $scope.inventory = $routeParams.model.inventory; $scope.factModulePickersLabelLeft = "Compare facts collected on"; $scope.factModulePickersLabelRight = "To facts collected on"; - $scope.modules = - [{ name: 'packages', - displayName: 'Packages', - compareKey: ['release', 'version'], - nameKey: 'name', - isActive: true, - displayType: 'flat' - }, - { name: 'services', - compareKey: ['state', 'source'], - nameKey: 'name', - displayName: 'Services', - isActive: false, - displayType: 'flat' - }, - { name: 'files', - displayName: 'Files', - nameKey: 'path', - compareKey: ['size', 'mode', 'md5', 'mtime', 'gid', 'uid'], - isActive: false, - displayType: 'flat' - }, - { name: 'ansible', - displayName: 'Ansible', - isActive: false, - displayType: 'nested' - } - ]; + $scope.modules = initialFactData.moduleOptions; // Use this to determine how to orchestrate the services var viewType = hostIds.length > 1 ? 'multiHost' : 'singleHost'; var searchConfig = - { leftDate: initialFactData.leftDate, - rightDate: initialFactData.rightDate + { leftRange: initialFactData.leftSearchRange, + rightRange: initialFactData.rightSearchRange }; - $scope.leftDate = initialFactData.leftDate.from; - $scope.rightDate = initialFactData.rightDate.from; + $scope.leftDate = initialFactData.leftSearchRange.from; + $scope.rightDate = initialFactData.rightSearchRange.from; function setHeaderValues(viewType) { if (viewType === 'singleHost') { - $scope.comparisonLeftHeader = $scope.leftDate; - $scope.comparisonRightHeader = $scope.rightDate; + $scope.comparisonLeftHeader = $scope.leftScanDate; + $scope.comparisonRightHeader = $scope.rightScanDate; } else { - $scope.comparisonLeftHeader = hostIds[0]; - $scope.comparisonRightHeader = hostIds[1]; + $scope.comparisonLeftHeader = hosts[0].name; + $scope.comparisonRightHeader = hosts[1].name; } } function reloadData(params, initialData) { + searchConfig = _.merge({}, searchConfig, params); var factData = initialData; - var leftDate = searchConfig.leftDate; - var rightDate = searchConfig.rightDate; + var leftRange = searchConfig.leftRange; + var rightRange = searchConfig.rightRange; var activeModule = searchConfig.module; + var leftScanDate, rightScanDate; + if (!factData) { - factData = getDataForComparison( - hostIds, - activeModule.name, - leftDate, - rightDate); + factData = + getDataForComparison( + hostIds, + activeModule.name, + leftRange, + rightRange) + .thenAll(function(factDataAndModules) { + var responses = factDataAndModules[1]; + var data = _.pluck(responses, 'fact'); + + leftScanDate = moment(responses[0].timestamp); + rightScanDate = moment(responses[1].timestamp); + + return data; + }, true); } waitIndicator('start'); - _(factData) - .thenAll(_.partial(compareFacts, activeModule)) + return _(factData) + .thenAll(function(facts) { + // Make sure we always start comparison against + // a non-empty array + // + // Partition with _.isEmpty will give me an array + // with empty arrays in index 0, and non-empty + // arrays in index 1 + // + + // Save the position of the data so we + // don't lose it later + + facts[0].position = 'left'; + facts[1].position = 'right'; + + var splitFacts = _.partition(facts, _.isEmpty); + var emptyScans = splitFacts[0]; + var nonEmptyScans = splitFacts[1]; + + if (_.isEmpty(nonEmptyScans)) { + // we have NO data, throw an error + throw { + name: 'NoScanData', + message: 'No scans ran on eithr of the dates you selected. Please try selecting different dates.', + dateValues: + { leftDate: $scope.leftDate.clone(), + rightDate: $scope.rightDate.clone() + } + }; + } else if (nonEmptyScans.length === 1) { + // one of them is not empty, throw an error + throw { + name: 'InsufficientScanData', + message: 'No scans ran on one of the selected dates. Please try selecting a different date.', + dateValue: emptyScans[0].position === 'left' ? $scope.leftDate.clone() : $scope.rightDate.clone() + }; + } + + // all scans have data, rejoice! + return facts; + + }) + .then(_.partial(compareFacts, activeModule)) .then(function(info) { + // Clear out any errors from the previous run... + $scope.error = null; + $scope.factData = info; - setHeaderValues(viewType); + setHeaderValues(viewType, leftScanDate, rightScanDate); }).finally(function() { waitIndicator('stop'); - }) - .value(); + }); } $scope.setActiveModule = function(newModuleName, initialData) { @@ -120,9 +154,12 @@ function controller($rootScope, $location.replace(); $location.search('module', newModuleName); - reloadData( - { module: newModule - }, initialData); + reloadData({ module: newModule + }, initialData) + + .catch(function(error) { + $scope.error = error; + }).value(); }; function dateWatcher(dateProperty) { @@ -141,13 +178,16 @@ function controller($rootScope, var params = {}; params[dateProperty] = newDate; - reloadData(params); + reloadData(params) + .catch(function(error) { + $scope.error = error; + }).value(); }; } - $scope.$watch('leftDate', dateWatcher('leftDate'), true); + $scope.$watch('leftDate', dateWatcher('leftRange'), true); - $scope.$watch('rightDate', dateWatcher('rightDate'), true); + $scope.$watch('rightDate', dateWatcher('rightRange'), true); $scope.setActiveModule(initialFactData.moduleName, initialFactData); } diff --git a/awx/ui/static/js/system-tracking/system-tracking.partial.html b/awx/ui/static/js/system-tracking/system-tracking.partial.html index a05967ef5d..ab3732d5c8 100644 --- a/awx/ui/static/js/system-tracking/system-tracking.partial.html +++ b/awx/ui/static/js/system-tracking/system-tracking.partial.html @@ -1,79 +1,92 @@ -
-
- {{ factModulePickersLabelLeft }} - -
-
- {{ factModulePickersLabelRight }} - -
-
+
+ + + + + - - -
-

- There were no facts collected for that module in the selected date range. Please pick a different range or module and try again. -

-
-
-
-

{{comparisonLeftHeader|stringOrDate:'L'}}

-

{{comparisonRightHeader|stringOrDate:'L'}}

-
-
-
-

- {{group.displayKeyPath}} -

-

- {{group.displayKeyPath}} -

-

- {{group.displayKeyPath}} -

-
- {{group.displayKeyPath}} -
+
+
+ {{ factModulePickersLabelLeft }} +
-
-
-
-

+

+ {{ factModulePickersLabelRight }} + +
+
+ + + +
+

+ There were no facts collected on the dates you selected ({{error.dateValues.leftDate|amDateFormat:'L'}} and {{error.dateValues.rightDate|amDateFormat:'L'}}). Please pick a different range or module and try again. +

+

+ There were no facts collected on one of the dates you selected ({{error.dateValue|amDateFormat:'L'}}). Please select a different date and try again. +

+

+ To setup or run scan jobs, edit the "{{inventory.name}}" inventory and select "Scan Jobs Templates". +

+
+
+
+

{{comparisonLeftHeader|stringOrDate:'L LT'}}

+

{{comparisonRightHeader|stringOrDate:'L LT'}}

+
+
+
+

+ {{group.displayKeyPath}} +

+

+ {{group.displayKeyPath}} +

+

+ {{group.displayKeyPath}} +

+
+ {{group.displayKeyPath}} +
+
+
+
+
+

+ {{fact.keyName}} +

+

+ {{fact.value1}} +

+

+ {{fact.value2}} +

+
+
+
+

{{fact.keyName}}

-

+

{{fact.value1}}

-

+

{{fact.value2}}

-
-

- {{fact.keyName}} -

-

- {{fact.value1}} -

-

- {{fact.value2}} -

-
-
+
diff --git a/awx/ui/static/js/system-tracking/system-tracking.route.js b/awx/ui/static/js/system-tracking/system-tracking.route.js index ca78ed6b61..26cc66472b 100644 --- a/awx/ui/static/js/system-tracking/system-tracking.route.js +++ b/awx/ui/static/js/system-tracking/system-tracking.route.js @@ -21,8 +21,8 @@ export default { var hostIds = $route.current.params.hosts.split(','); var moduleParam = $location.search().module || 'packages'; - var leftDate = searchDateRange('2015-05-26'); - var rightDate = searchDateRange('2015-05-26'); + var leftDate = searchDateRange('yesterday'); + var rightDate = searchDateRange(); if (hostIds.length === 1) { hostIds = hostIds.concat(hostIds[0]); @@ -30,13 +30,23 @@ export default { var data = getDataForComparison(hostIds, moduleParam, leftDate, rightDate). - thenThru(function(factData) { - factData.leftDate = leftDate; - factData.rightDate = rightDate; - factData.moduleName = moduleParam; - return factData; - }) - .value(); + thenAll(function(factDataAndModules) { + var moduleOptions = factDataAndModules[0]; + var factResponses = factDataAndModules[1]; + var factData = _.pluck(factResponses, 'fact'); + + factData.leftSearchRange = leftDate; + factData.rightSearchRange = rightDate; + + factData.leftScanDate = moment(factResponses[0].timestamp); + factData.rightScanDate = moment(factResponses[0].timestamp); + + factData.moduleName = moduleParam; + factData.moduleOptions = moduleOptions; + + return factData; + }, true) + .value(); return data; @@ -48,8 +58,8 @@ export default { 'Rest', 'GetBasePath', function($route, $q, rest, getBasePath) { - if ($route.current.params.inventory) { - return $q.when(true); + if ($route.current.params.hasModelKey('inventory')) { + return $q.when($route.current.params.model.inventory); } var inventoryId = $route.current.params.inventory; @@ -62,17 +72,17 @@ export default { }); } ], - filters: + hosts: [ '$route', '$q', 'Rest', 'GetBasePath', function($route, $q, rest, getBasePath) { - if ($route.current.params.hosts) { - return $q.when(true); + if ($route.current.params.hasModelKey('hosts')) { + return $q.when($route.current.params.model.hosts); } - var hostIds = $route.current.params.filters.split(','); + var hostIds = $route.current.params.hosts.split(','); var hosts = hostIds.map(function(hostId) { diff --git a/awx/ui/tests/unit/system-tracking/single-host-data.service-test.js b/awx/ui/tests/unit/system-tracking/single-host-data.service-test.js index 7e9f88c626..fd054c2b38 100644 --- a/awx/ui/tests/unit/system-tracking/single-host-data.service-test.js +++ b/awx/ui/tests/unit/system-tracking/single-host-data.service-test.js @@ -37,7 +37,7 @@ describeModule(systemTracking.name) "module" : "package", "timestamp": '2015-05-07T14:57:37', "related" : { - "fact_view" : "/api/v1/hosts/1/fact_view/?module=packages&datetime=2015-05-07T14%3A57%3A37Z" + "fact_view" : "/hosts/1/fact_versions/?module=packages&from=2015-05-05T00:00:00-04:00&to=2015-05-06T00:00:00-04:00" } }, result = { @@ -46,7 +46,10 @@ describeModule(systemTracking.name) } }; - var actual = service.getFacts(version); + var actual = service.getFacts(version) + .then(function(response) { + return response.fact; + }); restStub.succeedAt(version.related.fact_view, result); restStub.flush();