Merge branch 'devel' of https://github.com/jmferrer/awx into change_openshift_vars_path

This commit is contained in:
jmferrer 2018-10-16 09:55:05 +02:00
commit f6600887bc
37 changed files with 13 additions and 2172 deletions

View File

@ -3073,11 +3073,6 @@ class JobTemplateSurveySpec(GenericAPIView):
return Response(dict(error=_(
"The {min_or_max} limit in survey question {idx} expected to be integer."
).format(min_or_max=key, **context)))
if 'choices' in survey_item:
if not isinstance(survey_item['choices'], six.string_types):
return Response(dict(error=_(
"Choices in survey question {idx} expected to be string."
).format(**context)))
if qtype in ['multiplechoice', 'multiselect'] and 'choices' not in survey_item:
return Response(dict(error=_(
"Survey question {idx} of type {survey_item[type]} must specify choices.".format(**context)

View File

@ -145,7 +145,9 @@ class PoolWorker(object):
# if this process has any pending messages requeue them
for _ in range(self.qsize):
try:
orphaned.append(self.queue.get(block=False))
message = self.queue.get(block=False)
if message != 'QUIT':
orphaned.append(message)
except QueueEmpty:
break # qsize is not always _totally_ up to date
if len(orphaned):
@ -328,11 +330,12 @@ class AutoscalePool(WorkerPool):
# send them to another worker
logger.error('worker pid:{} is gone (exit={})'.format(w.pid, w.exitcode))
if w.current_task:
try:
for j in UnifiedJob.objects.filter(celery_task_id=w.current_task['uuid']):
reaper.reap_job(j, 'failed')
except Exception:
logger.exception('failed to reap job UUID {}'.format(w.current_task['uuid']))
if w.current_task != 'QUIT':
try:
for j in UnifiedJob.objects.filter(celery_task_id=w.current_task['uuid']):
reaper.reap_job(j, 'failed')
except Exception:
logger.exception('failed to reap job UUID {}'.format(w.current_task['uuid']))
orphaned.extend(w.orphaned_tasks)
self.workers.remove(w)
elif w.idle and len(self.workers) > self.min_workers:

View File

@ -383,7 +383,6 @@ class TestSurveySpecValidation:
({'type': 'foo'}, 'allowed question types'),
({'type': u'🐉'}, 'allowed question types'),
({'type': 'multiplechoice'}, 'multiplechoice must specify choices'),
({'type': 'multiplechoice', 'choices': 45}, 'Choices in survey question 0 expected to be string'),
({'type': 'integer', 'min': 'foo'}, 'min limit in survey question 0 expected to be integer'),
({'question_name': 42}, "'question_name' in survey question 0 expected to be string.")
])

View File

@ -113,11 +113,6 @@
@import '../../src/shared/upgrade/upgrade.block.less';
@import '../../src/smart-status/smart-status.block.less';
@import '../../src/workflow-results/standard-out.block.less';
@import '../../src/system-tracking/date-picker/date-picker.block.less';
@import '../../src/system-tracking/fact-data-table/fact-data-table.block.less';
@import '../../src/system-tracking/fact-module-filter.block.less';
@import '../../src/system-tracking/fact-module-pickers.block.less';
@import '../../src/system-tracking/system-tracking-container.block.less';
@import '../../src/templates/prompt/prompt.block.less';
@import '../../src/templates/job_templates/multi-credential/multi-credential.block.less';
@import '../../src/templates/labels/labelsList.block.less';

View File

@ -10,7 +10,6 @@ if ($basePath) {
}
import start from './app.start';
import systemTracking from './system-tracking/main';
import inventoriesHosts from './inventories-hosts/main';
import inventoryScripts from './inventory-scripts/main';
import credentials from './credentials/main';
@ -36,6 +35,7 @@ import RestServices from './rest/main';
import access from './access/main';
import scheduler from './scheduler/main';
import instanceGroups from './instance-groups/main';
import shared from './shared/main';
import atFeatures from '~features';
import atLibComponents from '~components';
@ -62,13 +62,13 @@ angular
'gettext',
'Timezones',
'lrInfiniteScroll',
shared.name,
about.name,
access.name,
license.name,
RestServices.name,
browserData.name,
configuration.name,
systemTracking.name,
inventoriesHosts.name,
inventoryScripts.name,
credentials.name,

View File

@ -80,8 +80,6 @@ export default ['$scope', 'NestedHostsListDefinition', '$rootScope', 'GetBasePat
$scope.hostsSelected = null;
}
}
$scope.systemTrackingDisabled = ($scope.hostsSelected && $scope.hostsSelected.length > 2) ? true : false;
});
}
@ -160,15 +158,6 @@ export default ['$scope', 'NestedHostsListDefinition', '$rootScope', 'GetBasePat
});
};
$scope.systemTracking = function(){
var hostIds = _.map($scope.hostsSelected, (host) => host.id);
$state.go('systemTracking', {
inventoryId: $state.params.inventory_id,
hosts: $scope.hostsSelected,
hostIds: hostIds
});
};
$scope.setAdhocPattern = function(){
var pattern = _($scope.hostsSelected)
.map(function(item){

View File

@ -73,8 +73,6 @@ export default ['$scope', 'ListDefinition', '$rootScope', 'GetBasePath',
$scope.hostsSelected = null;
}
}
$scope.systemTrackingDisabled = ($scope.hostsSelected && $scope.hostsSelected.length > 2) ? true : false;
});
}
@ -153,15 +151,6 @@ export default ['$scope', 'ListDefinition', '$rootScope', 'GetBasePath',
$state.go('inventories.addSmartInventory');
};
$scope.systemTracking = function(){
var hostIds = _.map($scope.hostsSelected, (host) => host.id);
$state.go('systemTracking', {
inventoryId: $state.params.inventory_id ? $state.params.inventory_id : $state.params.smartinventory_id,
hosts: $scope.hostsSelected,
hostIds: hostIds
});
};
$scope.setAdhocPattern = function(){
var pattern = _($scope.hostsSelected)
.map(function(item){

View File

@ -1,129 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
function lodashAsPromised($q) {
var lodash = _.runInContext();
var _aw = {};
var toExtend =
[ 'map',
'filter',
'reduce',
'pluck',
'compact',
'xor',
'groupBy',
'thru'
];
function log(label, obj) {
/* jshint ignore:start */
console.log(label, obj);
/* jshint ignore:end */
return obj;
}
function _reject(reason) {
return $q.reject(reason);
}
function _promise(value) {
return $q.when(value);
}
function _then(promise, fn) {
return promise.then(function(value) {
if (_.isFunction(value.then)) {
return _then(value, fn);
} else {
return fn(value);
}
});
}
function _catch(promise, fn) {
return promise.catch(fn);
}
function _finally(promise, fn) {
return promise.finally(fn);
}
function thenAll(arr, then, deep) {
var doAll = function (arr) {
var promise = $q.all(arr).then(function (resolvedArr) {
if (deep) {
return lodash(resolvedArr)
.thenMap(function (elem) {
if (_.isArray(elem)) {
return thenAll(elem, null, true);
} else {
return elem;
}
})
.thenAll()
.value();
} else {
return resolvedArr;
}
});
return then ? promise.then(then) : promise;
};
if (arr.then) {
return arr.then(doAll);
} else {
return doAll(arr);
}
}
function thenFlatten(array, isShallow, callback, thisArg) {
return thenAll(array, _.partialRight(_.flatten, isShallow, callback, thisArg), true);
}
function wrapCallback(method, callback, thisArg, collection) {
return method(collection, function(value, index, collection) {
if (_.isUndefined(value)) {
return callback(value, index, collection);
} else if (_.isFunction(value.then)) {
return value.then(_.partialRight(callback, index, collection));
} else {
return callback(value, index, collection);
}
}, thisArg);
}
function promiseWrapper(method, collection, callback, thisArg) {
if (_.isFunction(collection.then)) {
return collection.then(
_.partial(wrapCallback, method, callback, thisArg));
} else {
return $q.all(collection)
.then(function(unwrappedResults) {
return method(unwrappedResults, callback, thisArg);
});
}
}
toExtend.forEach(function(fnName) {
var wrappedName = 'then' + _.capitalize(fnName);
_aw[wrappedName] = _.partial(promiseWrapper, _[fnName]);
});
_aw.promise = _promise;
_aw.reject = _reject;
_aw.then = _then;
_aw.catch = _catch;
_aw.finally = _finally;
_aw.thenAll = thenAll;
_aw.thenFlatten = thenFlatten;
_aw.log = log;
lodash.mixin(_aw);
return lodash;
}
export default ['$q', lodashAsPromised];

View File

@ -10,7 +10,6 @@ import lookupModal from './lookup/main';
import smartSearch from './smart-search/main';
import paginate from './paginate/main';
import columnSort from './column-sort/main';
import lodashAsPromised from './lodash-as-promised';
import filters from './filters/main';
import truncatedText from './truncated-text.directive';
import stateExtender from './stateExtender.provider';
@ -34,6 +33,7 @@ import orgAdminLookup from './org-admin-lookup/main';
import limitPanels from './limit-panels/main';
import multiSelectPreview from './multi-select-preview/main';
import credentialTypesLookup from './credentialTypesLookup.factory';
import utilities from './Utilities';
export default
angular.module('shared', [
@ -64,11 +64,11 @@ angular.module('shared', [
orgAdminLookup.name,
limitPanels.name,
multiSelectPreview.name,
utilities.name,
'ngCookies',
'angular-duration-format'
])
.factory('stateDefinitions', stateDefinitions)
.factory('lodashAsPromised', lodashAsPromised)
.factory('credentialTypesLookup', credentialTypesLookup)
.directive('truncatedText', truncatedText)
.provider('$stateExtender', stateExtender);

View File

@ -1,31 +0,0 @@
var $injector = angular.injector(['ng']);
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;

View File

@ -1,229 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
function slotFactValues(basisPosition, basisValue, comparatorValue) {
var leftValue, rightValue;
if (basisPosition === 'left') {
leftValue = basisValue;
rightValue = comparatorValue;
} else {
rightValue = basisValue;
leftValue = comparatorValue;
}
return { left: leftValue,
right: rightValue
};
}
/*
* @name flatCompare
* @description
* Takes two separate fact objects and combines any differences into a single result
* object, formatted for display.
*
* @param {Tower.SystemTracking.ResolvedFact} basisFacts - The facts we will use as the basis for this comparison
* @param {Tower.SystemTracking.ResolvedFact} comparatorFacts - The facts we will compare against the basis
* @param {Tower.SystemTracking.RenderOptions} renderOptions - Options specified in the module for rendering values
*
* @returns {Array.<Tower.SystemTracking.FactComparisonDescriptor>}
*
* @typedef {Object} Tower.SystemTracking.RenderOptions
* @property {string} nameKey - The name of the key that is used to locate facts from basisFacts in comparatorFacts
* @property {string|string[]} compareKey - A single key or list of keys to compare in the two fact collections
* @property {Tower.SystemTracking.KeyNameMap} keyNameMap - An object mapping existing key names to new key names for display
* @property {Tower.SystemTracking.FactTemplate} factTemplate - An optional template used as the string for comparing and displaying a fact
* @property {boolean} supportsValueArray - Indicates that the module we're rendering supports values in an array of strings rather than just a string; iterim measure to make sure modules with multiple facts matching nameKey don't get evaluated because we won't know how to dipslay them
*
* @typedef {(string|boolean)} Tower.SystemTracking.KeyNameMapValue - The name you want to use for the display of a key or "true" to indicate a key should be displayed without changing its name (all keys are hidden by default)
*
* @typedef {Object.<string, Tower.SystemTracking.KeyNameMapValue>} Tower.SystemTracking.KeyNameMap - An object whose keys are the names of keys that exist in a fact and whose values control how that key is displayed
*
* @typedef {{displayKeyPath: string, nestingLevel: number, facts: Array.<Tower.SystemTracking.FactComparisonResult>}} Tower.SystemTracking.FactComparisonDescriptor
*
* @typedef {{keyName: string, value1: Tower.SystemTracking.FactComparisonValue, value2: Tower.SystemTracking.FactComparisonValue, isDivergent: boolean}} Tower.SystemTracking.FactComparisonResult
*
* @typedef {(string|string[])} Tower.SystemTracking.FactComparisonValue
*
*/
export default
function flatCompare(basisFacts,
comparatorFacts, renderOptions) {
var nameKey = renderOptions.nameKey;
var compareKeys = renderOptions.compareKey;
var keyNameMap = renderOptions.keyNameMap;
var factTemplate = renderOptions.factTemplate;
return basisFacts.facts.reduce(function(arr, basisFact) {
// First, make sure this fact hasn't already been processed, if it
// has don't process it again
//
if (_.any(arr, { displayKeyPath: basisFact[nameKey] })) {
return arr;
}
var searcher = {};
searcher[nameKey] = basisFact[nameKey];
var containsValueArray = false;
var matchingFacts = _.where(comparatorFacts.facts, searcher);
var comparisonResults;
var diffs;
// If this fact exists more than once in `basisFacts`, then
// we need to process it differently
//
var otherBasisFacts = _.where(basisFacts.facts, searcher);
if (renderOptions.supportsValueArray && otherBasisFacts.length > 1) {
comparisonResults = processFactsForValueArray(basisFacts.position, otherBasisFacts, matchingFacts);
} else {
comparisonResults = processFactsForSingleValue(basisFact, matchingFacts[0] || {});
}
function processFactsForValueArray(basisPosition, basisFacts, comparatorFacts) {
function renderFactValues(facts) {
return facts.map(function(fact) {
return factTemplate.render(fact);
});
}
var basisFactValues = renderFactValues(basisFacts);
var comparatorFactValues = renderFactValues(comparatorFacts);
var slottedValues = slotFactValues(basisPosition, basisFactValues, comparatorFactValues);
var remaining = _.difference(slottedValues.left, slottedValues.right);
if (_.isEmpty(remaining)) {
slottedValues.isDivergent = false;
} else {
slottedValues.isDivergent = true;
containsValueArray = true;
}
return slottedValues;
}
function processFactsForSingleValue(basisFact, comparatorFact) {
// Perform comparison and get back comparisonResults; like:
// { 'value':
// { leftValue: 'blah',
// rightValue: 'doo'
// }
// };
//
return _.reduce(compareKeys, function(result, compareKey) {
var isNestedDisplay = false;
var basisFactValue = basisFact[compareKey];
var comparatorFactValue = comparatorFact[compareKey];
var slottedValues;
if (_.isUndefined(basisFactValue) && _.isUndefined(comparatorFactValue)) {
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 (!renderOptions.supportsValueArray) {
// comparatorFact = comparatorFact[0];
// }
basisFactValue = template.render(basisFact);
comparatorFactValue = template.render(comparatorFact);
slottedValues = slotFactValues(basisFacts.position,
basisFactValue,
comparatorFactValue);
if (slottedValues.left !== slottedValues.right) {
slottedValues.isDivergent = true;
} else {
slottedValues.isDivergent = false;
}
if (isNestedDisplay) {
result[compareKey] = slottedValues;
} else {
result = slottedValues;
}
return result;
}, {});
}
var hasDiffs =
_.any(comparisonResults, { isDivergent: true }) ||
comparisonResults.isDivergent === true;
if (hasDiffs && typeof factTemplate.render === 'function') {
diffs =
{ keyName: basisFact[nameKey],
value1: comparisonResults.left,
value2: comparisonResults.right
};
} else if (hasDiffs) {
diffs =
_(comparisonResults).map(function(slottedValues, key) {
var keyName = key;
if (keyNameMap && keyNameMap[key]) {
keyName = keyNameMap[key];
}
return { keyName: keyName,
value1: slottedValues.left,
value2: slottedValues.right,
isDivergent: slottedValues.isDivergent
};
}).compact()
.value();
}
var descriptor =
{ displayKeyPath: basisFact[nameKey],
nestingLevel: 0,
containsValueArray: containsValueArray,
facts: diffs
};
return arr.concat(descriptor);
}, []).filter(function(diff) {
return !_.isEmpty(diff.facts);
});
}

View File

@ -1,55 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import compareNestedFacts from './nested';
import compareFlatFacts from './flat';
import FactTemplate from './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 (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
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: _.isPlainObject(renderOptions.factTemplate),
leftData: facts[0].facts,
rightData: facts[1].facts
};
})
.value();
} else {
return { factData: compareNestedFacts(facts[0], facts[1]),
isNestedDisplay: true
};
}
}

View File

@ -1,204 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export function formatFacts(diffedResults) {
var loggingEnabled = false;
function log(msg, obj) {
if (loggingEnabled) {
/* jshint ignore:start */
console.log(msg, obj);
/* jshint ignore:end */
}
return obj;
}
function isFlatFactArray(fact) {
// Flat arrays will have the index as their
// keyName
return !_.isNaN(Number(fact.keyName));
}
function isNestedFactArray(fact) {
// Nested arrays will have the index as the last element
// in the keypath
return !_.isNaN(Number(_.last(fact.keyPath)));
}
function isFactArray(fact) {
return isNestedFactArray(fact) || isFlatFactArray(fact);
}
// Explode flat results into groups based matching
// parent keypaths
var grouped = _.groupBy(diffedResults, function(obj) {
var leftKeyPathStr = obj.keyPath.join('.');
log('obj.keyPath', obj.keyPath);
return log(' reduced key', _.reduce(diffedResults, function(result, obj2) {
log(' obj2.keyPath', obj2.keyPath);
var rightKeyPathStr = obj2.keyPath.join('.');
if (isFactArray(obj)) {
log(' number hit!', Number(_.last(obj.keyPath)));
return obj.keyPath.slice(0,-1);
} else if (rightKeyPathStr && leftKeyPathStr !== rightKeyPathStr && log(' intersection', _.intersection(obj.keyPath, obj2.keyPath).join('.')) === rightKeyPathStr) {
log(' hit!');
return obj2.keyPath;
} else {
log(' else hit!');
return result;
}
}, obj.keyPath)).join('.');
});
var normalized = _.mapValues(grouped, function(arr, rootKey) {
log('processing', rootKey);
var nestingLevel = 0;
var trailingLength;
return _(arr).sortBy('keyPath.length').tap(function(arr) {
// Initialize trailing length to the shortest keyPath length
// in the array (first item because we know it's sorted now)
trailingLength = arr[0].keyPath.length;
}).map(function(obj) {
var keyPathStr = obj.keyPath.join('.');
log(' calculating displayKeyPath for', keyPathStr);
var rootKeyPath = rootKey.split('.');
var displayKeyPath;
// var factArrayIndex;
var isFactArrayProp = isFactArray(obj);
if (obj.keyPath.length > trailingLength) {
nestingLevel++;
trailingLength = obj.keyPath.length;
}
if (isNestedFactArray(obj)) {
// factArrayIndex = obj.keyPath.length > 1 ? Number(_.last(obj.keyPath)) : obj.keyName;
displayKeyPath = _.initial(obj.keyPath).join('.');
// } else {
// displayKeyPath = _.difference(obj.keyPath, rootKeyPath).join('.');
} else {
displayKeyPath = rootKeyPath.join('.');
}
obj.displayKeyPath = displayKeyPath;
obj.nestingLevel = nestingLevel;
// obj.arrayPosition = factArrayIndex;
obj.isArrayMember = isFactArrayProp;
return obj;
}).value();
});
var flattened = _.reduce(normalized, function(flat, value) {
var groupedValues = _.groupBy(value, 'displayKeyPath');
var groupArr =
_.reduce(groupedValues, function(groupArr, facts, key) {
var isArray = facts[0].isArrayMember;
var nestingLevel = facts[0].nestingLevel;
if (isArray) {
facts = _(facts)
.groupBy('arrayPosition')
.values()
.value();
}
var displayObj =
{ keyPath: key.split('.'),
displayKeyPath: key,
isNestedDisplay: true,
facts: facts,
isFactArray: isArray,
nestingLevel: nestingLevel
};
return groupArr.concat(displayObj);
}, []);
return flat.concat(groupArr);
}, []);
return flattened;
}
export function findFacts(factData) {
var leftData = factData[0].facts;
var rightData = factData[1].facts;
// For value types (boolean, number) or complex objects use the value
// as is. For collection types, return 'absent' if it's empty
// otherwise, use the value as is.
//
function factValueOrAbsent(value) {
if (_.isBoolean(value) ||
_.isNumber(value)) {
return value;
}
if (_.isNull(value) || _.isEmpty(value)) {
return 'absent';
}
return value;
}
function factObject(keyPath, key, leftValue, rightValue) {
var obj =
{ keyPath: keyPath,
keyName: key
};
leftValue = factValueOrAbsent(leftValue);
rightValue = factValueOrAbsent(rightValue);
if (factData[0].position === 'left') {
obj.value1 = leftValue;
obj.value2 = rightValue;
} else {
obj.value1 = rightValue;
obj.value2 = leftValue;
}
return obj;
}
function descend(parentValue, parentKey, parentKeys) {
if (_.isObject(parentValue)) {
return _.reduce(parentValue, function(all, value, key) {
var merged = descend(value, key, parentKeys.concat(key));
return all.concat(merged);
}, []);
} else {
// default value in _.get ('absent') only hits if it's undefined, so we need to handle other falsey cases with ||
var rightValue =
_.get(rightData,
parentKeys);
return factObject(
// TODO: Currently parentKeys is getting passed with the final key
// as the last element. Figure out how to have it passed
// in correctly, so that it's all the keys leading up to
// the value, but not the value's key itself
// In the meantime, slicing the last element off the array
parentKeys.slice(0,-1),
parentKey,
parentValue,
rightValue);
}
}
return _.reduce(leftData, function(mergedFacts, parentValue, parentKey) {
var merged = descend(parentValue, parentKey, [parentKey]);
return _.flatten(mergedFacts.concat(merged));
}, []);
}

View File

@ -1,76 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {formatFacts, findFacts} from './nested-helpers';
export default function nestedCompare(basisFacts, comparatorFacts) {
if (_.isEmpty(basisFacts.facts)) {
var tmp = _.merge({}, basisFacts);
basisFacts = _.merge({}, comparatorFacts);
comparatorFacts = tmp;
}
var factsList = [basisFacts, comparatorFacts];
factsList = findFacts(factsList);
factsList = compareFacts(factsList);
return formatFacts(factsList);
function compareFacts(factsList) {
function serializedFactKey(fact) {
return fact.keyPath.join('.');
}
var groupedByParent =
_.groupBy(factsList, function(fact) {
return serializedFactKey(fact);
});
var diffed = _.mapValues(groupedByParent, function(facts) {
return facts.filter(function(fact) {
return fact.value1 !== fact.value2;
}).map(function(fact) {
// TODO: Can we determine a "compare order" and be able say
// which property is actually divergent?
return _.merge({}, fact, { isDivergent: true });
});
});
var itemsWithDiffs =
_.filter(factsList, function(facts) {
var groupedData = diffed[serializedFactKey(facts)];
return !_.isEmpty(groupedData);
});
var keysWithDiffs =
_.reduce(diffed, function(diffs, facts, key) {
diffs[key] =
facts.reduce(function(diffKeys, fact) {
if (fact.isDivergent) {
return diffKeys.concat(fact.keyName);
}
return diffKeys;
}, []);
return diffs;
}, {});
var factsWithDivergence =
_.mapValues(itemsWithDiffs, function(fact) {
var divergentKeys = keysWithDiffs[serializedFactKey(fact)];
if (divergentKeys) {
var isDivergent = _.include(divergentKeys, fact.keyName);
return _.merge({}, fact, { isDivergent: isDivergent });
} else {
return _.merge({}, fact, { isDivergent: false });
}
});
return factsWithDivergence;
}
}

View File

@ -1,42 +0,0 @@
export default
function dedupeVersions(nonEmptyResults) {
if (_.any(nonEmptyResults, 'versions.length', 0)) {
return _.pluck(nonEmptyResults, 'versions[0]');
}
// if the version that will be displayed on the left is before the
// version that will be displayed on the right, flip them
if (nonEmptyResults[0].versions[0].timestamp > nonEmptyResults[1].versions[0].timestamp) {
nonEmptyResults = nonEmptyResults.reverse();
}
var firstTimestamp = nonEmptyResults[0].versions[0].timestamp;
var hostIdsWithDupes =
_(nonEmptyResults)
.pluck('versions[0]')
.filter('timestamp', firstTimestamp)
.map(function(version, index) {
return nonEmptyResults[index].hostId;
})
.value();
if (hostIdsWithDupes.length === 1) {
return _.pluck(nonEmptyResults, 'versions[0]');
}
var bestScanResults = _.max(nonEmptyResults, "versions.length");
return nonEmptyResults.map(function(scan, index) {
var hasDupe =
_.include(hostIdsWithDupes, scan.hostId);
if (hasDupe && index === 1) {
return bestScanResults.versions[0];
} else {
return bestScanResults.versions[1];
}
});
}

View File

@ -1,65 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['Rest', 'GetBasePath', 'ProcessErrors', 'lodashAsPromised',
function (Rest, GetBasePath, ProcessErrors, _) {
function buildUrl (host_id, module, startDate, endDate) {
var url = GetBasePath('hosts') + host_id + '/fact_versions/',
params= [["module", module] , ['from', startDate.format()], ['to', endDate.format()]];
params = params.filter(function(p){
return !_.isEmpty(p[1]);
});
params = params.map(function(p){
return p.join("=");
}).join("&");
url = _.compact([url, params]).join("?");
return url;
}
return {
getFacts: function(version) {
var promise;
Rest.setUrl(version.related.fact_view);
promise = Rest.get();
return promise.then(function (response) {
return response.data;
}).catch(function (response) {
ProcessErrors(null, response.data, response.status, null, {
hdr: 'Error!',
msg: 'Failed to get license info. GET returned status: ' +
response.status
});
});
},
getVersion: function(versionParams){
//move the build url into getVersion and have the
// parameters passed into this
var promise;
var hostId = versionParams.hostId;
var startDate = versionParams.dateRange.from;
var endDate = versionParams.dateRange.to;
var module = versionParams.moduleName;
var url = buildUrl(hostId, module, startDate, endDate);
Rest.setUrl(url);
promise = Rest.get();
return promise.then(function(response) {
versionParams.versions = response.data.results;
return versionParams;
}).catch(function (response) {
ProcessErrors(null, response.data, response.status, null, {
hdr: 'Error!',
msg: 'Failed to get license info. GET returned status: ' +
response.status
});
});
}
};
}];

View File

@ -1,68 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import dedupeVersions from './dedupe-versions';
export default
[ 'factScanDataService',
'resolveEmptyVersions',
'lodashAsPromised',
function(factScanDataService, resolveEmptyVersions, _) {
return function(hostIds, moduleName, leftDate, rightDate) {
var singleHostMode = false;
if (hostIds.length === 1) {
singleHostMode = true;
hostIds = hostIds.concat(hostIds[0]);
}
var hostVersionParams =
[{ hostId: hostIds[0],
dateRange: leftDate,
moduleName: moduleName
},
{ hostId: hostIds[1],
dateRange: rightDate,
moduleName: moduleName
}
];
return _(factScanDataService.getVersion(hostVersionParams[1]))
.then(function(result) {
return resolveEmptyVersions(result);
}).thenAll(function(firstResult) {
return factScanDataService.getVersion(hostVersionParams[0])
.then(function(secondResult) {
if (_.isEmpty(secondResult.versions)) {
secondResult = resolveEmptyVersions(secondResult);
}
return [firstResult, secondResult];
});
}).thenAll(function(results) {
var finalSet;
if (singleHostMode) {
finalSet = dedupeVersions(results.reverse());
} else {
finalSet = _.pluck(results, 'versions[0]').reverse();
}
return finalSet;
}).thenMap(function(versionData) {
if (versionData) {
return factScanDataService.getFacts(versionData);
} else {
return { fact: [] };
}
}).thenAll(function(hostFacts) {
return hostFacts;
});
};
}
];

View File

@ -1,79 +0,0 @@
var moduleConfig =
{ 'packages':
{ compareKey: ['release', 'version'],
nameKey: 'name',
sortKey: 1,
factTemplate: "{{epoch|append:':'}}{{version}}-{{release}}{{arch|prepend:'.'}}",
supportsValueArray: true
},
'services':
{ compareKey: ['state', 'source'],
nameKey: 'name',
factTemplate: '{{state}} ({{source}})',
sortKey: 2
},
'files':
{ compareKey: ['size', 'mode', 'checksum', 'mtime', 'gid', 'uid'],
keyNameMap:
{ 'uid': 'ownership'
},
factTemplate:
{ 'uid': 'user id: {{uid}}, group id: {{gid}}',
'mode': true,
'checksum': true,
'size': true,
'mtime': '{{mtime|formatEpoch}}'
},
nameKey: 'path',
sortKey: 3
},
'ansible':
{ sortKey: 4
},
'custom':
{
}
};
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);
}
];

