Merge pull request #2373 from marshmalien/always_nodes_ui

Display WF always nodes in conjunction with success and failure

Reviewed-by: https://github.com/softwarefactory-project-zuul[bot]
This commit is contained in:
softwarefactory-project-zuul[bot]
2018-10-12 22:43:32 +00:00
committed by GitHub
11 changed files with 444 additions and 434 deletions

View File

@@ -3490,10 +3490,6 @@ class WorkflowJobTemplateNodeChildrenBaseList(WorkflowsEnforcementMixin, Enforce
return getattr(parent, self.relationship).all() return getattr(parent, self.relationship).all()
def is_valid_relation(self, parent, sub, created=False): def is_valid_relation(self, parent, sub, created=False):
mutex_list = ('success_nodes', 'failure_nodes') if self.relationship == 'always_nodes' else ('always_nodes',)
for relation in mutex_list:
if getattr(parent, relation).all().exists():
return {'Error': _('Cannot associate {0} when {1} have been associated.').format(self.relationship, relation)}
if created: if created:
return None return None

View File

@@ -7,6 +7,7 @@ from awx.main.models.workflow import WorkflowJob, WorkflowJobNode, WorkflowJobTe
from awx.main.models.jobs import JobTemplate, Job from awx.main.models.jobs import JobTemplate, Job
from awx.main.models.projects import ProjectUpdate from awx.main.models.projects import ProjectUpdate
from awx.main.scheduler.dag_workflow import WorkflowDAG from awx.main.scheduler.dag_workflow import WorkflowDAG
from awx.api.versioning import reverse
# Django # Django
from django.test import TransactionTestCase from django.test import TransactionTestCase
@@ -196,9 +197,15 @@ class TestWorkflowJobTemplate:
assert test_view.is_valid_relation(node_assoc, nodes[1]) == {'Error': 'Multiple parent relationship not allowed.'} assert test_view.is_valid_relation(node_assoc, nodes[1]) == {'Error': 'Multiple parent relationship not allowed.'}
# test mutex validation # test mutex validation
test_view.relationship = 'failure_nodes' test_view.relationship = 'failure_nodes'
node_assoc_1 = WorkflowJobTemplateNode.objects.create(workflow_job_template=wfjt)
assert (test_view.is_valid_relation(nodes[2], node_assoc_1) == def test_always_success_failure_creation(self, wfjt, admin, get):
{'Error': 'Cannot associate failure_nodes when always_nodes have been associated.'}) wfjt_node = wfjt.workflow_job_template_nodes.all()[1]
node = WorkflowJobTemplateNode.objects.create(workflow_job_template=wfjt)
wfjt_node.always_nodes.add(node)
assert len(node.get_parent_nodes()) == 1
url = reverse('api:workflow_job_template_node_list') + str(wfjt_node.id) + '/'
resp = get(url, admin)
assert node.id in resp.data['always_nodes']
def test_wfjt_unique_together_with_org(self, organization): def test_wfjt_unique_together_with_org(self, organization):
wfjt1 = WorkflowJobTemplate(name='foo', organization=organization) wfjt1 = WorkflowJobTemplate(name='foo', organization=organization)

View File

@@ -112,7 +112,6 @@ function TemplatesStrings (BaseString) {
RUN: t.s('RUN'), RUN: t.s('RUN'),
CHECK: t.s('CHECK'), CHECK: t.s('CHECK'),
SELECT: t.s('SELECT'), SELECT: t.s('SELECT'),
EDGE_CONFLICT: t.s('EDGE CONFLICT'),
DELETED: t.s('DELETED'), DELETED: t.s('DELETED'),
START: t.s('START'), START: t.s('START'),
DETAILS: t.s('DETAILS'), DETAILS: t.s('DETAILS'),

View File

@@ -102,13 +102,6 @@
width: 90px; width: 90px;
color: @default-interface-txt; color: @default-interface-txt;
} }
.WorkflowChart-conflictIcon {
color: @default-err;
}
.WorkflowChart-conflictText {
width: 90px;
color: @default-interface-txt;
}
.WorkflowChart-activeNode { .WorkflowChart-activeNode {
fill: @default-link; fill: @default-link;
} }

View File

@@ -327,16 +327,6 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : ""; return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : "";
}).each(wrap); }).each(wrap);
thisNode.append("foreignObject")
.attr("x", 54)
.attr("y", 45)
.style("font-size","0.7em")
.attr("class", "WorkflowChart-conflictText")
.html(function () {
return `<span class=\"WorkflowChart-conflictIcon\">\uf06a</span><span> ${TemplatesStrings.get('workflow_maker.EDGE_CONFLICT')}</span>`;
})
.style("display", function(d) { return (d.edgeConflict && !d.placeholder) ? null : "none"; });
thisNode.append("foreignObject") thisNode.append("foreignObject")
.attr("x", 62) .attr("x", 62)
.attr("y", 22) .attr("y", 22)
@@ -831,9 +821,6 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
t.selectAll(".WorkflowChart-deletedText") t.selectAll(".WorkflowChart-deletedText")
.style("display", function(d){ return d.unifiedJobTemplate || d.placeholder ? "none" : null; }); .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") t.selectAll(".WorkflowChart-activeNode")
.style("display", function(d) { return d.isActiveEdit ? null : "none"; }); .style("display", function(d) { return d.isActiveEdit ? null : "none"; });

View File

