diff --git a/awx/ui/client/features/templates/templates.strings.js b/awx/ui/client/features/templates/templates.strings.js index 5e8afaf8bd..baaae1f783 100644 --- a/awx/ui/client/features/templates/templates.strings.js +++ b/awx/ui/client/features/templates/templates.strings.js @@ -124,9 +124,12 @@ function TemplatesStrings (BaseString) { INVENTORY_WILL_NOT_OVERRIDE: t.s('The inventory of this node will not be overridden by the parent workflow inventory.'), INVENTORY_PROMPT_WILL_OVERRIDE: t.s('The inventory of this node will be overridden if a parent workflow inventory is provided at launch.'), INVENTORY_PROMPT_WILL_NOT_OVERRIDE: t.s('The inventory of this node will not be overridden if a parent workflow inventory is provided at launch.'), - EDIT_LINK: ({ parentName, childName }) => t.s('EDIT LINK | {{parentName}} to {{childName}}', { parentName, childName }), - VIEW_LINK: ({ parentName, childName }) => t.s('VIEW LINK | {{parentName}} to {{childName}}', { parentName, childName }) - } + ADD_LINK: t.s('ADD LINK'), + EDIT_LINK: t.s('EDIT LINK'), + VIEW_LINK: t.s('VIEW LINK'), + NEW_LINK: t.s('Please click on an available node to form a new link.'), + UNLINK: t.s('UNLINK') + }; } TemplatesStrings.$inject = ['BaseStringService']; diff --git a/awx/ui/client/src/templates/main.js b/awx/ui/client/src/templates/main.js index 1737771029..60e20aafc8 100644 --- a/awx/ui/client/src/templates/main.js +++ b/awx/ui/client/src/templates/main.js @@ -14,7 +14,6 @@ import prompt from './prompt/main'; import workflowChart from './workflows/workflow-chart/main'; import workflowMaker from './workflows/workflow-maker/main'; import workflowControls from './workflows/workflow-controls/main'; -import workflowService from './workflows/workflow.service'; import WorkflowForm from './workflows.form'; import InventorySourcesList from './inventory-sources.list'; import TemplateList from './templates.list'; @@ -35,7 +34,6 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p workflowChart.name, workflowMaker.name, workflowControls.name ]) .service('TemplatesService', templatesService) - .service('WorkflowService', workflowService) .factory('WorkflowForm', WorkflowForm) // TODO: currently being kept arround for rbac selection, templates within projects and orgs, etc. .factory('TemplateList', TemplateList) 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 b2deec2bf3..0d5130a418 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 @@ -16,7 +16,7 @@ .WorkflowChart-linkHovering .WorkflowChart-linkOverlay { cursor: pointer; opacity: 1; - fill: @cgrey; + fill: #E1E1E1; } .WorkflowChart-linkHovering .WorkflowChart-linkPath { @@ -27,9 +27,11 @@ .WorkflowChart-link polygon, .WorkflowChart-link .WorkflowChart-betweenNodesIcon, .WorkflowChart-node .WorkflowChart-nodeAddCircle, +.WorkflowChart-node .WorkflowChart-linkCircle, .WorkflowChart-node .WorkflowChart-nodeRemoveCircle, .WorkflowChart-node .WorkflowChart-nodeAddIcon, -.WorkflowChart-node .WorkflowChart-nodeRemoveIcon { +.WorkflowChart-node .WorkflowChart-nodeRemoveIcon, +.WorkflowChart-node .WorkflowChart-nodeLinkIcon { opacity: 0; } @@ -41,6 +43,14 @@ fill: @default-succ-hov; } +.WorkflowChart-node .WorkflowChart-linkCircle { + fill: @default-link; +} + +.WorkflowChart-linkCircle.WorkflowChart-linkButtonHovering { + fill: @default-link-hov; +} + .WorkflowChart-node .WorkflowChart-nodeRemoveCircle { fill: @default-err; } @@ -53,20 +63,27 @@ fill: @default-secondary-bg; } -.WorkflowChart-rect.WorkflowChart-placeholder { +.WorkflowChart-rect.WorkflowChart-isNodeBeingAdded { stroke-dasharray: 3; } -.WorkflowChart-node .WorkflowChart-transparentRect { +.WorkflowChart-node .WorkflowChart-nodeOverlay--transparent { fill: @default-bg; opacity: 0; } +.WorkflowChart-node .WorkflowChart-nodeOverlay--disabled { + fill: @default-dark; + opacity: 0.2; +} + .WorkflowChart-alwaysShowAdd circle, .WorkflowChart-alwaysShowAdd path, .WorkflowChart-alwaysShowAdd .WorkflowChart-betweenNodesIcon, .WorkflowChart-nodeHovering .WorkflowChart-nodeAddCircle, .WorkflowChart-nodeHovering .WorkflowChart-nodeAddIcon, +.WorkflowChart-nodeHovering .WorkflowChart-linkCircle, +.WorkflowChart-nodeHovering .WorkflowChart-nodeLinkIcon, .WorkflowChart-nodeHovering .WorkflowChart-nodeRemoveCircle, .WorkflowChart-nodeHovering .WorkflowChart-nodeRemoveIcon, .WorkflowChart-addHovering circle, @@ -76,7 +93,7 @@ opacity: 1; } -.WorkflowChart-link.WorkflowChart-placeholder { +.WorkflowChart-link.WorkflowChart-isNodeBeingAdded { stroke-dasharray: 3; } @@ -184,3 +201,11 @@ .WorkflowChart-dashedNode { stroke-dasharray: 5,5; } + +.WorkflowChart-nodeLinkIcon { + color: @default-bg; +} + +.WorkflowChart-nodeHovering .WorkflowChart-addLinkCircle { + 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 59feb22066..cb4b392e7c 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 @@ -9,13 +9,14 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge return { scope: { - treeData: '=', - canAddWorkflowJobTemplate: '=', - workflowJobTemplateObj: '=', - addNode: '&', + treeState: '=', + readOnly: '<', + addNodeWithoutChild: '&', + addNodeWithChild: '&', editNode: '&', deleteNode: '&', editLink: '&', + selectNodeForLinking: '&', workflowZoomed: '&', mode: '@' }, @@ -23,22 +24,19 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge link: function(scope, element) { let marginLeft = 20, - i = 0, nodeW = 180, nodeH = 60, rootW = 60, rootH = 40, startNodeOffsetY = scope.mode === 'details' ? 17 : 10, - verticalSpaceBetweenNodes = 20, maxNodeTextLength = 27, windowHeight, windowWidth, - tree, - line, zoomObj, baseSvg, svgGroup, - graphLoaded; + graphLoaded, + force; scope.dimensionsSet = false; @@ -56,16 +54,11 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge }); function init() { - tree = d3.layout.tree() - .nodeSize([nodeH + verticalSpaceBetweenNodes,nodeW]) - .separation(function(a, b) { - // This should tighten up some of the other nodes so there's not so much wasted space - return a.parent === b.parent ? 1 : 1.25; - }); - - line = d3.svg.line() - .x(function(d){return d.x;}) - .y(function(d){return d.y;}); + force = d3.layout.force() + .gravity(0) + .charge(-60) + .linkDistance(300) + .size([windowHeight, windowWidth]); zoomObj = d3.behavior.zoom().scaleExtent([0.5, 2]); @@ -104,27 +97,6 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge return dimensions; } - function lineData(d){ - - let sourceX = d.source.isStartNode ? d.source.y + rootW : d.source.y + nodeW; - let sourceY = d.source.isStartNode ? d.source.x + startNodeOffsetY + rootH / 2 : d.source.x + nodeH / 2; - let targetX = d.target.y; - let targetY = d.target.x + nodeH / 2; - - let points = [ - { - x: sourceX, - y: sourceY - }, - { - x: targetX, - y: targetY - } - ]; - - return line(points); - } - // TODO: this function is hacky and we need to come up with a better solution // see: http://stackoverflow.com/questions/15975440/add-ellipses-to-overflowing-text-in-svg#answer-27723752 function wrap(text) { @@ -229,27 +201,384 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge svgGroup.attr("transform", "translate(" + marginLeft + "," + (windowHeight/2 - (nodeH*scaleToFit/2) + startNodeOffsetFromGraphCenter) + ")scale(" + scaleToFit + ")"); zoomObj.translate([marginLeft - scaleToFit*marginLeft, windowHeight/2 - (nodeH*scaleToFit/2) + startNodeOffsetFromGraphCenter - ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scaleToFit)]); - } function update() { - let userCanAddEdit = (scope.workflowJobTemplateObj && scope.workflowJobTemplateObj.summary_fields && scope.workflowJobTemplateObj.summary_fields.user_capabilities && scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) || scope.canAddWorkflowJobTemplate; if(scope.dimensionsSet) { - // Declare the nodes - let nodes = tree.nodes(scope.treeData), - links = tree.links(nodes); + let links = svgGroup.selectAll(".WorkflowChart-link") + .data(scope.treeState.arrayOfLinksForChart, function(d) { return `${d.source.id}-${d.target.id}`; }); - let node = svgGroup.selectAll("g.WorkflowChart-node") - .data(nodes, function(d) { - d.y = d.depth * 240; - return d.id || (d.id = ++i); + // Remove any stale links + links.exit().remove(); + + // Update existing links + baseSvg.selectAll(".WorkflowChart-link") + .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id;}); + + baseSvg.selectAll(".WorkflowChart-linkPath") + .attr("class", function(d) { + return (d.source.isNodeBeingAdded || d.target.isNodeBeingAdded) ? "WorkflowChart-linkPath WorkflowChart-isNodeBeingAdded" : "WorkflowChart-linkPath"; + }) + .attr('stroke', function(d) { + let edgeType = d.edgeType; + if(edgeType) { + if(edgeType === "failure") { + return "#d9534f"; + } else if(edgeType === "success") { + return "#5cb85c"; + } else if(edgeType === "always"){ + return "#337ab7"; + } else if (edgeType === "placeholder") { + return "#B9B9B9"; + } + } + else { + return "#D7D7D7"; + } }); - let nodeEnter = node.enter().append("g") - .attr("class", "WorkflowChart-node") - .attr("id", function(d){return "node-" + d.id;}) - .attr("parent", function(d){return d.parent ? d.parent.id : null;}) - .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; }); + baseSvg.selectAll(".WorkflowChart-linkOverlay") + .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-overlay";}) + .attr("class", function(d) { + let linkClasses = ["WorkflowChart-linkOverlay"]; + if (d.isLinkBeingEdited) { + linkClasses.push("WorkflowChart-link--active"); + } + return linkClasses.join(' '); + }); + + baseSvg.selectAll(".WorkflowChart-circleBetweenNodes") + .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-add";}) + .style("display", function(d) { return (scope.treeState.isLinkMode || d.source.isNodeBeingAdded || d.target.isNodeBeingAdded || scope.readOnly) ? "none" : null; }); + + baseSvg.selectAll(".WorkflowChart-betweenNodesIcon") + .style("display", function(d) { return (scope.treeState.isLinkMode || d.source.isNodeBeingAdded || d.target.isNodeBeingAdded || scope.readOnly) ? "none" : null; }); + + + // Add any new links + let linkEnter = links.enter().append("g") + .attr("class", "WorkflowChart-link") + .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id;}); + + linkEnter.append("polygon", "g") + .attr("class", function(d) { + let linkClasses = ["WorkflowChart-linkOverlay"]; + if (d.isLinkBeingEdited) { + linkClasses.push("WorkflowChart-link--active"); + } + return linkClasses.join(' '); + }) + .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-overlay";}) + .call(edit_link) + .on("mouseover", function(d) { + if(!scope.treeState.isLinkMode && !d.source.isStartNode && !d.source.isNodeBeingAdded && !d.target.isNodeBeingAdded && scope.mode !== 'details') { + d3.select("#link-" + d.source.id + "-" + d.target.id) + .classed("WorkflowChart-linkHovering", true); + + let xPos, yPos, arrowClass; + if (d.source.x === d.target.x) { + xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - (100/2); + yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 100; + arrowClass = 'WorkflowChart-tooltipArrow--down'; + } else { + xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - 115; + yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 50; + arrowClass = 'WorkflowChart-tooltipArrow--right'; + } + + let edgeTypeLabel; + + switch(d.edgeType) { + case "always": + edgeTypeLabel = TemplatesStrings.get('workflow_maker.ALWAYS'); + break; + case "success": + edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_SUCCESS'); + break; + case "failure": + edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_FAILURE'); + break; + } + + let linkInstructionText = !scope.readOnly ? TemplatesStrings.get('workflow_maker.EDIT_LINK_TOOLTIP') : TemplatesStrings.get('workflow_maker.VIEW_LINK_TOOLTIP'); + + svgGroup.append("foreignObject") + .attr("transform", `translate(${xPos},${yPos})`) + .attr("width", 100) + .attr("height", 60) + .attr("class", "WorkflowChart-tooltip") + .html(function(){ + return `
${TemplatesStrings.get('workflow_maker.RUN')}: ${edgeTypeLabel}
${linkInstructionText}
`; + }); + } + + }) + .on("mouseout", function(d){ + if(!d.source.isStartNode && !d.target.isNodeBeingAdded && scope.mode !== 'details') { + d3.select("#link-" + d.source.id + "-" + d.target.id) + .classed("WorkflowChart-linkHovering", false); + } + $('.WorkflowChart-tooltip').remove(); + }); + + // Add entering links in the parent’s old position. + linkEnter.append("line") + .attr("class", function(d) { + return (d.source.isNodeBeingAdded || d.target.isNodeBeingAdded) ? "WorkflowChart-linkPath WorkflowChart-isNodeBeingAdded" : "WorkflowChart-linkPath"; + }) + .call(edit_link) + .on("mouseenter", function(d) { + if(!scope.treeState.isLinkMode && !d.source.isStartNode && !d.source.isNodeBeingAdded && !d.target.isNodeBeingAdded && scope.mode !== 'details') { + d3.select("#link-" + d.source.id + "-" + d.target.id) + .classed("WorkflowChart-linkHovering", true); + + let xPos, yPos, arrowClass; + if (d.source.x === d.target.x) { + xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - (100/2); + yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 100; + arrowClass = 'WorkflowChart-tooltipArrow--down'; + } else { + xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - 115; + yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 50; + arrowClass = 'WorkflowChart-tooltipArrow--right'; + } + + let edgeTypeLabel; + + switch(d.edgeType) { + case "always": + edgeTypeLabel = TemplatesStrings.get('workflow_maker.ALWAYS'); + break; + case "success": + edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_SUCCESS'); + break; + case "failure": + edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_FAILURE'); + break; + } + + let linkInstructionText = !scope.readOnly ? TemplatesStrings.get('workflow_maker.EDIT_LINK_TOOLTIP') : TemplatesStrings.get('workflow_maker.VIEW_LINK_TOOLTIP'); + + svgGroup.append("foreignObject") + .attr("transform", `translate(${xPos},${yPos})`) + .attr("width", 100) + .attr("height", 60) + .attr("class", "WorkflowChart-tooltip") + .html(function(){ + return `
${TemplatesStrings.get('workflow_maker.RUN')}: ${edgeTypeLabel}
${linkInstructionText}
`; + }); + } + }) + .on("mouseleave", function(d){ + if(!d.source.isStartNode && !d.target.isNodeBeingAdded && scope.mode !== 'details') { + d3.select("#link-" + d.source.id + "-" + d.target.id) + .classed("WorkflowChart-linkHovering", false); + } + $('.WorkflowChart-tooltip').remove(); + }) + .attr('stroke', function(d) { + let edgeType = d.edgeType; + if(d.edgeType) { + if(edgeType === "failure") { + return "#d9534f"; + } else if(edgeType === "success") { + return "#5cb85c"; + } else if(edgeType === "always"){ + return "#337ab7"; + } else if (edgeType === "placeholder") { + return "#B9B9B9"; + } + } + else { + return "#D7D7D7"; + } + }); + + linkEnter.append("circle") + .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-add";}) + .attr("r", 10) + .attr("class", "WorkflowChart-addCircle WorkflowChart-circleBetweenNodes") + .style("display", function(d) { return (scope.treeState.isLinkMode || d.source.isNodeBeingAdded || d.target.isNodeBeingAdded || scope.readOnly) ? "none" : null; }) + .call(add_node_with_child) + .on("mouseover", function(d) { + d3.select("#link-" + d.source.id + "-" + d.target.id) + .classed("WorkflowChart-addHovering", true); + }) + .on("mouseout", function(d){ + d3.select("#link-" + d.source.id + "-" + d.target.id) + .classed("WorkflowChart-addHovering", false); + }); + + linkEnter.append("path") + .attr("class", "WorkflowChart-betweenNodesIcon") + .style("fill", "white") + .attr("d", d3.svg.symbol() + .size(60) + .type("cross") + ) + .style("display", function(d) { return (scope.treeState.isLinkMode || d.source.isNodeBeingAdded || d.target.isNodeBeingAdded || scope.readOnly) ? "none" : null; }) + .call(add_node_with_child) + .on("mouseover", function(d) { + d3.select("#link-" + d.source.id + "-" + d.target.id) + .classed("WorkflowChart-addHovering", true); + }) + .on("mouseout", function(d){ + d3.select("#link-" + d.source.id + "-" + d.target.id) + .classed("WorkflowChart-addHovering", false); + }); + + // Create references to all the link elements so that they can be transitioned + // properly in the tick function + let linkLines = svgGroup.selectAll(".WorkflowChart-link line"); + let linkPolygons = svgGroup.selectAll(".WorkflowChart-link polygon"); + let linkAddBetweenCircle = svgGroup.selectAll(".WorkflowChart-link circle"); + let linkAddBetweenIcon = svgGroup.selectAll(".WorkflowChart-betweenNodesIcon"); + + let nodes = svgGroup.selectAll('.WorkflowChart-node') + .data(scope.treeState.arrayOfNodesForChart, function(d) { return d.id; }); + + // Remove any stale nodes + nodes.exit().remove(); + + // Update existing nodes + baseSvg.selectAll(".WorkflowChart-nodeAddCircle") + .style("display", function(d) { return scope.treeState.isLinkMode || d.isNodeBeingAdded || scope.readOnly ? "none" : null; }); + + baseSvg.selectAll(".WorkflowChart-nodeAddIcon") + .style("display", function(d) { return scope.treeState.isLinkMode || d.isNodeBeingAdded || scope.readOnly ? "none" : null; }); + + baseSvg.selectAll(".WorkflowChart-linkCircle") + .style("display", function(d) { return scope.treeState.isLinkMode || d.isNodeBeingAdded || scope.readOnly ? "none" : null; }); + + baseSvg.selectAll(".WorkflowChart-nodeLinkIcon") + .style("display", function(d) { return scope.treeState.isLinkMode || d.isNodeBeingAdded || scope.readOnly ? "none" : null; }); + + baseSvg.selectAll(".WorkflowChart-nodeRemoveCircle") + .style("display", function(d) { return scope.treeState.isLinkMode || d.isNodeBeingAdded || scope.readOnly ? "none" : null; }); + + baseSvg.selectAll(".WorkflowChart-nodeRemoveIcon") + .style("display", function(d) { return scope.treeState.isLinkMode || d.isNodeBeingAdded || scope.readOnly ? "none" : null; }); + + baseSvg.selectAll(".WorkflowChart-rect") + .attr('stroke', function(d) { + if(d.job && d.job.status) { + if(d.job.status === "successful"){ + return "#5cb85c"; + } + else if (d.job.status === "failed" || d.job.status === "error" || d.job.status === "cancelled") { + return "#d9534f"; + } + else { + return "#D7D7D7"; + } + } + else { + return "#D7D7D7"; + } + }) + .attr("class", function(d) { + let classString = d.isNodeBeingAdded ? "WorkflowChart-rect WorkflowChart-isNodeBeingAdded" : "WorkflowChart-rect"; + classString += !d.unifiedJobTemplate ? " WorkflowChart-dashedNode" : ""; + return classString; + }); + + baseSvg.selectAll(".WorkflowChart-nodeOverlay") + .attr("class", function(d) { return d.isInvalidLinkTarget ? "WorkflowChart-nodeOverlay WorkflowChart-nodeOverlay--disabled" : "WorkflowChart-nodeOverlay WorkflowChart-nodeOverlay--transparent"; }); + + baseSvg.selectAll(".WorkflowChart-nodeTypeCircle") + .style("display", function(d) { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update" ) ? null : "none"; }); + + baseSvg.selectAll(".WorkflowChart-nodeTypeLetter") + .text(function (d) { + return (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update")) ? "P" : (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? "I" : ""); + }) + .style("display", function(d) { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? null : "none"; }); + + baseSvg.selectAll(".WorkflowChart-nodeStatus") + .attr("class", function(d) { + + let statusClass = "WorkflowChart-nodeStatus "; + + if(d.job){ + switch(d.job.status) { + case "pending": + statusClass += "WorkflowChart-nodeStatus--running"; + break; + case "waiting": + statusClass += "WorkflowChart-nodeStatus--running"; + break; + case "running": + statusClass += "WorkflowChart-nodeStatus--running"; + break; + case "successful": + statusClass += "WorkflowChart-nodeStatus--success"; + break; + case "failed": + statusClass += "WorkflowChart-nodeStatus--failed"; + break; + case "error": + statusClass += "WorkflowChart-nodeStatus--failed"; + break; + case "canceled": + statusClass += "WorkflowChart-nodeStatus--canceled"; + break; + } + } + + return statusClass; + }) + .style("display", function(d) { return d.job && d.job.status ? null : "none"; }) + .transition() + .duration(0) + .attr("r", 6) + .each(function(d) { + if(d.job && d.job.status && (d.job.status === "pending" || d.job.status === "waiting" || d.job.status === "running")) { + // Pulse the circle + var circle = d3.select(this); + (function repeat() { + circle = circle.transition() + .duration(2000) + .attr("r", 6) + .transition() + .duration(2000) + .attr("r", 0) + .ease('sine') + .each("end", repeat); + })(); + } + }); + + baseSvg.selectAll(".WorkflowChart-nameText") + .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 20 : nodeW / 2; }) + .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : nodeH / 2; }) + .attr("text-anchor", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? "inherit" : "middle"; }) + .text(function (d) { + const name = _.get(d, 'unifiedJobTemplate.name'); + return name ? wrap(name) : ""; + }); + + baseSvg.selectAll(".WorkflowChart-detailsLink") + .style("display", function(d){ return d.job && d.job.status && d.job.id ? null : "none"; }); + + baseSvg.selectAll(".WorkflowChart-deletedText") + .style("display", function(d){ return d.unifiedJobTemplate || d.isNodeBeingAdded ? "none" : null; }); + + baseSvg.selectAll(".WorkflowChart-activeNode") + .style("display", function(d) { return d.isNodeBeingEdited ? null : "none"; }); + + baseSvg.selectAll(".WorkflowChart-elapsed") + .style("display", function(d) { return (d.job && d.job.elapsed) ? null : "none"; }); + + baseSvg.selectAll(".WorkflowChart-addLinkCircle") + .attr("fill", function(d) { return scope.treeState.addLinkSource === d.id ? "#337AB7" : "#D7D7D7"; }) + .style("display", function(d) { return scope.treeState.isLinkMode && !d.isInvalidLinkTarget ? null : "none"; }); + + // Add new nodes + const nodeEnter = nodes + .enter() + .append('g') + .attr("class", "WorkflowChart-node") + .attr("id", function(d){return "node-" + d.id;}); nodeEnter.each(function(d) { let thisNode = d3.select(this); @@ -275,16 +604,22 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge .attr("ry", 5) .attr("fill", "#5cb85c") .attr("class", "WorkflowChart-rootNode") - .call(add_node); + .call(add_node_without_child); thisNode.append("text") .attr("x", 13) .attr("y", 30) .attr("dy", ".35em") .attr("class", "WorkflowChart-startText") .text(function () { return TemplatesStrings.get('workflow_maker.START'); }) - .call(add_node); + .call(add_node_without_child); } else { + thisNode.append("circle") + .attr("cy", nodeH/2) + .attr("cx", nodeW) + .attr("r", 8) + .attr("class", "WorkflowChart-addLinkCircle") + .style("display", function() { return scope.treeState.isLinkMode ? null : "none"; }); thisNode.append("rect") .attr("width", nodeW) .attr("height", nodeH) @@ -308,15 +643,15 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge }) .attr('stroke-width', "2px") .attr("class", function(d) { - let classString = d.placeholder ? "WorkflowChart-rect WorkflowChart-placeholder" : "WorkflowChart-rect"; - classString += !d.unifiedJobTemplate ? " WorkflowChart-dashedNode" : ""; + let classString = d.isNodeBeingAdded ? "WorkflowChart-rect WorkflowChart-isNodeBeingAdded" : "WorkflowChart-rect"; + classString += !_.get(d, 'unifiedJobTemplate.name') ? " WorkflowChart-dashedNode" : ""; return classString; }); thisNode.append("path") .attr("d", rounded_rect(1, 0, 5, nodeH, 5, 1, 0, 1, 0)) .attr("class", "WorkflowChart-activeNode") - .style("display", function(d) { return d.isActiveEdit ? null : "none"; }); + .style("display", function(d) { return d.isNodeBeingEdited ? null : "none"; }); thisNode.append("text") .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 20 : nodeW / 2; }) @@ -325,8 +660,9 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge .attr("text-anchor", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? "inherit" : "middle"; }) .attr("class", "WorkflowChart-defaultText WorkflowChart-nameText") .text(function (d) { - return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : ""; - }).each(wrap); + const name = _.get(d, 'unifiedJobTemplate.name'); + return name ? wrap(name) : ""; + }); thisNode.append("foreignObject") .attr("x", 62) @@ -337,7 +673,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge .html(function () { return `${TemplatesStrings.get('workflow_maker.DELETED')}`; }) - .style("display", function(d) { return d.unifiedJobTemplate || d.placeholder ? "none" : null; }); + .style("display", function(d) { return d.unifiedJobTemplate || d.isNodeBeingAdded ? "none" : null; }); thisNode.append("circle") .attr("cy", nodeH) @@ -398,8 +734,8 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge thisNode.append("rect") .attr("width", nodeW) .attr("height", nodeH) - .attr("class", "WorkflowChart-transparentRect") - .call(edit_node) + .attr("class", function(d) { return d.isInvalidLinkTarget ? "WorkflowChart-nodeOverlay WorkflowChart-nodeOverlay--disabled" : "WorkflowChart-nodeOverlay WorkflowChart-nodeOverlay--transparent"; }) + .call(node_click) .on("mouseover", function(d) { if(!d.isStartNode) { let resourceName = (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : ""; @@ -416,7 +752,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge // the user is hovering over is at the very end of the list. This way the tooltip will appear on top // of all other nodes. svgGroup.selectAll("g.WorkflowChart-node").sort(function (a) { - return (a.id !== d.id) ? -1 : 1; + return (a.index !== d.index) ? -1 : 1; }); // Render the tooltip quickly in the dom and then remove. This lets us know how big the tooltip is so that we can place // it properly on the workflow @@ -436,12 +772,35 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge return "
" + $filter('sanitize')(resourceName) + "
"; }); } + + if (scope.treeState.isLinkMode && !d.isInvalidLinkTarget && scope.treeState.addLinkSource !== d.id) { + let sourceNode = d3.select(`#node-${scope.treeState.addLinkSource}`); + const sourceNodeX = d3.transform(sourceNode.attr("transform")).translate[0]; + const sourceNodeY = d3.transform(sourceNode.attr("transform")).translate[1]; + + let targetNode = d3.select(`#node-${d.id}`); + const targetNodeX = d3.transform(targetNode.attr("transform")).translate[0]; + const targetNodeY = d3.transform(targetNode.attr("transform")).translate[1]; + + $('.WorkflowChart-potentialLink').remove(); + + svgGroup.insert("line", '.WorkflowChart-node') + .attr("class", "WorkflowChart-potentialLink") + .attr("x1", sourceNodeX + nodeW/2) + .attr("x2", targetNodeX + nodeW/2) + .attr("y1", sourceNodeY + nodeH/2) + .attr("y2", targetNodeY + nodeH/2) + .style("stroke-dasharray","5,5") + .style("stroke-width", "2") + .style("stroke", "#D7D7D7"); + } d3.select("#node-" + d.id) .classed("WorkflowChart-nodeHovering", true); } }) .on("mouseout", function(d){ $('.WorkflowChart-tooltip').remove(); + $('.WorkflowChart-potentialLink').remove(); if(!d.isStartNode) { d3.select("#node-" + d.id) .classed("WorkflowChart-nodeHovering", false); @@ -462,8 +821,8 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge .attr("cx", nodeW) .attr("r", 10) .attr("class", "WorkflowChart-addCircle WorkflowChart-nodeAddCircle") - .style("display", function(d) { return d.placeholder || !(userCanAddEdit) ? "none" : null; }) - .call(add_node) + .style("display", function(d) { return d.isNodeBeingAdded || scope.readOnly ? "none" : null; }) + .call(add_node_without_child) .on("mouseover", function(d) { d3.select("#node-" + d.id) .classed("WorkflowChart-nodeHovering", true); @@ -484,8 +843,8 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge .size(60) .type("cross") ) - .style("display", function(d) { return d.placeholder || !(userCanAddEdit) ? "none" : null; }) - .call(add_node) + .style("display", function(d) { return d.isNodeBeingAdded || scope.readOnly ? "none" : null; }) + .call(add_node_without_child) .on("mouseover", function(d) { d3.select("#node-" + d.id) .classed("WorkflowChart-nodeHovering", true); @@ -498,13 +857,57 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge d3.select("#node-" + d.id + "-add") .classed("WorkflowChart-addHovering", false); }); + thisNode.append("circle") + .attr("id", function(d){return "node-" + d.id + "-link";}) + .attr("cx", nodeW) + .attr("cy", nodeH/2) + .attr("r", 10) + .attr("class", "WorkflowChart-linkCircle") + .style("display", function(d) { return d.isNodeBeingAdded || scope.readOnly ? "none" : null; }) + .call(add_link) + .on("mouseover", function(d) { + d3.select("#node-" + d.id) + .classed("WorkflowChart-nodeHovering", true); + d3.select("#node-" + d.id + "-link") + .classed("WorkflowChart-linkButtonHovering", true); + }) + .on("mouseout", function(d){ + d3.select("#node-" + d.id) + .classed("WorkflowChart-nodeHovering", false); + d3.select("#node-" + d.id + "-link") + .classed("WorkflowChart-linkButtonHovering", false); + }); + // TODO: clean up the placement of this icon... this works but it's not + // clean + thisNode.append("foreignObject") + .attr("x", nodeW - 6) + .attr("y", nodeH/2 - 9) + .style("font-size","14px") + .html(function () { + return ``; + }) + .attr("class", "WorkflowChart-nodeLinkIcon") + .style("display", function(d) { return d.isNodeBeingAdded || scope.readOnly ? "none" : null; }) + .call(add_link) + .on("mouseover", function(d) { + d3.select("#node-" + d.id) + .classed("WorkflowChart-nodeHovering", true); + d3.select("#node-" + d.id + "-link") + .classed("WorkflowChart-linkButtonHovering", true); + }) + .on("mouseout", function(d){ + d3.select("#node-" + d.id) + .classed("WorkflowChart-nodeHovering", false); + d3.select("#node-" + d.id + "-link") + .classed("WorkflowChart-linkButtonHovering", false); + }); thisNode.append("circle") .attr("id", function(d){return "node-" + d.id + "-remove";}) .attr("cx", nodeW) .attr("cy", nodeH) .attr("r", 10) .attr("class", "WorkflowChart-nodeRemoveCircle") - .style("display", function(d) { return (d.canDelete === false || d.placeholder || !(userCanAddEdit)) ? "none" : null; }) + .style("display", function(d) { return (d.isStartNode || d.isNodeBeingAdded || scope.readOnly) ? "none" : null; }) .call(remove_node) .on("mouseover", function(d) { d3.select("#node-" + d.id) @@ -526,7 +929,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge .size(60) .type("cross") ) - .style("display", function(d) { return (d.canDelete === false || d.placeholder || !(userCanAddEdit)) ? "none" : null; }) + .style("display", function(d) { return (d.isStartNode || d.isNodeBeingAdded || scope.readOnly) ? "none" : null; }) .call(remove_node) .on("mouseover", function(d) { d3.select("#node-" + d.id) @@ -600,37 +1003,32 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge } }); - node.exit().remove(); - - if(nodes && nodes.length > 1 && !graphLoaded) { - zoomToFitChart(); - } + // if(scope.treeState.arrayOfNodesForChart && scope.treeState.arrayOfNodesForChart > 1 && !graphLoaded) { + // zoomToFitChart(); + // } graphLoaded = true; - let link = svgGroup.selectAll("g.WorkflowChart-link") - .data(links, function(d) { - return d.source.id + "-" + d.target.id; - }); + // This will make sure that all the link elements appear before the nodes in the dom + svgGroup.selectAll(".WorkflowChart-node").order(); - let linkEnter = link.enter().append("g") - .attr("class", "WorkflowChart-link") - .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id;}); + let tick = (e) => { + var k = 6 * e.alpha; - linkEnter.append("polygon", "g") - .attr("class", function(d) { - let linkClasses = ["WorkflowChart-linkOverlay"]; - if (d.source.isLinkEditParent && d.target.isLinkEditChild) { - linkClasses.push("WorkflowChart-link--active"); - } - return linkClasses.join(' '); - }) - .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-overlay";}) + // TODO: replace hard-coded 60 here + linkLines + .each(function(d) { d.source.y -= k; d.target.y += k; }) + .attr("x1", function(d) { return d.target.y; }) + .attr("y1", function(d) { return d.target.x + (nodeH/2); }) + .attr("x2", function(d) { return d.source.index === 0 ? (scope.mode === 'details' ? d.source.y + 25 : d.source.y + 60) : (d.source.y + nodeW); }) + .attr("y2", function(d) { return d.source.x + (nodeH/2); }); + + linkPolygons .attr("points",function(d) { - let x1 = d.source.y + nodeW; - let y1 = d.source.x + nodeH / 2; - let x2 = d.target.y; - let y2 = d.target.x + nodeH / 2; + let x1 = d.target.y; + let y1 = d.target.x + (nodeH/2); + let x2 = d.source.index === 0 ? (d.source.y + 60) : (d.source.y + nodeW); + let y2 = d.source.x + (nodeH/2); let slope = (y2 - y1)/(x2-x1); let yIntercept = y1 - slope*x1; let orthogonalDistance = 8; @@ -641,419 +1039,38 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge const pt4 = [x1, slope*x1 + yIntercept - orthogonalDistance*Math.sqrt(1+slope*slope)].join(","); return [pt1, pt2, pt3, pt4].join(" "); - }) - .call(edit_link) - .on("mouseover", function(d) { - if(!d.source.isStartNode && !d.source.placeholder && !d.target.placeholder && scope.mode !== 'details') { - d3.select("#link-" + d.source.id + "-" + d.target.id) - .classed("WorkflowChart-linkHovering", true); - - let xPos, yPos, arrowClass; - if (d.source.x === d.target.x) { - xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - (100/2); - yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 100; - arrowClass = 'WorkflowChart-tooltipArrow--down'; - } else { - xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - 115; - yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 50; - arrowClass = 'WorkflowChart-tooltipArrow--right'; - } - - let edgeTypeLabel; - - switch(d.target.edgeType) { - case "always": - edgeTypeLabel = TemplatesStrings.get('workflow_maker.ALWAYS'); - break; - case "success": - edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_SUCCESS'); - break; - case "failure": - edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_FAILURE'); - break; - } - - let linkInstructionText = _.get(scope, 'workflowJobTemplateObj.summary_fields.user_capabilities.edit') ? TemplatesStrings.get('workflow_maker.EDIT_LINK_TOOLTIP') : TemplatesStrings.get('workflow_maker.VIEW_LINK_TOOLTIP'); - - linkEnter.append("foreignObject") - .attr("x", xPos) - .attr("y", yPos) - .attr("width", 100) - .attr("height", 60) - .attr("class", "WorkflowChart-tooltip") - .html(function(){ - return `
${TemplatesStrings.get('workflow_maker.RUN')}: ${edgeTypeLabel}
${linkInstructionText}
`; - }); - } - - }) - .on("mouseout", function(d){ - if(!d.source.isStartNode && !d.target.placeholder && scope.mode !== 'details') { - d3.select("#link-" + d.source.id + "-" + d.target.id) - .classed("WorkflowChart-linkHovering", false); - } - $('.WorkflowChart-tooltip').remove(); }); - // Add entering links in the parent’s old position. - linkEnter.append("path", "g") - .attr("class", function(d) { - return (d.source.placeholder || d.target.placeholder) ? "WorkflowChart-linkPath WorkflowChart-placeholder" : "WorkflowChart-linkPath"; - }) - .attr("d", lineData) - .call(edit_link) - .on("mouseenter", function(d) { - if(!d.source.isStartNode && !d.source.placeholder && !d.target.placeholder && scope.mode !== 'details') { - d3.select("#link-" + d.source.id + "-" + d.target.id) - .classed("WorkflowChart-linkHovering", true); + linkAddBetweenCircle + .attr("cx", function(d) { + return (d.source.isStartNode) ? (d.target.y + d.source.y + rootW) / 2 : (d.target.y + d.source.y + nodeW) / 2; + }) + .attr("cy", function(d) { + return (d.source.isStartNode) ? ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 : (d.target.x + d.source.x + nodeH) / 2; + }); - let xPos, yPos, arrowClass; - if (d.source.x === d.target.x) { - xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - (100/2); - yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 100; - arrowClass = 'WorkflowChart-tooltipArrow--down'; - } else { - xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - 115; - yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 50; - arrowClass = 'WorkflowChart-tooltipArrow--right'; - } + linkAddBetweenIcon + .attr("transform", function(d) { + let translate; + if(d.source.isStartNode) { + translate = "translate(" + (d.target.y + d.source.y + rootW) / 2 + "," + ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 + ")"; + } + else { + translate = "translate(" + (d.target.y + d.source.y + nodeW) / 2 + "," + (d.target.x + d.source.x + nodeH) / 2 + ")"; + } + return translate; + }); - let edgeTypeLabel; + nodes + .attr("transform", function(d) { + return "translate(" + d.y + "," + d.x + ")"; }); + }; - switch(d.target.edgeType) { - case "always": - edgeTypeLabel = TemplatesStrings.get('workflow_maker.ALWAYS'); - break; - case "success": - edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_SUCCESS'); - break; - case "failure": - edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_FAILURE'); - break; - } - - let linkInstructionText = _.get(scope, 'workflowJobTemplateObj.summary_fields.user_capabilities.edit') ? TemplatesStrings.get('workflow_maker.EDIT_LINK_TOOLTIP') : TemplatesStrings.get('workflow_maker.VIEW_LINK_TOOLTIP'); - - linkEnter.append("foreignObject") - .attr("x", xPos) - .attr("y", yPos) - .attr("width", 100) - .attr("height", 60) - .attr("class", "WorkflowChart-tooltip") - .html(function(){ - return `
${TemplatesStrings.get('workflow_maker.RUN')}: ${edgeTypeLabel}
${linkInstructionText}
`; - }); - } - }) - .on("mouseleave", function(d){ - if(!d.source.isStartNode && !d.target.placeholder && scope.mode !== 'details') { - d3.select("#link-" + d.source.id + "-" + d.target.id) - .classed("WorkflowChart-linkHovering", false); - } - $('.WorkflowChart-tooltip').remove(); - }) - .attr('stroke', function(d) { - if(d.target.edgeType) { - if(d.target.edgeType === "failure") { - return "#d9534f"; - } - else if(d.target.edgeType === "success") { - return "#5cb85c"; - } - else if(d.target.edgeType === "always"){ - return "#337ab7"; - } - } - else { - return "#D7D7D7"; - } - }); - - linkEnter.append("circle") - .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-add";}) - .attr("cx", function(d) { - return (d.source.isStartNode) ? (d.target.y + d.source.y + rootW) / 2 : (d.target.y + d.source.y + nodeW) / 2; - }) - .attr("cy", function(d) { - return (d.source.isStartNode) ? ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 : (d.target.x + d.source.x + nodeH) / 2; - }) - .attr("r", 10) - .attr("class", "WorkflowChart-addCircle WorkflowChart-circleBetweenNodes") - .style("display", function(d) { return (d.source.placeholder || d.target.placeholder || !(userCanAddEdit)) ? "none" : null; }) - .call(add_node_between) - .on("mouseover", function(d) { - d3.select("#link-" + d.source.id + "-" + d.target.id) - .classed("WorkflowChart-addHovering", true); - }) - .on("mouseout", function(d){ - d3.select("#link-" + d.source.id + "-" + d.target.id) - .classed("WorkflowChart-addHovering", false); - }); - - linkEnter.append("path") - .attr("class", "WorkflowChart-betweenNodesIcon") - .style("fill", "white") - .attr("transform", function(d) { - let translate; - if(d.source.isStartNode) { - translate = "translate(" + (d.target.y + d.source.y + rootW) / 2 + "," + ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 + ")"; - } - else { - translate = "translate(" + (d.target.y + d.source.y + nodeW) / 2 + "," + (d.target.x + d.source.x + nodeH) / 2 + ")"; - } - return translate; - }) - .attr("d", d3.svg.symbol() - .size(60) - .type("cross") - ) - .style("display", function(d) { return (d.source.placeholder || d.target.placeholder || !(userCanAddEdit)) ? "none" : null; }) - .call(add_node_between) - .on("mouseover", function(d) { - d3.select("#link-" + d.source.id + "-" + d.target.id) - .classed("WorkflowChart-addHovering", true); - }) - .on("mouseout", function(d){ - d3.select("#link-" + d.source.id + "-" + d.target.id) - .classed("WorkflowChart-addHovering", false); - }); - - link.exit().remove(); - - // Transition nodes and links to their new positions. - let t = baseSvg.transition(); - - t.selectAll(".WorkflowChart-nodeAddCircle") - .style("display", function(d) { return d.placeholder || !(userCanAddEdit) ? "none" : null; }); - - t.selectAll(".WorkflowChart-nodeAddIcon") - .style("display", function(d) { return d.placeholder || !(userCanAddEdit) ? "none" : null; }); - - t.selectAll(".WorkflowChart-nodeRemoveCircle") - .style("display", function(d) { return (d.canDelete === false || d.placeholder || !(userCanAddEdit)) ? "none" : null; }); - - t.selectAll(".WorkflowChart-nodeRemoveIcon") - .style("display", function(d) { return (d.canDelete === false || d.placeholder || !(userCanAddEdit)) ? "none" : null; }); - - t.selectAll(".WorkflowChart-linkPath") - .attr("class", function(d) { - return (d.source.placeholder || d.target.placeholder) ? "WorkflowChart-linkPath WorkflowChart-placeholder" : "WorkflowChart-linkPath"; - }) - .attr("d", lineData) - .attr('stroke', function(d) { - if(d.target.edgeType) { - if(d.target.edgeType === "failure") { - return "#d9534f"; - } - else if(d.target.edgeType === "success") { - return "#5cb85c"; - } - else if(d.target.edgeType === "always"){ - return "#337ab7"; - } - } - else { - return "#D7D7D7"; - } - }); - - t.selectAll(".WorkflowChart-circleBetweenNodes") - .style("display", function(d) { return (d.source.placeholder || d.target.placeholder || !(userCanAddEdit)) ? "none" : null; }) - .attr("cx", function(d) { - return (d.source.isStartNode) ? (d.target.y + d.source.y + rootW) / 2 : (d.target.y + d.source.y + nodeW) / 2; - }) - .attr("cy", function(d) { - return (d.source.isStartNode) ? ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 : (d.target.x + d.source.x + nodeH) / 2; - }); - - t.selectAll(".WorkflowChart-linkOverlay") - .attr("class", function(d) { - let linkClasses = ["WorkflowChart-linkOverlay"]; - if (d.source.isLinkEditParent && d.target.isLinkEditChild) { - linkClasses.push("WorkflowChart-link--active"); - } - return linkClasses.join(' '); - }) - .attr("points",function(d) { - let x1 = d.source.y + nodeW; - let y1 = d.source.x + nodeH / 2; - let x2 = d.target.y; - let y2 = d.target.x + nodeH / 2; - let slope = (y2 - y1)/(x2-x1); - let yIntercept = y1 - slope*x1; - let orthogonalDistance = 8; - - const pt1 = [x1, slope*x1 + yIntercept + orthogonalDistance*Math.sqrt(1+slope*slope)].join(","); - const pt2 = [x2, slope*x2 + yIntercept + orthogonalDistance*Math.sqrt(1+slope*slope)].join(","); - const pt3 = [x2, slope*x2 + yIntercept - orthogonalDistance*Math.sqrt(1+slope*slope)].join(","); - const pt4 = [x1, slope*x1 + yIntercept - orthogonalDistance*Math.sqrt(1+slope*slope)].join(","); - - return [pt1, pt2, pt3, pt4].join(" "); - }); - - t.selectAll(".WorkflowChart-betweenNodesIcon") - .style("display", function(d) { return (d.source.placeholder || d.target.placeholder || !(userCanAddEdit)) ? "none" : null; }) - .attr("transform", function(d) { - let translate; - if(d.source.isStartNode) { - translate = "translate(" + (d.target.y + d.source.y + rootW) / 2 + "," + ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 + ")"; - } - else { - translate = "translate(" + (d.target.y + d.source.y + nodeW) / 2 + "," + (d.target.x + d.source.x + nodeH) / 2 + ")"; - } - return translate; - }); - - t.selectAll(".WorkflowChart-rect") - .attr('stroke', function(d) { - if(d.job && d.job.status) { - if(d.job.status === "successful"){ - return "#5cb85c"; - } - else if (d.job.status === "failed" || d.job.status === "error" || d.job.status === "cancelled") { - return "#d9534f"; - } - else { - return "#D7D7D7"; - } - } - else { - return "#D7D7D7"; - } - }) - .attr("class", function(d) { - let classString = d.placeholder ? "WorkflowChart-rect WorkflowChart-placeholder" : "WorkflowChart-rect"; - classString += !d.unifiedJobTemplate ? " WorkflowChart-dashedNode" : ""; - return classString; - }); - - t.selectAll(".WorkflowChart-node") - .attr("parent", function(d){return d.parent ? d.parent.id : null;}) - .attr("transform", function(d) {d.px = d.x; d.py = d.y; return "translate(" + d.y + "," + d.x + ")"; }); - - t.selectAll(".WorkflowChart-nodeTypeCircle") - .style("display", function (d) { - return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || - d.unifiedJobTemplate.unified_job_type === "project_update" || - d.unifiedJobTemplate.type === "inventory_source" || - d.unifiedJobTemplate.unified_job_type === "inventory_update" || - d.unifiedJobTemplate.type === "workflow_job_template" || - d.unifiedJobTemplate.unified_job_type === "workflow_job") ? null : "none"; - }); - - t.selectAll(".WorkflowChart-nodeTypeLetter") - .text(function (d) { - let nodeTypeLetter = ""; - if (d.unifiedJobTemplate && d.unifiedJobTemplate.type) { - switch (d.unifiedJobTemplate.type) { - case "project": - nodeTypeLetter = "P"; - break; - case "inventory_source": - nodeTypeLetter = "I"; - break; - case "workflow_job_template": - nodeTypeLetter = "W"; - break; - } - } else if (d.unifiedJobTemplate && d.unifiedJobTemplate.unified_job_type) { - switch (d.unifiedJobTemplate.unified_job_type) { - case "project_update": - nodeTypeLetter = "P"; - break; - case "inventory_update": - nodeTypeLetter = "I"; - break; - case "workflow_job": - nodeTypeLetter = "W"; - break; - } - } - return nodeTypeLetter; - }) - .style("display", function (d) { - return d.unifiedJobTemplate && - (d.unifiedJobTemplate.type === "project" || - d.unifiedJobTemplate.unified_job_type === "project_update" || - d.unifiedJobTemplate.type === "inventory_source" || - d.unifiedJobTemplate.unified_job_type === "inventory_update" || - d.unifiedJobTemplate.type === "workflow_job_template" || - d.unifiedJobTemplate.unified_job_type === "workflow_job") ? null : "none"; - }); - - t.selectAll(".WorkflowChart-nodeStatus") - .attr("class", function(d) { - - let statusClass = "WorkflowChart-nodeStatus "; - - if(d.job){ - switch(d.job.status) { - case "pending": - statusClass += "WorkflowChart-nodeStatus--running"; - break; - case "waiting": - statusClass += "WorkflowChart-nodeStatus--running"; - break; - case "running": - statusClass += "WorkflowChart-nodeStatus--running"; - break; - case "successful": - statusClass += "WorkflowChart-nodeStatus--success"; - break; - case "failed": - statusClass += "WorkflowChart-nodeStatus--failed"; - break; - case "error": - statusClass += "WorkflowChart-nodeStatus--failed"; - break; - case "canceled": - statusClass += "WorkflowChart-nodeStatus--canceled"; - break; - } - } - - return statusClass; - }) - .style("display", function(d) { return d.job && d.job.status ? null : "none"; }) - .transition() - .duration(0) - .attr("r", 6) - .each(function(d) { - if(d.job && d.job.status && (d.job.status === "pending" || d.job.status === "waiting" || d.job.status === "running")) { - // Pulse the circle - var circle = d3.select(this); - (function repeat() { - circle = circle.transition() - .duration(2000) - .attr("r", 6) - .transition() - .duration(2000) - .attr("r", 0) - .ease('sine') - .each("end", repeat); - })(); - } - }); - - t.selectAll(".WorkflowChart-nameText") - .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 20 : nodeW / 2; }) - .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : nodeH / 2; }) - .attr("text-anchor", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? "inherit" : "middle"; }) - .text(function (d) { - return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? wrap(d.unifiedJobTemplate.name) : ""; - }); - - t.selectAll(".WorkflowChart-detailsLink") - .style("display", function(d){ return d.job && d.job.status && d.job.id ? null : "none"; }); - - t.selectAll(".WorkflowChart-deletedText") - .style("display", function(d){ return d.unifiedJobTemplate || d.placeholder ? "none" : null; }); - - t.selectAll(".WorkflowChart-activeNode") - .style("display", function(d) { return d.isActiveEdit ? null : "none"; }); - - t.selectAll(".WorkflowChart-elapsed") - .style("display", function(d) { return (d.job && d.job.elapsed) ? null : "none"; }); + force + .nodes(scope.treeState.arrayOfNodesForChart) + .links(scope.treeState.arrayOfLinksForChart) + .on("tick", tick) + .start(); } else if(!scope.watchDimensionsSet){ scope.watchDimensionsSet = scope.$watch('dimensionsSet', function(){ @@ -1066,23 +1083,21 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge } } - function add_node() { + function add_node_without_child() { this.on("click", function(d) { - if((scope.workflowJobTemplateObj && scope.workflowJobTemplateObj.summary_fields && scope.workflowJobTemplateObj.summary_fields.user_capabilities && scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) || scope.canAddWorkflowJobTemplate) { - scope.addNode({ - parent: d, - betweenTwoNodes: false + if(!scope.readOnly && !scope.treeState.isLinkMode) { + scope.addNodeWithoutChild({ + parent: d }); } }); } - function add_node_between() { + function add_node_with_child() { this.on("click", function(d) { - if((scope.workflowJobTemplateObj && scope.workflowJobTemplateObj.summary_fields && scope.workflowJobTemplateObj.summary_fields.user_capabilities && scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) || scope.canAddWorkflowJobTemplate) { - scope.addNode({ - parent: d, - betweenTwoNodes: true + if(!scope.readOnly && !scope.treeState.isLinkMode) { + scope.addNodeWithChild({ + link: d }); } }); @@ -1090,7 +1105,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge function remove_node() { this.on("click", function(d) { - if((scope.workflowJobTemplateObj && scope.workflowJobTemplateObj.summary_fields && scope.workflowJobTemplateObj.summary_fields.user_capabilities && scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) || scope.canAddWorkflowJobTemplate) { + if(!d.isStartNode && !scope.readOnly && !scope.treeState.isLinkMode) { scope.deleteNode({ nodeToDelete: d }); @@ -1098,30 +1113,41 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge }); } - function edit_node() { + function node_click() { this.on("click", function(d) { - if(d.canEdit){ - scope.editNode({ - nodeToEdit: d - }); + if(!d.isStartNode && !scope.readOnly){ + if(scope.treeState.isLinkMode && !d.isInvalidLinkTarget) { + $('.WorkflowChart-potentialLink').remove(); + scope.selectNodeForLinking({ + nodeToStartLink: d + }); + } else if(!scope.treeState.isLinkMode) { + scope.editNode({ + nodeToEdit: d + }); + } + } }); } function edit_link() { this.on("click", function(d) { - if(!d.source.isStartNode && !d.source.placeholder && !d.target.placeholder && scope.mode !== 'details'){ + if(!scope.treeState.isLinkMode && !d.source.isStartNode && !d.source.isNodeBeingAdded && !d.target.isNodeBeingAdded && scope.mode !== 'details'){ scope.editLink({ - parentId: d.source.id, - childId: d.target.id + linkToEdit: d }); } }); } - function link_node() { + function add_link() { this.on("click", function(d) { - alert('this does not work, don\'t click it'); + if (!scope.readOnly && !scope.treeState.isLinkMode) { + scope.selectNodeForLinking({ + nodeToStartLink: d + }); + } }); } @@ -1170,15 +1196,8 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge }); } - scope.$watch('canAddWorkflowJobTemplate', function() { - // Redraw the graph if permissions change - if(scope.treeData) { - update(); - } - }); - scope.$on('refreshWorkflowChart', function(){ - if(scope.treeData) { + if(scope.treeState) { update(); } }); @@ -1199,10 +1218,12 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge zoomToFitChart(); }); - let clearWatchTreeData = scope.$watch('treeData', function(newVal) { + let clearWatchTreeState = scope.$watch('treeState.arrayOfNodesForChart', function(newVal) { if(newVal) { + // scope.treeState.arrayOfNodesForChart + update(); - clearWatchTreeData(); + clearWatchTreeState(); } }); diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-link-form.controller.js b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-link-form.controller.js index 7c95cd97dd..bbf8f095c3 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-link-form.controller.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-link-form.controller.js @@ -4,8 +4,8 @@ * All Rights Reserved *************************************************/ -export default ['$scope', 'TemplatesStrings', 'CreateSelect2', '$timeout', - function($scope, TemplatesStrings, CreateSelect2, $timeout) { +export default ['$scope', 'TemplatesStrings', 'CreateSelect2', + function($scope, TemplatesStrings, CreateSelect2) { $scope.strings = TemplatesStrings; $scope.edgeTypeOptions = [ diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-link-form.directive.js b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-link-form.directive.js index 8591b9a728..ee0a447a92 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-link-form.directive.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-link-form.directive.js @@ -13,7 +13,8 @@ export default ['templateUrl', linkConfig: '<', readOnly: '<', cancel: '&', - select: '&' + select: '&', + unlink: '&' }, restrict: 'E', templateUrl: templateUrl('templates/workflows/workflow-maker/forms/workflow-link-form'), diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-link-form.partial.html b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-link-form.partial.html index a09dcd0618..2bf0315447 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-link-form.partial.html +++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-link-form.partial.html @@ -1,6 +1,8 @@ -
{{readOnly ? strings.get('workflow_maker.VIEW_LINK', {parentName: linkConfig.parent.name, childName: linkConfig.child.name}) : strings.get('workflow_maker.EDIT_LINK', {parentName: linkConfig.parent.name, childName: linkConfig.child.name}) }}
-
-
+
{{readOnly ? strings.get('workflow_maker.VIEW_LINK') : (linkConfig.mode === 'add' ? strings.get('workflow_maker.ADD_LINK') : strings.get('workflow_maker.EDIT_LINK')) }} | {{linkConfig.parent.name}} {{linkConfig.child ? 'to ' + linkConfig.child.name : ''}}
+
+
+
{{:: strings.get('workflow_maker.NEW_LINK')}}
+
-
-
- - - -
+ +
+
+ + + +
diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.controller.js b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.controller.js index 2310c067bf..08b1592f3d 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.controller.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.controller.js @@ -5,11 +5,11 @@ *************************************************/ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService', 'Rest', '$q', - 'WorkflowService', 'TemplatesStrings', 'CreateSelect2', 'Empty', 'generateList', 'QuerySet', - 'GetBasePath', 'TemplateList', 'ProjectList', 'InventorySourcesList', + 'TemplatesStrings', 'CreateSelect2', 'Empty', 'generateList', 'QuerySet', + 'GetBasePath', 'TemplateList', 'ProjectList', 'InventorySourcesList', 'ProcessErrors', function($scope, TemplatesService, JobTemplate, PromptService, Rest, $q, - WorkflowService, TemplatesStrings, CreateSelect2, Empty, generateList, qs, - GetBasePath, TemplateList, ProjectList, InventorySourcesList + TemplatesStrings, CreateSelect2, Empty, generateList, qs, + GetBasePath, TemplateList, ProjectList, InventorySourcesList, ProcessErrors ) { let promptWatcher, credentialsWatcher, surveyQuestionWatcher, listPromises = []; @@ -55,48 +55,6 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService projectList.disableRowValue = 'readOnly'; $scope.projectList = projectList; - $scope.$watch('node', (newNode, oldNode) => { - if (oldNode.id !== newNode.id) { - setupNodeForm(); - } - }); - - $scope.$watchGroup(['templates', 'projects', 'inventory_sources', 'activeTab'], () => { - // TODO: make this more concise - switch($scope.activeTab) { - case 'jobs': - $scope.templates.forEach(function(row, i) { - if(_.hasIn($scope, 'node.unifiedJobTemplate.id') && row.id === $scope.node.unifiedJobTemplate.id) { - $scope.templates[i].checked = 1; - } - else { - $scope.templates[i].checked = 0; - } - }); - break; - case 'project_syncs': - $scope.projects.forEach(function(row, i) { - if(_.hasIn($scope, 'node.unifiedJobTemplate.id') && row.id === $scope.node.unifiedJobTemplate.id) { - $scope.projects[i].checked = 1; - } - else { - $scope.projects[i].checked = 0; - } - }); - break; - case 'inventory_syncs': - $scope.inventory_sources.forEach(function(row, i) { - if(_.hasIn($scope, 'node.unifiedJobTemplate.id') && row.id === $scope.node.unifiedJobTemplate.id) { - $scope.inventory_sources[i].checked = 1; - } - else { - $scope.inventory_sources[i].checked = 0; - } - }); - break; - } - }); - const checkCredentialsForRequiredPasswords = () => { let credentialRequiresPassword = false; $scope.promptData.prompts.credentials.value.forEach((credential) => { @@ -135,14 +93,44 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService } }; + const finishConfiguringAdd = () => { + $scope.activeTab = "jobs"; + const alwaysOption = { + label: $scope.strings.get('workflow_maker.ALWAYS'), + value: 'always' + }; + const successOption = { + label: $scope.strings.get('workflow_maker.ON_SUCCESS'), + value: 'success' + }; + const failureOption = { + label: $scope.strings.get('workflow_maker.ON_FAILURE'), + value: 'failure' + }; + $scope.edgeTypeOptions = [alwaysOption]; + switch($scope.nodeConfig.newNodeIsRoot) { + case true: + $scope.edgeType = alwaysOption; + break; + case false: + $scope.edgeType = successOption; + $scope.edgeTypeOptions.push(successOption, failureOption); + break; + } + CreateSelect2({ + element: '#workflow_node_edge_3', + multiple: false + }); + + $scope.nodeFormDataLoaded = true; + }; + const finishConfiguringEdit = () => { let jobTemplate = new JobTemplate(); - console.log($scope.node); - - if (!_.isEmpty($scope.node.promptData)) { - $scope.promptData = _.cloneDeep($scope.node.promptData); + if (_.get($scope, 'nodeConfig.node.promptData') && !_.isEmpty($scope.nodeConfig.node.promptData)) { + $scope.promptData = _.cloneDeep($scope.nodeConfig.node.promptData); const launchConf = $scope.promptData.launchConf; if (!launchConf.survey_enabled && @@ -162,7 +150,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService } else { $scope.showPromptButton = true; - if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'node.originalNodeObj.summary_fields.inventory')) { + if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'nodeConfig.node.originalNodeObject.summary_fields.inventory')) { $scope.promptModalMissingReqFields = true; } else { $scope.promptModalMissingReqFields = false; @@ -170,13 +158,13 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService } $scope.nodeFormDataLoaded = true; } else if ( - _.get($scope, 'node.unifiedJobTemplate.unified_job_type') === 'job_template' || - _.get($scope, 'node.unifiedJobTemplate.type') === 'job_template' + _.get($scope, 'nodeConfig.node.fullUnifiedJobTemplateObject.unified_job_type') === 'job_template' || + _.get($scope, 'nodeConfig.node.fullUnifiedJobTemplateObject.type') === 'job_template' ) { - let promises = [jobTemplate.optionsLaunch($scope.node.unifiedJobTemplate.id), jobTemplate.getLaunch($scope.node.unifiedJobTemplate.id)]; + let promises = [jobTemplate.optionsLaunch($scope.nodeConfig.node.fullUnifiedJobTemplateObject.id), jobTemplate.getLaunch($scope.nodeConfig.node.fullUnifiedJobTemplateObject.id)]; - if (_.has($scope, 'node.originalNodeObj.related.credentials')) { - Rest.setUrl($scope.node.originalNodeObj.related.credentials); + if (_.has($scope, 'nodeConfig.node.originalNodeObject.related.credentials')) { + Rest.setUrl($scope.nodeConfig.node.originalNodeObject.related.credentials); promises.push(Rest.get()); } @@ -189,7 +177,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService let prompts = PromptService.processPromptValues({ launchConf: responses[1].data, launchOptions: responses[0].data, - currentValues: $scope.node.originalNodeObj + currentValues: $scope.nodeConfig.node.originalNodeObject }); let defaultCredsWithoutOverrides = []; @@ -222,7 +210,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService prompts.credentials.value = workflowNodeCredentials.concat(defaultCredsWithoutOverrides); - if ((!$scope.node.unifiedJobTemplate.inventory && !launchConf.ask_inventory_on_launch) || !$scope.node.unifiedJobTemplate.project) { + if ((!$scope.nodeConfig.node.fullUnifiedJobTemplateObject.inventory && !launchConf.ask_inventory_on_launch) || !$scope.nodeConfig.node.fullUnifiedJobTemplateObject.project) { $scope.selectedTemplateInvalid = true; } else { $scope.selectedTemplateInvalid = false; @@ -264,7 +252,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService } else { $scope.showPromptButton = true; - if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'node.originalNodeObj.summary_fields.inventory')) { + if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'nodeConfig.node.originalNodeObject.summary_fields.inventory')) { $scope.promptModalMissingReqFields = true; } else { $scope.promptModalMissingReqFields = false; @@ -272,24 +260,24 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService if (responses[1].data.survey_enabled) { // go out and get the survey questions - jobTemplate.getSurveyQuestions($scope.node.unifiedJobTemplate.id) + jobTemplate.getSurveyQuestions($scope.nodeConfig.node.fullUnifiedJobTemplateObject.id) .then((surveyQuestionRes) => { let processed = PromptService.processSurveyQuestions({ surveyQuestions: surveyQuestionRes.data.spec, - extra_data: _.cloneDeep($scope.node.originalNodeObj.extra_data) + extra_data: _.cloneDeep($scope.nodeConfig.node.originalNodeObject.extra_data) }); $scope.missingSurveyValue = processed.missingSurveyValue; $scope.extraVars = (processed.extra_data === '' || _.isEmpty(processed.extra_data)) ? '---' : '---\n' + jsyaml.safeDump(processed.extra_data); - $scope.node.promptData = $scope.promptData = { + $scope.nodeConfig.node.promptData = $scope.promptData = { launchConf: launchConf, launchOptions: launchOptions, prompts: prompts, surveyQuestions: surveyQuestionRes.data.spec, - template: $scope.node.unifiedJobTemplate.id + template: $scope.nodeConfig.node.fullUnifiedJobTemplateObject.id }; surveyQuestionWatcher = $scope.$watch('promptData.surveyQuestions', () => { @@ -309,11 +297,11 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService $scope.nodeFormDataLoaded = true; }); } else { - $scope.node.promptData = $scope.promptData = { + $scope.nodeConfig.node.promptData = $scope.promptData = { launchConf: launchConf, launchOptions: launchOptions, prompts: prompts, - template: $scope.node.unifiedJobTemplate.id + template: $scope.nodeConfig.node.fullUnifiedJobTemplateObject.id }; checkCredentialsForRequiredPasswords(); @@ -328,12 +316,12 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService $scope.nodeFormDataLoaded = true; } - if (_.get($scope, 'node.unifiedJobTemplate')) { - if (_.get($scope, 'node.unifiedJobTemplate.type') === "job_template") { + if (_.get($scope, 'nodeConfig.node.fullUnifiedJobTemplateObject')) { + if (_.get($scope, 'nodeConfig.node.fullUnifiedJobTemplateObject.type') === "job_template") { $scope.activeTab = "jobs"; } - $scope.selectedTemplate = $scope.node.unifiedJobTemplate; + $scope.selectedTemplate = $scope.nodeConfig.node.fullUnifiedJobTemplateObject; if ($scope.selectedTemplate.unified_job_type) { switch ($scope.selectedTemplate.unified_job_type) { @@ -364,79 +352,6 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService $scope.activeTab = "jobs"; } - if ($scope.mode === 'add') { - const alwaysOption = { - label: $scope.strings.get('workflow_maker.ALWAYS'), - value: 'always' - }; - const successOption = { - label: $scope.strings.get('workflow_maker.ON_SUCCESS'), - value: 'success' - }; - const failureOption = { - label: $scope.strings.get('workflow_maker.ON_FAILURE'), - value: 'failure' - }; - $scope.edgeTypeOptions = [alwaysOption]; - switch($scope.node.isRoot) { - case true: - $scope.edgeType = alwaysOption; - break; - case false: - $scope.edgeType = successOption; - $scope.edgeTypeOptions.push(successOption, failureOption); - break; - } - CreateSelect2({ - element: '#workflow_node_edge_3', - multiple: false - }); - - $scope.nodeFormDataLoaded = true; - } - }; - // Determine whether or not we need to go out and GET this nodes unified job template - // in order to determine whether or not prompt fields are needed - - $scope.openPromptModal = function() { - $scope.promptData.triggerModalOpen = true; - }; - - $scope.toggle_row = function(selectedRow) { - if (!$scope.readOnly) { - // TODO: make this more concise - switch($scope.activeTab) { - case 'jobs': - $scope.templates.forEach(function(row, i) { - if (row.id === selectedRow.id) { - $scope.templates[i].checked = 1; - - } else { - $scope.templates[i].checked = 0; - } - }); - break; - case 'project_syncs': - $scope.projects.forEach(function(row, i) { - if (row.id === selectedRow.id) { - $scope.projects[i].checked = 1; - } else { - $scope.projects[i].checked = 0; - } - }); - break; - case 'inventory_syncs': - $scope.inventory_sources.forEach(function(row, i) { - if (row.id === selectedRow.id) { - $scope.inventory_sources[i].checked = 1; - } else { - $scope.inventory_sources[i].checked = 0; - } - }); - break; - } - templateManuallySelected(selectedRow); - } }; const templateManuallySelected = (selectedTemplate) => { @@ -597,7 +512,7 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService page_size: '5', order_by: 'name', not__source: '' - } + }; $scope.inventory_sources = []; $scope.inventory_source_dataset = {}; @@ -612,26 +527,113 @@ export default ['$scope', 'TemplatesService', 'JobTemplateModel', 'PromptService $q.all(listPromises) .then(() => { - if (!$scope.node.isNew && !$scope.node.edited && $scope.node.unifiedJobTemplate && $scope.node.unifiedJobTemplate.unified_job_type && $scope.node.unifiedJobTemplate.unified_job_type === 'job') { - // This is a node that we got back from the api with an incomplete - // unified job template so we're going to pull down the whole object - - TemplatesService.getUnifiedJobTemplate($scope.node.unifiedJobTemplate.id) - .then(function(data) { - $scope.node.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 + if ($scope.nodeConfig.mode === "edit") { + // Make sure that we have the full unified job template object + if (!$scope.nodeConfig.node.fullUnifiedJobTemplate && _.get($scope, 'nodeConfig.node.originalNodeObject.summary_fields.unified_job_template.unified_job_type') === 'job') { + // This is a node that we got back from the api with an incomplete + // unified job template so we're going to pull down the whole object + TemplatesService.getUnifiedJobTemplate($scope.nodeConfig.node.originalNodeObject.summary_fields.unified_job_template.id) + .then(function({data}) { + $scope.nodeConfig.node.fullUnifiedJobTemplateObject = 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 + }); }); - }); + } else { + finishConfiguringEdit(); + } } else { - finishConfiguringEdit(); + finishConfiguringAdd(); } }); - } + }; + + $scope.openPromptModal = function() { + $scope.promptData.triggerModalOpen = true; + }; + + $scope.toggle_row = function(selectedRow) { + if (!$scope.readOnly) { + // TODO: make this more concise + switch($scope.activeTab) { + case 'jobs': + $scope.templates.forEach(function(row, i) { + if (row.id === selectedRow.id) { + $scope.templates[i].checked = 1; + + } else { + $scope.templates[i].checked = 0; + } + }); + break; + case 'project_syncs': + $scope.projects.forEach(function(row, i) { + if (row.id === selectedRow.id) { + $scope.projects[i].checked = 1; + } else { + $scope.projects[i].checked = 0; + } + }); + break; + case 'inventory_syncs': + $scope.inventory_sources.forEach(function(row, i) { + if (row.id === selectedRow.id) { + $scope.inventory_sources[i].checked = 1; + } else { + $scope.inventory_sources[i].checked = 0; + } + }); + break; + } + templateManuallySelected(selectedRow); + } + }; + + $scope.$watch('nodeConfig.nodeId', (newNodeId, oldNodeId) => { + if (newNodeId !== oldNodeId) { + setupNodeForm(); + } + }); + + $scope.$watchGroup(['templates', 'projects', 'inventory_sources', 'activeTab'], () => { + // TODO: make this more concise + switch($scope.activeTab) { + case 'jobs': + $scope.templates.forEach(function(row, i) { + if(_.hasIn($scope, 'nodeConfig.node.fullUnifiedJobTemplateObject.id') && row.id === $scope.nodeConfig.node.fullUnifiedJobTemplateObject.id) { + $scope.templates[i].checked = 1; + } + else { + $scope.templates[i].checked = 0; + } + }); + break; + case 'project_syncs': + $scope.projects.forEach(function(row, i) { + if(_.hasIn($scope, 'nodeConfig.node.fullUnifiedJobTemplateObject.id') && row.id === $scope.nodeConfig.node.fullUnifiedJobTemplateObject.id) { + $scope.projects[i].checked = 1; + } + else { + $scope.projects[i].checked = 0; + } + }); + break; + case 'inventory_syncs': + $scope.inventory_sources.forEach(function(row, i) { + if(_.hasIn($scope, 'nodeConfig.node.fullUnifiedJobTemplateObject.id') && row.id === $scope.nodeConfig.node.fullUnifiedJobTemplateObject.id) { + $scope.inventory_sources[i].checked = 1; + } + else { + $scope.inventory_sources[i].checked = 0; + } + }); + break; + } + }); setupNodeForm(); } diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.directive.js b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.directive.js index 2a273a4e0f..119e88908d 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.directive.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.directive.js @@ -10,8 +10,7 @@ export default ['templateUrl', function(templateUrl) { return { scope: { - mode: '<', - node: '=', + nodeConfig: '<', cancel: '&', select: '&', readOnly: '<' diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html index c6c239f405..4677547f47 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html +++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html @@ -1,5 +1,5 @@
-
{{mode === 'edit' ? node.unifiedJobTemplate.name : strings.get('workflow_maker.ADD_A_TEMPLATE')}}
+
{{nodeConfig.mode === 'edit' ? node.unifiedJobTemplate.name : strings.get('workflow_maker.ADD_A_TEMPLATE')}}
{{strings.get('workflow_maker.JOBS')}}
{{strings.get('workflow_maker.PROJECT_SYNC')}}
@@ -118,7 +118,7 @@ {{:: strings.get('workflows.CREDENTIAL_WITH_PASS') }}
-
+
{{strings.get('workflow_maker.TOTAL_TEMPLATES')}} - +
- + +
- + - +
- +
diff --git a/awx/ui/client/src/templates/workflows/workflow.service.js b/awx/ui/client/src/templates/workflows/workflow.service.js deleted file mode 100644 index 6b5ef1e45a..0000000000 --- a/awx/ui/client/src/templates/workflows/workflow.service.js +++ /dev/null @@ -1,294 +0,0 @@ -export default ['$q', function($q){ - return { - searchTree: function(params) { - // params.element - // params.matchingId - // params.byNodeId - - let prospectiveId = params.byNodeId ? params.element.nodeId : params.element.id; - - if(prospectiveId === params.matchingId){ - return params.element; - }else if (params.element.children && params.element.children.length > 0){ - let result = null; - const thisService = this; - _.forEach(params.element.children, function(child) { - result = thisService.searchTree({ - element: child, - matchingId: params.matchingId, - byNodeId: params.byNodeId ? params.byNodeId : false - }); - if(result) { - return false; - } - }); - return result; - } - return null; - }, - removeNodeFromTree: function(params) { - // params.tree - // params.nodeToBeDeleted - - let parentNode = this.searchTree({ - element: params.tree, - matchingId: params.nodeToBeDeleted.parent.id - }); - let nodeToBeDeleted = this.searchTree({ - element: parentNode, - matchingId: params.nodeToBeDeleted.id - }); - - if(nodeToBeDeleted.children) { - _.forEach(nodeToBeDeleted.children, function(child) { - if(nodeToBeDeleted.isRoot) { - child.isRoot = true; - child.edgeType = "always"; - } - child.parent = parentNode; - parentNode.children.push(child); - }); - } - - _.forEach(parentNode.children, function(child, index) { - if(child.id === params.nodeToBeDeleted.id) { - parentNode.children.splice(index, 1); - return false; - } - }); - }, - addPlaceholderNode: function(params) { - // params.parent - // params.betweenTwoNodes - // params.tree - // params.id - - let placeholder = { - children: [], - c: "#D7D7D7", - id: params.id, - canDelete: true, - canEdit: false, - canAddTo: true, - placeholder: true, - isNew: true, - edited: false, - isRoot: (params.betweenTwoNodes) ? _.get(params, 'parent.source.isStartNode', false) : _.get(params, 'parent.isStartNode', false) - }; - - let parentNode = (params.betweenTwoNodes) ? this.searchTree({element: params.tree, matchingId: params.parent.source.id}) : this.searchTree({element: params.tree, matchingId: params.parent.id}); - let placeholderRef; - - if (params.betweenTwoNodes) { - _.forEach(parentNode.children, function(child, index) { - if (child.id === params.parent.target.id) { - child.isRoot = false; - placeholder.children.push(child); - parentNode.children[index] = placeholder; - placeholderRef = parentNode.children[index]; - child.parent = parentNode.children[index]; - return false; - } - }); - } else { - if (parentNode.children) { - parentNode.children.push(placeholder); - placeholderRef = parentNode.children[parentNode.children.length - 1]; - } else { - parentNode.children = [placeholder]; - placeholderRef = parentNode.children[0]; - } - } - - return placeholderRef; - }, - getSiblingConnectionTypes: function(params) { - // params.parentId - // params.childId - // params.tree - - let siblingConnectionTypes = {}; - - let parentNode = this.searchTree({ - element: params.tree, - matchingId: params.parentId - }); - - if(parentNode.children && parentNode.children.length > 0) { - // Loop across them and add the types as keys to siblingConnectionTypes - _.forEach(parentNode.children, function(child) { - if(child.id !== params.childId && !child.placeholder && child.edgeType) { - siblingConnectionTypes[child.edgeType] = true; - } - }); - } - - return Object.keys(siblingConnectionTypes); - }, - buildTree: function(params) { - //params.workflowNodes - - let deferred = $q.defer(); - - let _this = this; - - let treeData = { - data: { - id: 1, - canDelete: false, - canEdit: false, - canAddTo: true, - isStartNode: true, - unifiedJobTemplate: { - name: "Workflow Launch" - }, - children: [], - deletedNodes: [], - totalNodes: 0 - }, - nextIndex: 2 - }; - - let nodesArray = params.workflowNodes; - let nodesObj = {}; - let nonRootNodeIds = []; - let allNodeIds = []; - - // Determine which nodes are root nodes - _.forEach(nodesArray, function(node) { - nodesObj[node.id] = _.clone(node); - - allNodeIds.push(node.id); - - _.forEach(node.success_nodes, function(nodeId){ - nonRootNodeIds.push(nodeId); - }); - _.forEach(node.failure_nodes, function(nodeId){ - nonRootNodeIds.push(nodeId); - }); - _.forEach(node.always_nodes, function(nodeId){ - nonRootNodeIds.push(nodeId); - }); - }); - - let rootNodes = _.difference(allNodeIds, nonRootNodeIds); - - // Loop across the root nodes and re-build the tree - _.forEach(rootNodes, function(rootNodeId) { - let branch = _this.buildBranch({ - nodeId: rootNodeId, - edgeType: "always", - nodesObj: nodesObj, - isRoot: true, - treeData: treeData - }); - - treeData.data.children.push(branch); - }); - - deferred.resolve(treeData); - - return deferred.promise; - }, - buildBranch: function(params) { - // params.nodeId - // params.parentId - // params.edgeType - // params.nodesObj - // params.isRoot - // params.treeData - - let _this = this; - - let treeNode = { - children: [], - c: "#D7D7D7", - id: params.treeData.nextIndex, - nodeId: params.nodeId, - canDelete: true, - canEdit: true, - canAddTo: true, - placeholder: false, - edgeType: params.edgeType, - isNew: false, - edited: false, - originalEdge: params.edgeType, - originalNodeObj: _.clone(params.nodesObj[params.nodeId]), - promptValues: {}, - isRoot: params.isRoot ? params.isRoot : false - }; - - params.treeData.data.totalNodes++; - - params.treeData.nextIndex++; - - if(params.parentId) { - treeNode.originalParentId = params.parentId; - } - - if(params.nodesObj[params.nodeId].summary_fields) { - if(params.nodesObj[params.nodeId].summary_fields.job) { - treeNode.job = _.clone(params.nodesObj[params.nodeId].summary_fields.job); - } - - if(params.nodesObj[params.nodeId].summary_fields.unified_job_template) { - treeNode.unifiedJobTemplate = _.clone(params.nodesObj[params.nodeId].summary_fields.unified_job_template); - } - } - - // Loop across the success nodes and add them recursively - _.forEach(params.nodesObj[params.nodeId].success_nodes, function(successNodeId) { - treeNode.children.push(_this.buildBranch({ - nodeId: successNodeId, - parentId: params.nodeId, - edgeType: "success", - nodesObj: params.nodesObj, - treeData: params.treeData - })); - }); - - // failure nodes - _.forEach(params.nodesObj[params.nodeId].failure_nodes, function(failureNodesId) { - treeNode.children.push(_this.buildBranch({ - nodeId: failureNodesId, - parentId: params.nodeId, - edgeType: "failure", - nodesObj: params.nodesObj, - treeData: params.treeData - })); - }); - - // always nodes - _.forEach(params.nodesObj[params.nodeId].always_nodes, function(alwaysNodesId) { - treeNode.children.push(_this.buildBranch({ - nodeId: alwaysNodesId, - parentId: params.nodeId, - edgeType: "always", - nodesObj: params.nodesObj, - treeData: params.treeData - })); - }); - - return treeNode; - }, - updateStatusOfNode: function(params) { - // params.treeData - // params.nodeId - // params.status - - let matchingNode = this.searchTree({ - element: params.treeData.data, - matchingId: params.nodeId, - byNodeId: true - }); - - if(matchingNode) { - matchingNode.job = { - status: params.status, - id: params.unified_job_id - }; - } - - }, - }; -}]; diff --git a/awx/ui/client/src/workflow-results/workflow-results.controller.js b/awx/ui/client/src/workflow-results/workflow-results.controller.js index f40e8867d1..3b297cc073 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.controller.js +++ b/awx/ui/client/src/workflow-results/workflow-results.controller.js @@ -1,10 +1,13 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions', 'jobLabels', 'workflowNodes', '$scope', 'ParseTypeChange', - 'ParseVariableString', 'WorkflowService', 'count', '$state', 'i18n', - 'moment', '$filter', function(workflowData, workflowResultsService, + 'ParseVariableString', 'count', '$state', 'i18n', + 'moment', function(workflowData, workflowResultsService, workflowDataOptions, jobLabels, workflowNodes, $scope, ParseTypeChange, - ParseVariableString, WorkflowService, count, $state, i18n, moment, $filter) { + ParseVariableString, count, $state, i18n, moment) { var runTimeElapsedTimer = null; + let workflowMakerNodeIdCounter = 1; + let nodeIdToMakerIdMapping = {}; + let chartNodeIdToIndexMapping = {}; var getLinks = function() { var getLink = function(key) { @@ -113,11 +116,8 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions', $scope.workflow_nodes = workflowNodes; $scope.workflowOptions = workflowDataOptions.actions.GET; $scope.labels = jobLabels; - $scope.count = count.val; $scope.showManualControls = false; - $scope.showKey = false; - $scope.toggleKey = () => $scope.showKey = !$scope.showKey; - $scope.keyClassList = `{ 'Key-menuIcon--active': showKey }`; + $scope.readOnly = true; // Start elapsed time updater for job known to be running if ($scope.workflow.started !== null && $scope.workflow.status === 'running') { @@ -167,25 +167,96 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions', $scope.varsTooltip= i18n._('Read only view of extra variables added to the workflow.'); $scope.varsLabel = i18n._('Extra Variables'); - // Click binding for the expand/collapse button on the standard out log $scope.stdoutFullScreen = false; - WorkflowService.buildTree({ - workflowNodes: workflowNodes - }).then(function(data){ - $scope.treeData = data; - - // TODO: I think that the workflow chart directive (and eventually d3) is meddling with - // this treeData object and removing the children object for some reason (?) - // This happens on occasion and I think is a race condition (?) - if(!$scope.treeData.data.children) { - $scope.treeData.data.children = []; + let nonRootNodeIds = []; + let allNodeIds = []; + let arrayOfLinksForChart = []; + let arrayOfNodesForChart = [ + { + index: 0, + id: workflowMakerNodeIdCounter, + isStartNode: true, + unifiedJobTemplate: { + name: "START" + }, + fixed: true, + x: 0, + y: 0 } + ]; - $scope.canAddWorkflowJobTemplate = false; + workflowMakerNodeIdCounter++; + // Assign each node an ID - 0 is reserved for the start node. We need to + // make sure that we have an ID on every node including new nodes so the + // ID returned by the api won't do + workflowNodes.forEach((node) => { + node.workflowMakerNodeId = workflowMakerNodeIdCounter; + const nodeObj = { + index: workflowMakerNodeIdCounter-1, + id: workflowMakerNodeIdCounter, + unifiedJobTemplate: node.summary_fields.unified_job_template + }; + if(node.summary_fields.job) { + nodeObj.job = node.summary_fields.job; + } + if(node.summary_fields.unified_job_template) { + nodeObj.unifiedJobTemplate = node.summary_fields.unified_job_template; + } + arrayOfNodesForChart.push(nodeObj); + allNodeIds.push(node.id); + nodeIdToMakerIdMapping[node.id] = node.workflowMakerNodeId; + chartNodeIdToIndexMapping[workflowMakerNodeIdCounter] = workflowMakerNodeIdCounter-1; + workflowMakerNodeIdCounter++; }); + workflowNodes.forEach((node) => { + const sourceIndex = chartNodeIdToIndexMapping[node.workflowMakerNodeId]; + node.success_nodes.forEach((nodeId) => { + const targetIndex = chartNodeIdToIndexMapping[nodeIdToMakerIdMapping[nodeId]]; + arrayOfLinksForChart.push({ + source: arrayOfNodesForChart[sourceIndex], + target: arrayOfNodesForChart[targetIndex], + edgeType: "success" + }); + nonRootNodeIds.push(nodeId); + }); + node.failure_nodes.forEach((nodeId) => { + const targetIndex = chartNodeIdToIndexMapping[nodeIdToMakerIdMapping[nodeId]]; + arrayOfLinksForChart.push({ + source: arrayOfNodesForChart[sourceIndex], + target: arrayOfNodesForChart[targetIndex], + edgeType: "failure" + }); + nonRootNodeIds.push(nodeId); + }); + node.always_nodes.forEach((nodeId) => { + const targetIndex = chartNodeIdToIndexMapping[nodeIdToMakerIdMapping[nodeId]]; + arrayOfLinksForChart.push({ + source: arrayOfNodesForChart[sourceIndex], + target: arrayOfNodesForChart[targetIndex], + edgeType: "always" + }); + nonRootNodeIds.push(nodeId); + }); + }); + + let uniqueNonRootNodeIds = Array.from(new Set(nonRootNodeIds)); + + let rootNodes = _.difference(allNodeIds, uniqueNonRootNodeIds); + + rootNodes.forEach((rootNodeId) => { + const targetIndex = chartNodeIdToIndexMapping[nodeIdToMakerIdMapping[rootNodeId]]; + arrayOfLinksForChart.push({ + source: arrayOfNodesForChart[0], + target: arrayOfNodesForChart[targetIndex], + edgeType: "always" + }); + }); + + $scope.treeState = { arrayOfNodesForChart, arrayOfLinksForChart }; + } $scope.toggleStdoutFullscreen = function() { @@ -285,12 +356,11 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions', runTimeElapsedTimer = workflowResultsService.createOneSecondTimer(moment(), updateWorkflowJobElapsedTimer); } - WorkflowService.updateStatusOfNode({ - treeData: $scope.treeData, - nodeId: data.workflow_node_id, - status: data.status, - unified_job_id: data.unified_job_id - }); + $scope.treeState.arrayOfNodesForChart[chartNodeIdToIndexMapping[nodeIdToMakerIdMapping[data.workflow_node_id]]].job = { + id: data.unified_job_id, + status: data.status + }; + $scope.workflow_nodes.forEach(node => { if(parseInt(node.id) === parseInt(data.workflow_node_id)){ @@ -300,8 +370,6 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions', } }); - $scope.count = workflowResultsService - .getCounts($scope.workflow_nodes); $scope.$broadcast("refreshWorkflowChart"); } getLabelsAndTooltips(); diff --git a/awx/ui/client/src/workflow-results/workflow-results.partial.html b/awx/ui/client/src/workflow-results/workflow-results.partial.html index 48dacb6e3f..39a9116fdf 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.partial.html +++ b/awx/ui/client/src/workflow-results/workflow-results.partial.html @@ -363,7 +363,14 @@ - + +