View File

@ -1,12 +0,0 @@
import factScanDataService from './fact-scan-data.service';
import getDataForComparison from './get-data-for-comparison.factory';
import getModuleOptions from './get-module-options.factory';
import resolveEmptyVersions from './resolve-empty-versions.factory';
import shared from '../../shared/main';
export default
angular.module('systemTracking.dataServices', [shared.name])
.factory('getModuleOptions', getModuleOptions)
.factory('getDataForComparison', getDataForComparison)
.factory('resolveEmptyVersions', resolveEmptyVersions)
.service('factScanDataService', factScanDataService);

View File

@ -1,33 +0,0 @@
import moment from '../../shared/moment/moment';
function resolveEmptyVersions(service, _, candidate, previousResult) {
candidate = _.merge({}, candidate);
// theoretically, returning no versions, but also returning only a single version for _a particular date_ acts as an empty version problem. If there is only one version returned, you'll need to check previous dates as well.
if (_.isEmpty(candidate.versions) || candidate.versions.length === 1) {
// this marks the end of the date put in the datepicker. this needs to be the end, rather than the beginning of the date, because you may have returned one valid version on the date the datepicker had in it.
var originalStartDate = candidate.dateRange.to.clone();
if (!_.isUndefined(previousResult)) {
candidate.dateRange.from = moment(previousResult.versions[0].timestamp);
} else {
candidate.dateRange.from = originalStartDate.clone().subtract(1, 'year');
}
candidate.dateRange.to = originalStartDate;
return service.getVersion(candidate);
}
return _.promise(candidate);
}
export default
[ 'factScanDataService',
'lodashAsPromised',
function(factScanDataService, lodash) {
return _.partial(resolveEmptyVersions, factScanDataService, lodash);
}
];