@@ -4,13 +4,12 @@
* All Rights Reserved * All Rights Reserved
*************************************************/ *************************************************/
export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', export default ['$scope', 'WorkflowService', 'TemplatesService',
'$state', 'ProcessErrors', 'CreateSelect2', '$q', 'JobTemplateModel', 'ProcessErrors', 'CreateSelect2', '$q', 'JobTemplateModel',
'Empty', 'PromptService', 'Rest', 'TemplatesStrings', '$timeout', 'Empty', 'PromptService', 'Rest', 'TemplatesStrings', '$timeout',
'i18n', function ($scope, WorkflowService, TemplatesService,
function($scope, WorkflowService, GetBasePath, TemplatesService, ProcessErrors, CreateSelect2, $q, JobTemplate,
$state, ProcessErrors, CreateSelect2, $q, JobTemplate, Empty, PromptService, Rest, TemplatesStrings, $timeout) {
Empty, PromptService, Rest, TemplatesStrings, $timeout, i18n) {
let promptWatcher, surveyQuestionWatcher, credentialsWatcher; let promptWatcher, surveyQuestionWatcher, credentialsWatcher;
@@ -31,24 +30,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
value: "check" value: "check"
}]; }];
$scope.edgeFlags = { $scope.edgeTypeOptions = createEdgeTypeOptions();
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'
}
];
let editRequests = []; let editRequests = [];
let associateRequests = []; let associateRequests = [];
@@ -59,6 +41,22 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.toggleKey = () => $scope.showKey = !$scope.showKey; $scope.toggleKey = () => $scope.showKey = !$scope.showKey;
$scope.keyClassList = `{ 'Key-menuIcon--active': 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() { function resetNodeForm() {
$scope.workflowMakerFormConfig.nodeMode = "idle"; $scope.workflowMakerFormConfig.nodeMode = "idle";
delete $scope.selectedTemplate; delete $scope.selectedTemplate;
@@ -74,7 +72,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
// params.parentId // params.parentId
// params.node // params.node
let buildSendableNodeData = function() { let buildSendableNodeData = function () {
// Create the node // Create the node
let sendableNodeData = { let sendableNodeData = {
unified_job_template: params.node.unifiedJobTemplate.id, unified_job_template: params.node.unifiedJobTemplate.id,
@@ -122,7 +120,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
return sendableNodeData; return sendableNodeData;
}; };
let continueRecursing = function(parentId) { let continueRecursing = function (parentId) {
$scope.totalIteratedNodes++; $scope.totalIteratedNodes++;
if ($scope.totalIteratedNodes === $scope.treeData.data.totalNodes) { if ($scope.totalIteratedNodes === $scope.treeData.data.totalNodes) {
@@ -130,7 +128,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
completionCallback(); completionCallback();
} else { } else {
if (params.node.children && params.node.children.length > 0) { 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") { if (child.edgeType === "success") {
recursiveNodeUpdates({ recursiveNodeUpdates({
parentId: parentId, parentId: parentId,
@@ -155,49 +153,51 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
if (params.node.isNew) { if (params.node.isNew) {
TemplatesService.addWorkflowNode({ TemplatesService.addWorkflowNode({
url: $scope.treeData.workflow_job_template_obj.related.workflow_nodes, url: $scope.treeData.workflow_job_template_obj.related.workflow_nodes,
data: buildSendableNodeData() data: buildSendableNodeData()
}) })
.then(function(data) { .then(function (data) {
if (!params.node.isRoot) { if (!params.node.isRoot) {
associateRequests.push({ associateRequests.push({
parentId: params.parentId, parentId: params.parentId,
nodeId: data.data.id, nodeId: data.data.id,
edge: params.node.edgeType 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 { } else {
if (params.node.edited || !params.node.originalParentId || (params.node.originalParentId && params.parentId !== params.node.originalParentId)) { 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() data: buildSendableNodeData()
}); });
if (_.get(params, 'node.promptData.launchConf.ask_credential_on_launch')){ if (_.get(params, 'node.promptData.launchConf.ask_credential_on_launch')) {
let credentialsNotInPriorCredentials = params.node.promptData.prompts.credentials.value.filter(function(credFromPrompt) { 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 : []; let defaultCreds = _.get(params, 'node.promptData.launchConf.defaults.credentials', []);
return !defaultCreds.some(function(defaultCred) { return !defaultCreds.some(function (defaultCred) {
return credFromPrompt.id === defaultCred.id; return credFromPrompt.id === defaultCred.id;
}); });
}); });
let credentialsToAdd = credentialsNotInPriorCredentials.filter(function(credNotInPrior) { let credentialsToAdd = credentialsNotInPriorCredentials.filter(function (credNotInPrior) {
let previousOverrides = params.node.promptData.prompts.credentials.previousOverrides ? params.node.promptData.prompts.credentials.previousOverrides : []; let previousOverrides = _.get(params, 'node.promptData.prompts.credentials.previousOverrides', []);
return !previousOverrides.some(function(priorCred) { return !previousOverrides.some(function (priorCred) {
return credNotInPrior.id === priorCred.id; return credNotInPrior.id === priorCred.id;
}); });
}); });
let credentialsToRemove = []; let credentialsToRemove = [];
if (_.has(params, 'node.promptData.prompts.credentials.previousOverrides')) { if (_.has(params, 'node.promptData.prompts.credentials.previousOverrides')) {
credentialsToRemove = params.node.promptData.prompts.credentials.previousOverrides.filter(function(priorCred) { credentialsToRemove = params.node.promptData.prompts.credentials.previousOverrides.filter(function (priorCred) {
return !credentialsNotInPriorCredentials.some(function(credNotInPrior) { return !credentialsNotInPriorCredentials.some(function (credNotInPrior) {
return priorCred.id === credNotInPrior.id; return priorCred.id === credNotInPrior.id;
}); });
}); });
} }
credentialsToAdd.forEach((credentialToAdd) => { credentialsToAdd.forEach((credentialToAdd) => {
credentialRequests.push({ credentialRequests.push({
id: params.node.nodeId, id: params.node.nodeId,
data: { data: {
id: credentialToAdd.id id: credentialToAdd.id
} }
}); });
}); });
credentialsToRemove.forEach((credentialToRemove) => { credentialsToRemove.forEach((credentialToRemove) => {
credentialRequests.push({ credentialRequests.push({
id: params.node.nodeId, id: params.node.nodeId,
data: { data: {
id: credentialToRemove.id, id: credentialToRemove.id,
disassociate: true disassociate: true
} }
}); });
}); });
} }
} }
if (params.node.originalParentId && (params.parentId !== params.node.originalParentId || params.node.originalEdge !== params.node.edgeType)) { if (params.node.originalParentId && (params.parentId !== params.node.originalParentId || params.node.originalEdge !== params.node.edgeType)) {
let parentIsDeleted = false; let parentIsDeleted = false;
_.forEach($scope.treeData.data.deletedNodes, function(deletedNode) { _.forEach($scope.treeData.data.deletedNodes, function (deletedNode) {
if (deletedNode === params.node.originalParentId) { if (deletedNode === params.node.originalParentId) {
parentIsDeleted = true; parentIsDeleted = true;
} }
@@ -297,44 +297,15 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
} }
} }
let updateEdgeDropdownOptions = (optionsToInclude) => { let updateEdgeDropdownOptions = (edgeTypeValue) => {
// Not passing optionsToInclude will include all by default // Not passing an edgeTypeValue 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 = [];
optionsToInclude.forEach((optionToInclude) => { if (edgeTypeValue) {
if (optionToInclude === "always") { $scope.edgeTypeOptions = _.filter(createEdgeTypeOptions(), {
$scope.edgeTypeOptions.push({ 'value': edgeTypeValue
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'
});
}
}); });
} else {
$scope.edgeTypeOptions = createEdgeTypeOptions();
} }
CreateSelect2({ CreateSelect2({
@@ -347,9 +318,9 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
let credentialRequiresPassword = false; let credentialRequiresPassword = false;
$scope.promptData.prompts.credentials.value.forEach((credential) => { $scope.promptData.prompts.credentials.value.forEach((credential) => {
if ((credential.passwords_needed && if ((credential.passwords_needed &&
credential.passwords_needed.length > 0) || credential.passwords_needed.length > 0) ||
(_.has(credential, 'inputs.vault_password') && (_.has(credential, 'inputs.vault_password') &&
credential.inputs.vault_password === "ASK") credential.inputs.vault_password === "ASK")
) { ) {
credentialRequiresPassword = true; credentialRequiresPassword = true;
} }
@@ -364,7 +335,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
'missingSurveyValue' 'missingSurveyValue'
]; ];
promptWatcher = $scope.$watchGroup(promptDataToWatch, function() { promptWatcher = $scope.$watchGroup(promptDataToWatch, function () {
let missingPromptValue = false; let missingPromptValue = false;
if ($scope.missingSurveyValue) { if ($scope.missingSurveyValue) {
missingPromptValue = true; 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 // Revert the data to the master which was created when the dialog was opened
$scope.treeData.data = angular.copy($scope.treeDataMaster); $scope.treeData.data = angular.copy($scope.treeDataMaster);
$scope.closeDialog(); $scope.closeDialog();
}; };
$scope.saveWorkflowMaker = function() { $scope.saveWorkflowMaker = function () {
$scope.totalIteratedNodes = 0; $scope.totalIteratedNodes = 0;
if ($scope.treeData && $scope.treeData.data && $scope.treeData.data.children && $scope.treeData.data.children.length > 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({ return TemplatesService.disassociateWorkflowNode({
parentId: request.parentId, parentId: request.parentId,
nodeId: request.nodeId, 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({ return TemplatesService.editWorkflowNode({
id: request.id, id: request.id,
data: request.data 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); return TemplatesService.deleteWorkflowJobTemplateNode(nodeId);
}); });
$q.all(disassociatePromises.concat(editNodePromises, deletePromises)) $q.all(disassociatePromises.concat(editNodePromises, deletePromises))
.then(function() { .then(function () {
let credentialPromises = credentialRequests.map(function(request) { let credentialPromises = credentialRequests.map(function (request) {
return TemplatesService.postWorkflowNodeCredential({ return TemplatesService.postWorkflowNodeCredential({
id: request.id, id: request.id,
data: request.data data: request.data
});
}); });
});
let associatePromises = associateRequests.map(function(request) { let associatePromises = associateRequests.map(function (request) {
return TemplatesService.associateWorkflowNode({ return TemplatesService.associateWorkflowNode({
parentId: request.parentId, parentId: request.parentId,
nodeId: request.nodeId, nodeId: request.nodeId,
edge: request.edge edge: request.edge
});
}); });
});
$q.all(associatePromises.concat(credentialPromises)) return $q.all(associatePromises.concat(credentialPromises))
.then(function() { .then(function () {
$scope.closeDialog(); $scope.closeDialog();
}).catch(({data, status}) => { });
}).catch(({
data,
status
}) => {
ProcessErrors($scope, data, status, null, {}); 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({ recursiveNodeUpdates({
node: child node: child
}, completionCallback); }, completionCallback);
}); });
} else { } else {
let deletePromises = $scope.treeData.data.deletedNodes.map(function(nodeId) { let deletePromises = $scope.treeData.data.deletedNodes.map(function (nodeId) {
return TemplatesService.deleteWorkflowJobTemplateNode(nodeId); return TemplatesService.deleteWorkflowJobTemplateNode(nodeId);
}); });
$q.all(deletePromises) $q.all(deletePromises)
.then(function() { .then(function () {
$scope.closeDialog(); $scope.closeDialog();
}); });
} }
}; };
/* ADD NODE FUNCTIONS */ /* ADD NODE FUNCTIONS */
$scope.startAddNode = function(parent, betweenTwoNodes) { $scope.startAddNode = function (parent, betweenTwoNodes) {
if ($scope.placeholderNode || $scope.nodeBeingEdited) { if ($scope.placeholderNode || $scope.nodeBeingEdited) {
$scope.cancelNodeForm(); $scope.cancelNodeForm();
@@ -481,41 +453,29 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.treeData.nextIndex++; $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 // 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))) { if (parent && ((betweenTwoNodes && parent.source.isStartNode) || (!betweenTwoNodes && parent.isStartNode))) {
// We don't want to give the user the option to select // This node will always be executed
// a type as this node will always be executed updateEdgeDropdownOptions('always');
updateEdgeDropdownOptions(["always"]); edgeType = {
edgeType = {label: $scope.strings.get('workflow_maker.ALWAYS'), value: "always"}; label: $scope.strings.get('workflow_maker.ALWAYS'),
value: "always"
};
} else { } else {
if (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) { updateEdgeDropdownOptions();
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();
}
} }
// Reset the edgeConflict flag
resetEdgeConflict();
$scope.edgeType = edgeType; $scope.edgeType = edgeType;
$scope.$broadcast("refreshWorkflowChart"); $scope.$broadcast("refreshWorkflowChart");
}; };
$scope.confirmNodeForm = function() { $scope.confirmNodeForm = function () {
if ($scope.workflowMakerFormConfig.nodeMode === "add") { if ($scope.workflowMakerFormConfig.nodeMode === "add") {
if ($scope.selectedTemplate && $scope.edgeType && $scope.edgeType.value) { if ($scope.selectedTemplate && $scope.edgeType && $scope.edgeType.value) {
@@ -565,13 +525,10 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.promptData = null; $scope.promptData = null;
// Reset the edgeConflict flag
resetEdgeConflict();
$scope.$broadcast("refreshWorkflowChart"); $scope.$broadcast("refreshWorkflowChart");
}; };
$scope.cancelNodeForm = function() { $scope.cancelNodeForm = function () {
if ($scope.workflowMakerFormConfig.nodeMode === "add") { if ($scope.workflowMakerFormConfig.nodeMode === "add") {
// Remove the placeholder node from the tree // Remove the placeholder node from the tree
WorkflowService.removeNodeFromTree({ WorkflowService.removeNodeFromTree({
@@ -598,9 +555,6 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.selectedTemplateInvalid = false; $scope.selectedTemplateInvalid = false;
$scope.showPromptButton = false; $scope.showPromptButton = false;
// Reset the edgeConflict flag
resetEdgeConflict();
// Reset the form // Reset the form
resetNodeForm(); resetNodeForm();
@@ -609,7 +563,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
/* EDIT NODE FUNCTIONS */ /* EDIT NODE FUNCTIONS */
$scope.startEditNode = function(nodeToEdit) { $scope.startEditNode = function (nodeToEdit) {
if (!$scope.nodeBeingEdited || ($scope.nodeBeingEdited && $scope.nodeBeingEdited.id !== nodeToEdit.id)) { if (!$scope.nodeBeingEdited || ($scope.nodeBeingEdited && $scope.nodeBeingEdited.id !== nodeToEdit.id)) {
if ($scope.placeholderNode || $scope.nodeBeingEdited) { if ($scope.placeholderNode || $scope.nodeBeingEdited) {
@@ -636,7 +590,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.nodeBeingEdited.isActiveEdit = true; $scope.nodeBeingEdited.isActiveEdit = true;
let finishConfiguringEdit = function() { let finishConfiguringEdit = function () {
let jobTemplate = new JobTemplate(); let jobTemplate = new JobTemplate();
@@ -656,8 +610,8 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
!launchConf.credential_needed_to_start && !launchConf.credential_needed_to_start &&
!launchConf.ask_variables_on_launch && !launchConf.ask_variables_on_launch &&
launchConf.variables_needed_to_start.length === 0) { launchConf.variables_needed_to_start.length === 0) {
$scope.showPromptButton = false; $scope.showPromptButton = false;
$scope.promptModalMissingReqFields = false; $scope.promptModalMissingReqFields = false;
} else { } else {
$scope.showPromptButton = true; $scope.showPromptButton = true;
@@ -729,7 +683,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
let credentialRequiresPassword = false; let credentialRequiresPassword = false;
prompts.credentials.value.forEach((credential) => { prompts.credentials.value.forEach((credential) => {
if(credential.inputs) { if (credential.inputs) {
if ((credential.inputs.password && credential.inputs.password === "ASK") || if ((credential.inputs.password && credential.inputs.password === "ASK") ||
(credential.inputs.become_password && credential.inputs.become_password === "ASK") || (credential.inputs.become_password && credential.inputs.become_password === "ASK") ||
(credential.inputs.ssh_key_unlock && credential.inputs.ssh_key_unlock === "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.credential_needed_to_start &&
!launchConf.ask_variables_on_launch && !launchConf.ask_variables_on_launch &&
launchConf.variables_needed_to_start.length === 0) { launchConf.variables_needed_to_start.length === 0) {
$scope.showPromptButton = false; $scope.showPromptButton = false;
$scope.promptModalMissingReqFields = false; $scope.promptModalMissingReqFields = false;
} else { } else {
$scope.showPromptButton = true; $scope.showPromptButton = true;
@@ -816,7 +770,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
watchForPromptChanges(); watchForPromptChanges();
} }
} }
}); });
} }
if (_.get($scope, 'nodeBeingEdited.unifiedJobTemplate')) { if (_.get($scope, 'nodeBeingEdited.unifiedJobTemplate')) {
@@ -855,32 +809,30 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.workflowMakerFormConfig.activeTab = "jobs"; $scope.workflowMakerFormConfig.activeTab = "jobs";
} }
let siblingConnectionTypes = WorkflowService.getSiblingConnectionTypes({ let edgeDropdownOptions = null;
tree: $scope.treeData.data,
parentId: parent.id,
childId: nodeToEdit.id
});
let edgeDropdownOptions = null; // Select RUN dropdown option
switch ($scope.nodeBeingEdited.edgeType) {
switch($scope.nodeBeingEdited.edgeType) {
case "always": case "always":
$scope.edgeType = {label: i18n._("Always"), value: "always"}; $scope.edgeType = {
if (siblingConnectionTypes.length === 1 && _.includes(siblingConnectionTypes, "always") || $scope.nodeBeingEdited.isRoot) { label: $scope.strings.get('workflow_maker.ALWAYS'),
edgeDropdownOptions = ["always"]; value: "always"
};
if ($scope.nodeBeingEdited.isRoot) {
edgeDropdownOptions = 'always';
} }
break; break;
case "success": case "success":
$scope.edgeType = {label: i18n._("On Success"), value: "success"}; $scope.edgeType = {
if (siblingConnectionTypes.length !== 0 && (!_.includes(siblingConnectionTypes, "always"))) { label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
edgeDropdownOptions = ["success", "failure"]; value: "success"
} };
break; break;
case "failure": case "failure":
$scope.edgeType = {label: i18n._("On Failure"), value: "failure"}; $scope.edgeType = {
if (siblingConnectionTypes.length !== 0 && (!_.includes(siblingConnectionTypes, "always"))) { label: $scope.strings.get('workflow_maker.ON_FAILURE'),
edgeDropdownOptions = ["success", "failure"]; value: "failure"
} };
break; break;
} }
@@ -897,15 +849,18 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
// unified job template so we're going to pull down the whole object // unified job template so we're going to pull down the whole object
TemplatesService.getUnifiedJobTemplate($scope.nodeBeingEdited.unifiedJobTemplate.id) TemplatesService.getUnifiedJobTemplate($scope.nodeBeingEdited.unifiedJobTemplate.id)
.then(function(data) { .then(function (data) {
$scope.nodeBeingEdited.unifiedJobTemplate = _.clone(data.data.results[0]); $scope.nodeBeingEdited.unifiedJobTemplate = _.clone(data.data.results[0]);
finishConfiguringEdit(); finishConfiguringEdit();
}, function(error) { }, function ({ data, status, config }) {
ProcessErrors($scope, error.data, error.status, null, { ProcessErrors($scope, data, status, null, {
hdr: 'Error!', hdr: $scope.strings.get('error.HEADER'),
msg: 'Failed to get unified job template. GET returned ' + msg: $scope.strings.get('error.CALL', {
'status: ' + error.status path: `${config.url}`,
}); action: `${config.method}`,
status
})
});
}); });
} else { } else {
finishConfiguringEdit(); finishConfiguringEdit();
@@ -922,16 +877,16 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
$scope.deleteOverlayVisible = false; $scope.deleteOverlayVisible = false;
} }
$scope.startDeleteNode = function(nodeToDelete) { $scope.startDeleteNode = function (nodeToDelete) {
$scope.nodeToBeDeleted = nodeToDelete; $scope.nodeToBeDeleted = nodeToDelete;
$scope.deleteOverlayVisible = true; $scope.deleteOverlayVisible = true;
}; };
$scope.cancelDeleteNode = function() { $scope.cancelDeleteNode = function () {
resetDeleteNode(); resetDeleteNode();
}; };
$scope.confirmDeleteNode = function() { $scope.confirmDeleteNode = function () {
if ($scope.nodeToBeDeleted) { if ($scope.nodeToBeDeleted) {
// TODO: turn this into a promise so that we can handle errors // TODO: turn this into a promise so that we can handle errors
@@ -949,95 +904,56 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
resetNodeForm(); resetNodeForm();
} }
// Reset the edgeConflict flag
resetEdgeConflict();
resetDeleteNode(); resetDeleteNode();
$scope.$broadcast("refreshWorkflowChart"); $scope.$broadcast("refreshWorkflowChart");
if ($scope.placeholderNode) { 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) { if ($scope.placeholderNode.isRoot) {
updateEdgeDropdownOptions(["always"]); updateEdgeDropdownOptions('always');
edgeType = {label: "Always", value: "always"}; edgeType = {
} else { label: $scope.strings.get('workflow_maker.ALWAYS'),
// we need to update the possible edges based on any new siblings value: "always"
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"]);
} else { } else {
updateEdgeDropdownOptions(); updateEdgeDropdownOptions();
} }
switch($scope.nodeBeingEdited.edgeType) { $scope.edgeType = edgeType;
case "always": } else if ($scope.nodeBeingEdited) {
$scope.edgeType = {label: i18n._("Always"), value: "always"};
if ( switch ($scope.nodeBeingEdited.edgeType) {
_.includes(siblingConnectionTypes, "always") && case "always":
!_.includes(siblingConnectionTypes, "success") && $scope.edgeType = {
!_.includes(siblingConnectionTypes, "failure") label: $scope.strings.get('workflow_maker.ALWAYS'),
) { value: "always"
updateEdgeDropdownOptions(["always"]); };
} else { if ($scope.nodeBeingEdited.isRoot) {
updateEdgeDropdownOptions(); updateEdgeDropdownOptions('always');
} } else {
break; updateEdgeDropdownOptions();
case "success": }
$scope.edgeType = {label: i18n._("On Success"), value: "success"}; break;
if ( case "success":
(_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) && $scope.edgeType = {
!_.includes(siblingConnectionTypes, "always") label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
) { value: "success"
updateEdgeDropdownOptions(["success", "failure"]); };
} else { updateEdgeDropdownOptions();
updateEdgeDropdownOptions(); break;
} case "failure":
break; $scope.edgeType = {
case "failure": label: $scope.strings.get('workflow_maker.ON_FAILURE'),
$scope.edgeType = {label: i18n._("On Failure"), value: "failure"}; value: "failure"
if ( };
(_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) && updateEdgeDropdownOptions();
!_.includes(siblingConnectionTypes, "always") break;
) { }
updateEdgeDropdownOptions(["success", "failure"]);
} else {
updateEdgeDropdownOptions();
}
break;
}
} }
$scope.treeData.data.totalNodes--; $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) { if ($scope.workflowMakerFormConfig.activeTab !== tab) {
$scope.workflowMakerFormConfig.activeTab = tab; $scope.workflowMakerFormConfig.activeTab = tab;
} }
}; };
$scope.templateManuallySelected = function(selectedTemplate) { $scope.templateManuallySelected = function (selectedTemplate) {
if (promptWatcher) { if (promptWatcher) {
promptWatcher(); promptWatcher();
@@ -1100,8 +1016,8 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
!launchConf.credential_needed_to_start && !launchConf.credential_needed_to_start &&
!launchConf.ask_variables_on_launch && !launchConf.ask_variables_on_launch &&
launchConf.variables_needed_to_start.length === 0) { launchConf.variables_needed_to_start.length === 0) {
$scope.showPromptButton = false; $scope.showPromptButton = false;
$scope.promptModalMissingReqFields = false; $scope.promptModalMissingReqFields = false;
} else { } else {
$scope.showPromptButton = true; $scope.showPromptButton = true;
@@ -1168,56 +1084,47 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
} }
}; };
function resetEdgeConflict(){ $scope.toggleManualControls = function () {
$scope.edgeFlags.conflict = false;
WorkflowService.checkForEdgeConflicts({
treeData: $scope.treeData.data,
edgeFlags: $scope.edgeFlags
});
}
$scope.toggleManualControls = function() {
$scope.showManualControls = !$scope.showManualControls; $scope.showManualControls = !$scope.showManualControls;
}; };
$scope.panChart = function(direction) { $scope.panChart = function (direction) {
$scope.$broadcast('panWorkflowChart', { $scope.$broadcast('panWorkflowChart', {
direction: direction direction: direction
}); });
}; };
$scope.zoomChart = function(zoom) { $scope.zoomChart = function (zoom) {
$scope.$broadcast('zoomWorkflowChart', { $scope.$broadcast('zoomWorkflowChart', {
zoom: zoom zoom: zoom
}); });
}; };
$scope.resetChart = function() { $scope.resetChart = function () {
$scope.$broadcast('resetWorkflowChart'); $scope.$broadcast('resetWorkflowChart');
}; };
$scope.workflowZoomed = function(zoom) { $scope.workflowZoomed = function (zoom) {
$scope.$broadcast('workflowZoomed', { $scope.$broadcast('workflowZoomed', {
zoom: zoom zoom: zoom
}); });
}; };
$scope.zoomToFitChart = function() { $scope.zoomToFitChart = function () {
$scope.$broadcast('zoomToFitChart'); $scope.$broadcast('zoomToFitChart');
}; };
$scope.openPromptModal = function() { $scope.openPromptModal = function () {
$scope.promptData.triggerModalOpen = true; $scope.promptData.triggerModalOpen = true;
}; };
let allNodes = []; let allNodes = [];
let page = 1; let page = 1;
let buildTreeFromNodes = function(){ let buildTreeFromNodes = function () {
WorkflowService.buildTree({ WorkflowService.buildTree({
workflowNodes: allNodes workflowNodes: allNodes
}).then(function(data){ }).then(function (data) {
$scope.treeData = data; $scope.treeData = data;
// TODO: I think that the workflow chart directive (and eventually d3) is meddling with // 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 // Get the workflow nodes
TemplatesService.getWorkflowJobTemplateNodes($scope.workflowJobTemplateObj.id, page) TemplatesService.getWorkflowJobTemplateNodes($scope.workflowJobTemplateObj.id, page)
.then(function(data){ .then(function (data) {
for(var i=0; i<data.data.results.length; i++) { for (var i = 0; i < data.data.results.length; i++) {
allNodes.push(data.data.results[i]); allNodes.push(data.data.results[i]);
} }
if (data.data.next) { if (data.data.next) {
// Get the next page // Get the next page
page++; page++;
getNodes(); getNodes();
} else { } else {
// This is the last page // This is the last page
buildTreeFromNodes(); buildTreeFromNodes();
} }
}, function(error){ }, function ({ data, status, config }) {
ProcessErrors($scope, error.data, error.status, null, { ProcessErrors($scope, data, status, null, {
hdr: 'Error!', hdr: $scope.strings.get('error.HEADER'),
msg: 'Failed to get workflow job template nodes. GET returned ' + msg: $scope.strings.get('error.CALL', {
'status: ' + error.status path: `${config.url}`,
action: `${config.method}`,
status
})
});
}); });
});
}; };
getNodes(); getNodes();
updateEdgeDropdownOptions(); updateEdgeDropdownOptions();
} }
]; ];

View File

@@ -139,7 +139,7 @@
</div> </div>
<div class="WorkflowMaker-buttonHolder"> <div class="WorkflowMaker-buttonHolder">
<button type="button" class="btn btn-sm WorkflowMaker-cancelButton" ng-click="closeWorkflowMaker()"> {{:: strings.get('CLOSE') }}</button> <button type="button" class="btn btn-sm WorkflowMaker-cancelButton" ng-click="closeWorkflowMaker()"> {{:: strings.get('CLOSE') }}</button>
<button type="button" class="btn btn-sm WorkflowMaker-saveButton" ng-click="saveWorkflowMaker()" ng-show="workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate" ng-disabled="edgeFlags.conflict || workflowMakerFormConfig.nodeMode === 'add'"> {{:: strings.get('SAVE') }}</button> <button type="button" class="btn btn-sm WorkflowMaker-saveButton" ng-click="saveWorkflowMaker()" ng-show="workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate" ng-disabled="workflowMakerFormConfig.nodeMode === 'add'"> {{:: strings.get('SAVE') }}</button>
</div> </div>
<prompt prompt-data="promptData" action-text="{{:: strings.get('prompt.CONFIRM')}}" prevent-creds-with-passwords="preventCredsWithPasswords" read-only-prompts="!(workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)"></prompt> <prompt prompt-data="promptData" action-text="{{:: strings.get('prompt.CONFIRM')}}" prevent-creds-with-passwords="preventCredsWithPasswords" read-only-prompts="!(workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)"></prompt>
</div> </div>

View File

@@ -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;
}
}
}; };
}]; }];

View File

@@ -0,0 +1,8 @@
exports.command = function findThenClick (selector) {
this.waitForElementPresent(selector, () => {
this.moveToElement(selector, 0, 0, () => {
this.click(selector);
});
});
return this;
};

View File

@@ -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();
}
};

View File

@@ -13,7 +13,7 @@ The CRUD operations against a workflow job template and its corresponding workfl
### Workflow Nodes ### Workflow Nodes
Workflow Nodes are containers of workflow spawned job resources and function as nodes of workflow decision trees. Like that of workflow itself, the two types of workflow nodes are workflow job template nodes and workflow job nodes. Workflow Nodes are containers of workflow spawned job resources and function as nodes of workflow decision trees. Like that of workflow itself, the two types of workflow nodes are workflow job template nodes and workflow job nodes.
Workflow job template nodes are listed and created under endpoint `/workflow_job_templates/\d+/workflow_nodes/` to be associated with underlying workflow job template, or directly under endpoint `/workflow_job_template_nodes/`. The most important fields of a workflow job template node are `success_nodes`, `failure_nodes`, `always_nodes`, `unified_job_template` and `workflow_job_template`. The former three are lists of workflow job template nodes that, in union, forms the set of all its child nodes, in specific, `success_nodes` are triggered when parnent node job succeeds, `failure_nodes` are triggered when parent node job fails, and `always_nodes` are triggered regardless of whether parent job succeeds or fails; The later two reference the job template resource it contains and workflow job template it belongs to. Workflow job template nodes are listed and created under endpoint `/workflow_job_templates/\d+/workflow_nodes/` to be associated with underlying workflow job template, or directly under endpoint `/workflow_job_template_nodes/`. The most important fields of a workflow job template node are `success_nodes`, `failure_nodes`, `always_nodes`, `unified_job_template` and `workflow_job_template`. The former three are lists of workflow job template nodes that, in union, forms the set of all its child nodes, in specific, `success_nodes` are triggered when parent node job succeeds, `failure_nodes` are triggered when parent node job fails, and `always_nodes` are triggered regardless of whether parent job succeeds or fails; The later two reference the job template resource it contains and workflow job template it belongs to.
#### Workflow Node Launch Configuration #### Workflow Node Launch Configuration
@@ -30,12 +30,13 @@ the launch configurations on workflow nodes.
The tree-graph structure of a workflow is enforced by associating workflow job template nodes via endpoints `/workflow_job_template_nodes/\d+/*_nodes/`, where `*` has options `success`, `failure` and `always`. However there are restrictions that must be enforced when setting up new connections. Here are the three restrictions that will raise validation error when break: The tree-graph structure of a workflow is enforced by associating workflow job template nodes via endpoints `/workflow_job_template_nodes/\d+/*_nodes/`, where `*` has options `success`, `failure` and `always`. However there are restrictions that must be enforced when setting up new connections. Here are the three restrictions that will raise validation error when break:
* Cycle restriction: According to tree definition, no cycle is allowed. * Cycle restriction: According to tree definition, no cycle is allowed.
* Convergent restriction: Different paths should not come into the same node, in other words, a node cannot have multiple parents. * Convergent restriction: Different paths should not come into the same node, in other words, a node cannot have multiple parents.
* Mutex restriction: A node cannot have all three types of child nodes. It contains either always nodes only, or any type other than always nodes.
> Note: A node can now have all three types of child nodes.
### Workflow Run Details ### Workflow Run Details
A typical workflow run starts by either POSTing to endpoint `/workflow_job_templates/\d+/launch/`, or being triggered automatically by related schedule. At the very first, the workflow job template creats workflow job, and all related workflow job template nodes create workflow job nodes. Right after that, all root nodes are populated with corresponding job resources and start running. If nothing goes wrong, each decision tree will follow its own route to completion. The entire workflow finishes running when all its decision trees complete. A typical workflow run starts by either POSTing to endpoint `/workflow_job_templates/\d+/launch/`, or being triggered automatically by related schedule. At the very first, the workflow job template creates workflow job, and all related workflow job template nodes create workflow job nodes. Right after that, all root nodes are populated with corresponding job resources and start running. If nothing goes wrong, each decision tree will follow its own route to completion. The entire workflow finishes running when all its decision trees complete.
As stated, workflow job templates can be created with populated `extra_vars`. These `extra_vars` are combined with the `extra_vars` of any job template launched by the workflow with higher variable precedence, meaning they will overwrite job template variables with the same name. Note before the extra_vars set is applied as runtime job extra variables, it might be expaneded and over-written by the cumulative job artifacts of ancestor nodes. The meaning of 'cumulative' here is children overwritting parent. For example, if a node has a parent node and a grandparent node, and both ancestors generate job artifacts, then the job artifacts of grandparent node is overwritten by that of parent node to form the set of cumulative job artifacts of the current node. As stated, workflow job templates can be created with populated `extra_vars`. These `extra_vars` are combined with the `extra_vars` of any job template launched by the workflow with higher variable precedence, meaning they will overwrite job template variables with the same name. Note before the extra_vars set is applied as runtime job extra variables, it might be expanded and over-written by the cumulative job artifacts of ancestor nodes. The meaning of 'cumulative' here is children overwriting parent. For example, if a node has a parent node and a grandparent node, and both ancestors generate job artifacts, then the job artifacts of grandparent node is overwritten by that of parent node to form the set of cumulative job artifacts of the current node.
Job resources spawned by workflow jobs are needed by workflow to run correctly. Therefore deletion of spawned job resources is blocked while the underlying workflow job is executing. Job resources spawned by workflow jobs are needed by workflow to run correctly. Therefore deletion of spawned job resources is blocked while the underlying workflow job is executing.
@@ -84,7 +85,7 @@ Artifact support starts in Ansible and is carried through in Tower. The `set_sta
* Verify that workflow job template nodes can be created under, or (dis)associated with workflow job templates. * Verify that workflow job template nodes can be created under, or (dis)associated with workflow job templates.
* Verify that only the permitted types of job template types can be associated with a workflow job template node. Currently the permitted types are *job templates, inventory sources and projects*. * Verify that only the permitted types of job template types can be associated with a workflow job template node. Currently the permitted types are *job templates, inventory sources and projects*.
* Verify that workflow job template nodes under the same workflow job template can be associated to form parent-child relationship of decision trees. In specific, one node takes another as its child node by POSTing another node's id to one of the three endpoints: `/success_nodes/`, `/failure_nodes/` and `/always_nodes/`. * Verify that workflow job template nodes under the same workflow job template can be associated to form parent-child relationship of decision trees. In specific, one node takes another as its child node by POSTing another node's id to one of the three endpoints: `/success_nodes/`, `/failure_nodes/` and `/always_nodes/`.
* Verify that workflow job template nodes are not allowed to have invalid association. Any attempt that causes invalidity will trigger 400-level response. The three types of invalid associations are cycle, convergence(multiple parent) and mutex('always' XOR the rest). * Verify that workflow job template nodes are not allowed to have invalid association. Any attempt that causes invalidity will trigger 400-level response. The three types of invalid associations are cycle, convergence(multiple parent).
* Verify that a workflow job template can be successfully copied and the created workflow job template does not miss any field that should be copied or intentionally modified. * Verify that a workflow job template can be successfully copied and the created workflow job template does not miss any field that should be copied or intentionally modified.
* Verify that if a user has no access to any of the related resources of a workflow job template node, that node will not be copied and will have `null` as placeholder. * Verify that if a user has no access to any of the related resources of a workflow job template node, that node will not be copied and will have `null` as placeholder.
* Verify that `artifacts` is populated when `set_stats` is used in Ansible >= v2.2.1.0-0.3.rc3. * Verify that `artifacts` is populated when `set_stats` is used in Ansible >= v2.2.1.0-0.3.rc3.