From 4d9523afa4e56fc68587a5a7d2ae93950f2c93d0 Mon Sep 17 00:00:00 2001 From: adamscmRH Date: Thu, 27 Sep 2018 17:36:04 -0400 Subject: [PATCH 1/9] lift always node mutex restriction --- awx/api/views/__init__.py | 4 ---- awx/main/tests/functional/models/test_workflow.py | 2 -- docs/workflow.md | 11 ++++++----- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 8d502eb5c9..f452195333 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -3438,10 +3438,6 @@ class WorkflowJobTemplateNodeChildrenBaseList(WorkflowsEnforcementMixin, Enforce return getattr(parent, self.relationship).all() 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: return None diff --git a/awx/main/tests/functional/models/test_workflow.py b/awx/main/tests/functional/models/test_workflow.py index 0514fc8bda..637109eb55 100644 --- a/awx/main/tests/functional/models/test_workflow.py +++ b/awx/main/tests/functional/models/test_workflow.py @@ -197,8 +197,6 @@ class TestWorkflowJobTemplate: # test mutex validation 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) == - {'Error': 'Cannot associate failure_nodes when always_nodes have been associated.'}) def test_wfjt_unique_together_with_org(self, organization): wfjt1 = WorkflowJobTemplate(name='foo', organization=organization) diff --git a/docs/workflow.md b/docs/workflow.md index cd4232c17c..7a16a70d57 100644 --- a/docs/workflow.md +++ b/docs/workflow.md @@ -13,7 +13,7 @@ The CRUD operations against a workflow job template and its corresponding workfl ### 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 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 @@ -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: * 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. -* 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 -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. @@ -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 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 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 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. From ad566cc6518292212313918d391892b21d8f8dad Mon Sep 17 00:00:00 2001 From: adamscmRH Date: Fri, 28 Sep 2018 16:22:57 -0400 Subject: [PATCH 2/9] tests for always_nodes --- awx/main/tests/functional/models/test_workflow.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/awx/main/tests/functional/models/test_workflow.py b/awx/main/tests/functional/models/test_workflow.py index 637109eb55..c61c9a1926 100644 --- a/awx/main/tests/functional/models/test_workflow.py +++ b/awx/main/tests/functional/models/test_workflow.py @@ -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.projects import ProjectUpdate from awx.main.scheduler.dag_workflow import WorkflowDAG +from awx.api.versioning import reverse # Django from django.test import TransactionTestCase @@ -196,7 +197,15 @@ class TestWorkflowJobTemplate: assert test_view.is_valid_relation(node_assoc, nodes[1]) == {'Error': 'Multiple parent relationship not allowed.'} # test mutex validation test_view.relationship = 'failure_nodes' - node_assoc_1 = WorkflowJobTemplateNode.objects.create(workflow_job_template=wfjt) + + def test_always_success_failure_creation(self, wfjt, admin, get): + 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): wfjt1 = WorkflowJobTemplate(name='foo', organization=organization) From a2f4e36e47ec5cdc71d4b34b9457cee497d017f7 Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Thu, 4 Oct 2018 23:01:20 -0400 Subject: [PATCH 3/9] Show all wf options when node is not a root node * Edge type of root node is always "always" * If node is not a root node, show all options: always, success, fail * Remove edge conflict logic --- .../features/templates/templates.strings.js | 1 - .../workflow-chart/workflow-chart.block.less | 7 - .../workflow-chart.directive.js | 13 - .../workflow-maker.controller.js | 627 ++++++++---------- .../workflow-maker.partial.html | 2 +- .../templates/workflows/workflow.service.js | 34 - 6 files changed, 268 insertions(+), 416 deletions(-) diff --git a/awx/ui/client/features/templates/templates.strings.js b/awx/ui/client/features/templates/templates.strings.js index 673895c46a..08f4ae8926 100644 --- a/awx/ui/client/features/templates/templates.strings.js +++ b/awx/ui/client/features/templates/templates.strings.js @@ -112,7 +112,6 @@ function TemplatesStrings (BaseString) { RUN: t.s('RUN'), CHECK: t.s('CHECK'), SELECT: t.s('SELECT'), - EDGE_CONFLICT: t.s('EDGE CONFLICT'), DELETED: t.s('DELETED'), START: t.s('START'), DETAILS: t.s('DETAILS'), 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..025d139abb 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,13 @@ * 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, i18n) { let promptWatcher, surveyQuestionWatcher, credentialsWatcher; @@ -31,24 +31,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 +42,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 +73,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 +121,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 +129,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 +154,49 @@ 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 = 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 }); - } - - 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 +207,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 = params.node.promptData.launchConf.defaults.credentials ? 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 = params.node.promptData.prompts.credentials.previousOverrides ? 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 +296,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 +317,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 +334,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 +351,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 +372,73 @@ 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}) => { + $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, {}); }); - }).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 +457,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 +529,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 +559,6 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', $scope.selectedTemplateInvalid = false; $scope.showPromptButton = false; - // Reset the edgeConflict flag - resetEdgeConflict(); - // Reset the form resetNodeForm(); @@ -609,7 +567,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 +594,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', $scope.nodeBeingEdited.isActiveEdit = true; - let finishConfiguringEdit = function() { + let finishConfiguringEdit = function () { let jobTemplate = new JobTemplate(); @@ -656,8 +614,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 +687,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 +714,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 +774,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', watchForPromptChanges(); } } - }); + }); } if (_.get($scope, 'nodeBeingEdited.unifiedJobTemplate')) { @@ -855,32 +813,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,10 +853,10 @@ 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) { + }, function (error) { ProcessErrors($scope, error.data, error.status, null, { hdr: 'Error!', msg: 'Failed to get unified job template. GET returned ' + @@ -922,16 +878,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 +905,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 +962,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 +1017,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 +1085,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 +1142,32 @@ 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; - } - } }; }]; From 1b25dd01279b06fb3db40e7cf650e92e306db6dd Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Tue, 9 Oct 2018 14:21:59 -0400 Subject: [PATCH 4/9] Fix ui-lint error --- .../workflows/workflow-maker/workflow-maker.controller.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 025d139abb..b86f6efc8c 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 @@ -7,10 +7,9 @@ export default ['$scope', 'WorkflowService', 'TemplatesService', 'ProcessErrors', 'CreateSelect2', '$q', 'JobTemplateModel', 'Empty', 'PromptService', 'Rest', 'TemplatesStrings', '$timeout', - 'i18n', function ($scope, WorkflowService, TemplatesService, ProcessErrors, CreateSelect2, $q, JobTemplate, - Empty, PromptService, Rest, TemplatesStrings, $timeout, i18n) { + Empty, PromptService, Rest, TemplatesStrings, $timeout) { let promptWatcher, surveyQuestionWatcher, credentialsWatcher; From b02677a8d08e5fa4549f1cd3c46368c011234002 Mon Sep 17 00:00:00 2001 From: Daniel Sami Date: Tue, 9 Oct 2018 16:32:24 -0400 Subject: [PATCH 5/9] Initial commit for UI tests for always nodes --- awx/ui/test/e2e/commands/findThenClick.js | 7 +++ .../e2e/tests/test-workflow-visualizer.js | 53 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 awx/ui/test/e2e/commands/findThenClick.js create mode 100644 awx/ui/test/e2e/tests/test-workflow-visualizer.js diff --git a/awx/ui/test/e2e/commands/findThenClick.js b/awx/ui/test/e2e/commands/findThenClick.js new file mode 100644 index 0000000000..908f1a190b --- /dev/null +++ b/awx/ui/test/e2e/commands/findThenClick.js @@ -0,0 +1,7 @@ +exports.command = function findThenClick (selector) { + this + .waitForElementPresent(selector) + .moveToElement(selector, 0, 0) + .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..497de1b52b --- /dev/null +++ b/awx/ui/test/e2e/tests/test-workflow-visualizer.js @@ -0,0 +1,53 @@ +import { + getInventorySource, + getJobTemplate, + getProject, + getWorkflowTemplate +} from '../fixtures'; + +let data; +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 = ""; +const leafNode = "//g[contains(@id, 'node-1')]"; +const project = "//td[contains(text(), 'test-actions-project')]"; +const edgeTypeDropdown = "//span[contains(@id, 'select2-workflow_node_edge-container')]"; +const alwaysDropdown = "//*[@id='select2-workflow_node_edge-result-elm7-always']" +const successDropdown = "//*[@id='select2-workflow_node_edge-result-veyc-success']" +const failureDropdown = "//*[@id='select2-workflow_node_edge-result-xitr-failure']" + +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(1000) + .findThenClick(workflowSelector) + .findThenClick(workflowVisualizerBtn); + }, + 'verify that workflow visualizer root node can only be set to always': client => { + client + .useXpath() + .findThenClick(rootNode) + .findThenClick(project) + .findThenClick(edgeTypeDropdown); + }, +}; From 8fceaf88108dc52f298a7e88c74e94d86220b6c9 Mon Sep 17 00:00:00 2001 From: Daniel Sami Date: Thu, 11 Oct 2018 12:14:00 -0400 Subject: [PATCH 6/9] Tests for UI workflow always nodes --- awx/ui/test/e2e/commands/findThenClick.js | 9 +- .../e2e/tests/test-workflow-visualizer.js | 95 +++++++++++++++++-- 2 files changed, 90 insertions(+), 14 deletions(-) diff --git a/awx/ui/test/e2e/commands/findThenClick.js b/awx/ui/test/e2e/commands/findThenClick.js index 908f1a190b..178cf3a784 100644 --- a/awx/ui/test/e2e/commands/findThenClick.js +++ b/awx/ui/test/e2e/commands/findThenClick.js @@ -1,7 +1,8 @@ exports.command = function findThenClick (selector) { - this - .waitForElementPresent(selector) - .moveToElement(selector, 0, 0) - .click(selector); + this.waitForElementPresent(selector, function() { + this.moveToElement(selector, 0, 0, function() { + 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 index 497de1b52b..67e7053d49 100644 --- a/awx/ui/test/e2e/tests/test-workflow-visualizer.js +++ b/awx/ui/test/e2e/tests/test-workflow-visualizer.js @@ -11,13 +11,23 @@ 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 = ""; -const leafNode = "//g[contains(@id, 'node-1')]"; -const project = "//td[contains(text(), 'test-actions-project')]"; -const edgeTypeDropdown = "//span[contains(@id, 'select2-workflow_node_edge-container')]"; -const alwaysDropdown = "//*[@id='select2-workflow_node_edge-result-elm7-always']" -const successDropdown = "//*[@id='select2-workflow_node_edge-result-veyc-success']" -const failureDropdown = "//*[@id='select2-workflow_node_edge-result-xitr-failure']" +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) => { @@ -39,7 +49,7 @@ module.exports = { .resizeWindow(1200, 1000) .useXpath() .findThenClick(workflowTemplateNavTab) - .pause(1000) + .pause(1500) .findThenClick(workflowSelector) .findThenClick(workflowVisualizerBtn); }, @@ -47,7 +57,72 @@ module.exports = { client .useXpath() .findThenClick(rootNode) - .findThenClick(project) - .findThenClick(edgeTypeDropdown); + .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) + .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, function() { + client.pause(500); + // Concatenating the xpaths lets us click the proper node + client.click(childNode+nodeAdd); + }) + .pause(1000) + .findThenClick(testActionsJob) + .pause(1000) + .findThenClick(edgeTypeDropdownBar) + .waitForElementPresent(successDropdown) + .waitForElementPresent(failureDropdown) + .waitForElementPresent(alwaysDropdown) + .findThenClick(alwaysDropdown) + .click(selectButton); + }, + 'Verify node-shifting behavior upon deletion': client => { + client + .findThenClick(newChildNode) + .pause(1000) + .findThenClick(edgeTypeDropdownBar) + .findThenClick(successDropdown) + .click(selectButton) + .moveToElement(newChildNode, 0, 0, function() { + client.pause(500); + client.click(newChildNode+nodeAdd); + }) + .pause(1000) + .findThenClick(testActionsJob) + .pause(1000) + .findThenClick(edgeTypeDropdownBar) + .waitForElementPresent(successDropdown) + .waitForElementPresent(failureDropdown) + .waitForElementPresent(alwaysDropdown) + .findThenClick(alwaysDropdown) + .click(selectButton) + .moveToElement(newChildNode, 0, 0, function() { + client.pause(500); + client.click(newChildNode+nodeRemove); + }) + .pause(1000) + .findThenClick(deleteConfirmation) + .findThenClick(leafNode) + .pause(1000) + .findThenClick(edgeTypeDropdownBar) + .waitForElementPresent(successDropdown) + .waitForElementPresent(failureDropdown) + .waitForElementPresent(alwaysDropdown); + } }; From 1e3c2294603758ccb27414deb056508db749a3b9 Mon Sep 17 00:00:00 2001 From: Daniel Sami Date: Thu, 11 Oct 2018 12:24:55 -0400 Subject: [PATCH 7/9] lint fixes --- awx/ui/test/e2e/commands/findThenClick.js | 4 ++-- awx/ui/test/e2e/tests/test-workflow-visualizer.js | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/awx/ui/test/e2e/commands/findThenClick.js b/awx/ui/test/e2e/commands/findThenClick.js index 178cf3a784..82dc2b62f9 100644 --- a/awx/ui/test/e2e/commands/findThenClick.js +++ b/awx/ui/test/e2e/commands/findThenClick.js @@ -1,6 +1,6 @@ exports.command = function findThenClick (selector) { - this.waitForElementPresent(selector, function() { - this.moveToElement(selector, 0, 0, function() { + this.waitForElementPresent(selector, () => { + this.moveToElement(selector, 0, 0, () => { this.click(selector); }); }); diff --git a/awx/ui/test/e2e/tests/test-workflow-visualizer.js b/awx/ui/test/e2e/tests/test-workflow-visualizer.js index 67e7053d49..6a118566fd 100644 --- a/awx/ui/test/e2e/tests/test-workflow-visualizer.js +++ b/awx/ui/test/e2e/tests/test-workflow-visualizer.js @@ -17,7 +17,7 @@ const leafNode = "//*[@id='node-6']"; const nodeAdd = "//*[contains(@class, 'nodeAddCross')]"; const nodeRemove = "//*[contains(@class, 'nodeRemoveCross')]"; -//one of the jobs or projects or inventories +// one of the jobs or projects or inventories const testActionsProject = "//td[contains(text(), 'test-actions-project')]"; const testActionsJob = "//td[contains(text(), 'test-actions-job')]"; @@ -77,10 +77,10 @@ module.exports = { 'verify that a sibling node can be any edge type': client => { client .useXpath() - .moveToElement(childNode, 0, 0, function() { + .moveToElement(childNode, 0, 0, () => { client.pause(500); // Concatenating the xpaths lets us click the proper node - client.click(childNode+nodeAdd); + client.click(childNode + nodeAdd); }) .pause(1000) .findThenClick(testActionsJob) @@ -99,9 +99,9 @@ module.exports = { .findThenClick(edgeTypeDropdownBar) .findThenClick(successDropdown) .click(selectButton) - .moveToElement(newChildNode, 0, 0, function() { + .moveToElement(newChildNode, 0, 0, () => { client.pause(500); - client.click(newChildNode+nodeAdd); + client.click(newChildNode + nodeAdd); }) .pause(1000) .findThenClick(testActionsJob) @@ -112,9 +112,9 @@ module.exports = { .waitForElementPresent(alwaysDropdown) .findThenClick(alwaysDropdown) .click(selectButton) - .moveToElement(newChildNode, 0, 0, function() { + .moveToElement(newChildNode, 0, 0, () => { client.pause(500); - client.click(newChildNode+nodeRemove); + client.click(newChildNode + nodeRemove); }) .pause(1000) .findThenClick(deleteConfirmation) From 27da141889ae22955249c6bd43830be4ee5bba1b Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Thu, 11 Oct 2018 13:13:01 -0400 Subject: [PATCH 8/9] Address review comments --- .../workflow-maker.controller.js | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) 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 b86f6efc8c..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 @@ -170,7 +170,7 @@ export default ['$scope', 'WorkflowService', 'TemplatesService', // 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 : []; + let defaultCreds = _.get(params, 'node.promptData.launchConf.defaults.credentials', []); return !defaultCreds.some(function (defaultCred) { return credFromPrompt.id === defaultCred.id; }); @@ -188,12 +188,14 @@ export default ['$scope', 'WorkflowService', 'TemplatesService', 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 + }, 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 + }) }); }); } else { @@ -208,14 +210,14 @@ export default ['$scope', 'WorkflowService', 'TemplatesService', 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 : []; + 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 : []; + let previousOverrides = _.get(params, 'node.promptData.prompts.credentials.previousOverrides', []); return !previousOverrides.some(function (priorCred) { return credNotInPrior.id === priorCred.id; }); @@ -400,14 +402,9 @@ export default ['$scope', 'WorkflowService', 'TemplatesService', }); }); - $q.all(associatePromises.concat(credentialPromises)) + return $q.all(associatePromises.concat(credentialPromises)) .then(function () { $scope.closeDialog(); - }).catch(({ - data, - status - }) => { - ProcessErrors($scope, data, status, null, {}); }); }).catch(({ data, @@ -855,12 +852,15 @@ export default ['$scope', 'WorkflowService', 'TemplatesService', .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(); @@ -1156,11 +1156,14 @@ export default ['$scope', 'WorkflowService', 'TemplatesService', // This is the last page buildTreeFromNodes(); } - }, function (error) { - ProcessErrors($scope, error.data, error.status, null, { - hdr: 'Error!', - msg: 'Failed to get workflow job template nodes. 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 + }) }); }); }; From c25d2084657eeb24ee739bbb3025d9227fc7de03 Mon Sep 17 00:00:00 2001 From: Daniel Sami Date: Fri, 12 Oct 2018 10:18:49 -0400 Subject: [PATCH 9/9] added browser close at end, waits for spinners --- .../test/e2e/tests/test-workflow-visualizer.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/awx/ui/test/e2e/tests/test-workflow-visualizer.js b/awx/ui/test/e2e/tests/test-workflow-visualizer.js index 6a118566fd..5d6a3d570b 100644 --- a/awx/ui/test/e2e/tests/test-workflow-visualizer.js +++ b/awx/ui/test/e2e/tests/test-workflow-visualizer.js @@ -6,6 +6,7 @@ import { } 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')]"; @@ -50,6 +51,7 @@ module.exports = { .useXpath() .findThenClick(workflowTemplateNavTab) .pause(1500) + .waitForElementNotVisible(spinny) .findThenClick(workflowSelector) .findThenClick(workflowVisualizerBtn); }, @@ -68,6 +70,7 @@ module.exports = { .useXpath() .findThenClick(childNode) .pause(1000) + .waitForElementNotVisible(spinny) .findThenClick(edgeTypeDropdownBar) .waitForElementPresent(successDropdown) .waitForElementPresent(failureDropdown) @@ -79,12 +82,15 @@ module.exports = { .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) @@ -96,16 +102,20 @@ module.exports = { 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) @@ -114,15 +124,21 @@ module.exports = { .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(); } };