View File

@ -1,37 +0,0 @@
.DatePicker {
flex: 1 0 auto;
display: flex;
&-icon {
flex: initial;
padding: 6px 12px;
font-size: 14px;
border-radius: 4px 0 0 4px;
border: 1px solid @b7grey;
border-right: 0;
background-color: @default-bg;
}
&-icon:hover {
background-color: @f6grey;
}
&-icon:focus,
&-icon:active {
background-color: @f6grey;
}
&-input {
flex: 1 0 auto;
border-radius: 0 4px 4px 0;
border: 1px solid @b7grey;
padding: 6px 12px;
background-color: @fcgrey;
}
&-input:focus,
&-input:active {
outline-offset: 0;
outline: 0;
}
}

View File

@ -1,104 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/* jshint unused: vars */
export default ['moment',
'templateUrl',
function(moment, templateUrl) {
return {
restrict: 'E',
scope: {
date: '=',
minDate: '=',
autoUpdate: '=?',
inputClass: '&'
},
templateUrl: templateUrl('system-tracking/date-picker/date-picker'),
link: function(scope, element, attrs) {
// We need to make sure this _never_ recurses, which sometimes happens
// with two-way binding.
var mustUpdateValue = true;
scope.autoUpdate = scope.autoUpdate === false ? false : true;
scope.$watch('minDate', function(newValue) {
if (newValue) {
if (scope.autoUpdate && scope.date.isBefore(newValue)) {
scope.date = newValue;
}
$('.date', element).datepicker('setStartDate', newValue.toDate());
}
});
scope.$watch('date', function(newValue) {
if (newValue) {
mustUpdateValue = false;
scope.dateValue = newValue.format('L');
}
}, true);
scope.$watch('dateValue', function(newValue) {
var newDate = moment(newValue, ['L'], moment.locale());
if (newValue && !newDate.isValid()) {
scope.error = "That is not a valid date.";
} else if (newValue) {
scope.date = newDate;
}
mustUpdateValue = true;
});
var localeData =
moment.localeData();
var dateFormat = momentFormatToDPFormat(localeData._longDateFormat.L);
var localeKey = momentLocaleToDPLocale(moment.locale());
element.find(".DatePicker").addClass("input-prepend date");
element.find(".DatePicker").find(".DatePicker-icon").addClass("add-on");
$(".date").datepicker({
autoclose: true,
language: localeKey,
format: dateFormat
});
function momentLocaleToDPLocale(localeKey) {
var parts = localeKey.split('-');
if (parts.length === 2) {
var lastPart = parts[1].toUpperCase();
return [parts[0], lastPart].join('-');
} else {
return localeKey;
}
}
function momentFormatToDPFormat(momentFormat) {
var resultFormat = momentFormat;
function lowerCase(str) {
return str.toLowerCase();
}
resultFormat = resultFormat.replace(/D{1,2}/, lowerCase);
if (/MMM/.test(resultFormat)) {
resultFormat = resultFormat.replace('MMM', 'M');
} else {
resultFormat = resultFormat.replace(/M{1,2}/, 'm');
}
resultFormat = resultFormat.replace(/Y{2,4}/, lowerCase);
return resultFormat;
}
}
};
}
];

