@@ -73,8 +73,8 @@
name="login_password"
id="login-password"
class="form-control LoginModal-field"
- ng-model="login_password" required
- autocomplete="off">
+ ng-model="login_password"
+ required>
}
- *
- * @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.
} 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.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);
- });
-
- }
diff --git a/awx/ui/client/src/system-tracking/compare-facts/main.js b/awx/ui/client/src/system-tracking/compare-facts/main.js
deleted file mode 100644
index df8aa67e15..0000000000
--- a/awx/ui/client/src/system-tracking/compare-facts/main.js
+++ /dev/null
@@ -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
- };
- }
-}
diff --git a/awx/ui/client/src/system-tracking/compare-facts/nested-helpers.js b/awx/ui/client/src/system-tracking/compare-facts/nested-helpers.js
deleted file mode 100644
index 3a8fea8926..0000000000
--- a/awx/ui/client/src/system-tracking/compare-facts/nested-helpers.js
+++ /dev/null
@@ -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));
-
- }, []);
-}
diff --git a/awx/ui/client/src/system-tracking/compare-facts/nested.js b/awx/ui/client/src/system-tracking/compare-facts/nested.js
deleted file mode 100644
index 31e98526a7..0000000000
--- a/awx/ui/client/src/system-tracking/compare-facts/nested.js
+++ /dev/null
@@ -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;
-
- }
-}
diff --git a/awx/ui/client/src/system-tracking/data-services/dedupe-versions.js b/awx/ui/client/src/system-tracking/data-services/dedupe-versions.js
deleted file mode 100644
index e6f2704008..0000000000
--- a/awx/ui/client/src/system-tracking/data-services/dedupe-versions.js
+++ /dev/null
@@ -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];
- }
- });
-
- }
diff --git a/awx/ui/client/src/system-tracking/data-services/fact-scan-data.service.js b/awx/ui/client/src/system-tracking/data-services/fact-scan-data.service.js
deleted file mode 100644
index 9805dfdf2d..0000000000
--- a/awx/ui/client/src/system-tracking/data-services/fact-scan-data.service.js
+++ /dev/null
@@ -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
- });
- });
- }
- };
-}];
diff --git a/awx/ui/client/src/system-tracking/data-services/get-data-for-comparison.factory.js b/awx/ui/client/src/system-tracking/data-services/get-data-for-comparison.factory.js
deleted file mode 100644
index 0c29b2433c..0000000000
--- a/awx/ui/client/src/system-tracking/data-services/get-data-for-comparison.factory.js
+++ /dev/null
@@ -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;
- });
- };
- }
- ];
diff --git a/awx/ui/client/src/system-tracking/data-services/get-module-options.factory.js b/awx/ui/client/src/system-tracking/data-services/get-module-options.factory.js
deleted file mode 100644
index 5a6b6f5eb4..0000000000
--- a/awx/ui/client/src/system-tracking/data-services/get-module-options.factory.js
+++ /dev/null
@@ -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);
- }
- ];
diff --git a/awx/ui/client/src/system-tracking/data-services/main.js b/awx/ui/client/src/system-tracking/data-services/main.js
deleted file mode 100644
index d08b7baec7..0000000000
--- a/awx/ui/client/src/system-tracking/data-services/main.js
+++ /dev/null
@@ -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);
diff --git a/awx/ui/client/src/system-tracking/data-services/resolve-empty-versions.factory.js b/awx/ui/client/src/system-tracking/data-services/resolve-empty-versions.factory.js
deleted file mode 100644
index 7c7552af87..0000000000
--- a/awx/ui/client/src/system-tracking/data-services/resolve-empty-versions.factory.js
+++ /dev/null
@@ -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);
- }
- ];
diff --git a/awx/ui/client/src/system-tracking/date-picker/date-picker.block.less b/awx/ui/client/src/system-tracking/date-picker/date-picker.block.less
deleted file mode 100644
index 207cfca6a5..0000000000
--- a/awx/ui/client/src/system-tracking/date-picker/date-picker.block.less
+++ /dev/null
@@ -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;
- }
-}
diff --git a/awx/ui/client/src/system-tracking/date-picker/date-picker.directive.js b/awx/ui/client/src/system-tracking/date-picker/date-picker.directive.js
deleted file mode 100644
index ca972bfacf..0000000000
--- a/awx/ui/client/src/system-tracking/date-picker/date-picker.directive.js
+++ /dev/null
@@ -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;
- }
- }
- };
- }
-];
diff --git a/awx/ui/client/src/system-tracking/date-picker/date-picker.partial.html b/awx/ui/client/src/system-tracking/date-picker/date-picker.partial.html
deleted file mode 100644
index 74bd409a6f..0000000000
--- a/awx/ui/client/src/system-tracking/date-picker/date-picker.partial.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
diff --git a/awx/ui/client/src/system-tracking/date-picker/main.js b/awx/ui/client/src/system-tracking/date-picker/main.js
deleted file mode 100644
index 21bbf0432b..0000000000
--- a/awx/ui/client/src/system-tracking/date-picker/main.js
+++ /dev/null
@@ -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);
diff --git a/awx/ui/client/src/system-tracking/fact-data-table/fact-data-table.block.less b/awx/ui/client/src/system-tracking/fact-data-table/fact-data-table.block.less
deleted file mode 100644
index 62512a73cc..0000000000
--- a/awx/ui/client/src/system-tracking/fact-data-table/fact-data-table.block.less
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/awx/ui/client/src/system-tracking/fact-data-table/fact-data-table.directive.js b/awx/ui/client/src/system-tracking/fact-data-table/fact-data-table.directive.js
deleted file mode 100644
index 1c99f546a0..0000000000
--- a/awx/ui/client/src/system-tracking/fact-data-table/fact-data-table.directive.js
+++ /dev/null
@@ -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: '='
- }
- };
- }
- ];
diff --git a/awx/ui/client/src/system-tracking/fact-data-table/fact-data-table.partial.html b/awx/ui/client/src/system-tracking/fact-data-table/fact-data-table.partial.html
deleted file mode 100644
index efb66303bc..0000000000
--- a/awx/ui/client/src/system-tracking/fact-data-table/fact-data-table.partial.html
+++ /dev/null
@@ -1,93 +0,0 @@
-
-
- The selected fact scans were identical for this module. Showing all facts from the latest selected scan instead.
-
-
-
-
- Comparing facts from:
-
-
-
-
- No scans for
- {{leftHostname}}
- on
-
- {{leftHostname}} -
- {{leftScanDate|longDate}}
- {{rightScanDate|longDate}}
-
-
- No scans for {{rightHostname}} on
- {{rightHostname}} -
- {{rightScanDate|longDate}}
-
-
-
-
-
- {{group.facts.keyName}}
-
-
- {{value}}
-
-
-
-
- {{value}}
-
-
-
-
- {{group.facts.keyName}}
- {{group.facts.value1}}
- {{group.facts.value2}}
-
-
-
-
-
-
-
-
-
- {{group.displayKeyPath}}
-
-
-
-
-
-
- {{fact.keyName}}
-
-
- {{fact.keyPath[0]}}.{{fact.keyName}}
-
-
- {{fact.value1}}
-
-
- {{fact.value2}}
-
-
-
-
-
- {{fact.keyName}}
-
-
- {{fact.keyPath[0]}}.{{fact.keyName}}
-
-
- {{fact.value1}}
-
-
- {{fact.value2}}
-
-
-
-
-
-
-
diff --git a/awx/ui/client/src/system-tracking/fact-data-table/main.js b/awx/ui/client/src/system-tracking/fact-data-table/main.js
deleted file mode 100644
index d77cd22fee..0000000000
--- a/awx/ui/client/src/system-tracking/fact-data-table/main.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import factDataTable from './fact-data-table.directive';
-
-export default
- angular.module('systemTracking.factDataTable', [])
- .directive('factDataTable', factDataTable);
diff --git a/awx/ui/client/src/system-tracking/fact-module-filter.block.less b/awx/ui/client/src/system-tracking/fact-module-filter.block.less
deleted file mode 100644
index 23466ebfe9..0000000000
--- a/awx/ui/client/src/system-tracking/fact-module-filter.block.less
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/awx/ui/client/src/system-tracking/fact-module-pickers.block.less b/awx/ui/client/src/system-tracking/fact-module-pickers.block.less
deleted file mode 100644
index 6c5ebb4551..0000000000
--- a/awx/ui/client/src/system-tracking/fact-module-pickers.block.less
+++ /dev/null
@@ -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;
-}
diff --git a/awx/ui/client/src/system-tracking/format-facts.js b/awx/ui/client/src/system-tracking/format-facts.js
deleted file mode 100644
index 32ff9fed6c..0000000000
--- a/awx/ui/client/src/system-tracking/format-facts.js
+++ /dev/null
@@ -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);
-
-}
diff --git a/awx/ui/client/src/system-tracking/main.js b/awx/ui/client/src/system-tracking/main.js
deleted file mode 100644
index ae1ec2a51b..0000000000
--- a/awx/ui/client/src/system-tracking/main.js
+++ /dev/null
@@ -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);
- }]);
diff --git a/awx/ui/client/src/system-tracking/search-date-range.js b/awx/ui/client/src/system-tracking/search-date-range.js
deleted file mode 100644
index 3b969672d6..0000000000
--- a/awx/ui/client/src/system-tracking/search-date-range.js
+++ /dev/null
@@ -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')
- };
-}
diff --git a/awx/ui/client/src/system-tracking/system-tracking-container.block.less b/awx/ui/client/src/system-tracking/system-tracking-container.block.less
deleted file mode 100644
index c8252662ff..0000000000
--- a/awx/ui/client/src/system-tracking/system-tracking-container.block.less
+++ /dev/null
@@ -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;
- }
-}
diff --git a/awx/ui/client/src/system-tracking/system-tracking.controller.js b/awx/ui/client/src/system-tracking/system-tracking.controller.js
deleted file mode 100644
index 2892fdd347..0000000000
--- a/awx/ui/client/src/system-tracking/system-tracking.controller.js
+++ /dev/null
@@ -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
- ];
diff --git a/awx/ui/client/src/system-tracking/system-tracking.partial.html b/awx/ui/client/src/system-tracking/system-tracking.partial.html
deleted file mode 100644
index f920a0fdee..0000000000
--- a/awx/ui/client/src/system-tracking/system-tracking.partial.html
+++ /dev/null
@@ -1,66 +0,0 @@
-
-
-
-
-
-
-
- {{ factModulePickersLabelLeft }}
-
-
-
- There were no fact scans on this date, using a prior scan instead.
-
-
-
- {{ factModulePickersLabelRight }}
-
-
-
- There were no fact scans on this date, using a prior scan instead.
-
-
-
-
-
-
-
-
-
-
-
-
- There was insufficient scan data for one or both of the dates you selected. Please try selecting a different date or module.
-
-
- There was insufficient scan data for the date you selected. Please try selecting a different date or module.
-
-
-
- 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.
-
-
- The two fact scans were identical for this module.
-
-
- We were not able to find any facts collected for this inventory or module. To setup or run scan jobs, edit the "{{inventory.name}}" inventory and check "Store Ansible Facts."
-
-
-
-
-
diff --git a/awx/ui/client/src/system-tracking/system-tracking.route.js b/awx/ui/client/src/system-tracking/system-tracking.route.js
deleted file mode 100644
index da803afc91..0000000000
--- a/awx/ui/client/src/system-tracking/system-tracking.route.js
+++ /dev/null
@@ -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);
- }
-
- ]
- }
-};
diff --git a/awx/ui/client/src/templates/job_templates/job-template.form.js b/awx/ui/client/src/templates/job_templates/job-template.form.js
index 7b75e16415..54bbe2c70d 100644
--- a/awx/ui/client/src/templates/job_templates/job-template.form.js
+++ b/awx/ui/client/src/templates/job_templates/job-template.form.js
@@ -245,7 +245,7 @@ function(NotificationsList, i18n) {
dataTitle: i18n._('Ansible Environment'),
dataContainer: 'body',
dataPlacement: 'right',
- ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)',
+ ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)',
ngShow: 'custom_virtualenvs_options.length > 1'
},
instance_groups: {
diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less
index 88a416bb73..52242d6387 100644
--- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less
+++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less
@@ -102,13 +102,6 @@
width: 90px;
color: @default-interface-txt;
}
-.WorkflowChart-conflictIcon {
- color: @default-err;
-}
-.WorkflowChart-conflictText {
- width: 90px;
- color: @default-interface-txt;
-}
.WorkflowChart-activeNode {
fill: @default-link;
}
diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js
index ce25538807..75defa010f 100644
--- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js
+++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js
@@ -327,16 +327,6 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : "";
}).each(wrap);
- thisNode.append("foreignObject")
- .attr("x", 54)
- .attr("y", 45)
- .style("font-size","0.7em")
- .attr("class", "WorkflowChart-conflictText")
- .html(function () {
- return `
\uf06a ${TemplatesStrings.get('workflow_maker.EDGE_CONFLICT')}`;
- })
- .style("display", function(d) { return (d.edgeConflict && !d.placeholder) ? null : "none"; });
-
thisNode.append("foreignObject")
.attr("x", 62)
.attr("y", 22)
@@ -831,9 +821,6 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
t.selectAll(".WorkflowChart-deletedText")
.style("display", function(d){ return d.unifiedJobTemplate || d.placeholder ? "none" : null; });
- t.selectAll(".WorkflowChart-conflictText")
- .style("display", function(d) { return (d.edgeConflict && !d.placeholder) ? null : "none"; });
-
t.selectAll(".WorkflowChart-activeNode")
.style("display", function(d) { return d.isActiveEdit ? null : "none"; });
diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js
index d76229c0f9..f94c1f3422 100644
--- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js
+++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js
@@ -4,13 +4,12 @@
* All Rights Reserved
*************************************************/
-export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
- '$state', 'ProcessErrors', 'CreateSelect2', '$q', 'JobTemplateModel',
+export default ['$scope', 'WorkflowService', 'TemplatesService',
+ 'ProcessErrors', 'CreateSelect2', '$q', 'JobTemplateModel',
'Empty', 'PromptService', 'Rest', 'TemplatesStrings', '$timeout',
- 'i18n',
- function($scope, WorkflowService, GetBasePath, TemplatesService,
- $state, ProcessErrors, CreateSelect2, $q, JobTemplate,
- Empty, PromptService, Rest, TemplatesStrings, $timeout, i18n) {
+ function ($scope, WorkflowService, TemplatesService,
+ ProcessErrors, CreateSelect2, $q, JobTemplate,
+ Empty, PromptService, Rest, TemplatesStrings, $timeout) {
let promptWatcher, surveyQuestionWatcher, credentialsWatcher;
@@ -31,24 +30,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
value: "check"
}];
- $scope.edgeFlags = {
- conflict: false
- };
-
- $scope.edgeTypeOptions = [
- {
- label: $scope.strings.get('workflow_maker.ALWAYS'),
- value: 'always'
- },
- {
- label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
- value: 'success'
- },
- {
- label: $scope.strings.get('workflow_maker.ON_FAILURE'),
- value: 'failure'
- }
- ];
+ $scope.edgeTypeOptions = createEdgeTypeOptions();
let editRequests = [];
let associateRequests = [];
@@ -59,6 +41,22 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.toggleKey = () => $scope.showKey = !$scope.showKey;
$scope.keyClassList = `{ 'Key-menuIcon--active': showKey }`;
+ function createEdgeTypeOptions() {
+ return ([{
+ label: $scope.strings.get('workflow_maker.ALWAYS'),
+ value: 'always'
+ },
+ {
+ label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
+ value: 'success'
+ },
+ {
+ label: $scope.strings.get('workflow_maker.ON_FAILURE'),
+ value: 'failure'
+ }
+ ]);
+ }
+
function resetNodeForm() {
$scope.workflowMakerFormConfig.nodeMode = "idle";
delete $scope.selectedTemplate;
@@ -74,7 +72,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
// params.parentId
// params.node
- let buildSendableNodeData = function() {
+ let buildSendableNodeData = function () {
// Create the node
let sendableNodeData = {
unified_job_template: params.node.unifiedJobTemplate.id,
@@ -122,7 +120,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
return sendableNodeData;
};
- let continueRecursing = function(parentId) {
+ let continueRecursing = function (parentId) {
$scope.totalIteratedNodes++;
if ($scope.totalIteratedNodes === $scope.treeData.data.totalNodes) {
@@ -130,7 +128,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
completionCallback();
} else {
if (params.node.children && params.node.children.length > 0) {
- _.forEach(params.node.children, function(child) {
+ _.forEach(params.node.children, function (child) {
if (child.edgeType === "success") {
recursiveNodeUpdates({
parentId: parentId,
@@ -155,49 +153,51 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
if (params.node.isNew) {
TemplatesService.addWorkflowNode({
- url: $scope.treeData.workflow_job_template_obj.related.workflow_nodes,
- data: buildSendableNodeData()
- })
- .then(function(data) {
+ url: $scope.treeData.workflow_job_template_obj.related.workflow_nodes,
+ data: buildSendableNodeData()
+ })
+ .then(function (data) {
- if (!params.node.isRoot) {
- associateRequests.push({
- parentId: params.parentId,
- nodeId: data.data.id,
- edge: params.node.edgeType
+ if (!params.node.isRoot) {
+ associateRequests.push({
+ parentId: params.parentId,
+ nodeId: data.data.id,
+ edge: params.node.edgeType
+ });
+ }
+
+ if (_.get(params, 'node.promptData.launchConf.ask_credential_on_launch')) {
+ // This finds the credentials that were selected in the prompt but don't occur
+ // in the template defaults
+ let credentialsToPost = params.node.promptData.prompts.credentials.value.filter(function (credFromPrompt) {
+ let defaultCreds = _.get(params, 'node.promptData.launchConf.defaults.credentials', []);
+ return !defaultCreds.some(function (defaultCred) {
+ return credFromPrompt.id === defaultCred.id;
+ });
+ });
+
+ credentialsToPost.forEach((credentialToPost) => {
+ credentialRequests.push({
+ id: data.data.id,
+ data: {
+ id: credentialToPost.id
+ }
+ });
+ });
+ }
+
+ params.node.isNew = false;
+ continueRecursing(data.data.id);
+ }, function ({ data, config, status }) {
+ ProcessErrors($scope, data, status, null, {
+ hdr: $scope.strings.get('error.HEADER'),
+ msg: $scope.strings.get('error.CALL', {
+ path: `${config.url}`,
+ action: `${config.method}`,
+ status
+ })
});
- }
-
- if (_.get(params, 'node.promptData.launchConf.ask_credential_on_launch')){
- // This finds the credentials that were selected in the prompt but don't occur
- // in the template defaults
- let credentialsToPost = params.node.promptData.prompts.credentials.value.filter(function(credFromPrompt) {
- let defaultCreds = params.node.promptData.launchConf.defaults.credentials ? params.node.promptData.launchConf.defaults.credentials : [];
- return !defaultCreds.some(function(defaultCred) {
- return credFromPrompt.id === defaultCred.id;
- });
- });
-
- credentialsToPost.forEach((credentialToPost) => {
- credentialRequests.push({
- id: data.data.id,
- data: {
- id: credentialToPost.id
- }
- });
- });
- }
-
- params.node.isNew = false;
- continueRecursing(data.data.id);
- }, function(error) {
- ProcessErrors($scope, error.data, error.status, null, {
- hdr: 'Error!',
- msg: 'Failed to add workflow node. ' +
- 'POST returned status: ' +
- error.status
});
- });
} else {
if (params.node.edited || !params.node.originalParentId || (params.node.originalParentId && params.parentId !== params.node.originalParentId)) {
@@ -208,56 +208,56 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
data: buildSendableNodeData()
});
- if (_.get(params, 'node.promptData.launchConf.ask_credential_on_launch')){
- let credentialsNotInPriorCredentials = params.node.promptData.prompts.credentials.value.filter(function(credFromPrompt) {
- let defaultCreds = params.node.promptData.launchConf.defaults.credentials ? params.node.promptData.launchConf.defaults.credentials : [];
- return !defaultCreds.some(function(defaultCred) {
- return credFromPrompt.id === defaultCred.id;
- });
- });
+ if (_.get(params, 'node.promptData.launchConf.ask_credential_on_launch')) {
+ let credentialsNotInPriorCredentials = params.node.promptData.prompts.credentials.value.filter(function (credFromPrompt) {
+ let defaultCreds = _.get(params, 'node.promptData.launchConf.defaults.credentials', []);
+ return !defaultCreds.some(function (defaultCred) {
+ return credFromPrompt.id === defaultCred.id;
+ });
+ });
- let credentialsToAdd = credentialsNotInPriorCredentials.filter(function(credNotInPrior) {
- let previousOverrides = params.node.promptData.prompts.credentials.previousOverrides ? params.node.promptData.prompts.credentials.previousOverrides : [];
- return !previousOverrides.some(function(priorCred) {
- return credNotInPrior.id === priorCred.id;
- });
- });
+ let credentialsToAdd = credentialsNotInPriorCredentials.filter(function (credNotInPrior) {
+ let previousOverrides = _.get(params, 'node.promptData.prompts.credentials.previousOverrides', []);
+ return !previousOverrides.some(function (priorCred) {
+ return credNotInPrior.id === priorCred.id;
+ });
+ });
- let credentialsToRemove = [];
+ let credentialsToRemove = [];
- if (_.has(params, 'node.promptData.prompts.credentials.previousOverrides')) {
- credentialsToRemove = params.node.promptData.prompts.credentials.previousOverrides.filter(function(priorCred) {
- return !credentialsNotInPriorCredentials.some(function(credNotInPrior) {
- return priorCred.id === credNotInPrior.id;
- });
- });
- }
+ if (_.has(params, 'node.promptData.prompts.credentials.previousOverrides')) {
+ credentialsToRemove = params.node.promptData.prompts.credentials.previousOverrides.filter(function (priorCred) {
+ return !credentialsNotInPriorCredentials.some(function (credNotInPrior) {
+ return priorCred.id === credNotInPrior.id;
+ });
+ });
+ }
- credentialsToAdd.forEach((credentialToAdd) => {
- credentialRequests.push({
- id: params.node.nodeId,
- data: {
- id: credentialToAdd.id
- }
- });
- });
+ credentialsToAdd.forEach((credentialToAdd) => {
+ credentialRequests.push({
+ id: params.node.nodeId,
+ data: {
+ id: credentialToAdd.id
+ }
+ });
+ });
- credentialsToRemove.forEach((credentialToRemove) => {
- credentialRequests.push({
- id: params.node.nodeId,
- data: {
- id: credentialToRemove.id,
- disassociate: true
- }
- });
- });
- }
+ credentialsToRemove.forEach((credentialToRemove) => {
+ credentialRequests.push({
+ id: params.node.nodeId,
+ data: {
+ id: credentialToRemove.id,
+ disassociate: true
+ }
+ });
+ });
+ }
}
if (params.node.originalParentId && (params.parentId !== params.node.originalParentId || params.node.originalEdge !== params.node.edgeType)) {
let parentIsDeleted = false;
- _.forEach($scope.treeData.data.deletedNodes, function(deletedNode) {
+ _.forEach($scope.treeData.data.deletedNodes, function (deletedNode) {
if (deletedNode === params.node.originalParentId) {
parentIsDeleted = true;
}
@@ -297,44 +297,15 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
}
}
- let updateEdgeDropdownOptions = (optionsToInclude) => {
- // Not passing optionsToInclude will include all by default
- if (!optionsToInclude) {
- $scope.edgeTypeOptions = [
- {
- label: i18n._('Always'),
- value: 'always'
- },
- {
- label: i18n._('On Success'),
- value: 'success'
- },
- {
- label: i18n._('On Failure'),
- value: 'failure'
- }
- ];
- } else {
- $scope.edgeTypeOptions = [];
+ let updateEdgeDropdownOptions = (edgeTypeValue) => {
+ // Not passing an edgeTypeValue will include all by default
- optionsToInclude.forEach((optionToInclude) => {
- if (optionToInclude === "always") {
- $scope.edgeTypeOptions.push({
- label: $scope.strings.get('workflow_maker.ALWAYS'),
- value: 'always'
- });
- } else if (optionToInclude === "success") {
- $scope.edgeTypeOptions.push({
- label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
- value: 'success'
- });
- } else if (optionToInclude === "failure") {
- $scope.edgeTypeOptions.push({
- label: $scope.strings.get('workflow_maker.ON_FAILURE'),
- value: 'failure'
- });
- }
+ if (edgeTypeValue) {
+ $scope.edgeTypeOptions = _.filter(createEdgeTypeOptions(), {
+ 'value': edgeTypeValue
});
+ } else {
+ $scope.edgeTypeOptions = createEdgeTypeOptions();
}
CreateSelect2({
@@ -347,9 +318,9 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
let credentialRequiresPassword = false;
$scope.promptData.prompts.credentials.value.forEach((credential) => {
if ((credential.passwords_needed &&
- credential.passwords_needed.length > 0) ||
+ credential.passwords_needed.length > 0) ||
(_.has(credential, 'inputs.vault_password') &&
- credential.inputs.vault_password === "ASK")
+ credential.inputs.vault_password === "ASK")
) {
credentialRequiresPassword = true;
}
@@ -364,7 +335,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
'missingSurveyValue'
];
- promptWatcher = $scope.$watchGroup(promptDataToWatch, function() {
+ promptWatcher = $scope.$watchGroup(promptDataToWatch, function () {
let missingPromptValue = false;
if ($scope.missingSurveyValue) {
missingPromptValue = true;
@@ -381,20 +352,20 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
}
};
- $scope.closeWorkflowMaker = function() {
+ $scope.closeWorkflowMaker = function () {
// Revert the data to the master which was created when the dialog was opened
$scope.treeData.data = angular.copy($scope.treeDataMaster);
$scope.closeDialog();
};
- $scope.saveWorkflowMaker = function() {
+ $scope.saveWorkflowMaker = function () {
$scope.totalIteratedNodes = 0;
if ($scope.treeData && $scope.treeData.data && $scope.treeData.data.children && $scope.treeData.data.children.length > 0) {
- let completionCallback = function() {
+ let completionCallback = function () {
- let disassociatePromises = disassociateRequests.map(function(request) {
+ let disassociatePromises = disassociateRequests.map(function (request) {
return TemplatesService.disassociateWorkflowNode({
parentId: request.parentId,
nodeId: request.nodeId,
@@ -402,67 +373,68 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
});
});
- let editNodePromises = editRequests.map(function(request) {
+ let editNodePromises = editRequests.map(function (request) {
return TemplatesService.editWorkflowNode({
id: request.id,
data: request.data
});
});
- let deletePromises = $scope.treeData.data.deletedNodes.map(function(nodeId) {
+ let deletePromises = $scope.treeData.data.deletedNodes.map(function (nodeId) {
return TemplatesService.deleteWorkflowJobTemplateNode(nodeId);
});
$q.all(disassociatePromises.concat(editNodePromises, deletePromises))
- .then(function() {
+ .then(function () {
- let credentialPromises = credentialRequests.map(function(request) {
- return TemplatesService.postWorkflowNodeCredential({
- id: request.id,
- data: request.data
+ let credentialPromises = credentialRequests.map(function (request) {
+ return TemplatesService.postWorkflowNodeCredential({
+ id: request.id,
+ data: request.data
+ });
});
- });
- let associatePromises = associateRequests.map(function(request) {
- return TemplatesService.associateWorkflowNode({
- parentId: request.parentId,
- nodeId: request.nodeId,
- edge: request.edge
+ let associatePromises = associateRequests.map(function (request) {
+ return TemplatesService.associateWorkflowNode({
+ parentId: request.parentId,
+ nodeId: request.nodeId,
+ edge: request.edge
+ });
});
- });
- $q.all(associatePromises.concat(credentialPromises))
- .then(function() {
- $scope.closeDialog();
- }).catch(({data, status}) => {
+ return $q.all(associatePromises.concat(credentialPromises))
+ .then(function () {
+ $scope.closeDialog();
+ });
+ }).catch(({
+ data,
+ status
+ }) => {
ProcessErrors($scope, data, status, null, {});
});
- }).catch(({data, status}) => {
- ProcessErrors($scope, data, status, null, {});
- });
};
- _.forEach($scope.treeData.data.children, function(child) {
+ _.forEach($scope.treeData.data.children, function (child) {
recursiveNodeUpdates({
node: child
}, completionCallback);
});
} else {
- let deletePromises = $scope.treeData.data.deletedNodes.map(function(nodeId) {
+ let deletePromises = $scope.treeData.data.deletedNodes.map(function (nodeId) {
return TemplatesService.deleteWorkflowJobTemplateNode(nodeId);
});
$q.all(deletePromises)
- .then(function() {
- $scope.closeDialog();
- });
+ .then(function () {
+ $scope.closeDialog();
+ });
}
};
/* ADD NODE FUNCTIONS */
- $scope.startAddNode = function(parent, betweenTwoNodes) {
+ $scope.startAddNode = function (parent, betweenTwoNodes) {
if ($scope.placeholderNode || $scope.nodeBeingEdited) {
$scope.cancelNodeForm();
@@ -481,41 +453,29 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.treeData.nextIndex++;
- let siblingConnectionTypes = WorkflowService.getSiblingConnectionTypes({
- tree: $scope.treeData.data,
- parentId: betweenTwoNodes ? parent.source.id : parent.id,
- childId: $scope.placeholderNode.id
- });
-
// Set the default to success
- let edgeType = {label: $scope.strings.get('workflow_maker.ON_SUCCESS'), value: "success"};
+ let edgeType = {
+ label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
+ value: "success"
+ };
if (parent && ((betweenTwoNodes && parent.source.isStartNode) || (!betweenTwoNodes && parent.isStartNode))) {
- // We don't want to give the user the option to select
- // a type as this node will always be executed
- updateEdgeDropdownOptions(["always"]);
- edgeType = {label: $scope.strings.get('workflow_maker.ALWAYS'), value: "always"};
+ // This node will always be executed
+ updateEdgeDropdownOptions('always');
+ edgeType = {
+ label: $scope.strings.get('workflow_maker.ALWAYS'),
+ value: "always"
+ };
} else {
- if (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) {
- updateEdgeDropdownOptions(["success", "failure"]);
- edgeType = {label: $scope.strings.get('workflow_maker.ON_SUCCESS'), value: "success"};
- } else if (_.includes(siblingConnectionTypes, "always")) {
- updateEdgeDropdownOptions(["always"]);
- edgeType = {label: $scope.strings.get('workflow_maker.ALWAYS'), value: "always"};
- } else {
- updateEdgeDropdownOptions();
- }
+ updateEdgeDropdownOptions();
}
- // Reset the edgeConflict flag
- resetEdgeConflict();
-
$scope.edgeType = edgeType;
$scope.$broadcast("refreshWorkflowChart");
};
- $scope.confirmNodeForm = function() {
+ $scope.confirmNodeForm = function () {
if ($scope.workflowMakerFormConfig.nodeMode === "add") {
if ($scope.selectedTemplate && $scope.edgeType && $scope.edgeType.value) {
@@ -565,13 +525,10 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.promptData = null;
- // Reset the edgeConflict flag
- resetEdgeConflict();
-
$scope.$broadcast("refreshWorkflowChart");
};
- $scope.cancelNodeForm = function() {
+ $scope.cancelNodeForm = function () {
if ($scope.workflowMakerFormConfig.nodeMode === "add") {
// Remove the placeholder node from the tree
WorkflowService.removeNodeFromTree({
@@ -598,9 +555,6 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.selectedTemplateInvalid = false;
$scope.showPromptButton = false;
- // Reset the edgeConflict flag
- resetEdgeConflict();
-
// Reset the form
resetNodeForm();
@@ -609,7 +563,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
/* EDIT NODE FUNCTIONS */
- $scope.startEditNode = function(nodeToEdit) {
+ $scope.startEditNode = function (nodeToEdit) {
if (!$scope.nodeBeingEdited || ($scope.nodeBeingEdited && $scope.nodeBeingEdited.id !== nodeToEdit.id)) {
if ($scope.placeholderNode || $scope.nodeBeingEdited) {
@@ -636,7 +590,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.nodeBeingEdited.isActiveEdit = true;
- let finishConfiguringEdit = function() {
+ let finishConfiguringEdit = function () {
let jobTemplate = new JobTemplate();
@@ -656,8 +610,8 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
!launchConf.credential_needed_to_start &&
!launchConf.ask_variables_on_launch &&
launchConf.variables_needed_to_start.length === 0) {
- $scope.showPromptButton = false;
- $scope.promptModalMissingReqFields = false;
+ $scope.showPromptButton = false;
+ $scope.promptModalMissingReqFields = false;
} else {
$scope.showPromptButton = true;
@@ -729,7 +683,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
let credentialRequiresPassword = false;
prompts.credentials.value.forEach((credential) => {
- if(credential.inputs) {
+ if (credential.inputs) {
if ((credential.inputs.password && credential.inputs.password === "ASK") ||
(credential.inputs.become_password && credential.inputs.become_password === "ASK") ||
(credential.inputs.ssh_key_unlock && credential.inputs.ssh_key_unlock === "ASK") ||
@@ -756,8 +710,8 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
!launchConf.credential_needed_to_start &&
!launchConf.ask_variables_on_launch &&
launchConf.variables_needed_to_start.length === 0) {
- $scope.showPromptButton = false;
- $scope.promptModalMissingReqFields = false;
+ $scope.showPromptButton = false;
+ $scope.promptModalMissingReqFields = false;
} else {
$scope.showPromptButton = true;
@@ -816,7 +770,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
watchForPromptChanges();
}
}
- });
+ });
}
if (_.get($scope, 'nodeBeingEdited.unifiedJobTemplate')) {
@@ -855,32 +809,30 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.workflowMakerFormConfig.activeTab = "jobs";
}
- let siblingConnectionTypes = WorkflowService.getSiblingConnectionTypes({
- tree: $scope.treeData.data,
- parentId: parent.id,
- childId: nodeToEdit.id
- });
+ let edgeDropdownOptions = null;
- let edgeDropdownOptions = null;
-
- switch($scope.nodeBeingEdited.edgeType) {
+ // Select RUN dropdown option
+ switch ($scope.nodeBeingEdited.edgeType) {
case "always":
- $scope.edgeType = {label: i18n._("Always"), value: "always"};
- if (siblingConnectionTypes.length === 1 && _.includes(siblingConnectionTypes, "always") || $scope.nodeBeingEdited.isRoot) {
- edgeDropdownOptions = ["always"];
+ $scope.edgeType = {
+ label: $scope.strings.get('workflow_maker.ALWAYS'),
+ value: "always"
+ };
+ if ($scope.nodeBeingEdited.isRoot) {
+ edgeDropdownOptions = 'always';
}
break;
case "success":
- $scope.edgeType = {label: i18n._("On Success"), value: "success"};
- if (siblingConnectionTypes.length !== 0 && (!_.includes(siblingConnectionTypes, "always"))) {
- edgeDropdownOptions = ["success", "failure"];
- }
+ $scope.edgeType = {
+ label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
+ value: "success"
+ };
break;
case "failure":
- $scope.edgeType = {label: i18n._("On Failure"), value: "failure"};
- if (siblingConnectionTypes.length !== 0 && (!_.includes(siblingConnectionTypes, "always"))) {
- edgeDropdownOptions = ["success", "failure"];
- }
+ $scope.edgeType = {
+ label: $scope.strings.get('workflow_maker.ON_FAILURE'),
+ value: "failure"
+ };
break;
}
@@ -897,15 +849,18 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
// unified job template so we're going to pull down the whole object
TemplatesService.getUnifiedJobTemplate($scope.nodeBeingEdited.unifiedJobTemplate.id)
- .then(function(data) {
+ .then(function (data) {
$scope.nodeBeingEdited.unifiedJobTemplate = _.clone(data.data.results[0]);
finishConfiguringEdit();
- }, function(error) {
- ProcessErrors($scope, error.data, error.status, null, {
- hdr: 'Error!',
- msg: 'Failed to get unified job template. GET returned ' +
- 'status: ' + error.status
- });
+ }, function ({ data, status, config }) {
+ ProcessErrors($scope, data, status, null, {
+ hdr: $scope.strings.get('error.HEADER'),
+ msg: $scope.strings.get('error.CALL', {
+ path: `${config.url}`,
+ action: `${config.method}`,
+ status
+ })
+ });
});
} else {
finishConfiguringEdit();
@@ -922,16 +877,16 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.deleteOverlayVisible = false;
}
- $scope.startDeleteNode = function(nodeToDelete) {
+ $scope.startDeleteNode = function (nodeToDelete) {
$scope.nodeToBeDeleted = nodeToDelete;
$scope.deleteOverlayVisible = true;
};
- $scope.cancelDeleteNode = function() {
+ $scope.cancelDeleteNode = function () {
resetDeleteNode();
};
- $scope.confirmDeleteNode = function() {
+ $scope.confirmDeleteNode = function () {
if ($scope.nodeToBeDeleted) {
// TODO: turn this into a promise so that we can handle errors
@@ -949,95 +904,56 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
resetNodeForm();
}
- // Reset the edgeConflict flag
- resetEdgeConflict();
-
resetDeleteNode();
$scope.$broadcast("refreshWorkflowChart");
if ($scope.placeholderNode) {
- let edgeType = {label: "On Success", value: "success"};
+ let edgeType = {
+ label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
+ value: "success"
+ };
+
if ($scope.placeholderNode.isRoot) {
- updateEdgeDropdownOptions(["always"]);
- edgeType = {label: "Always", value: "always"};
- } else {
- // we need to update the possible edges based on any new siblings
- let siblingConnectionTypes = WorkflowService.getSiblingConnectionTypes({
- tree: $scope.treeData.data,
- parentId: $scope.placeholderNode.parent.id,
- childId: $scope.placeholderNode.id
- });
-
- if (
- (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) &&
- !_.includes(siblingConnectionTypes, "always")
- ) {
- updateEdgeDropdownOptions(["success", "failure"]);
- } else if (
- _.includes(siblingConnectionTypes, "always") &&
- !_.includes(siblingConnectionTypes, "success") &&
- !_.includes(siblingConnectionTypes, "failure")
- ) {
- updateEdgeDropdownOptions(["always"]);
- edgeType = {label: "Always", value: "always"};
- } else {
- updateEdgeDropdownOptions();
- }
-
- }
- $scope.edgeType = edgeType;
- } else if ($scope.nodeBeingEdited) {
- let siblingConnectionTypes = WorkflowService.getSiblingConnectionTypes({
- tree: $scope.treeData.data,
- parentId: $scope.nodeBeingEdited.parent.id,
- childId: $scope.nodeBeingEdited.id
- });
-
- if (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) {
- updateEdgeDropdownOptions(["success", "failure"]);
- } else if (_.includes(siblingConnectionTypes, "always") && $scope.nodeBeingEdited.edgeType === "always") {
- updateEdgeDropdownOptions(["always"]);
+ updateEdgeDropdownOptions('always');
+ edgeType = {
+ label: $scope.strings.get('workflow_maker.ALWAYS'),
+ value: "always"
+ };
} else {
updateEdgeDropdownOptions();
}
- switch($scope.nodeBeingEdited.edgeType) {
- case "always":
- $scope.edgeType = {label: i18n._("Always"), value: "always"};
- if (
- _.includes(siblingConnectionTypes, "always") &&
- !_.includes(siblingConnectionTypes, "success") &&
- !_.includes(siblingConnectionTypes, "failure")
- ) {
- updateEdgeDropdownOptions(["always"]);
- } else {
- updateEdgeDropdownOptions();
- }
- break;
- case "success":
- $scope.edgeType = {label: i18n._("On Success"), value: "success"};
- if (
- (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) &&
- !_.includes(siblingConnectionTypes, "always")
- ) {
- updateEdgeDropdownOptions(["success", "failure"]);
- } else {
- updateEdgeDropdownOptions();
- }
- break;
- case "failure":
- $scope.edgeType = {label: i18n._("On Failure"), value: "failure"};
- if (
- (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) &&
- !_.includes(siblingConnectionTypes, "always")
- ) {
- updateEdgeDropdownOptions(["success", "failure"]);
- } else {
- updateEdgeDropdownOptions();
- }
- break;
- }
+ $scope.edgeType = edgeType;
+ } else if ($scope.nodeBeingEdited) {
+
+ switch ($scope.nodeBeingEdited.edgeType) {
+ case "always":
+ $scope.edgeType = {
+ label: $scope.strings.get('workflow_maker.ALWAYS'),
+ value: "always"
+ };
+ if ($scope.nodeBeingEdited.isRoot) {
+ updateEdgeDropdownOptions('always');
+ } else {
+ updateEdgeDropdownOptions();
+ }
+ break;
+ case "success":
+ $scope.edgeType = {
+ label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
+ value: "success"
+ };
+ updateEdgeDropdownOptions();
+ break;
+ case "failure":
+ $scope.edgeType = {
+ label: $scope.strings.get('workflow_maker.ON_FAILURE'),
+ value: "failure"
+ };
+ updateEdgeDropdownOptions();
+ break;
+ }
}
$scope.treeData.data.totalNodes--;
@@ -1045,13 +961,13 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
};
- $scope.toggleFormTab = function(tab) {
+ $scope.toggleFormTab = function (tab) {
if ($scope.workflowMakerFormConfig.activeTab !== tab) {
$scope.workflowMakerFormConfig.activeTab = tab;
}
};
- $scope.templateManuallySelected = function(selectedTemplate) {
+ $scope.templateManuallySelected = function (selectedTemplate) {
if (promptWatcher) {
promptWatcher();
@@ -1100,8 +1016,8 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
!launchConf.credential_needed_to_start &&
!launchConf.ask_variables_on_launch &&
launchConf.variables_needed_to_start.length === 0) {
- $scope.showPromptButton = false;
- $scope.promptModalMissingReqFields = false;
+ $scope.showPromptButton = false;
+ $scope.promptModalMissingReqFields = false;
} else {
$scope.showPromptButton = true;
@@ -1168,56 +1084,47 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
}
};
- function resetEdgeConflict(){
- $scope.edgeFlags.conflict = false;
-
- WorkflowService.checkForEdgeConflicts({
- treeData: $scope.treeData.data,
- edgeFlags: $scope.edgeFlags
- });
- }
-
- $scope.toggleManualControls = function() {
+ $scope.toggleManualControls = function () {
$scope.showManualControls = !$scope.showManualControls;
};
- $scope.panChart = function(direction) {
+ $scope.panChart = function (direction) {
$scope.$broadcast('panWorkflowChart', {
direction: direction
});
};
- $scope.zoomChart = function(zoom) {
+ $scope.zoomChart = function (zoom) {
$scope.$broadcast('zoomWorkflowChart', {
zoom: zoom
});
};
- $scope.resetChart = function() {
+ $scope.resetChart = function () {
$scope.$broadcast('resetWorkflowChart');
};
- $scope.workflowZoomed = function(zoom) {
+ $scope.workflowZoomed = function (zoom) {
$scope.$broadcast('workflowZoomed', {
zoom: zoom
});
};
- $scope.zoomToFitChart = function() {
+ $scope.zoomToFitChart = function () {
$scope.$broadcast('zoomToFitChart');
};
- $scope.openPromptModal = function() {
+ $scope.openPromptModal = function () {
$scope.promptData.triggerModalOpen = true;
};
let allNodes = [];
let page = 1;
- let buildTreeFromNodes = function(){
+ let buildTreeFromNodes = function () {
WorkflowService.buildTree({
workflowNodes: allNodes
- }).then(function(data){
+ }).then(function (data) {
$scope.treeData = data;
// TODO: I think that the workflow chart directive (and eventually d3) is meddling with
@@ -1234,33 +1141,35 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
});
};
- let getNodes = function(){
+ let getNodes = function () {
// Get the workflow nodes
TemplatesService.getWorkflowJobTemplateNodes($scope.workflowJobTemplateObj.id, page)
- .then(function(data){
- for(var i=0; i
-
+
diff --git a/awx/ui/client/src/templates/workflows/workflow.service.js b/awx/ui/client/src/templates/workflows/workflow.service.js
index fe632a2e1d..6b5ef1e45a 100644
--- a/awx/ui/client/src/templates/workflows/workflow.service.js
+++ b/awx/ui/client/src/templates/workflows/workflow.service.js
@@ -290,39 +290,5 @@ export default ['$q', function($q){
}
},
- checkForEdgeConflicts: function(params) {
- //params.treeData
- //params.edgeFlags
-
- let hasAlways = false;
- let hasSuccessFailure = false;
- let _this = this;
-
- _.forEach(params.treeData.children, function(child) {
- // Flip the flag to false for now - we'll set it to true later on
- // if we detect a conflict
- child.edgeConflict = false;
- if(child.edgeType === 'always') {
- hasAlways = true;
- }
- else if(child.edgeType === 'success' || child.edgeType === 'failure') {
- hasSuccessFailure = true;
- }
-
- _this.checkForEdgeConflicts({
- treeData: child,
- edgeFlags: params.edgeFlags
- });
- });
-
- if(hasAlways && hasSuccessFailure) {
- // We have a conflict
- _.forEach(params.treeData.children, function(child) {
- child.edgeConflict = true;
- });
-
- params.edgeFlags.conflict = true;
- }
- }
};
}];
diff --git a/awx/ui/test/e2e/README.md b/awx/ui/test/e2e/README.md
index d56bdda0d1..f7a9a2e65b 100644
--- a/awx/ui/test/e2e/README.md
+++ b/awx/ui/test/e2e/README.md
@@ -6,8 +6,8 @@ This is an automated functional test suite for the front end.
The tests are written in Node.js and use the [Nightwatch](https://github.com/nightwatchjs/nightwatch) test runner.
#### Requirements
-- node.js 6.x LTS
-- npm 3.x LTS
+- node.js 8.x LTS
+- npm 5.x LTS
#### Installation
A successful invocation of `make ui-devel` will prepare your environment with the software
diff --git a/awx/ui/test/e2e/commands/findThenClick.js b/awx/ui/test/e2e/commands/findThenClick.js
new file mode 100644
index 0000000000..82dc2b62f9
--- /dev/null
+++ b/awx/ui/test/e2e/commands/findThenClick.js
@@ -0,0 +1,8 @@
+exports.command = function findThenClick (selector) {
+ this.waitForElementPresent(selector, () => {
+ this.moveToElement(selector, 0, 0, () => {
+ this.click(selector);
+ });
+ });
+ return this;
+};
diff --git a/awx/ui/test/e2e/tests/test-workflow-visualizer.js b/awx/ui/test/e2e/tests/test-workflow-visualizer.js
new file mode 100644
index 0000000000..5d6a3d570b
--- /dev/null
+++ b/awx/ui/test/e2e/tests/test-workflow-visualizer.js
@@ -0,0 +1,144 @@
+import {
+ getInventorySource,
+ getJobTemplate,
+ getProject,
+ getWorkflowTemplate
+} from '../fixtures';
+
+let data;
+const spinny = "//*[contains(@class, 'spinny')]";
+const workflowTemplateNavTab = "//at-side-nav-item[contains(@name, 'TEMPLATES')]";
+const workflowSelector = "//a[contains(text(), 'test-actions-workflow-template')]";
+const workflowVisualizerBtn = "//button[contains(@id, 'workflow_job_template_workflow_visualizer_btn')]";
+
+const rootNode = "//*[@id='node-2']";
+const childNode = "//*[@id='node-3']";
+const newChildNode = "//*[@id='node-5']";
+const leafNode = "//*[@id='node-6']";
+const nodeAdd = "//*[contains(@class, 'nodeAddCross')]";
+const nodeRemove = "//*[contains(@class, 'nodeRemoveCross')]";
+
+// one of the jobs or projects or inventories
+const testActionsProject = "//td[contains(text(), 'test-actions-project')]";
+const testActionsJob = "//td[contains(text(), 'test-actions-job')]";
+
+// dropdown bar which lets you select edge type
+const edgeTypeDropdownBar = "//span[contains(@id, 'select2-workflow_node_edge-container')]";
+const alwaysDropdown = "//li[contains(@id, 'select2-workflow_node_edge') and text()='Always']";
+const successDropdown = "//li[contains(@id, 'select2-workflow_node_edge') and text()='On Success']";
+const failureDropdown = "//li[contains(@id, 'select2-workflow_node_edge') and text()='On Failure']";
+const selectButton = "//*[@id='workflow_maker_select_btn']";
+const deleteConfirmation = "//button[@ng-click='confirmDeleteNode()']";
+
+module.exports = {
+ before: (client, done) => {
+ const resources = [
+ getInventorySource('test-actions'),
+ getJobTemplate('test-actions'),
+ getProject('test-actions'),
+ getWorkflowTemplate('test-actions'),
+ ];
+
+ Promise.all(resources)
+ .then(([source, template, project, workflow]) => {
+ data = { source, template, project, workflow };
+ done();
+ });
+ client
+ .login()
+ .waitForAngular()
+ .resizeWindow(1200, 1000)
+ .useXpath()
+ .findThenClick(workflowTemplateNavTab)
+ .pause(1500)
+ .waitForElementNotVisible(spinny)
+ .findThenClick(workflowSelector)
+ .findThenClick(workflowVisualizerBtn);
+ },
+ 'verify that workflow visualizer root node can only be set to always': client => {
+ client
+ .useXpath()
+ .findThenClick(rootNode)
+ .findThenClick(testActionsProject)
+ .findThenClick(edgeTypeDropdownBar)
+ .waitForElementNotPresent(successDropdown)
+ .waitForElementNotPresent(failureDropdown)
+ .waitForElementPresent(alwaysDropdown);
+ },
+ 'verify that a non-root node can be set to always/success/failure': client => {
+ client
+ .useXpath()
+ .findThenClick(childNode)
+ .pause(1000)
+ .waitForElementNotVisible(spinny)
+ .findThenClick(edgeTypeDropdownBar)
+ .waitForElementPresent(successDropdown)
+ .waitForElementPresent(failureDropdown)
+ .waitForElementPresent(alwaysDropdown)
+ .findThenClick(edgeTypeDropdownBar);
+ },
+ 'verify that a sibling node can be any edge type': client => {
+ client
+ .useXpath()
+ .moveToElement(childNode, 0, 0, () => {
+ client.pause(500);
+ client.waitForElementNotVisible(spinny);
+ // Concatenating the xpaths lets us click the proper node
+ client.click(childNode + nodeAdd);
+ })
+ .pause(1000)
+ .waitForElementNotVisible(spinny)
+ .findThenClick(testActionsJob)
+ .pause(1000)
+ .waitForElementNotVisible(spinny)
+ .findThenClick(edgeTypeDropdownBar)
+ .waitForElementPresent(successDropdown)
+ .waitForElementPresent(failureDropdown)
+ .waitForElementPresent(alwaysDropdown)
+ .findThenClick(alwaysDropdown)
+ .click(selectButton);
+ },
+ 'Verify node-shifting behavior upon deletion': client => {
+ client
+ .findThenClick(newChildNode)
+ .pause(1000)
+ .waitForElementNotVisible(spinny)
+ .findThenClick(edgeTypeDropdownBar)
+ .findThenClick(successDropdown)
+ .click(selectButton)
+ .moveToElement(newChildNode, 0, 0, () => {
+ client.pause(500);
+ client.waitForElementNotVisible(spinny);
+ client.click(newChildNode + nodeAdd);
+ })
+ .pause(1000)
+ .waitForElementNotVisible(spinny)
+ .findThenClick(testActionsJob)
+ .pause(1000)
+ .waitForElementNotVisible(spinny)
+ .findThenClick(edgeTypeDropdownBar)
+ .waitForElementPresent(successDropdown)
+ .waitForElementPresent(failureDropdown)
+ .waitForElementPresent(alwaysDropdown)
+ .findThenClick(alwaysDropdown)
+ .click(selectButton)
+ .moveToElement(newChildNode, 0, 0, () => {
+ client.pause(500);
+ client.waitForElementNotVisible(spinny);
+ client.click(newChildNode + nodeRemove);
+ })
+ .pause(1000)
+ .waitForElementNotVisible(spinny)
+ .findThenClick(deleteConfirmation)
+ .findThenClick(leafNode)
+ .pause(1000)
+ .waitForElementNotVisible(spinny)
+ .findThenClick(edgeTypeDropdownBar)
+ .waitForElementPresent(successDropdown)
+ .waitForElementPresent(failureDropdown)
+ .waitForElementPresent(alwaysDropdown);
+ },
+ after: client => {
+ client.end();
+ }
+};
diff --git a/docs/resource_copy.md b/docs/resource_copy.md
index da85d55225..77d6e6b820 100644
--- a/docs/resource_copy.md
+++ b/docs/resource_copy.md
@@ -102,7 +102,7 @@ available fields.
```
`CopyAPIView` will automatically detect sub objects of an object, and do a deep copy of all sub objects
-as a background celery task. There are sometimes permission issues with sub object copy. For example,
+as a background task. There are sometimes permission issues with sub object copy. For example,
when copying nodes of a workflow job template, there are cases where the user performing copy has no use
permission of related credential and inventory of some nodes, and it is desired those fields will be
`None`. In order to do that, developer should provide a static method `deep_copy_permission_check_func`
diff --git a/docs/task_manager_system.md b/docs/task_manager_system.md
index 4d1dd8daef..697474b02d 100644
--- a/docs/task_manager_system.md
+++ b/docs/task_manager_system.md
@@ -1,15 +1,15 @@
# Task Manager Overview
-The task manager is responsible for deciding when jobs should be introduced to celery for running. When choosing a task to run the considerations are: (1) creation time, (2) job dependency, (3) capacity.
+The task manager is responsible for deciding when jobs should scheduled to run. When choosing a task to run the considerations are: (1) creation time, (2) job dependency, (3) capacity.
-Independent jobs are ran in order of creation time, earliest first. Jobs with dependencies are also ran in creation time order within the group of job dependencies. Capacity is the final consideration when deciding to release a job to be ran by celery.
+Independent jobs are ran in order of creation time, earliest first. Jobs with dependencies are also ran in creation time order within the group of job dependencies. Capacity is the final consideration when deciding to release a job to be ran by the task dispatcher.
## Task Manager Architecture
The task manager has a single entry point, `Scheduler().schedule()`. The method may be called in parallel, at any time, as many times as the user wants. The `schedule()` function tries to aquire a single, global, lock using the Instance table first record in the database. If the lock cannot be aquired the method returns. The failure to aquire the lock indicates that there is another instance currently running `schedule()`.
### Hybrid Scheduler: Periodic + Event
-The `schedule()` function is ran (a) periodically by a celery task and (b) on job creation or completion. The task manager system would behave correctly if ran, exclusively, via (a) or (b). We chose to trigger `schedule()` via both mechanisms because of the nice properties I will now mention. (b) reduces the time from launch to running, resulting a better user experience. (a) is a fail-safe in case we miss code-paths, in the present and future, that change the 3 scheduling considerations for which we should call `schedule()` (i.e. adding new nodes to tower changes the capacity, obscure job error handling that fails a job)
+The `schedule()` function is ran (a) periodically by a background task and (b) on job creation or completion. The task manager system would behave correctly if ran, exclusively, via (a) or (b). We chose to trigger `schedule()` via both mechanisms because of the nice properties I will now mention. (b) reduces the time from launch to running, resulting a better user experience. (a) is a fail-safe in case we miss code-paths, in the present and future, that change the 3 scheduling considerations for which we should call `schedule()` (i.e. adding new nodes to tower changes the capacity, obscure job error handling that fails a job)
Emperically, the periodic task manager has served us well in the past and we will continue to rely on it with the added event-triggered `schedule()`.
### Scheduler Algorithm
@@ -17,14 +17,14 @@ The `schedule()` function is ran (a) periodically by a celery task and (b) on jo
* Detect finished workflow jobs
* Spawn next workflow jobs if needed
* For each pending jobs; start with oldest created job
- * If job is not blocked, and there is capacity in the instance group queue, then mark the as `waiting` and submit the job to celery.
+ * If job is not blocked, and there is capacity in the instance group queue, then mark the as `waiting` and submit the job to RabbitMQ.
### Job Lifecycle
| Job Status | State |
|:----------:|:------------------------------------------------------------------------------------------------------------------:|
| pending | Job launched.