diff --git a/awx/ui/client/src/forms/JobTemplates.js b/awx/ui/client/src/forms/JobTemplates.js index 9fe64f0469..c200258cc6 100644 --- a/awx/ui/client/src/forms/JobTemplates.js +++ b/awx/ui/client/src/forms/JobTemplates.js @@ -20,7 +20,7 @@ export default addTitle: i18n._('New Job Template'), editTitle: '{{ name }}', name: 'job_template', - breadcrumbName: 'JOB TEMPLATE', + breadcrumbName: i18n._('JOB TEMPLATE'), basePath: 'job_templates', // the top-most node of generated state tree stateTree: 'templates', diff --git a/awx/ui/client/src/forms/WorkflowMaker.js b/awx/ui/client/src/forms/WorkflowMaker.js index 5bd7788db1..9ed3c69b65 100644 --- a/awx/ui/client/src/forms/WorkflowMaker.js +++ b/awx/ui/client/src/forms/WorkflowMaker.js @@ -170,7 +170,7 @@ export default ngClick: 'cancelNodeForm()', ngShow: '!canAddWorkflowJobTemplate' }, - save: { + select: { ngClick: 'saveNodeForm()', ngDisabled: "workflow_maker_form.$invalid || !selectedTemplate", ngShow: 'canAddWorkflowJobTemplate' diff --git a/awx/ui/client/src/forms/Workflows.js b/awx/ui/client/src/forms/Workflows.js index d281ae0e0b..7f16ee6d06 100644 --- a/awx/ui/client/src/forms/Workflows.js +++ b/awx/ui/client/src/forms/Workflows.js @@ -16,9 +16,10 @@ export default .factory('WorkflowFormObject', ['i18n', function(i18n) { return { - addTitle: i18n._('New Workflow'), + addTitle: i18n._('New Workflow Job Template'), editTitle: '{{ name }}', name: 'workflow_job_template', + breadcrumbName: i18n._('WORKFLOW'), base: 'workflow', basePath: 'workflow_job_templates', // the top-most node of generated state tree diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index defdc6f5dc..d5850a9c8b 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -1683,6 +1683,10 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat button.label = i18n._('Save'); button['class'] = 'Form-saveButton'; } + if (btn === 'select') { + button.label = i18n._('Select'); + button['class'] = 'Form-saveButton'; + } if (btn === 'cancel') { button.label = i18n._('Cancel'); button['class'] = 'Form-cancelButton'; diff --git a/awx/ui/client/src/shared/lookup/lookup-modal.partial.html b/awx/ui/client/src/shared/lookup/lookup-modal.partial.html index 5371e8d454..13d1a8b4dc 100644 --- a/awx/ui/client/src/shared/lookup/lookup-modal.partial.html +++ b/awx/ui/client/src/shared/lookup/lookup-modal.partial.html @@ -17,7 +17,7 @@
diff --git a/awx/ui/client/src/templates/main.js b/awx/ui/client/src/templates/main.js index f9ea226a83..b458121def 100644 --- a/awx/ui/client/src/templates/main.js +++ b/awx/ui/client/src/templates/main.js @@ -401,8 +401,10 @@ angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplatesA } }, resolve: { - ListDefinition: ['InventoryList', function(list) { + ListDefinition: ['InventoryList', function(InventoryList) { // mutate the provided list definition here + let list = _.cloneDeep(InventoryList); + list.lookupConfirmText = 'SELECT'; return list; }], Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', @@ -451,8 +453,9 @@ angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplatesA } }, resolve: { - ListDefinition: ['CredentialList', function(list) { - // mutate the provided list definition here + ListDefinition: ['CredentialList', function(CredentialList) { + let list = _.cloneDeep(CredentialList); + list.lookupConfirmText = 'SELECT'; return list; }], Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', diff --git a/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js b/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js index e864beefa9..18dd35a37d 100644 --- a/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js +++ b/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js @@ -33,10 +33,6 @@ $scope.parseType = 'yaml'; $scope.includeWorkflowMaker = false; - $scope.editRequests = []; - $scope.associateRequests = []; - $scope.disassociateRequests = []; - function init() { // Select2-ify the lables input @@ -111,33 +107,6 @@ }); }); - // Get the workflow nodes - TemplatesService.getWorkflowJobTemplateNodes(id) - .then(function(data){ - - $scope.workflowTree = WorkflowService.buildTree({ - workflowNodes: data.data.results - }); - - // TODO: I think that the workflow chart directive (and eventually d3) is meddling with - // this workflowTree object and removing the children object for some reason (?) - // This happens on occasion and I think is a race condition (?) - if(!$scope.workflowTree.data.children) { - $scope.workflowTree.data.children = []; - } - - // In the partial, the workflow maker directive has an ng-if attribute which is pointed at this scope variable. - // It won't get included until this the tree has been built - I'm open to better ways of doing this. - $scope.includeWorkflowMaker = true; - - }, function(error){ - ProcessErrors($scope, error.data, error.status, form, { - hdr: 'Error!', - msg: 'Failed to get workflow job template nodes. GET returned ' + - 'status: ' + error.status - }); - }); - // Go out and GET the workflow job temlate data needed to populate the form TemplatesService.getWorkflowJobTemplate(id) .then(function(data){ @@ -180,6 +149,35 @@ $scope.url = workflowJobTemplateData.url; $scope.survey_enabled = workflowJobTemplateData.survey_enabled; + // Get the workflow nodes + TemplatesService.getWorkflowJobTemplateNodes(id) + .then(function(data){ + + $scope.workflowTree = WorkflowService.buildTree({ + workflowNodes: data.data.results + }); + + // TODO: I think that the workflow chart directive (and eventually d3) is meddling with + // this workflowTree object and removing the children object for some reason (?) + // This happens on occasion and I think is a race condition (?) + if(!$scope.workflowTree.data.children) { + $scope.workflowTree.data.children = []; + } + + $scope.workflowTree.workflow_job_template_obj = $scope.workflow_job_template_obj; + + // In the partial, the workflow maker directive has an ng-if attribute which is pointed at this scope variable. + // It won't get included until this the tree has been built - I'm open to better ways of doing this. + $scope.includeWorkflowMaker = true; + + }, function(error){ + ProcessErrors($scope, error.data, error.status, form, { + hdr: 'Error!', + msg: 'Failed to get workflow job template nodes. GET returned ' + + 'status: ' + error.status + }); + }); + }, function(error){ ProcessErrors($scope, error.data, error.status, form, { hdr: 'Error!', @@ -189,160 +187,6 @@ }); } - function recursiveNodeUpdates(params, completionCallback) { - // params.parentId - // params.node - - let generatePostUrl = function(){ - - let base = (params.parentId) ? GetBasePath('workflow_job_template_nodes') + params.parentId : $scope.workflow_job_template_obj.related.workflow_nodes; - - if(params.parentId) { - if(params.node.edgeType === 'success') { - base += "/success_nodes"; - } - else if(params.node.edgeType === 'failure') { - base += "/failure_nodes"; - } - else if(params.node.edgeType === 'always') { - base += "/always_nodes"; - } - } - - return base; - - }; - - let buildSendableNodeData = function() { - // Create the node - let sendableNodeData = { - unified_job_template: params.node.unifiedJobTemplate.id - }; - - // Check to see if the user has provided any prompt values that are different - // from the defaults in the job template - - if(params.node.unifiedJobTemplate.type === "job_template" && params.node.promptValues) { - if(params.node.unifiedJobTemplate.ask_credential_on_launch) { - sendableNodeData.credential = !params.node.promptValues.credential || params.node.unifiedJobTemplate.summary_fields.credential.id !== params.node.promptValues.credential.id ? params.node.promptValues.credential.id : null; - } - if(params.node.unifiedJobTemplate.ask_inventory_on_launch) { - sendableNodeData.inventory = !params.node.promptValues.inventory || params.node.unifiedJobTemplate.summary_fields.inventory.id !== params.node.promptValues.inventory.id ? params.node.promptValues.inventory.id : null; - } - if(params.node.unifiedJobTemplate.ask_limit_on_launch) { - sendableNodeData.limit = !params.node.promptValues.limit || params.node.unifiedJobTemplate.limit !== params.node.promptValues.limit ? params.node.promptValues.limit : null; - } - if(params.node.unifiedJobTemplate.ask_job_type_on_launch) { - sendableNodeData.job_type = !params.node.promptValues.job_type || params.node.unifiedJobTemplate.job_type !== params.node.promptValues.job_type ? params.node.promptValues.job_type : null; - } - if(params.node.unifiedJobTemplate.ask_tags_on_launch) { - sendableNodeData.job_tags = !params.node.promptValues.job_tags || params.node.unifiedJobTemplate.job_tags !== params.node.promptValues.job_tags ? params.node.promptValues.job_tags : null; - } - if(params.node.unifiedJobTemplate.ask_skip_tags_on_launch) { - sendableNodeData.skip_tags = !params.node.promptValues.skip_tags || params.node.unifiedJobTemplate.skip_tags !== params.node.promptValues.skip_tags ? params.node.promptValues.skip_tags : null; - } - } - - return sendableNodeData; - }; - - let continueRecursing = function(parentId) { - $scope.totalIteratedNodes++; - - if($scope.totalIteratedNodes === $scope.workflowTree.data.totalNodes) { - // We're done recursing, lets move on - completionCallback(); - } - else { - if(params.node.children && params.node.children.length > 0) { - _.forEach(params.node.children, function(child) { - if(child.edgeType === "success") { - recursiveNodeUpdates({ - parentId: parentId, - node: child - }, completionCallback); - } - else if(child.edgeType === "failure") { - recursiveNodeUpdates({ - parentId: parentId, - node: child - }, completionCallback); - } - else if(child.edgeType === "always") { - recursiveNodeUpdates({ - parentId: parentId, - node: child - }, completionCallback); - } - }); - } - } - }; - - if(params.node.isNew) { - - TemplatesService.addWorkflowNode({ - url: generatePostUrl(), - data: buildSendableNodeData() - }) - .then(function(data) { - continueRecursing(data.data.id); - }, function(error) { - ProcessErrors($scope, error.data, error.status, form, { - 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)) { - - if(params.node.edited) { - - $scope.editRequests.push({ - id: params.node.nodeId, - data: buildSendableNodeData() - }); - - } - - if((params.node.originalParentId && params.parentId !== params.node.originalParentId) || params.node.originalEdge !== params.node.edgeType) {//beep - - $scope.disassociateRequests.push({ - parentId: params.node.originalParentId, - nodeId: params.node.nodeId, - edge: params.node.originalEdge - }); - - // Can only associate if we have a parent. - // If we don't have a parent then this is a root node - // and the act of disassociating will make it a root node - if(params.parentId) { - $scope.associateRequests.push({ - parentId: params.parentId, - nodeId: params.node.nodeId, - edge: params.node.edgeType - }); - } - - } - else if(!params.node.originalParentId && params.parentId) { - // This used to be a root node but is now not a root node - $scope.associateRequests.push({ - parentId: params.parentId, - nodeId: params.node.nodeId, - edge: params.node.edgeType - }); - } - - } - - continueRecursing(params.node.nodeId); - } - } - $scope.openWorkflowMaker = function() { $state.go('.workflowMaker'); }; @@ -392,231 +236,97 @@ .filter("[data-label-is-present=true]") .map((i, val) => ({name: $(val).text()})); - $scope.totalIteratedNodes = 0; + TemplatesService.updateWorkflowJobTemplate({ + id: id, + data: data + }).then(function(){ - // TODO: this is the only way that I could figure out to get - // these promise arrays to play nicely. I tried to just append - // a single promise to deletePromises but it just wasn't working - let editWorkflowJobTemplate = [id].map(function(id) { - return TemplatesService.updateWorkflowJobTemplate({ - id: id, - data: data - }); - }); + var orgDefer = $q.defer(); + var associationDefer = $q.defer(); + var associatedLabelsDefer = $q.defer(); - if($scope.workflowTree && $scope.workflowTree.data && $scope.workflowTree.data.children && $scope.workflowTree.data.children.length > 0) { - let completionCallback = function() { - - let disassociatePromises = $scope.disassociateRequests.map(function(request) { - return TemplatesService.disassociateWorkflowNode({ - parentId: request.parentId, - nodeId: request.nodeId, - edge: request.edge + var getNext = function(data, arr, resolve) { + Rest.setUrl(data.next); + Rest.get() + .success(function (data) { + if (data.next) { + getNext(data, arr.concat(data.results), resolve); + } else { + resolve.resolve(arr.concat(data.results)); + } }); - }); - - let editNodePromises = $scope.editRequests.map(function(request) { - return TemplatesService.editWorkflowNode({ - id: request.id, - data: request.data - }); - }); - - $q.all(disassociatePromises.concat(editNodePromises).concat(editWorkflowJobTemplate)) - .then(function() { - - let associatePromises = $scope.associateRequests.map(function(request) { - return TemplatesService.associateWorkflowNode({ - parentId: request.parentId, - nodeId: request.nodeId, - edge: request.edge - }); - }); - - let deletePromises = $scope.workflowTree.data.deletedNodes.map(function(nodeId) { - return TemplatesService.deleteWorkflowJobTemplateNode(nodeId); - }); - - $q.all(associatePromises.concat(deletePromises)) - .then(function() { - - var orgDefer = $q.defer(); - var associationDefer = $q.defer(); - var associatedLabelsDefer = $q.defer(); - - var getNext = function(data, arr, resolve) { - Rest.setUrl(data.next); - Rest.get() - .success(function (data) { - if (data.next) { - getNext(data, arr.concat(data.results), resolve); - } else { - resolve.resolve(arr.concat(data.results)); - } - }); - }; - - Rest.setUrl($scope.workflow_job_template_obj.related.labels); - - Rest.get() - .success(function(data) { - if (data.next) { - getNext(data, data.results, associatedLabelsDefer); - } else { - associatedLabelsDefer.resolve(data.results); - } - }); - - associatedLabelsDefer.promise.then(function (current) { - current = current.map(data => data.id); - var labelsToAdd = $scope.labels - .map(val => val.value); - var labelsToDisassociate = current - .filter(val => labelsToAdd - .indexOf(val) === -1) - .map(val => ({id: val, disassociate: true})); - var labelsToAssociate = labelsToAdd - .filter(val => current - .indexOf(val) === -1) - .map(val => ({id: val, associate: true})); - var pass = labelsToDisassociate - .concat(labelsToAssociate); - associationDefer.resolve(pass); - }); - - Rest.setUrl(GetBasePath("organizations")); - Rest.get() - .success(function(data) { - orgDefer.resolve(data.results[0].id); - }); - - orgDefer.promise.then(function(orgId) { - var toPost = []; - $scope.newLabels = $scope.newLabels - .map(function(i, val) { - val.organization = orgId; - return val; - }); - - $scope.newLabels.each(function(i, val) { - toPost.push(val); - }); - - associationDefer.promise.then(function(arr) { - toPost = toPost - .concat(arr); - - Rest.setUrl($scope.workflow_job_template_obj.related.labels); - - var defers = []; - for (var i = 0; i < toPost.length; i++) { - defers.push(Rest.post(toPost[i])); - } - $q.all(defers) - .then(function() { - $state.go('templates.editWorkflowJobTemplate', {id: id}, {reload: true}); - }); - }); - }); - - }); - }); }; - _.forEach($scope.workflowTree.data.children, function(child) { - recursiveNodeUpdates({ - node: child - }, completionCallback); - }); - } - else { + Rest.setUrl($scope.workflow_job_template_obj.related.labels); - let deletePromises = $scope.workflowTree.data.deletedNodes.map(function(nodeId) { - return TemplatesService.deleteWorkflowJobTemplateNode(nodeId); - }); - - $q.all(deletePromises.concat(editWorkflowJobTemplate)) - .then(function() { - var orgDefer = $q.defer(); - var associationDefer = $q.defer(); - var associatedLabelsDefer = $q.defer(); - - var getNext = function(data, arr, resolve) { - Rest.setUrl(data.next); - Rest.get() - .success(function (data) { - if (data.next) { - getNext(data, arr.concat(data.results), resolve); - } else { - resolve.resolve(arr.concat(data.results)); - } - }); - }; - - Rest.setUrl($scope.workflow_job_template_obj.related.labels); - - Rest.get() - .success(function(data) { - if (data.next) { - getNext(data, data.results, associatedLabelsDefer); - } else { - associatedLabelsDefer.resolve(data.results); - } - }); - - associatedLabelsDefer.promise.then(function (current) { - current = current.map(data => data.id); - var labelsToAdd = $scope.labels - .map(val => val.value); - var labelsToDisassociate = current - .filter(val => labelsToAdd - .indexOf(val) === -1) - .map(val => ({id: val, disassociate: true})); - var labelsToAssociate = labelsToAdd - .filter(val => current - .indexOf(val) === -1) - .map(val => ({id: val, associate: true})); - var pass = labelsToDisassociate - .concat(labelsToAssociate); - associationDefer.resolve(pass); + Rest.get() + .success(function(data) { + if (data.next) { + getNext(data, data.results, associatedLabelsDefer); + } else { + associatedLabelsDefer.resolve(data.results); + } }); - Rest.setUrl(GetBasePath("organizations")); - Rest.get() - .success(function(data) { - orgDefer.resolve(data.results[0].id); + associatedLabelsDefer.promise.then(function (current) { + current = current.map(data => data.id); + var labelsToAdd = $scope.labels + .map(val => val.value); + var labelsToDisassociate = current + .filter(val => labelsToAdd + .indexOf(val) === -1) + .map(val => ({id: val, disassociate: true})); + var labelsToAssociate = labelsToAdd + .filter(val => current + .indexOf(val) === -1) + .map(val => ({id: val, associate: true})); + var pass = labelsToDisassociate + .concat(labelsToAssociate); + associationDefer.resolve(pass); + }); + + Rest.setUrl(GetBasePath("organizations")); + Rest.get() + .success(function(data) { + orgDefer.resolve(data.results[0].id); + }); + + orgDefer.promise.then(function(orgId) { + var toPost = []; + $scope.newLabels = $scope.newLabels + .map(function(i, val) { + val.organization = orgId; + return val; }); - orgDefer.promise.then(function(orgId) { - var toPost = []; - $scope.newLabels = $scope.newLabels - .map(function(i, val) { - val.organization = orgId; - return val; + $scope.newLabels.each(function(i, val) { + toPost.push(val); + }); + + associationDefer.promise.then(function(arr) { + toPost = toPost + .concat(arr); + + Rest.setUrl($scope.workflow_job_template_obj.related.labels); + + var defers = []; + for (var i = 0; i < toPost.length; i++) { + defers.push(Rest.post(toPost[i])); + } + $q.all(defers) + .then(function() { + $state.go('templates.editWorkflowJobTemplate', {id: id}, {reload: true}); }); - - $scope.newLabels.each(function(i, val) { - toPost.push(val); - }); - - associationDefer.promise.then(function(arr) { - toPost = toPost - .concat(arr); - - Rest.setUrl($scope.workflow_job_template_obj.related.labels); - - var defers = []; - for (var i = 0; i < toPost.length; i++) { - defers.push(Rest.post(toPost[i])); - } - $q.all(defers) - .then(function() { - $state.go('templates.editWorkflowJobTemplate', {id: id}, {reload: true}); - }); - }); }); }); - } + + }, function(error){ + ProcessErrors($scope, error.data, error.status, form, { + hdr: 'Error!', + msg: 'Failed to update workflow job template. PUT returned ' + + 'status: ' + error.status + }); + }); } catch (err) { Wait('stop'); 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 177ab6b35d..7263edbf02 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 @@ -29,6 +29,11 @@ fill: @default-interface-txt; } +.WorkflowChart-startText { + fill: @default-bg; + cursor: default; +} + .node .rect { fill: @default-secondary-bg; } @@ -97,3 +102,6 @@ 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 3b3bc0939c..b8d8ffcbdc 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 @@ -84,6 +84,25 @@ export default [ '$state', } } + function rounded_rect(x, y, w, h, r, tl, tr, bl, br) { + var retval; + retval = "M" + (x + r) + "," + y; + retval += "h" + (w - 2*r); + if (tr) { retval += "a" + r + "," + r + " 0 0 1 " + r + "," + r; } + else { retval += "h" + r; retval += "v" + r; } + retval += "v" + (h - 2*r); + if (br) { retval += "a" + r + "," + r + " 0 0 1 " + -r + "," + r; } + else { retval += "v" + r; retval += "h" + -r; } + retval += "h" + (2*r - w); + if (bl) { retval += "a" + r + "," + r + " 0 0 1 " + -r + "," + -r; } + else { retval += "h" + -r; retval += "v" + -r; } + retval += "v" + (2*r - h); + if (tl) { retval += "a" + r + "," + r + " 0 0 1 " + r + "," + -r; } + else { retval += "v" + -r; retval += "h" + r; } + retval += "z"; + return retval; + } + // This is the zoom function called by using the mousewheel/click and drag function naturalZoom() { let scale = d3.event.scale, @@ -163,33 +182,46 @@ export default [ '$state', .attr("fill", "#5cb85c") .attr("class", "WorkflowChart-rootNode") .call(add_node); - thisNode.append("path") - .style("fill", "white") - .attr("transform", function() { return "translate(" + 30 + "," + 30 + ")"; }) - .attr("d", d3.svg.symbol() - .size(120) - .type("cross") - ) - .call(add_node); thisNode.append("text") - .attr("x", 14) - .attr("y", 0) + .attr("x", 13) + .attr("y", 30) .attr("dy", ".35em") - .attr("class", "WorkflowChart-defaultText") - .text(function () { return "START"; }); + .attr("class", "WorkflowChart-startText") + .text(function () { return "START"; }) + .call(add_node); } - else { + else {//d.isActiveEdit thisNode.append("rect") .attr("width", rectW) .attr("height", rectH) .attr("rx", 5) .attr("ry", 5) - .attr('stroke', function(d) { return d.isActiveEdit ? "#337ab7" : "#D7D7D7"; }) - .attr('stroke-width', function(d){ return d.isActiveEdit ? "2px" : "1px"; }) + .attr('stroke', function(d) { + if(d.edgeType) { + if(d.edgeType === "failure") { + return "#d9534f"; + } + else if(d.edgeType === "success") { + return "#5cb85c"; + } + else if(d.edgeType === "always"){ + return "#337ab7"; + } + } + else { + return "#D7D7D7"; + } + }) + .attr('stroke-width', "2px") .attr("class", function(d) { return d.placeholder ? "rect placeholder" : "rect"; }); + thisNode.append("path") + .attr("d", rounded_rect(1, 0, 5, rectH, 5, 1, 0, 1, 0)) + .attr("class", "WorkflowChart-activeNode") + .style("display", function(d) { return d.isActiveEdit ? null : "none"; }); + thisNode.append("text") .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 20 : rectW / 2; }) .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 10 : rectH / 2; }) @@ -517,8 +549,22 @@ export default [ '$state', .attr("transform", function(d) { return "translate(" + (d.target.y + d.source.y + rectW) / 2 + "," + (d.target.x + d.source.x + rectH) / 2 + ")"; }); t.selectAll(".rect") - .attr('stroke', function(d) { return d.isActiveEdit ? "#337ab7" : "#D7D7D7"; }) - .attr('stroke-width', function(d){ return d.isActiveEdit ? "2px" : "1px"; }) + .attr('stroke', function(d) { + if(d.edgeType) { + if(d.edgeType === "failure") { + return "#d9534f"; + } + else if(d.edgeType === "success") { + return "#5cb85c"; + } + else if(d.edgeType === "always"){ + return "#337ab7"; + } + } + else { + return "#D7D7D7"; + } + }) .attr("class", function(d) { return d.placeholder ? "rect placeholder" : "rect"; }); @@ -601,6 +647,9 @@ export default [ '$state', 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"; }); + } function add_node() { diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less index 04ec4dd51a..4fd54ad08f 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less @@ -154,7 +154,7 @@ padding-left: 20px; } .WorkflowLegend-maker--right { - flex: 0 0 182px; + flex: 0 0 206px; text-align: right; padding-right: 20px; position: relative; @@ -226,7 +226,7 @@ } .WorkflowMaker-manualControls { position: absolute; - left: -122px; + left: -86px; height: 60px; width: 293px; background-color: @default-bg; 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 ac2fe30396..1e4f923a37 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 @@ -35,6 +35,10 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr showTypeOptions: false }; + $scope.editRequests = []; + $scope.associateRequests = []; + $scope.disassociateRequests = []; + function init() { $scope.treeDataMaster = angular.copy($scope.treeData.data); $scope.showManualControls = false; @@ -55,6 +59,160 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr $scope.workflowMakerFormConfig.activeTab = "jobs"; } + function recursiveNodeUpdates(params, completionCallback) { + // params.parentId + // params.node + + let generatePostUrl = function(){ + + let base = (params.parentId) ? GetBasePath('workflow_job_template_nodes') + params.parentId : $scope.treeData.workflow_job_template_obj.related.workflow_nodes; + + if(params.parentId) { + if(params.node.edgeType === 'success') { + base += "/success_nodes"; + } + else if(params.node.edgeType === 'failure') { + base += "/failure_nodes"; + } + else if(params.node.edgeType === 'always') { + base += "/always_nodes"; + } + } + + return base; + + }; + + let buildSendableNodeData = function() { + // Create the node + let sendableNodeData = { + unified_job_template: params.node.unifiedJobTemplate.id + }; + + // Check to see if the user has provided any prompt values that are different + // from the defaults in the job template + + if(params.node.unifiedJobTemplate.type === "job_template" && params.node.promptValues) { + if(params.node.unifiedJobTemplate.ask_credential_on_launch) { + sendableNodeData.credential = !params.node.promptValues.credential || params.node.unifiedJobTemplate.summary_fields.credential.id !== params.node.promptValues.credential.id ? params.node.promptValues.credential.id : null; + } + if(params.node.unifiedJobTemplate.ask_inventory_on_launch) { + sendableNodeData.inventory = !params.node.promptValues.inventory || params.node.unifiedJobTemplate.summary_fields.inventory.id !== params.node.promptValues.inventory.id ? params.node.promptValues.inventory.id : null; + } + if(params.node.unifiedJobTemplate.ask_limit_on_launch) { + sendableNodeData.limit = !params.node.promptValues.limit || params.node.unifiedJobTemplate.limit !== params.node.promptValues.limit ? params.node.promptValues.limit : null; + } + if(params.node.unifiedJobTemplate.ask_job_type_on_launch) { + sendableNodeData.job_type = !params.node.promptValues.job_type || params.node.unifiedJobTemplate.job_type !== params.node.promptValues.job_type ? params.node.promptValues.job_type : null; + } + if(params.node.unifiedJobTemplate.ask_tags_on_launch) { + sendableNodeData.job_tags = !params.node.promptValues.job_tags || params.node.unifiedJobTemplate.job_tags !== params.node.promptValues.job_tags ? params.node.promptValues.job_tags : null; + } + if(params.node.unifiedJobTemplate.ask_skip_tags_on_launch) { + sendableNodeData.skip_tags = !params.node.promptValues.skip_tags || params.node.unifiedJobTemplate.skip_tags !== params.node.promptValues.skip_tags ? params.node.promptValues.skip_tags : null; + } + } + + return sendableNodeData; + }; + + let continueRecursing = function(parentId) { + $scope.totalIteratedNodes++; + + if($scope.totalIteratedNodes === $scope.treeData.data.totalNodes) { + // We're done recursing, lets move on + completionCallback(); + } + else { + if(params.node.children && params.node.children.length > 0) { + _.forEach(params.node.children, function(child) { + if(child.edgeType === "success") { + recursiveNodeUpdates({ + parentId: parentId, + node: child + }, completionCallback); + } + else if(child.edgeType === "failure") { + recursiveNodeUpdates({ + parentId: parentId, + node: child + }, completionCallback); + } + else if(child.edgeType === "always") { + recursiveNodeUpdates({ + parentId: parentId, + node: child + }, completionCallback); + } + }); + } + } + }; + + if(params.node.isNew) { + + TemplatesService.addWorkflowNode({ + url: generatePostUrl(), + data: buildSendableNodeData() + }) + .then(function(data) { + continueRecursing(data.data.id); + }, function(error) { + ProcessErrors($scope, error.data, error.status, form, { + 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)) { + + if(params.node.edited) { + + $scope.editRequests.push({ + id: params.node.nodeId, + data: buildSendableNodeData() + }); + + } + + if((params.node.originalParentId && params.parentId !== params.node.originalParentId) || params.node.originalEdge !== params.node.edgeType) {//beep + + $scope.disassociateRequests.push({ + parentId: params.node.originalParentId, + nodeId: params.node.nodeId, + edge: params.node.originalEdge + }); + + // Can only associate if we have a parent. + // If we don't have a parent then this is a root node + // and the act of disassociating will make it a root node + if(params.parentId) { + $scope.associateRequests.push({ + parentId: params.parentId, + nodeId: params.node.nodeId, + edge: params.node.edgeType + }); + } + + } + else if(!params.node.originalParentId && params.parentId) { + // This used to be a root node but is now not a root node + $scope.associateRequests.push({ + parentId: params.parentId, + nodeId: params.node.nodeId, + edge: params.node.edgeType + }); + } + + } + + continueRecursing(params.node.nodeId); + } + } + $scope.lookUpInventory = function(){ $state.go('.inventory'); }; @@ -70,7 +228,66 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr }; $scope.saveWorkflowMaker = function() { - $scope.closeDialog(); + + $scope.totalIteratedNodes = 0; + + if($scope.treeData && $scope.treeData.data && $scope.treeData.data.children && $scope.treeData.data.children.length > 0) { + let completionCallback = function() { + + let disassociatePromises = $scope.disassociateRequests.map(function(request) { + return TemplatesService.disassociateWorkflowNode({ + parentId: request.parentId, + nodeId: request.nodeId, + edge: request.edge + }); + }); + + let editNodePromises = $scope.editRequests.map(function(request) { + return TemplatesService.editWorkflowNode({ + id: request.id, + data: request.data + }); + }); + + $q.all(disassociatePromises.concat(editNodePromises)) + .then(function() { + + let associatePromises = $scope.associateRequests.map(function(request) { + return TemplatesService.associateWorkflowNode({ + parentId: request.parentId, + nodeId: request.nodeId, + edge: request.edge + }); + }); + + let deletePromises = $scope.treeData.data.deletedNodes.map(function(nodeId) { + return TemplatesService.deleteWorkflowJobTemplateNode(nodeId); + }); + + $q.all(associatePromises.concat(deletePromises)) + .then(function() { + $scope.closeDialog(); + }); + }); + }; + + _.forEach($scope.treeData.data.children, function(child) { + recursiveNodeUpdates({ + node: child + }, completionCallback); + }); + } + else { + + let deletePromises = $scope.treeData.data.deletedNodes.map(function(nodeId) { + return TemplatesService.deleteWorkflowJobTemplateNode(nodeId); + }); + + $q.all(deletePromises) + .then(function() { + $scope.closeDialog(); + }); + } }; /* ADD NODE FUNCTIONS */ @@ -575,7 +792,7 @@ export default ['$scope', 'WorkflowService', 'generateList', 'TemplateList', 'Pr edgeFlags: $scope.edgeFlags }); } - + $scope.toggleManualControls = function() { $scope.showManualControls = !$scope.showManualControls; }; diff --git a/awx/ui/tests/spec/workflows/workflow-maker.controller-test.js b/awx/ui/tests/spec/workflows/workflow-maker.controller-test.js index 12bbf8ef74..6adc771b27 100644 --- a/awx/ui/tests/spec/workflows/workflow-maker.controller-test.js +++ b/awx/ui/tests/spec/workflows/workflow-maker.controller-test.js @@ -47,10 +47,10 @@ describe('Controller: WorkflowMaker', () => { })); - describe('scope.saveWorkflowMaker()', () => { + describe('scope.closeWorkflowMaker()', () => { it('should close the dialog', ()=>{ - scope.saveWorkflowMaker(); + scope.closeWorkflowMaker(); expect(scope.closeDialog).toHaveBeenCalled(); });