View File

@ -1,11 +0,0 @@
<div class="DatePicker">
<button class="DatePicker-icon" ng-disabled="disabled"><i class="fa fa-calendar"></i></button>
<input
class="DatePicker-input"
type="text"
readonly
ng-model="dateValue"
ng-class="inputClass()"
ng-disabled="disabled">
<p class="error" ng-if="error">{{error}}</p>
</div>

View File

@ -1,12 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import datePicker from './date-picker.directive';
export default
angular.module('systemTracking.datePicker',
[])
.directive('datePicker', datePicker);

View File

@ -1,138 +0,0 @@
/** @define FactDataTable */
.FactDataTable {
&-row, &-headingRow, &-groupHeadingRow {
display: flex;
}
&-row {
padding: .5em 0;
align-items: center;
transition: background-color 100ms, border-color 100ms;
border: solid 1px @default-bg;
&:nth-child(even) {
background-color: @default-no-items-bord;
}
&:hover {
background-color: @default-list-header-bg;
border-color: @default-border;
}
&--flexible {
max-height: initial;
align-items: flex-start;
}
}
&-repeat {
&:nth-child(even) {
background-color: @default-no-items-bord;
}
}
&-headingRow {
background-color: @default-list-header-bg;
border: none;
padding: 8px 12px;
.FactDataTable-column.FactDataTableHeading:first-child {
padding-left: 0;
}
}
&-groupHeadingRow {
background-color: @default-icon-hov;
padding: 1em 12px;
color: @default-interface-txt;
font-size: 14px;
}
&-column {
padding: 8px;
flex: 1 0 33%;
align-self: flex-start;
padding: 0;
margin: 0;
overflow: hidden;
padding: 8px;
word-wrap: break-word;
&--offsetLeft {
margin-left: 33%;
}
}
&-columnArray {
display: flex;
flex-direction: column;
}
&-columnMember {
margin-bottom: 16px;
&:last-child {
margin-bottom: inherit;
}
}
}
.FactDataError {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 200px;
border-radius: 5px;
border: 1px solid @list-no-items-bord;
background-color: @default-no-items-bord;
color: @list-no-items-txt;
text-transform: uppercase;
}
.FactDataGroup {
&-headings {
&:hover {
background-color: @default-bg;
border-color: transparent;
}
}
&-header {
padding: 0;
}
}
.FactDataTableHeading {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
font-size: 14px;
color: @default-interface-txt;
text-transform: uppercase;
&-host {
margin: 0;
}
&-date {
flex-basis: auto;
}
// Override some global styling on h3 tags
h3 {
margin: 1em initial;
}
}
.FactDatum {
&--divergent {
//margin-bottom: 0.5rem;
background-color: @default-err;
border: none;
color: @default-bg;
&:hover {
background-color: @default-err;
border: none;
}
}
}

