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}) }}
-
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.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 @@
-
+
+