diff --git a/awx/ui/static/js/shared/format-epoch/format-epoch.filter.js b/awx/ui/static/js/shared/format-epoch/format-epoch.filter.js new file mode 100644 index 0000000000..2216249732 --- /dev/null +++ b/awx/ui/static/js/shared/format-epoch/format-epoch.filter.js @@ -0,0 +1,16 @@ +export default +[ 'moment', + function(moment) { + return function(seconds, formatStr) { + if (!formatStr) { + formatStr = 'll LT'; + } + + var millis = seconds * 1000; + + return moment(millis).format(formatStr); + }; + } +]; + + diff --git a/awx/ui/static/js/shared/format-epoch/main.js b/awx/ui/static/js/shared/format-epoch/main.js new file mode 100644 index 0000000000..916a81c7ab --- /dev/null +++ b/awx/ui/static/js/shared/format-epoch/main.js @@ -0,0 +1,9 @@ +import formatEpoch from './format-epoch.filter'; +import moment from 'tower/shared/moment/main'; + +export default + angular.module('formatEpoch', + [ moment.name + ]) + .filter('formatEpoch', formatEpoch); + diff --git a/awx/ui/static/js/system-tracking/compare-facts.js b/awx/ui/static/js/system-tracking/compare-facts.js index 092c613361..b8e466f00a 100644 --- a/awx/ui/static/js/system-tracking/compare-facts.js +++ b/awx/ui/static/js/system-tracking/compare-facts.js @@ -9,26 +9,39 @@ import compareFlatFacts from './compare-facts/flat'; import FactTemplate from './compare-facts/fact-template'; export function compareFacts(module, facts) { + + + var renderOptions = _.merge({}, module); + // If the module has a template or includes a list of keys to display, // then perform a flat comparison, otherwise assume nested // - if (module.factTemplate || module.nameKey) { + if (renderOptions.factTemplate || renderOptions.nameKey) { // For flat structures we compare left-to-right, then right-to-left to // make sure we get a good comparison between both hosts - var compare = _.partialRight(compareFlatFacts, - module.nameKey, - module.compareKey, - new FactTemplate(module.factTemplate)); - var leftToRight = compare(facts[0], facts[1]); - var rightToLeft = compare(facts[1], facts[0]); + if (_.isPlainObject(renderOptions.factTemplate)) { + renderOptions.factTemplate = + _.mapValues(renderOptions.factTemplate, function(template) { + if (typeof template === 'string' || typeof template === 'function') { + return new FactTemplate(template); + } else { + return template; + } + }); + } else { + renderOptions.factTemplate = new FactTemplate(renderOptions.factTemplate); + } + + var leftToRight = compareFlatFacts(facts[0], facts[1], renderOptions); + var rightToLeft = compareFlatFacts(facts[1], facts[0], renderOptions); return _(leftToRight) .concat(rightToLeft) .unique('displayKeyPath') .thru(function(result) { return { factData: result, - isNestedDisplay: _.isUndefined(module.factTemplate) + isNestedDisplay: _.isPlainObject(renderOptions.factTemplate) }; }) .value(); diff --git a/awx/ui/static/js/system-tracking/compare-facts/fact-template.js b/awx/ui/static/js/system-tracking/compare-facts/fact-template.js new file mode 100644 index 0000000000..d4c0e2d5d8 --- /dev/null +++ b/awx/ui/static/js/system-tracking/compare-facts/fact-template.js @@ -0,0 +1,33 @@ +import stringFilters from 'tower/shared/string-filters/main'; +import formatEpoch from 'tower/shared/format-epoch/main'; + +var $injector = angular.injector(['ng', stringFilters.name, formatEpoch.name]); +var $interpolate = $injector.get('$interpolate'); + +function FactTemplate(templateString) { + this.templateString = templateString; +} + +function loadFactTemplate(factTemplate, fact) { + if (_.isFunction(factTemplate)) { + return factTemplate(fact); + } else { + return factTemplate; + } +} + +FactTemplate.prototype.render = function(factData) { + + if (_.isUndefined(factData) || _.isEmpty(factData)) { + return 'absent'; + } + + var template = loadFactTemplate(this.templateString, factData); + return $interpolate(template)(factData); +}; + +FactTemplate.prototype.hasTemplate = function() { + return !_.isUndefined(this.templateString); +}; + +export default FactTemplate; 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 b9a7ffd7df..2c2615158f 100644 --- a/awx/ui/static/js/system-tracking/compare-facts/flat.js +++ b/awx/ui/static/js/system-tracking/compare-facts/flat.js @@ -27,7 +27,6 @@ export default var nameKey = renderOptions.nameKey; var compareKeys = renderOptions.compareKey; var keyNameMap = renderOptions.keyNameMap; - var valueFormatter = renderOptions.valueFormatter; var factTemplate = renderOptions.factTemplate; @@ -35,8 +34,6 @@ export default var searcher = {}; searcher[nameKey] = basisFact[nameKey]; - var slottedValues, basisValue, comparatorValue; - var matchingFact = _.where(comparatorFacts.facts, searcher); var diffs; @@ -50,53 +47,75 @@ export default var comparisonResults = _.reduce(compareKeys, function(result, compareKey) { - if (_.isEmpty(matchingFact)) { - comparatorValue = 'absent'; - } else { - comparatorValue = matchingFact[0][compareKey]; - } + var comparatorFact = matchingFact[0] || {}; + var isNestedDisplay = false; var slottedValues = slotFactValues(basisFacts.position, basisFact[compareKey], - comparatorValue); + comparatorFact[compareKey]); if (_.isUndefined(slottedValues.left) && _.isUndefined(slottedValues.right)) { return result; } + var template = factTemplate; + + if (_.isObject(template) && template.hasOwnProperty(compareKey)) { + template = template[compareKey]; + + // 'true' means render the key without formatting + if (template === true) { + template = + { render: function(fact) { return fact[compareKey]; } + }; + } + + isNestedDisplay = true; + } else if (typeof template.hasTemplate === 'function' && !template.hasTemplate()) { + template = + { render: function(fact) { return fact[compareKey]; } + }; + isNestedDisplay = true; + } else if (typeof factTemplate.render === 'function') { + template = factTemplate; + } else if (!template.hasOwnProperty(compareKey)) { + return result; + } + + if (basisFacts.position === 'left') { + slottedValues.left = template.render(basisFact); + slottedValues.right = template.render(comparatorFact); + } else { + slottedValues.left = template.render(comparatorFact); + slottedValues.right = template.render(basisFact); + } + if (slottedValues.left !== slottedValues.right) { slottedValues.isDivergent = true; } else { slottedValues.isDivergent = false; } - result[compareKey] = slottedValues; + if (isNestedDisplay) { + result[compareKey] = slottedValues; + } else { + result = slottedValues; + } return result; }, {}); - var hasDiffs = _.any(comparisonResults, { isDivergent: true }); + var hasDiffs = + _.any(comparisonResults, { isDivergent: true }) || + comparisonResults.isDivergent === true; - if (hasDiffs && factTemplate.hasTemplate()) { + if (hasDiffs && typeof factTemplate.render === 'function') { - basisValue = factTemplate.render(basisFact); - - if (_.isEmpty(matchingFact)) { - comparatorValue = 'absent'; - } else { - comparatorValue = factTemplate.render(matchingFact[0]); - } - - if (!_.isEmpty(comparisonResults)) { - - slottedValues = slotFactValues(basisFact.position, basisValue, comparatorValue); - - diffs = - { keyName: basisFact[nameKey], - value1: slottedValues.left, - value2: slottedValues.right - }; - } + diffs = + { keyName: basisFact[nameKey], + value1: comparisonResults.left, + value2: comparisonResults.right + }; } else if (hasDiffs) { @@ -111,9 +130,7 @@ export default return { keyName: keyName, value1: slottedValues.left, - value1IsAbsent: slottedValues.left === 'absent', value2: slottedValues.right, - value2IsAbsent: slottedValues.right === 'absent', isDivergent: slottedValues.isDivergent }; }).compact() 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 index b55c262d51..f780cb12f7 100644 --- 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 @@ -13,8 +13,16 @@ var moduleConfig = }, 'files': { compareKey: ['size', 'mode', 'md5', 'mtime', 'gid', 'uid'], + keyNameMap: + { 'uid': 'ownership' + }, + factTemplate: + { 'uid': 'user id: {{uid}}, group id: {{gid}}', + 'mode': true, + 'md5': true, + 'mtime': '{{mtime|formatEpoch}}' + }, nameKey: 'path', - displayKeys: ['size', 'mode', 'mtime', 'uid', 'gid', 'md5'], sortKey: 3 }, 'ansible': diff --git a/awx/ui/tests/unit/system-tracking/compare-facts/flat-test.js b/awx/ui/tests/unit/system-tracking/compare-facts/flat-test.js index 076e527f66..2995be4caa 100644 --- a/awx/ui/tests/unit/system-tracking/compare-facts/flat-test.js +++ b/awx/ui/tests/unit/system-tracking/compare-facts/flat-test.js @@ -3,14 +3,31 @@ import compareFacts from 'tower/system-tracking/compare-facts/flat'; /* jshint node: true */ /* globals -expect, -_ */ -var chai = require('chai'); -var _ = require('lodash'); -var chaiThings = require('chai-things'); +var _, expect; -chai.use(chaiThings); +// This makes this test runnable in node OR karma. The sheer +// number of times I had to run this test made the karma +// workflow just too dang slow for me. Maybe this can +// be a pattern going forward? Not sure... +// +(function(global) { + var chai = global.chai || require('chai'); -global.expect = chai.expect; -global._ = _; + if (typeof window === 'undefined') { + var chaiThings = global.chaiThings || require('chai-things'); + chai.use(chaiThings); + } + + _ = global._ || require('lodash'); + expect = global.expect || chai.expect; + + global.expect = expect; + + + + global._ = _; + +})(typeof window === 'undefined' ? global : window); describe('CompareFacts.Flat', function() { @@ -92,16 +109,12 @@ describe('CompareFacts.Flat', function() { facts: [{ keyName: 'value', value1: 'bar', - value1IsAbsent: false, value2: 'baz', - value2IsAbsent: false, isDivergent: true }, { keyName: 'extra', value1: 'doo', - value1IsAbsent: false, value2: 'doo', - value2IsAbsent: false, isDivergent: false }] }]); @@ -130,16 +143,12 @@ describe('CompareFacts.Flat', function() { facts: [{ keyName: 'value', value1: 'bar', - value1IsAbsent: false, value2: 'baz', - value2IsAbsent: false, isDivergent: true }, { keyName: 'extra', value1: 'doo', - value1IsAbsent: false, value2: 'doo', - value2IsAbsent: false, isDivergent: false }] }]); @@ -172,48 +181,131 @@ describe('CompareFacts.Flat', function() { expect(result[0].facts).to.include.something.that.deep.equals( { keyName: 'blah', value1: 'doo', - value1IsAbsent: false, value2: 'doo', - value2IsAbsent: false, isDivergent: false }); }); - // it('allows formatting values with valueFormat parameter', function() { - // var valueFormat = - // function(key, values) { - // if (key === 'extra') { - // return 'formatted'; - // } - // } + it('allows flattening values with factTemplate parameter', function() { + var factTemplate = + { hasTemplate: + function() { + return true; + }, + render: function(fact) { + return 'value: ' + fact.value; + } + }; - // var result = compareFacts( - // { position: 'left', - // facts: - // [{ 'name': 'foo', - // 'value': 'bar', - // 'extra': 'doo' - // }] - // }, - // { position: 'right', - // facts: - // [{ 'name': 'foo', - // 'value': 'baz', - // 'extra': 'doo' - // }] - // }, 'name', ['value', 'extra', 'blah'], keyNameMap, defaultTemplate, ); + var result = compareFacts( + { position: 'left', + facts: + [{ 'name': 'foo', + 'value': 'bar', + 'extra': 'doo' + }] + }, + { position: 'right', + facts: + [{ 'name': 'foo', + 'value': 'baz', + 'extra': 'doo' + }] + }, options({ compareKey: ['value'], + factTemplate: factTemplate + })); - // expect(result[0].facts).to.include.something.that.deep.equals( - // { keyName: 'extra', - // value1: 'formatted', - // value1IsAbsent: false, - // value2: 'formatted', - // value2IsAbsent: false, - // isDivergent: false - // }); + expect(result[0].facts).to.deep.equal( + { keyName: 'foo', + value1: 'value: bar', + value2: 'value: baz' + }); + }); - // }); + it('allows formatting values with factTemplate parameter', function() { + var values = ['value1', 'value2']; + var factTemplate = + { 'value': + { hasTemplate: function() { + return true; + }, + render: function() { + return values.shift(); + } + }, + 'extra': true + }; + + var result = compareFacts( + { position: 'left', + facts: + [{ 'name': 'foo', + 'value': 'bar', + 'extra': 'doo' + }] + }, + { position: 'right', + facts: + [{ 'name': 'foo', + 'value': 'baz', + 'extra': 'doo' + }] + }, options({ compareKey: ['value'], + factTemplate: factTemplate + })); + + expect(result[0].facts).to.include.something.that.deep.equals( + { keyName: 'value', + value1: 'value1', + value2: 'value2', + isDivergent: true + }, + { keyName: 'extra', + value1: 'doo', + value2: 'doo', + isDivergent: false + }); + + }); + + it('compares values using the formatted values, not the raw ones', function() { + var values = ['value1', 'value2']; + var factTemplate = + { 'extra': + { render: function() { + return values.shift(); + } + } + }; + + var result = compareFacts( + { position: 'left', + facts: + [{ 'name': 'foo', + 'value': 'bar', + 'extra': 'doo' + }] + }, + { position: 'right', + facts: + [{ 'name': 'foo', + 'value': 'bar', + 'extra': 'doo' + }] + }, options({ factTemplate: factTemplate, + compareKey: ['extra'] + })); + + expect(result.length).to.be.greaterThan(0); + expect(result[0].facts).to.include.something.that.deep.equals( + { keyName: 'extra', + value1: 'value1', + value2: 'value2', + isDivergent: true + }); + + }); }); @@ -230,7 +322,7 @@ describe('CompareFacts.Flat', function() { return facts; } - it('uses "absent" for the missing value', function() { + it('keeps missing values as undefined', function() { var facts = factData([{ 'name': 'foo', 'value': 'bar' @@ -246,14 +338,45 @@ describe('CompareFacts.Flat', function() { facts: [{ keyName: 'value', value1: 'bar', - value1IsAbsent: false, - value2: 'absent', - value2IsAbsent: true, + value2: undefined, isDivergent: true }] }]); }); + it('still keeps missing values as undefined when using a template', function() { + + var factTemplate = + { hasTemplate: + function() { + return true; + }, + render: + function(fact) { + return fact.value; + } + }; + + var facts = factData([{ 'name': 'foo', + 'value': 'bar' + }]); + + var result = compareFacts(facts[0], facts[1], + options({ compareKey: ['value'], + factTemplate: factTemplate + })); + + expect(result).to.deep.equal( + [{ displayKeyPath: 'foo', + nestingLevel: 0, + facts: + { keyName: 'foo', + value1: 'bar', + value2: undefined + } + }]); + }); + it('includes given compare keys from basisFacts', function() { var facts = factData([{ 'name': 'foo', 'value': 'bar', @@ -270,45 +393,18 @@ describe('CompareFacts.Flat', function() { facts: [{ keyName: 'value', value1: 'bar', - value1IsAbsent: false, - value2: 'absent', - value2IsAbsent: true, + value2: undefined, isDivergent: true }, { keyName: 'extra', value1: 'doo', - value1IsAbsent: false, - value2: 'absent', - value2IsAbsent: true, + value2: undefined, isDivergent: true }] }]); }); - context('with factTemplate', function() { - it('does not attempt to render the absent fact', function() { - var facts = factData([{ 'name': 'foo' - }]); - - var renderCallCount = 0; - var factTemplate = - { render: function() { - renderCallCount++; - }, - hasTemplate: function() { return true; }, - template: "" - }; - - compareFacts(facts[0], facts[1], - options({ compareKey: ['value'], - factTemplate: factTemplate - })); - - expect(renderCallCount).to.equal(1); - - }); - }); }); context('with factTemplate', function() {