View File

@ -1,21 +0,0 @@
/* jshint unused: vars */
export default
[ 'templateUrl',
function(templateUrl) {
return { restrict: 'E',
templateUrl: templateUrl('system-tracking/fact-data-table/fact-data-table'),
scope:
{ leftHostname: '=',
rightHostname: '=',
leftScanDate: '=',
rightScanDate: '=',
leftDataNoScans: '=',
rightDataNoScans: '=',
isNestedDisplay: '=',
singleResultView: '=',
factData: '='
}
};
}
];

View File

@ -1,93 +0,0 @@
<section class="FactDataTable SystemTrackingContainer-main" ng-if="!error">
<div class="alert alert-info" ng-if="singleResultView">
The selected fact scans were identical for this module. Showing all facts from the latest selected scan instead.
</div>
<div class="FactDataTable-headingRow">
<span class="FactDataTable-column FactDataTableHeading">
<span class="FactDataTableHeading-label">
Comparing facts from:
</span>
</span>
<span class="FactDataTable-column FactDataTableHeading">
<span ng-if="leftDataNoScans">
<span class="u-unbold">No scans for</span>
{{leftHostname}}
<span class="u-unbold">on</span>
</span>
<span class="FactDataTableHeading-host" ng-if="!leftDataNoScans">{{leftHostname}}&ensp;-&ensp;</span>
<span class="FactDataTableHeading-date" ng-if="!singleResultView"> {{leftScanDate|longDate}}</span>
<span class="FactDataTableHeading-date" ng-if="singleResultView"> {{rightScanDate|longDate}}</span>
</span>
<span class="FactDataTable-column FactDataTableHeading" ng-if="!singleResultView">
<span ng-if="rightDataNoScans">No scans for {{rightHostname}} on</span>
<span class="FactDataTableHeading-host" ng-if="!rightDataNoScans">{{rightHostname}}&ensp;-&ensp;</span>
<span class="FactDataTableHeading-date">{{rightScanDate|longDate}}</span>
</span>
</div>
<div ng-if="!isNestedDisplay">
<div class="FactDataTable-repeat" ng-repeat="group in factData | orderBy: 'displayKeyPath'">
<div class="FactDataTable-row FactDataTable-row--flexible" ng-if="group.containsValueArray">
<span class="FactDataTable-column">{{group.facts.keyName}}</span>
<span class="FactDataTable-column FactDataTable-columnArray">
<span class="FactDataTable-columnMember u-wrappedText" ng-repeat="value in group.facts.value1" style="position: relative;">
{{value}}
</span>
</span>
<span class="FactDataTable-column FactDataTable-columnArray" ng-if="!singleResultView">
<span class="FactDataTable-columnMember u-wrappedText" ng-repeat="value in group.facts.value2" style="position: relative;">
{{value}}
</span>
</span>
</div>
<div class="FactDataTable-row" ng-if="!group.containsValueArray">
<span class="FactDataTable-column">{{group.facts.keyName}}</span>
<span class="FactDataTable-column u-wrappedText" style="position: relative;">{{group.facts.value1}}</span>
<span class="FactDataTable-column u-wrappedText" ng-if="!singleResultView" style="position: relative;">{{group.facts.value2}}</span>
</div>
</div>
</div>
<div ng-if="isNestedDisplay">
<div class="FactDataTable-factGroup FactDataGroup" ng-repeat="group in factData | orderBy: 'displayKeyPath'">
<div class="FactDataTable-groupHeadingRow" ng-if="group.displayKeyPath">
<span class="FactDataTable-column FactDataTable-column--full FactDataGroup-header">
{{group.displayKeyPath}}
</span>
</div>
<div class="FactDataGroup-facts" data-facts="{{group.facts}}">
<div class="FactDataTable-arrayGroup" ng-if="group.isFactArray" ng-repeat="arrayGroup in group.facts" data-array-group="{{arrayGroup}}">
<div class="FactDataTable-row FactDatum" ng-class="{'FactDatum--divergent': fact.isDivergent }" ng-repeat="fact in arrayGroup" data-fact="{{fact}}">
<span class="FactDatum-keyName FactDataTable-column" ng-if="!fact.isArrayMember">
{{fact.keyName}}
</span>
<span class="FactDatum-keyName FactDataTable-column u-wrappedText" ng-if="fact.isArrayMember">
{{fact.keyPath[0]}}.{{fact.keyName}}
</span>
<span class="FactDatum-value FactDataTable-column u-wrappedText" style="position: relative;">
{{fact.value1}}
</span>
<span class="FactDatum-value FactDataTable-column u-wrappedText" ng-if="!singleResultView" style="position: relative;">
{{fact.value2}}
</span>
</div>
</div>
<div class="FactDataTable-row FactDatum" ng-class="{'FactDatum--divergent': fact.isDivergent }" ng-repeat="fact in group.facts" ng-unless="group.isFactArray" data-fact="{{fact}}">
<span class="FactDataTable-column FactDatum-keyName" ng-if="!fact.isArrayMember">
{{fact.keyName}}
</span>
<span class="FactDataTable-column FactDatum-keyName" ng-if="fact.isArrayMember">
{{fact.keyPath[0]}}.{{fact.keyName}}
</span>
<span class="FactDataTable-column FactDatum-value u-wrappedText" style="position: relative;">
{{fact.value1}}
</span>
<span class="FactDataTable-column FactDatum-value u-wrappedText" ng-if="!singleResultView" style="position: relative;">
{{fact.value2}}
</span>
</div>
</div>
</div>
</div>
</section>

View File

@ -1,5 +0,0 @@
import factDataTable from './fact-data-table.directive';
export default
angular.module('systemTracking.factDataTable', [])
.directive('factDataTable', factDataTable);

View File

@ -1,54 +0,0 @@
/** @define FactModuleFilter */
.FactModuleFilter {
width: 100%;
display: flex;
flex-flow: row wrap;
margin-bottom: 2.8rem;
.btn, .btn-default {
margin-right: 20px;
width: 10rem;
}
&-module {
background-color: @enabled-item-background;
border-color: @default-icon-hov;
color: @default-interface-txt;
@media screen and (max-width: 750px) {
flex-basis: 50%;
}
&:active, &:focus {
// copied from bootstrap's .btn:focus
background-color: @default-icon-hov;
border-color: @enabled-item-border;
color: @enabled-item-text;
box-shadow: none;
z-index: 2;
}
&:hover {
border-color: @default-icon-hov;
}
}
&-module.is-active {
cursor: default;
z-index: 2;
background-color: @default-icon;
color: @default-bg;
border-color: @default-icon;
&:active, &:focus {
box-shadow: none;
}
&:hover {
background-color: @default-interface-txt;
}
}
}

View File

@ -1,16 +0,0 @@
.FactModulePickers-label {
padding-right: 0;
text-align: right;
font-size: 11px;
text-transform: uppercase;
color: @default-interface-txt;
line-height: 17px;
}
.FactModulePickers-warning {
float: right;
clear: both;
font-size: 12px;
width: 75%;
text-align: right;
}

View File

@ -1,13 +0,0 @@
export function formatFactForDisplay(fact, renderOptions) {
var factTemplate = renderOptions.factTemplate;
var template = factTemplate;
// if (!renderOptions.supportsValueArray) {
// comparatorFact = comparatorFact[0];
// }
return template.render(fact);
}

View File

@ -1,27 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import route from './system-tracking.route';
import controller from './system-tracking.controller';
import shared from '../shared/main';
import utilities from '../shared/Utilities';
import datePicker from './date-picker/main';
import dataServices from './data-services/main';
import factDataTable from './fact-data-table/main';
export default
angular.module('systemTracking',
[ utilities.name,
shared.name,
datePicker.name,
factDataTable.name,
dataServices.name
])
.controller('systemTracking', controller)
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route);
}]);

View File

@ -1,28 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import moment from '../shared/moment/moment';
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: date.clone().startOf('day'),
to: date.clone().add(1, 'day').startOf('day')
};
}

View File

@ -1,37 +0,0 @@
.SystemTrackingContainer {
display: flex;
flex-direction: column;
min-height: 85vh;
> * {
flex: 0 0 auto;
}
&-main {
flex: 1 0 auto;
align-self: stretch;
}
.DatePicker {
flex: none;
}
.DatePicker-input {
width: 100%;
flex: none;
background: @fcgrey;
}
.DatePicker-icon {
i {
color: @default-icon;
}
}
.SystemTrackingContainer-main {
margin-top: 20px;
}
.FactDatum--divergent {
background-color: @default-err;
}
}

View File

@ -1,336 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {searchDateRange} from './search-date-range';
import {compareFacts} from './compare-facts/main';
import {formatFactForDisplay} from './format-facts';
import FactTemplate from './compare-facts/fact-template';
function controller($scope,
$stateParams,
$location,
$q,
$state,
moduleOptions,
inventory,
hosts,
getDataForComparison,
waitIndicator,
moment,
_) {
// var inventoryId = $stateParams.id;
var hostIds = $stateParams.hostIds.split(',');
var moduleParam = $stateParams.module || 'packages';
$scope.compareMode =
hostIds.length === 1 ? 'single-host' : 'host-to-host';
$scope.hostIds = $stateParams.hosts;
$scope.inventory = inventory;
$scope.noModuleData = false;
// this means no scans have been run
if (_.isEmpty(moduleOptions)) {
$scope.noModuleData = true;
return;
}
if ($scope.compareMode === 'host-to-host') {
$scope.factModulePickersLabelLeft = "Compare latest facts collected across both hosts on or before";
} else {
$scope.factModulePickersLabelLeft = "Compare latest facts collected on or before";
$scope.factModulePickersLabelRight = "To latest facts collected on or before";
}
$scope.modules = _.clone(moduleOptions, true);
var leftSearchRange = searchDateRange();
var rightSearchRange = searchDateRange();
var searchConfig =
{ leftRange: leftSearchRange,
rightRange: rightSearchRange
};
$scope.leftDate = leftSearchRange.from;
$scope.rightDate = rightSearchRange.from;
$scope.leftHostname = hosts[0].name;
$scope.rightHostname = hosts.length > 1 ? hosts[1].name : hosts[0].name;
function reloadData(params) {
searchConfig = _.assign({}, searchConfig, params);
var leftRange = searchConfig.leftRange;
var rightRange = searchConfig.rightRange;
var activeModule = searchConfig.module;
if ($scope.compareMode === 'host-to-host') {
rightRange = searchConfig.leftRange;
}
$scope.leftDataNoScans = false;
$scope.rightDataNoScans = false;
$scope.leftDateWarning = false;
$scope.rightDateWarning = false;
$scope.singleFactOnly = false;
waitIndicator('start');
return getDataForComparison(
hostIds,
activeModule.name,
leftRange,
rightRange)
.then(function(responses) {
var data = [];
_.each(responses, function(response) {
if(response.fact) {
data.push(response.fact);
} else if (response.facts) {
data.push(response.facts);
}
});
if (_.isEmpty(data[0]) && _.isEmpty(data[1])) {
return _.reject({
name: 'NoScanData',
message: 'There was insufficient scan data for the date you selected. Please try selecting a different date or module.',
dateValues:
{ leftDate: $scope.leftDate.clone(),
rightDate: $scope.rightDate.clone()
}
});
}
if (_.isEmpty(data[0])) {
$scope.leftDataNoScans = true;
$scope.leftScanDate = $scope.leftDate;
} else {
$scope.leftScanDate = moment(responses[0].timestamp);
if (!$scope.leftScanDate.isSame($scope.leftDate, 'd')) {
$scope.leftDateWarning = true;
}
}
if (_.isEmpty(data[1])) {
$scope.rightDataNoScans = true;
$scope.rightScanDate = $scope.rightDate;
} else {
$scope.rightScanDate = moment(responses[1].timestamp);
if (!$scope.rightScanDate.isSame($scope.rightDate, 'd')) {
$scope.rightDateWarning = true;
}
}
return data;
})
.then(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
//
var wrappedFacts =
facts.map(function(facts, index) {
return { position: index === 0 ? 'left' : 'right',
isEmpty: _.isEmpty(facts),
facts: facts
};
});
var splitFacts = _.partition(facts, 'isEmpty');
var emptyScans = splitFacts[0];
var nonEmptyScans = splitFacts[1];
var result;
if (_.isEmpty(nonEmptyScans)) {
// we have NO data, throw an error
result = _.reject({
name: 'NoScanData',
message: 'No scans ran on either 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
result = _.reject({
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()
});
} else {
result = _.promise(wrappedFacts);
}
// all scans have data, rejoice!
return result;
})
.then(_.partial(compareFacts, activeModule))
.then(function(info) {
// Clear out any errors from the previous run...
$scope.error = null;
if (_.isEmpty(info.factData)) {
if ($scope.compareMode === 'host-to-host') {
info = _.reject({
name: 'NoScanDifferences',
message: 'No differences in the scans on the dates you selected. Please try selecting different dates.',
dateValues:
{ leftDate: $scope.leftDate.clone(),
rightDate: $scope.rightDate.clone()
}
});
} else {
$scope.singleFactOnly = true;
$scope.factData = info.leftData.map(function(fact) {
var keyNameMap = activeModule.keyNameMap;
var nameKey = activeModule.nameKey;
var renderOptions = _.merge({}, activeModule);
var isNestedDisplay = false;
var facts;
if (_.isObject(renderOptions.factTemplate) &&
_.isArray(renderOptions.compareKey)) {
isNestedDisplay = true;
var templates = _.mapValues(renderOptions.factTemplate, function(template, key) {
if (template === true) {
return { render: function(fact) {
return fact[key];
}
};
} else {
return new FactTemplate(template);
}
});
facts = _.map(templates, function(template, key) {
var keyName = key;
if (_.isObject(keyNameMap) && keyNameMap.hasOwnProperty(key)) {
keyName = keyNameMap[key];
}
renderOptions.factTemplate = template;
var formattedValue = formatFactForDisplay(fact, renderOptions);
return { keyName: keyName,
isNestedDisplay: true,
value1: formattedValue
};
});
} else {
renderOptions.factTemplate = new FactTemplate(renderOptions.factTemplate);
var formattedValue = formatFactForDisplay(fact, renderOptions);
isNestedDisplay = false;
facts = { keyName: fact[nameKey],
value1: formattedValue
};
}
$scope.isNestedDisplay = isNestedDisplay;
return { displayKeyPath: fact[renderOptions.nameKey],
nestingLevel: 0,
containsValueArray: false,
facts: facts
};
});
}
} else {
$scope.singleFactOnly = false;
$scope.factData = info.factData;
$scope.isNestedDisplay = info.isNestedDisplay;
}
return info;
}).catch(function(error) {
$scope.error = error;
}).finally(function() {
waitIndicator('stop');
});
}
$scope.setActiveModule = function(newModuleName, initialData) {
var newModule = _.find($scope.modules, function(module) {
return module.name === newModuleName;
});
if (newModule) {
if (newModule.isActive) {
return;
}
$scope.modules.forEach(function(module) {
module.isActive = false;
});
newModule.isActive = true;
$location.replace();
$location.search('module', newModuleName);
reloadData({ module: newModule
}, initialData).value();
}
};
function dateWatcher(dateProperty) {
return function(newValue, oldValue) {
// passing in `true` for the 3rd param to $watch should make
// angular use `angular.equals` for comparing these values;
// the watcher should not fire, but it still is. Therefore,
// using `moment.isSame` to keep from reloading data when the
// dates did not actually change
if (newValue.isSame(oldValue)) {
return;
}
var newDate = searchDateRange(newValue);
var params = {};
params[dateProperty] = newDate;
reloadData(params).value();
};
}
$scope.$watch('leftDate', dateWatcher('leftRange'), true);
$scope.$watch('rightDate', dateWatcher('rightRange'), true);
$scope.setActiveModule(moduleParam);
}
export default
[ '$scope',
'$stateParams',
'$location',
'$q',
'$state',
'moduleOptions',
'inventory',
'hosts',
'getDataForComparison',
'Wait',
'moment',
'lodashAsPromised',
controller
];

View File

@ -1,66 +0,0 @@
<section class="SystemTrackingContainer Panel" ng-if="noModuleData">
<section class="FactDataError SystemTrackingContainer-main">
<p class="FactDataError-note--full">
To set up scan jobs, create a <a ui-sref="inventoryJobTemplateAdd({inventory_id: inventory.id})">job template</a> that targets the "<a ui-sref="inventories.edit({inventory_id: inventory.id})">{{inventory.name}}</a>" inventory and check "Store Ansible Facts."
</p>
</section>
</section>
<section class="SystemTrackingContainer Panel" ng-hide="noModuleData">
<div class="row">
<div class="Form-title ng-binding col-md-3">SYSTEM TRACKING <div class="List-titleLockup"></div>{{inventory.name}}</div>
<div class="FactModulePickers row col-md-8 col-md-offset-1">
<div class="row">
<div class="FactModulePickers-dateContainer FactModulePickers-dateContainer--left col-md-6">
<span class="FactModulePickers-label col-md-6">{{ factModulePickersLabelLeft }}</span>
<date-picker date="leftDate" input-class='{ "u-input-info-alert": leftDateWarning }' class="col-md-5">
</date-picker>
<span class="FactModulePickers-warning" ng-class='{ "u-hiddenVisually": !leftDateWarning, "u-info-alert": leftDateWarning }'>
There were no fact scans on this date, using a prior scan instead.
</span>
</div>
<div class="FactModulePickers-dateContainer FactModulePickers-dateContainer--right col-md-6" ng-show="compareMode == 'single-host'">
<span class="FactModulePickers-label col-md-6">{{ factModulePickersLabelRight }}</span>
<date-picker date="rightDate" min-date="leftDate" input-class='{ "u-input-info-alert": rightDateWarning }' class="col-md-5">
</date-picker>
<span class="FactModulePickers-warning" ng-class='{ "u-hiddenVisually": !rightDateWarning, "u-info-alert": rightDateWarning }'>
There were no fact scans on this date, using a prior scan instead.
</span>
</div>
</div>
</div>
</div>
</div>
<nav class="FactModuleFilter">
<button ng-class="{ 'btn': true,
'btn-default': true,
'FactModuleFilter-module': true,
'is-active': module.isActive,
}" ng-click="setActiveModule(module.name)" ng-repeat="module in modules | orderBy: 'sortKey' track by $index">
{{module.displayName}}
</button>
</nav>
<section class="FactDataError SystemTrackingContainer-main" ng-if="error" ng-switch="error.name">
<p class="FactDataError-message" ng-switch-when="NoScanData">
<span ng-if="compareMode == 'single-host'">
There was insufficient scan data for one or both of the dates you selected. Please try selecting a different date or module.
</span>
<span ng-if="compareMode == 'host-to-host'">
There was insufficient scan data for the date you selected. Please try selecting a different date or module.
</span>
</p>
<p class="FactDataError-message" ng-switch-when="InsufficientScanData">
There were no facts collected on or before one of the dates you selected ({{error.dateValue|amDateFormat:'L'}}). Please select a different date and try again.
</p>
<p class="FactDataError-message" ng-switch-when="NoScanDifferences">
The two fact scans were identical for this module.
</p>
<p class="FactDataError-note" ng-switch-default>
We were not able to find any facts collected for this inventory or module. To setup or run scan jobs, edit the "<a link-to="inventoryEdit" model="{ inventory_id: inventory }">{{inventory.name}}</a>" inventory and check "Store Ansible Facts."
</p>
</section>
<fact-data-table ng-if="!error" left-hostname="leftHostname" right-hostname="rightHostname" left-scan-date="leftScanDate" right-scan-date="rightScanDate" left-data-no-scans="leftDataNoScans" right-data-no-scans="rightDataNoScans" is-nested-display="isNestedDisplay"
single-result-view="singleFactOnly" fact-data="factData">
</fact-data-table>
</section>

View File

@ -1,108 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {templateUrl} from '../shared/template-url/template-url.factory';
import { N_ } from '../i18n';
export default {
name: 'systemTracking',
route: '/inventories/:inventoryId/system-tracking/:hostIds',
controller: 'systemTracking',
templateUrl: templateUrl('system-tracking/system-tracking'),
params: {hosts: null, inventory: null},
reloadOnSearch: false,
ncyBreadcrumb: {
label: N_("SYSTEM TRACKING")
},
resolve: {
moduleOptions:
[ 'getModuleOptions',
'ProcessErrors',
'$stateParams',
function(getModuleOptions, ProcessErrors, $stateParams) {
var hostIds = $stateParams.hostIds.split(',');
var data =
getModuleOptions(hostIds[0])
.catch(function (response) {
ProcessErrors(null, response.data, response.status, null, {
hdr: 'Error!',
msg: 'Failed to get license info. GET returned status: ' +
response.status
});
})
.value();
return data;
}
],
inventory:
[ '$stateParams',
'$q',
'Rest',
'GetBasePath',
'ProcessErrors',
function($stateParams, $q, rest, getBasePath, ProcessErrors) {
if ($stateParams.inventory) {
return $q.when($stateParams.inventory);
}
var inventoryId = $stateParams.inventoryId;
var url = getBasePath('inventory') + inventoryId + '/';
rest.setUrl(url);
return rest.get()
.then(function(data) {
return data.data;
}).catch(function (response) {
ProcessErrors(null, response.data, response.status, null, {
hdr: 'Error!',
msg: 'Failed to get license info. GET returned status: ' +
response.status
});
});
}
],
hosts:
[ '$stateParams',
'$q',
'Rest',
'GetBasePath',
'ProcessErrors',
function($stateParams, $q, rest, getBasePath, ProcessErrors) {
if ($stateParams.hosts) {
return $q.when($stateParams.hosts);
}
var hostIds = $stateParams.hostIds.split(',');
var hosts =
hostIds.map(function(hostId) {
var url = getBasePath('hosts') +
hostId + '/';
rest.setUrl(url);
return rest.get()
.then(function(data) {
return data.data;
}).catch(function (response) {
ProcessErrors(null, response.data, response.status, null, {
hdr: 'Error!',
msg: 'Failed to get license info. GET returned status: ' +
response.status
});
});
});
return $q.all(hosts);
}
]
}
};