diff --git a/awx/ui/client/features/templates/templates.strings.js b/awx/ui/client/features/templates/templates.strings.js
index 0efcff23c4..785d9902cc 100644
--- a/awx/ui/client/features/templates/templates.strings.js
+++ b/awx/ui/client/features/templates/templates.strings.js
@@ -122,6 +122,7 @@ 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 })
}
}
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 52242d6387..f5e9bd7378 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
@@ -1,4 +1,9 @@
-.link circle, .link .linkCross, .node .addCircle, .node .removeCircle, .node .WorkflowChart-hoverPath {
+.link circle,
+.link polygon,
+.link .linkCross,
+.node circle,
+.node .linkIcon,
+.node .WorkflowChart-hoverPath {
opacity: 0;
}
@@ -18,6 +23,18 @@
fill: @default-err-hov;
}
+.node .linkCircle {
+ fill: @default-link;
+}
+
+.node .linkIcon {
+ color: @default-bg;
+}
+
+.linkCircle.removeHovering {
+ fill: @default-link-hov;
+}
+
.node {
font-size: 12px;
font-family: 'Open Sans', sans-serif, 'FontAwesome';
@@ -50,8 +67,12 @@
.WorkflowChart-alwaysShowAdd .linkCross,
.hovering .addCircle,
.hovering .removeCircle,
+.addHovering .betweenNodesCircle,
+.hovering .linkCircle,
+.hovering .linkIcon,
.hovering .WorkflowChart-hoverPath,
-.hovering .linkCross {
+.addHovering .linkCross {
+ cursor: pointer;
opacity: 1;
}
@@ -136,3 +157,17 @@
.WorkflowChart-dashedNode {
stroke-dasharray: 5,5;
}
+
+.linkOverlay {
+ fill: @default-interface-txt;
+}
+
+.linkActiveEdit.linkOverlay,
+.overlayHovering .linkOverlay {
+ cursor: pointer;
+ opacity: 0.4;
+}
+
+.overlayHovering .linkPath {
+ cursor: pointer;
+}
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 fefeecf385..bbf6610e3b 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
@@ -4,1187 +4,1188 @@
* All Rights Reserved
*************************************************/
-export default ['$state', 'moment', '$timeout', '$window', '$filter', 'Rest', 'GetBasePath', 'ProcessErrors', 'TemplatesStrings',
- function ($state, moment, $timeout, $window, $filter, Rest, GetBasePath, ProcessErrors, TemplatesStrings) {
+export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'GetBasePath', 'ProcessErrors', 'TemplatesStrings',
+ function($state, moment, $timeout, $window, $filter, Rest, GetBasePath, ProcessErrors, TemplatesStrings) {
- return {
- scope: {
- treeData: '=',
- canAddWorkflowJobTemplate: '=',
- workflowJobTemplateObj: '=',
- addNode: '&',
- editNode: '&',
- deleteNode: '&',
- workflowZoomed: '&',
- mode: '@'
- },
- restrict: 'E',
- link: function (scope, element) {
+ return {
+ scope: {
+ treeData: '=',
+ canAddWorkflowJobTemplate: '=',
+ workflowJobTemplateObj: '=',
+ addNode: '&',
+ editNode: '&',
+ deleteNode: '&',
+ editLink: '&',
+ workflowZoomed: '&',
+ mode: '@'
+ },
+ restrict: 'E',
+ 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;
+ 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;
- scope.dimensionsSet = false;
+ scope.dimensionsSet = false;
- $timeout(function () {
- let dimensions = calcAvailableScreenSpace();
+ $timeout(function(){
+ let dimensions = calcAvailableScreenSpace();
- windowHeight = dimensions.height;
- windowWidth = dimensions.width;
+ windowHeight = dimensions.height;
+ windowWidth = dimensions.width;
- $('.WorkflowMaker-chart').css("height", windowHeight);
+ $('.WorkflowMaker-chart').css("height", windowHeight);
- scope.dimensionsSet = true;
+ scope.dimensionsSet = true;
- init();
- });
+ init();
+ });
- function init() {
- tree = d3.layout.tree()
- .nodeSize([nodeH + verticalSpaceBetweenNodes, nodeW])
- .separation(function (a, b) {
+ 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;
+ line = d3.svg.line()
+ .x(function(d){return d.x;})
+ .y(function(d){return d.y;});
+
+ zoomObj = d3.behavior.zoom().scaleExtent([0.5, 2]);
+
+ baseSvg = d3.select(element[0]).append("svg")
+ .attr("class", "WorkflowChart-svg")
+ .call(zoomObj
+ .on("zoom", naturalZoom)
+ );
+
+ svgGroup = baseSvg.append("g")
+ .attr("id", "aw-workflow-chart-g")
+ .attr("transform", "translate(" + marginLeft + "," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")");
+ }
+
+ function calcAvailableScreenSpace() {
+ let dimensions = {};
+
+ if(scope.mode !== 'details') {
+ // This is the workflow editor
+ dimensions.height = $('.WorkflowMaker-contentLeft').outerHeight() - $('.WorkflowLegend-maker').outerHeight();
+ dimensions.width = $('#workflow-modal-dialog').width() - $('.WorkflowMaker-contentRight').outerWidth();
+ }
+ else {
+ // This is the workflow details view
+ let panel = $('.WorkflowResults-rightSide').children('.Panel')[0];
+ let panelWidth = $(panel).width();
+ let panelHeight = $(panel).height();
+ let headerHeight = $('.StandardOut-panelHeader').outerHeight();
+ let legendHeight = $('.WorkflowLegend-details').outerHeight();
+ let proposedHeight = panelHeight - headerHeight - legendHeight - 40;
+
+ dimensions.height = proposedHeight > 200 ? proposedHeight : 200;
+ dimensions.width = panelWidth;
+ }
+
+ 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) {
+ if(text && text.length > maxNodeTextLength) {
+ return text.substring(0,maxNodeTextLength) + '...';
+ }
+ else {
+ return text;
+ }
+ }
+
+ function rounded_rect(x, y, w, h, r, tl, tr, bl, br) {
+ var retval;
+ retval = "M" + (x + r) + "," + y;
+ retval += "h" + (w - 2*r);
+ if (tr) { retval += "a" + r + "," + r + " 0 0 1 " + r + "," + r; }
+ else { retval += "h" + r; retval += "v" + r; }
+ retval += "v" + (h - 2*r);
+ if (br) { retval += "a" + r + "," + r + " 0 0 1 " + -r + "," + r; }
+ else { retval += "v" + r; retval += "h" + -r; }
+ retval += "h" + (2*r - w);
+ if (bl) { retval += "a" + r + "," + r + " 0 0 1 " + -r + "," + -r; }
+ else { retval += "h" + -r; retval += "v" + -r; }
+ retval += "v" + (2*r - h);
+ if (tl) { retval += "a" + r + "," + r + " 0 0 1 " + r + "," + -r; }
+ else { retval += "v" + -r; retval += "h" + r; }
+ retval += "z";
+ return retval;
+ }
+
+ // This is the zoom function called by using the mousewheel/click and drag
+ function naturalZoom() {
+ let scale = d3.event.scale,
+ translation = d3.event.translate;
+
+ translation = [translation[0] + (marginLeft*scale), translation[1] + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)];
+
+ svgGroup.attr("transform", "translate(" + translation + ")scale(" + scale + ")");
+
+ scope.workflowZoomed({
+ zoom: scale
+ });
+ }
+
+ // This is the zoom that gets called when the user interacts with the manual zoom controls
+ function manualZoom(zoom) {
+ let scale = zoom / 100,
+ translation = zoomObj.translate(),
+ origZoom = zoomObj.scale(),
+ unscaledOffsetX = (translation[0] + ((windowWidth*origZoom) - windowWidth)/2)/origZoom,
+ unscaledOffsetY = (translation[1] + ((windowHeight*origZoom) - windowHeight)/2)/origZoom,
+ translateX = unscaledOffsetX*scale - ((scale*windowWidth)-windowWidth)/2,
+ translateY = unscaledOffsetY*scale - ((scale*windowHeight)-windowHeight)/2;
+
+ svgGroup.attr("transform", "translate(" + [translateX + (marginLeft*scale), translateY + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)] + ")scale(" + scale + ")");
+ zoomObj.scale(scale);
+ zoomObj.translate([translateX, translateY]);
+ }
+
+ function manualPan(direction) {
+ let scale = zoomObj.scale(),
+ distance = 150 * scale,
+ translateX,
+ translateY,
+ translateCoords = zoomObj.translate();
+ if (direction === 'left' || direction === 'right') {
+ translateX = direction === 'left' ? translateCoords[0] - distance : translateCoords[0] + distance;
+ translateY = translateCoords[1];
+ } else if (direction === 'up' || direction === 'down') {
+ translateX = translateCoords[0];
+ translateY = direction === 'up' ? translateCoords[1] - distance : translateCoords[1] + distance;
+ }
+ svgGroup.attr("transform", "translate(" + translateX + "," + (translateY + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)) + ")scale(" + scale + ")");
+ zoomObj.translate([translateX, translateY]);
+ }
+
+ function resetZoomAndPan() {
+ svgGroup.attr("transform", "translate(" + marginLeft + "," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")scale(" + 1 + ")");
+ // Update the zoomObj
+ zoomObj.scale(1);
+ zoomObj.translate([0,0]);
+ }
+
+ function zoomToFitChart() {
+ let graphDimensions = d3.select('#aw-workflow-chart-g')[0][0].getBoundingClientRect(),
+ startNodeDimensions = d3.select('.WorkflowChart-rootNode')[0][0].getBoundingClientRect(),
+ availableScreenSpace = calcAvailableScreenSpace(),
+ currentZoomValue = zoomObj.scale(),
+ unscaledH = graphDimensions.height/currentZoomValue,
+ unscaledW = graphDimensions.width/currentZoomValue,
+ scaleNeededForMaxHeight = (availableScreenSpace.height)/unscaledH,
+ scaleNeededForMaxWidth = (availableScreenSpace.width - marginLeft)/unscaledW,
+ lowerScale = Math.min(scaleNeededForMaxHeight, scaleNeededForMaxWidth),
+ scaleToFit = lowerScale < 0.5 ? 0.5 : (lowerScale > 2 ? 2 : Math.floor(lowerScale * 10)/10),
+ startNodeOffsetFromGraphCenter = Math.round((((rootH/2) + (startNodeDimensions.top/currentZoomValue)) - ((graphDimensions.top/currentZoomValue) + (unscaledH/2)))*scaleToFit);
+
+ manualZoom(scaleToFit*100);
+
+ scope.workflowZoomed({
+ zoom: scaleToFit
+ });
+
+ 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 node = svgGroup.selectAll("g.node")
+ .data(nodes, function(d) {
+ d.y = d.depth * 240;
+ return d.id || (d.id = ++i);
});
- zoomObj = d3.behavior.zoom().scaleExtent([0.5, 2]);
+ let nodeEnter = node.enter().append("g")
+ .attr("class", "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 = d3.select(element[0]).append("svg")
- .attr("class", "WorkflowChart-svg")
- .call(zoomObj
- .on("zoom", naturalZoom)
- );
-
- svgGroup = baseSvg.append("g")
- .attr("id", "aw-workflow-chart-g")
- .attr("transform", "translate(" + marginLeft + "," + (windowHeight / 2 - rootH / 2 - startNodeOffsetY) + ")");
- }
-
- function calcAvailableScreenSpace() {
- let dimensions = {};
-
- if (scope.mode !== 'details') {
- // This is the workflow editor
- dimensions.height = $('.WorkflowMaker-contentLeft').outerHeight() - $('.WorkflowLegend-maker').outerHeight();
- dimensions.width = $('#workflow-modal-dialog').width() - $('.WorkflowMaker-contentRight').outerWidth();
- } else {
- // This is the workflow details view
- let panel = $('.WorkflowResults-rightSide').children('.Panel')[0];
- let panelWidth = $(panel).width();
- let panelHeight = $(panel).height();
- let headerHeight = $('.StandardOut-panelHeader').outerHeight();
- let legendHeight = $('.WorkflowLegend-details').outerHeight();
- let proposedHeight = panelHeight - headerHeight - legendHeight - 40;
-
- dimensions.height = proposedHeight > 200 ? proposedHeight : 200;
- dimensions.width = panelWidth;
- }
-
- 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
+ nodeEnter.each(function(d) {
+ let thisNode = d3.select(this);
+ if(d.isStartNode && scope.mode === 'details') {
+ // Overwrite the default root height and width and replace it with a small blue square
+ rootW = 25;
+ rootH = 25;
+ thisNode.append("rect")
+ .attr("width", rootW)
+ .attr("height", rootH)
+ .attr("y", startNodeOffsetY)
+ .attr("rx", 5)
+ .attr("ry", 5)
+ .attr("fill", "#337ab7")
+ .attr("class", "WorkflowChart-rootNode");
}
- ];
-
- 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) {
- if (text && text.length > maxNodeTextLength) {
- return text.substring(0, maxNodeTextLength) + '...';
- } else {
- return text;
- }
- }
-
- function rounded_rect(x, y, w, h, r, tl, tr, bl, br) {
- var retval;
- retval = "M" + (x + r) + "," + y;
- retval += "h" + (w - 2 * r);
- if (tr) {
- retval += "a" + r + "," + r + " 0 0 1 " + r + "," + r;
- } else {
- retval += "h" + r;
- retval += "v" + r;
- }
- retval += "v" + (h - 2 * r);
- if (br) {
- retval += "a" + r + "," + r + " 0 0 1 " + -r + "," + r;
- } else {
- retval += "v" + r;
- retval += "h" + -r;
- }
- retval += "h" + (2 * r - w);
- if (bl) {
- retval += "a" + r + "," + r + " 0 0 1 " + -r + "," + -r;
- } else {
- retval += "h" + -r;
- retval += "v" + -r;
- }
- retval += "v" + (2 * r - h);
- if (tl) {
- retval += "a" + r + "," + r + " 0 0 1 " + r + "," + -r;
- } else {
- retval += "v" + -r;
- retval += "h" + r;
- }
- retval += "z";
- return retval;
- }
-
- // This is the zoom function called by using the mousewheel/click and drag
- function naturalZoom() {
- let scale = d3.event.scale,
- translation = d3.event.translate;
-
- translation = [translation[0] + (marginLeft * scale), translation[1] + ((windowHeight / 2 - rootH / 2 - startNodeOffsetY) * scale)];
-
- svgGroup.attr("transform", "translate(" + translation + ")scale(" + scale + ")");
-
- scope.workflowZoomed({
- zoom: scale
- });
- }
-
- // This is the zoom that gets called when the user interacts with the manual zoom controls
- function manualZoom(zoom) {
- let scale = zoom / 100,
- translation = zoomObj.translate(),
- origZoom = zoomObj.scale(),
- unscaledOffsetX = (translation[0] + ((windowWidth * origZoom) - windowWidth) / 2) / origZoom,
- unscaledOffsetY = (translation[1] + ((windowHeight * origZoom) - windowHeight) / 2) / origZoom,
- translateX = unscaledOffsetX * scale - ((scale * windowWidth) - windowWidth) / 2,
- translateY = unscaledOffsetY * scale - ((scale * windowHeight) - windowHeight) / 2;
-
- svgGroup.attr("transform", "translate(" + [translateX + (marginLeft * scale), translateY + ((windowHeight / 2 - rootH / 2 - startNodeOffsetY) * scale)] + ")scale(" + scale + ")");
- zoomObj.scale(scale);
- zoomObj.translate([translateX, translateY]);
- }
-
- function manualPan(direction) {
- let scale = zoomObj.scale(),
- distance = 150 * scale,
- translateX,
- translateY,
- translateCoords = zoomObj.translate();
- if (direction === 'left' || direction === 'right') {
- translateX = direction === 'left' ? translateCoords[0] - distance : translateCoords[0] + distance;
- translateY = translateCoords[1];
- } else if (direction === 'up' || direction === 'down') {
- translateX = translateCoords[0];
- translateY = direction === 'up' ? translateCoords[1] - distance : translateCoords[1] + distance;
- }
- svgGroup.attr("transform", "translate(" + translateX + "," + (translateY + ((windowHeight / 2 - rootH / 2 - startNodeOffsetY) * scale)) + ")scale(" + scale + ")");
- zoomObj.translate([translateX, translateY]);
- }
-
- function resetZoomAndPan() {
- svgGroup.attr("transform", "translate(" + marginLeft + "," + (windowHeight / 2 - rootH / 2 - startNodeOffsetY) + ")scale(" + 1 + ")");
- // Update the zoomObj
- zoomObj.scale(1);
- zoomObj.translate([0, 0]);
- }
-
- function zoomToFitChart() {
- let graphDimensions = d3.select('#aw-workflow-chart-g')[0][0].getBoundingClientRect(),
- startNodeDimensions = d3.select('.WorkflowChart-rootNode')[0][0].getBoundingClientRect(),
- availableScreenSpace = calcAvailableScreenSpace(),
- currentZoomValue = zoomObj.scale(),
- unscaledH = graphDimensions.height / currentZoomValue,
- unscaledW = graphDimensions.width / currentZoomValue,
- scaleNeededForMaxHeight = (availableScreenSpace.height) / unscaledH,
- scaleNeededForMaxWidth = (availableScreenSpace.width - marginLeft) / unscaledW,
- lowerScale = Math.min(scaleNeededForMaxHeight, scaleNeededForMaxWidth),
- scaleToFit = lowerScale < 0.5 ? 0.5 : (lowerScale > 2 ? 2 : Math.floor(lowerScale * 10) / 10),
- startNodeOffsetFromGraphCenter = Math.round((((rootH / 2) + (startNodeDimensions.top / currentZoomValue)) - ((graphDimensions.top / currentZoomValue) + (unscaledH / 2))) * scaleToFit);
-
- manualZoom(scaleToFit * 100);
-
- scope.workflowZoomed({
- zoom: scaleToFit
- });
-
- 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 node = svgGroup.selectAll("g.node")
- .data(nodes, function (d) {
- d.y = d.depth * 240;
- return d.id || (d.id = ++i);
- });
-
- let nodeEnter = node.enter().append("g")
- .attr("class", "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 + ")";
- });
-
- nodeEnter.each(function (d) {
- let thisNode = d3.select(this);
- if (d.isStartNode && scope.mode === 'details') {
- // Overwrite the default root height and width and replace it with a small blue square
- rootW = 25;
- rootH = 25;
- thisNode.append("rect")
- .attr("width", rootW)
- .attr("height", rootH)
- .attr("y", startNodeOffsetY)
- .attr("rx", 5)
- .attr("ry", 5)
- .attr("fill", "#337ab7")
- .attr("class", "WorkflowChart-rootNode");
- } else if (d.isStartNode && scope.mode !== 'details') {
- thisNode.append("rect")
- .attr("width", rootW)
- .attr("height", rootH)
- .attr("y", 10)
- .attr("rx", 5)
- .attr("ry", 5)
- .attr("fill", "#5cb85c")
- .attr("class", "WorkflowChart-rootNode")
- .call(add_node);
- 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);
- } else {
- thisNode.append("rect")
- .attr("width", nodeW)
- .attr("height", nodeH)
- .attr("rx", 5)
- .attr("ry", 5)
- .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 {
+ else if(d.isStartNode && scope.mode !== 'details') {
+ thisNode.append("rect")
+ .attr("width", rootW)
+ .attr("height", rootH)
+ .attr("y", 10)
+ .attr("rx", 5)
+ .attr("ry", 5)
+ .attr("fill", "#5cb85c")
+ .attr("class", "WorkflowChart-rootNode")
+ .call(add_node);
+ 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);
+ }
+ else {
+ thisNode.append("rect")
+ .attr("width", nodeW)
+ .attr("height", nodeH)
+ .attr("rx", 5)
+ .attr("ry", 5)
+ .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";
}
- })
- .attr('stroke-width', "2px")
- .attr("class", function (d) {
- let classString = d.placeholder ? "rect placeholder" : "rect";
- classString += !d.unifiedJobTemplate ? " 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";
- });
-
- thisNode.append("text")
- .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("dy", ".35em")
- .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);
-
- thisNode.append("foreignObject")
- .attr("x", 54)
- .attr("y", 45)
- .style("font-size", "0.7em")
- .attr("class", "WorkflowChart-conflictText")
- .html(function () {
- return `\uf06a ${TemplatesStrings.get('workflow_maker.EDGE_CONFLICT')}`;
- })
- .style("display", function (d) {
- return (d.edgeConflict && !d.placeholder) ? null : "none";
- });
-
- thisNode.append("foreignObject")
- .attr("x", 62)
- .attr("y", 22)
- .attr("dy", ".35em")
- .attr("text-anchor", "middle")
- .attr("class", "WorkflowChart-defaultText WorkflowChart-deletedText")
- .html(function () {
- return `${TemplatesStrings.get('workflow_maker.DELETED')}`;
- })
- .style("display", function (d) {
- return d.unifiedJobTemplate || d.placeholder ? "none" : null;
- });
-
- thisNode.append("circle")
- .attr("cy", nodeH)
- .attr("r", 10)
- .attr("class", "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";
- });
-
- thisNode.append("text")
- .attr("y", nodeH)
- .attr("dy", ".35em")
- .attr("text-anchor", "middle")
- .attr("class", "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";
- });
-
- thisNode.append("rect")
- .attr("width", nodeW)
- .attr("height", nodeH)
- .attr("class", "transparentRect")
- .call(edit_node)
- .on("mouseover", function (d) {
- if (!d.isStartNode) {
- let resourceName = (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : "";
- if (resourceName && resourceName.length > maxNodeTextLength) {
- // When the graph is initially rendered all the links come after the nodes (when you look at the dom).
- // SVG components are painted in order of appearance. There is no concept of z-index, only the order.
- // As such, we need to move the nodes after the links so that when the tooltip renders it shows up on top
- // of the links and not underneath them. I tried rendering the links before the nodes but that lead to
- // some weird link animation that I didn't care to try to fix.
- svgGroup.selectAll("g.node").each(function () {
- this.parentNode.appendChild(this);
- });
- // After the nodes have been properly placed after the links, we need to make sure that the node that
- // 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.node").sort(function (a) {
- return (a.id !== d.id) ? -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
- let tooltipDimensionChecker = $("
" + $filter('sanitize')(resourceName) + "
");
- $('body').append(tooltipDimensionChecker);
- let tipWidth = $(tooltipDimensionChecker).outerWidth();
- let tipHeight = $(tooltipDimensionChecker).outerHeight();
- $(tooltipDimensionChecker).remove();
-
- thisNode.append("foreignObject")
- .attr("x", (nodeW / 2) - (tipWidth / 2))
- .attr("y", (tipHeight + 15) * -1)
- .attr("width", tipWidth)
- .attr("height", tipHeight + 20)
- .attr("class", "WorkflowChart-tooltip")
- .html(function () {
- return "" + $filter('sanitize')(resourceName) + "
";
- });
- }
- d3.select("#node-" + d.id)
- .classed("hovering", true);
- }
- })
- .on("mouseout", function (d) {
- $('.WorkflowChart-tooltip').remove();
- if (!d.isStartNode) {
- d3.select("#node-" + d.id)
- .classed("hovering", false);
- }
- });
- thisNode.append("text")
- .attr("x", nodeW - 45)
- .attr("y", nodeH - 10)
- .attr("dy", ".35em")
- .attr("class", "WorkflowChart-detailsLink")
- .style("display", function (d) {
- return d.job && d.job.status && d.job.id ? null : "none";
- })
- .text(function () {
- return TemplatesStrings.get('workflow_maker.DETAILS');
- })
- .call(details);
- thisNode.append("circle")
- .attr("id", function (d) {
- return "node-" + d.id + "-add";
- })
- .attr("cx", nodeW)
- .attr("r", 10)
- .attr("class", "addCircle nodeCircle")
- .style("display", function (d) {
- return d.placeholder || !(userCanAddEdit) ? "none" : null;
- })
- .call(add_node)
- .on("mouseover", function (d) {
- d3.select("#node-" + d.id)
- .classed("hovering", true);
- d3.select("#node-" + d.id + "-add")
- .classed("addHovering", true);
- })
- .on("mouseout", function (d) {
- d3.select("#node-" + d.id)
- .classed("hovering", false);
- d3.select("#node-" + d.id + "-add")
- .classed("addHovering", false);
- });
- thisNode.append("path")
- .attr("class", "nodeAddCross WorkflowChart-hoverPath")
- .style("fill", "white")
- .attr("transform", function () {
- return "translate(" + nodeW + "," + 0 + ")";
- })
- .attr("d", d3.svg.symbol()
- .size(60)
- .type("cross")
- )
- .style("display", function (d) {
- return d.placeholder || !(userCanAddEdit) ? "none" : null;
- })
- .call(add_node)
- .on("mouseover", function (d) {
- d3.select("#node-" + d.id)
- .classed("hovering", true);
- d3.select("#node-" + d.id + "-add")
- .classed("addHovering", true);
- })
- .on("mouseout", function (d) {
- d3.select("#node-" + d.id)
- .classed("hovering", false);
- d3.select("#node-" + d.id + "-add")
- .classed("addHovering", false);
- });
- thisNode.append("circle")
- .attr("id", function (d) {
- return "node-" + d.id + "-remove";
- })
- .attr("cx", nodeW)
- .attr("cy", nodeH)
- .attr("r", 10)
- .attr("class", "removeCircle")
- .style("display", function (d) {
- return (d.canDelete === false || d.placeholder || !(userCanAddEdit)) ? "none" : null;
- })
- .call(remove_node)
- .on("mouseover", function (d) {
- d3.select("#node-" + d.id)
- .classed("hovering", true);
- d3.select("#node-" + d.id + "-remove")
- .classed("removeHovering", true);
- })
- .on("mouseout", function (d) {
- d3.select("#node-" + d.id)
- .classed("hovering", false);
- d3.select("#node-" + d.id + "-remove")
- .classed("removeHovering", false);
- });
- thisNode.append("path")
- .attr("class", "nodeRemoveCross WorkflowChart-hoverPath")
- .style("fill", "white")
- .attr("transform", function () {
- return "translate(" + nodeW + "," + nodeH + ") rotate(-45)";
- })
- .attr("d", d3.svg.symbol()
- .size(60)
- .type("cross")
- )
- .style("display", function (d) {
- return (d.canDelete === false || d.placeholder || !(userCanAddEdit)) ? "none" : null;
- })
- .call(remove_node)
- .on("mouseover", function (d) {
- d3.select("#node-" + d.id)
- .classed("hovering", true);
- d3.select("#node-" + d.id + "-remove")
- .classed("removeHovering", true);
- })
- .on("mouseout", function (d) {
- d3.select("#node-" + d.id)
- .classed("hovering", false);
- d3.select("#node-" + d.id + "-remove")
- .classed("removeHovering", false);
- });
-
- thisNode.append("circle")
- .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";
- })
- .attr("cy", 10)
- .attr("cx", 10)
- .attr("r", 6);
-
- thisNode.append("foreignObject")
- .attr("x", 5)
- .attr("y", 43)
- .style("font-size", "0.7em")
- .attr("class", "WorkflowChart-elapsed")
- .html(function (d) {
- if (d.job && d.job.elapsed) {
- let elapsedMs = d.job.elapsed * 1000;
- let elapsedMoment = moment.duration(elapsedMs);
- let paddedElapsedMoment = Math.floor(elapsedMoment.asHours()) < 10 ? "0" + Math.floor(elapsedMoment.asHours()) : Math.floor(elapsedMoment.asHours());
- let elapsedString = paddedElapsedMoment + moment.utc(elapsedMs).format(":mm:ss");
- return "" + elapsedString + "
";
- } else {
- return "";
- }
- })
- .style("display", function (d) {
- return (d.job && d.job.elapsed) ? null : "none";
- });
- }
- });
-
- node.exit().remove();
-
- if (nodes && nodes.length > 1 && !graphLoaded) {
- zoomToFitChart();
- }
-
- graphLoaded = true;
-
- let link = svgGroup.selectAll("g.link")
- .data(links, function (d) {
- return d.source.id + "-" + d.target.id;
- });
-
- let linkEnter = link.enter().append("g")
- .attr("class", "link")
- .attr("id", function (d) {
- return "link-" + d.source.id + "-" + d.target.id;
- });
-
- // Add entering links in the parent’s old position.
- linkEnter.insert("path", "g")
- .attr("class", function (d) {
- return (d.source.placeholder || d.target.placeholder) ? "linkPath placeholder" : "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";
- }
- });
-
- 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", "addCircle linkCircle")
- .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("hovering", true);
- d3.select("#link-" + d.source.id + "-" + d.target.id + "-add")
- .classed("addHovering", true);
- })
- .on("mouseout", function (d) {
- d3.select("#link-" + d.source.id + "-" + d.target.id)
- .classed("hovering", false);
- d3.select("#link-" + d.source.id + "-" + d.target.id + "-add")
- .classed("addHovering", false);
- });
-
- linkEnter.append("path")
- .attr("class", "linkCross")
- .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("hovering", true);
- d3.select("#link-" + d.source.id + "-" + d.target.id + "-add")
- .classed("addHovering", true);
- })
- .on("mouseout", function (d) {
- d3.select("#link-" + d.source.id + "-" + d.target.id)
- .classed("hovering", false);
- d3.select("#link-" + d.source.id + "-" + d.target.id + "-add")
- .classed("addHovering", false);
- });
-
- link.exit().remove();
-
- // Transition nodes and links to their new positions.
- let t = baseSvg.transition();
-
- t.selectAll(".nodeCircle")
- .style("display", function (d) {
- return d.placeholder || !(userCanAddEdit) ? "none" : null;
- });
-
- t.selectAll(".nodeAddCross")
- .style("display", function (d) {
- return d.placeholder || !(userCanAddEdit) ? "none" : null;
- });
-
- t.selectAll(".removeCircle")
- .style("display", function (d) {
- return (d.canDelete === false || d.placeholder || !(userCanAddEdit)) ? "none" : null;
- });
-
- t.selectAll(".nodeRemoveCross")
- .style("display", function (d) {
- return (d.canDelete === false || d.placeholder || !(userCanAddEdit)) ? "none" : null;
- });
-
- t.selectAll(".linkPath")
- .attr("class", function (d) {
- return (d.source.placeholder || d.target.placeholder) ? "linkPath placeholder" : "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(".linkCircle")
- .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(".linkCross")
- .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(".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 {
+ else {
return "#D7D7D7";
}
- } else {
- return "#D7D7D7";
- }
- })
- .attr("class", function (d) {
- let classString = d.placeholder ? "rect placeholder" : "rect";
- classString += !d.unifiedJobTemplate ? " WorkflowChart-dashedNode" : "";
- return classString;
- });
+ })
+ .attr('stroke-width', "2px")
+ .attr("class", function(d) {
+ let classString = d.placeholder ? "rect placeholder" : "rect";
+ classString += !d.unifiedJobTemplate ? " WorkflowChart-dashedNode" : "";
+ return classString;
+ });
- t.selectAll(".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 + ")";
- });
+ 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"; });
- t.selectAll(".WorkflowChart-nodeTypeCircle")
- .style("display", function (d) {
- return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" ||
+ thisNode.append("text")
+ .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("dy", ".35em")
+ .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);
+
+ thisNode.append("foreignObject")
+ .attr("x", 62)
+ .attr("y", 22)
+ .attr("dy", ".35em")
+ .attr("text-anchor", "middle")
+ .attr("class", "WorkflowChart-defaultText WorkflowChart-deletedText")
+ .html(function () {
+ return `${TemplatesStrings.get('workflow_maker.DELETED')}`;
+ })
+ .style("display", function(d) { return d.unifiedJobTemplate || d.placeholder ? "none" : null; });
+
+ thisNode.append("circle")
+ .attr("cy", nodeH)
+ .attr("r", 10)
+ .attr("class", "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";
});
+ thisNode.append("text")
+ .attr("y", nodeH)
+ .attr("dy", ".35em")
+ .attr("text-anchor", "middle")
+ .attr("class", "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-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;
+ thisNode.append("rect")
+ .attr("width", nodeW)
+ .attr("height", nodeH)
+ .attr("class", "transparentRect")
+ .call(edit_node)
+ .on("mouseover", function(d) {
+ if(!d.isStartNode) {
+ let resourceName = (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : "";
+ if(resourceName && resourceName.length > maxNodeTextLength) {
+ // When the graph is initially rendered all the links come after the nodes (when you look at the dom).
+ // SVG components are painted in order of appearance. There is no concept of z-index, only the order.
+ // As such, we need to move the nodes after the links so that when the tooltip renders it shows up on top
+ // of the links and not underneath them. I tried rendering the links before the nodes but that lead to
+ // some weird link animation that I didn't care to try to fix.
+ svgGroup.selectAll("g.node").each(function() {
+ this.parentNode.appendChild(this);
+ });
+ // After the nodes have been properly placed after the links, we need to make sure that the node that
+ // 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.node").sort(function (a) {
+ return (a.id !== d.id) ? -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
+ let tooltipDimensionChecker = $("" + $filter('sanitize')(resourceName) + "
");
+ $('body').append(tooltipDimensionChecker);
+ let tipWidth = $(tooltipDimensionChecker).outerWidth();
+ let tipHeight = $(tooltipDimensionChecker).outerHeight();
+ $(tooltipDimensionChecker).remove();
+
+ thisNode.append("foreignObject")
+ .attr("x", (nodeW / 2) - (tipWidth / 2))
+ .attr("y", (tipHeight + 15) * -1)
+ .attr("width", tipWidth)
+ .attr("height", tipHeight+20)
+ .attr("class", "WorkflowChart-tooltip")
+ .html(function(){
+ return "" + $filter('sanitize')(resourceName) + "
";
+ });
+ }
+ d3.select("#node-" + d.id)
+ .classed("hovering", true);
}
- } 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;
+ })
+ .on("mouseout", function(d){
+ $('.WorkflowChart-tooltip').remove();
+ if(!d.isStartNode) {
+ d3.select("#node-" + d.id)
+ .classed("hovering", false);
+ }
+ });
+ thisNode.append("text")
+ .attr("x", nodeW - 45)
+ .attr("y", nodeH - 10)
+ .attr("dy", ".35em")
+ .attr("class", "WorkflowChart-detailsLink")
+ .style("display", function(d){ return d.job && d.job.status && d.job.id ? null : "none"; })
+ .text(function () {
+ return TemplatesStrings.get('workflow_maker.DETAILS');
+ })
+ .call(details);
+ thisNode.append("circle")
+ .attr("id", function(d){return "node-" + d.id + "-add";})
+ .attr("cx", nodeW)
+ .attr("r", 10)
+ .attr("class", "addCircle nodeCircle")
+ .style("display", function(d) { return d.placeholder || !(userCanAddEdit) ? "none" : null; })
+ .call(add_node)
+ .on("mouseover", function(d) {
+ d3.select("#node-" + d.id)
+ .classed("hovering", true);
+ d3.select("#node-" + d.id + "-add")
+ .classed("addHovering", true);
+ })
+ .on("mouseout", function(d){
+ d3.select("#node-" + d.id)
+ .classed("hovering", false);
+ d3.select("#node-" + d.id + "-add")
+ .classed("addHovering", false);
+ });
+ thisNode.append("path")
+ .attr("class", "nodeAddCross WorkflowChart-hoverPath")
+ .style("fill", "white")
+ .attr("transform", function() { return "translate(" + nodeW + "," + 0 + ")"; })
+ .attr("d", d3.svg.symbol()
+ .size(60)
+ .type("cross")
+ )
+ .style("display", function(d) { return d.placeholder || !(userCanAddEdit) ? "none" : null; })
+ .call(add_node)
+ .on("mouseover", function(d) {
+ d3.select("#node-" + d.id)
+ .classed("hovering", true);
+ d3.select("#node-" + d.id + "-add")
+ .classed("addHovering", true);
+ })
+ .on("mouseout", function(d){
+ d3.select("#node-" + d.id)
+ .classed("hovering", false);
+ d3.select("#node-" + d.id + "-add")
+ .classed("addHovering", false);
+ });
+ thisNode.append("circle")
+ .attr("id", function(d){return "node-" + d.id + "-remove";})
+ .attr("cx", nodeW)
+ .attr("cy", nodeH)
+ .attr("r", 10)
+ .attr("class", "removeCircle")
+ .style("display", function(d) { return (d.canDelete === false || d.placeholder || !(userCanAddEdit)) ? "none" : null; })
+ .call(remove_node)
+ .on("mouseover", function(d) {
+ d3.select("#node-" + d.id)
+ .classed("hovering", true);
+ d3.select("#node-" + d.id + "-remove")
+ .classed("removeHovering", true);
+ })
+ .on("mouseout", function(d){
+ d3.select("#node-" + d.id)
+ .classed("hovering", false);
+ d3.select("#node-" + d.id + "-remove")
+ .classed("removeHovering", false);
+ });
+ thisNode.append("path")
+ .attr("class", "nodeRemoveCross WorkflowChart-hoverPath")
+ .style("fill", "white")
+ .attr("transform", function() { return "translate(" + nodeW + "," + nodeH + ") rotate(-45)"; })
+ .attr("d", d3.svg.symbol()
+ .size(60)
+ .type("cross")
+ )
+ .style("display", function(d) { return (d.canDelete === false || d.placeholder || !(userCanAddEdit)) ? "none" : null; })
+ .call(remove_node)
+ .on("mouseover", function(d) {
+ d3.select("#node-" + d.id)
+ .classed("hovering", true);
+ d3.select("#node-" + d.id + "-remove")
+ .classed("removeHovering", true);
+ })
+ .on("mouseout", function(d){
+ d3.select("#node-" + d.id)
+ .classed("hovering", false);
+ d3.select("#node-" + d.id + "-remove")
+ .classed("removeHovering", 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", "linkCircle nodeCircle")
+ // .style("display", function(d) { return d.placeholder || !(userCanAddEdit) ? "none" : null; })
+ // .call(link_node)
+ // .on("mouseover", function(d) {
+ // d3.select("#node-" + d.id)
+ // .classed("hovering", true);
+ // d3.select("#node-" + d.id + "-link")
+ // .classed("addHovering", true);
+ // })
+ // .on("mouseout", function(d){
+ // d3.select("#node-" + d.id)
+ // .classed("hovering", false);
+ // d3.select("#node-" + d.id + "-link")
+ // .classed("addHovering", 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", "linkIcon")
+ // .style("display", function(d) { return d.placeholder || !(userCanAddEdit) ? "none" : null; })
+ // .call(link_node)
+ // .on("mouseover", function(d) {
+ // d3.select("#node-" + d.id)
+ // .classed("hovering", true);
+ // d3.select("#node-" + d.id + "-link")
+ // .classed("addHovering", true);
+ // })
+ // .on("mouseout", function(d){
+ // d3.select("#node-" + d.id)
+ // .classed("hovering", false);
+ // d3.select("#node-" + d.id + "-link")
+ // .classed("addHovering", false);
+ // });
+
+ thisNode.append("circle")
+ .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"; })
+ .attr("cy", 10)
+ .attr("cx", 10)
+ .attr("r", 6);
+
+ thisNode.append("foreignObject")
+ .attr("x", 5)
+ .attr("y", 43)
+ .style("font-size","0.7em")
+ .attr("class", "WorkflowChart-elapsed")
+ .html(function (d) {
+ if(d.job && d.job.elapsed) {
+ let elapsedMs = d.job.elapsed * 1000;
+ let elapsedMoment = moment.duration(elapsedMs);
+ let paddedElapsedMoment = Math.floor(elapsedMoment.asHours()) < 10 ? "0" + Math.floor(elapsedMoment.asHours()) : Math.floor(elapsedMoment.asHours());
+ let elapsedString = paddedElapsedMoment + moment.utc(elapsedMs).format(":mm:ss");
+ return "" + elapsedString + "
";
+ }
+ else {
+ return "";
+ }
+ })
+ .style("display", function(d) { return (d.job && d.job.elapsed) ? null : "none"; });
+ }
+ });
+
+ node.exit().remove();
+
+ if(nodes && nodes.length > 1 && !graphLoaded) {
+ zoomToFitChart();
+ }
+
+ graphLoaded = true;
+
+ let link = svgGroup.selectAll("g.link")
+ .data(links, function(d) {
+ return d.source.id + "-" + d.target.id;
+ });
+
+ let linkEnter = link.enter().append("g")
+ .attr("class", "link")
+ .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id;});
+
+ linkEnter.append("polygon", "g")
+ .attr("class", function(d) {
+ let linkClasses = ["linkOverlay"];
+ if (d.source.isLinkEditParent && d.target.isLinkEditChild) {
+ linkClasses.push("linkActiveEdit");
+ }
+ return linkClasses.join(' ');
+ })
+ .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-overlay";})
+ .attr("points",function(d) {
+ const pt1 = [d.source.y + nodeW, d.source.x + 10 + nodeH/2].join(",");
+ const pt2 = [d.target.y,d.target.x + 10 + nodeH/2].join(",");
+ const pt3 = [d.target.y,d.target.x - 10 + nodeH/2].join(",");
+ const pt4 = [d.source.y + nodeW,d.source.x - 10 + nodeH/2].join(",");
+ return [pt1, pt2, pt3, pt4].join(" ");
+ })
+ .call(edit_link)
+ .on("mouseover", function(d) {
+ if(!d.source.isStartNode && !d.target.placeholder && scope.mode !== 'details') {
+ d3.select("#link-" + d.source.id + "-" + d.target.id)
+ .classed("overlayHovering", true);
+ }
+ })
+ .on("mouseout", function(d){
+ if(!d.source.isStartNode && !d.target.placeholder && scope.mode !== 'details') {
+ d3.select("#link-" + d.source.id + "-" + d.target.id)
+ .classed("overlayHovering", false);
+ }
+ });
+
+ // Add entering links in the parent’s old position.
+ linkEnter.append("path", "g")
+ .attr("class", function(d) {
+ return (d.source.placeholder || d.target.placeholder) ? "linkPath placeholder" : "linkPath";
+ })
+ .attr("d", lineData)
+ .call(edit_link)
+ .on("mouseover", function(d) {
+ if(!d.source.isStartNode && !d.target.placeholder && scope.mode !== 'details') {
+ d3.select("#link-" + d.source.id + "-" + d.target.id)
+ .classed("overlayHovering", true);
+ }
+ })
+ .on("mouseout", function(d){
+ if(!d.source.isStartNode && !d.target.placeholder && scope.mode !== 'details') {
+ d3.select("#link-" + d.source.id + "-" + d.target.id)
+ .classed("overlayHovering", false);
+ }
+ })
+ .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", "addCircle betweenNodesCircle")
+ .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("addHovering", true);
+ })
+ .on("mouseout", function(d){
+ d3.select("#link-" + d.source.id + "-" + d.target.id)
+ .classed("addHovering", false);
+ });
+
+ linkEnter.append("path")
+ .attr("class", "linkCross")
+ .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("addHovering", true);
+ })
+ .on("mouseout", function(d){
+ d3.select("#link-" + d.source.id + "-" + d.target.id)
+ .classed("addHovering", false);
+ });
+
+ link.exit().remove();
+
+ // Transition nodes and links to their new positions.
+ let t = baseSvg.transition();
+
+ t.selectAll(".nodeCircle")
+ .style("display", function(d) { return d.placeholder || !(userCanAddEdit) ? "none" : null; });
+
+ t.selectAll(".nodeAddCross")
+ .style("display", function(d) { return d.placeholder || !(userCanAddEdit) ? "none" : null; });
+
+ t.selectAll(".removeCircle")
+ .style("display", function(d) { return (d.canDelete === false || d.placeholder || !(userCanAddEdit)) ? "none" : null; });
+
+ t.selectAll(".nodeRemoveCross")
+ .style("display", function(d) { return (d.canDelete === false || d.placeholder || !(userCanAddEdit)) ? "none" : null; });
+
+ t.selectAll(".linkPath")
+ .attr("class", function(d) {
+ return (d.source.placeholder || d.target.placeholder) ? "linkPath placeholder" : "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";
}
}
- return nodeTypeLetter;
- })
- .style("display", function (d) {
- return d.unifiedJobTemplate &&
- (d.unifiedJobTemplate.type === "project" ||
+ else {
+ return "#D7D7D7";
+ }
+ });
+
+ t.selectAll(".betweenNodesCircle")
+ .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(".linkOverlay")
+ .attr("class", function(d) {
+ let linkClasses = ["linkOverlay"];
+ if (d.source.isLinkEditParent && d.target.isLinkEditChild) {
+ linkClasses.push("linkActiveEdit");
+ }
+ return linkClasses.join(' ');
+ })
+ .attr("points",function(d) {
+ const pt1 = [d.source.y + nodeW, d.source.x + 10 + nodeH/2].join(",");
+ const pt2 = [d.target.y,d.target.x + 10 + nodeH/2].join(",");
+ const pt3 = [d.target.y,d.target.x - 10 + nodeH/2].join(",");
+ const pt4 = [d.source.y + nodeW,d.source.x - 10 + nodeH/2].join(",");
+ return [pt1, pt2, pt3, pt4].join(" ");
+ });
+
+ t.selectAll(".linkCross")
+ .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(".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 ? "rect placeholder" : "rect";
+ classString += !d.unifiedJobTemplate ? " WorkflowChart-dashedNode" : "";
+ return classString;
+ });
+
+ t.selectAll(".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-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;
- }
+ 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;
}
-
- 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);
- })();
+ } 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-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-nodeStatus")
+ .attr("class", function(d) {
- t.selectAll(".WorkflowChart-detailsLink")
- .style("display", function (d) {
- return d.job && d.job.status && d.job.id ? null : "none";
- });
+ let statusClass = "WorkflowChart-nodeStatus ";
- t.selectAll(".WorkflowChart-deletedText")
- .style("display", function (d) {
- return d.unifiedJobTemplate || d.placeholder ? "none" : null;
- });
+ 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;
+ }
+ }
- t.selectAll(".WorkflowChart-conflictText")
- .style("display", function (d) {
- return (d.edgeConflict && !d.placeholder) ? null : "none";
- });
-
- t.selectAll(".WorkflowChart-activeNode")
- .style("display", function (d) {
- return d.isActiveEdit ? null : "none";
- });
-
- t.selectAll(".WorkflowChart-elapsed")
- .style("display", function (d) {
- return (d.job && d.job.elapsed) ? null : "none";
- });
- } else if (!scope.watchDimensionsSet) {
- scope.watchDimensionsSet = scope.$watch('dimensionsSet', function () {
- if (scope.dimensionsSet) {
- scope.watchDimensionsSet();
- scope.watchDimensionsSet = null;
- update();
+ 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);
+ })();
}
});
- }
- }
- function add_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) {
- scope.addNode({
- parent: d,
- betweenTwoNodes: false
- });
- }
- });
- }
-
- function add_node_between() {
- 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
- });
- }
- });
- }
-
- 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) {
- scope.deleteNode({
- nodeToDelete: d
- });
- }
- });
- }
-
- function edit_node() {
- this.on("click", function (d) {
- if (d.canEdit) {
- scope.editNode({
- nodeToEdit: d
- });
- }
- });
- }
-
- function details() {
- this.on("mouseover", function () {
- d3.select(this).style("text-decoration", "underline");
- });
- this.on("mouseout", function () {
- d3.select(this).style("text-decoration", null);
- });
- this.on("click", function (d) {
-
- let goToJobResults = function (job_type) {
- if (job_type === 'job') {
- $state.go('output', {
- id: d.job.id,
- type: 'playbook'
- });
- } else if (job_type === 'inventory_update') {
- $state.go('output', {
- id: d.job.id,
- type: 'inventory'
- });
- } else if (job_type === 'project_update') {
- $state.go('output', {
- id: d.job.id,
- type: 'project'
- });
- } else if (job_type === 'workflow_job') {
- $state.go('workflowResults', {
- id: d.job.id,
- });
- }
- };
-
- if (d.job.type) {
- goToJobResults(d.job.type);
- }
- else {
- // We don't have access to the job type and have to make
- // a GET request in order to find out what type job this was
- // so that we can route the user to the correct stdout view
- Rest.setUrl(GetBasePath("workflow_jobs") + `${d.originalNodeObj.workflow_job}/workflow_nodes/?order_by=id`);
- Rest.get()
- .then(function (res) {
- if (res.data.results && res.data.results.length > 0) {
- const { results } = res.data;
- const job = results.filter(result => result.summary_fields.job.id === d.job.id);
- goToJobResults(job[0].summary_fields.job.type);
- }
- })
- .catch(({
- data,
- status
- }) => {
- ProcessErrors(scope, data, status, null, {
- hdr: 'Error!',
- msg: 'Unable to get job: ' + status
- });
- });
- }
- });
- }
-
- scope.$watch('canAddWorkflowJobTemplate', function () {
- // Redraw the graph if permissions change
- if (scope.treeData) {
- update();
- }
- });
-
- scope.$on('refreshWorkflowChart', function () {
- if (scope.treeData) {
- update();
- }
- });
-
- scope.$on('panWorkflowChart', function (evt, params) {
- manualPan(params.direction);
- });
-
- scope.$on('resetWorkflowChart', function () {
- resetZoomAndPan();
- });
-
- scope.$on('zoomWorkflowChart', function (evt, params) {
- manualZoom(params.zoom);
- });
-
- scope.$on('zoomToFitChart', function () {
- zoomToFitChart();
- });
-
- let clearWatchTreeData = scope.$watch('treeData', function (newVal) {
- if (newVal) {
- update();
- clearWatchTreeData();
- }
- });
-
- function onResize() {
- let dimensions = calcAvailableScreenSpace();
-
- $('.WorkflowMaker-chart').css("height", dimensions.height);
- }
-
- function cleanUpResize() {
- angular.element($window).off('resize', onResize);
- }
-
- if (scope.mode === 'details') {
- angular.element($window).on('resize', onResize);
- scope.$on('$destroy', cleanUpResize);
-
- scope.$on('workflowDetailsResized', function () {
- $('.WorkflowMaker-chart').hide();
- $timeout(function () {
- onResize();
- $('.WorkflowMaker-chart').show();
+ 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) : "";
});
- });
- } else {
- scope.$on('workflowMakerModalResized', function () {
- let dimensions = calcAvailableScreenSpace();
- $('.WorkflowMaker-chart').css("height", dimensions.height);
+ 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"; });
+ }
+ else if(!scope.watchDimensionsSet){
+ scope.watchDimensionsSet = scope.$watch('dimensionsSet', function(){
+ if(scope.dimensionsSet) {
+ scope.watchDimensionsSet();
+ scope.watchDimensionsSet = null;
+ update();
+ }
});
}
}
- };
- }
-];
+
+ function add_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) {
+ scope.addNode({
+ parent: d,
+ betweenTwoNodes: false
+ });
+ }
+ });
+ }
+
+ function add_node_between() {
+ 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
+ });
+ }
+ });
+ }
+
+ 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) {
+ scope.deleteNode({
+ nodeToDelete: d
+ });
+ }
+ });
+ }
+
+ function edit_node() {
+ this.on("click", function(d) {
+ if(d.canEdit){
+ scope.editNode({
+ nodeToEdit: d
+ });
+ }
+ });
+ }
+
+ function edit_link() {
+ this.on("click", function(d) {
+ if(!d.source.isStartNode && !d.target.placeholder && scope.mode !== 'details'){
+ // What if the node is new? it won't have a nodeId right?
+ scope.editLink({
+ parentId: d.source.nodeId,
+ childId: d.target.nodeId
+ });
+ }
+ });
+ }
+
+ function link_node() {
+ this.on("click", function(d) {
+ alert('this does not work, don\'t click it');
+ });
+ }
+
+ function details() {
+ this.on("mouseover", function() {
+ d3.select(this).style("text-decoration", "underline");
+ });
+ this.on("mouseout", function() {
+ d3.select(this).style("text-decoration", null);
+ });
+ this.on("click", function(d) {
+
+ let goToJobResults = function(job_type) {
+ if(job_type === 'job') {
+ $state.go('output', {id: d.job.id, type: 'playbook'});
+ }
+ else if(job_type === 'inventory_update') {
+ $state.go('output', {id: d.job.id, type: 'inventory'});
+ }
+ else if(job_type === 'project_update') {
+ $state.go('output', {id: d.job.id, type: 'project'});
+ }
+ };
+
+ if(d.job.id) {
+ if(d.unifiedJobTemplate) {
+ goToJobResults(d.unifiedJobTemplate.unified_job_type);
+ }
+ else {
+ // We don't have access to the unified resource and have to make
+ // a GET request in order to find out what type job this was
+ // so that we can route the user to the correct stdout view
+
+ Rest.setUrl(GetBasePath("unified_jobs") + "?id=" + d.job.id);
+ Rest.get()
+ .then(function (res) {
+ if(res.data.results && res.data.results.length > 0) {
+ goToJobResults(res.data.results[0].type);
+ }
+ })
+ .catch(({data, status}) => {
+ ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Unable to get job: ' + status });
+ });
+ }
+ }
+ });
+ }
+
+ scope.$watch('canAddWorkflowJobTemplate', function() {
+ // Redraw the graph if permissions change
+ if(scope.treeData) {
+ update();
+ }
+ });
+
+ scope.$on('refreshWorkflowChart', function(){
+ if(scope.treeData) {
+ update();
+ }
+ });
+
+ scope.$on('panWorkflowChart', function(evt, params) {
+ manualPan(params.direction);
+ });
+
+ scope.$on('resetWorkflowChart', function(){
+ resetZoomAndPan();
+ });
+
+ scope.$on('zoomWorkflowChart', function(evt, params) {
+ manualZoom(params.zoom);
+ });
+
+ scope.$on('zoomToFitChart', function() {
+ zoomToFitChart();
+ });
+
+ let clearWatchTreeData = scope.$watch('treeData', function(newVal) {
+ if(newVal) {
+ update();
+ clearWatchTreeData();
+ }
+ });
+
+ function onResize(){
+ let dimensions = calcAvailableScreenSpace();
+
+ $('.WorkflowMaker-chart').css("width", dimensions.width);
+ $('.WorkflowMaker-chart').css("height", dimensions.height);
+ }
+
+ function cleanUpResize() {
+ angular.element($window).off('resize', onResize);
+ }
+
+ if(scope.mode === 'details') {
+ angular.element($window).on('resize', onResize);
+ scope.$on('$destroy', cleanUpResize);
+
+ scope.$on('workflowDetailsResized', function(){
+ $('.WorkflowMaker-chart').hide();
+ $timeout(function(){
+ onResize();
+ $('.WorkflowMaker-chart').show();
+ });
+ });
+ }
+ else {
+ scope.$on('workflowMakerModalResized', function(){
+ let dimensions = calcAvailableScreenSpace();
+
+ $('.WorkflowMaker-chart').css("width", dimensions.width);
+ $('.WorkflowMaker-chart').css("height", dimensions.height);
+ });
+ }
+ }
+ };
+}];
diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/forms/main.js b/awx/ui/client/src/templates/workflows/workflow-maker/forms/main.js
new file mode 100644
index 0000000000..426eaa131c
--- /dev/null
+++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/main.js
@@ -0,0 +1,7 @@
+import workflowLinkForm from './workflow-link-form.directive';
+import workflowNodeForm from './workflow-node-form.directive';
+
+export default
+ angular.module('templates.workflowMaker.forms', [])
+ .directive('workflowLinkForm', workflowLinkForm)
+ .directive('workflowNodeForm', workflowNodeForm);
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
new file mode 100644
index 0000000000..7c95cd97dd
--- /dev/null
+++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-link-form.controller.js
@@ -0,0 +1,38 @@
+/*************************************************
+ * Copyright (c) 2018 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+export default ['$scope', 'TemplatesStrings', 'CreateSelect2', '$timeout',
+ function($scope, TemplatesStrings, CreateSelect2, $timeout) {
+ $scope.strings = TemplatesStrings;
+
+ $scope.edgeTypeOptions = [
+ {
+ label: $scope.strings.get('workflow_maker.ALWAYS'),
+ value: 'always'
+ },
+ {
+ label: $scope.strings.get('workflow_maker.ON_SUCCESS'),
+ value: 'success'
+ },
+ {
+ label: $scope.strings.get('workflow_maker.ON_FAILURE'),
+ value: 'failure'
+ }
+ ];
+
+ $scope.$watch('linkConfig.edgeType', () => {
+ if (_.has($scope, 'linkConfig.edgeType')) {
+ $scope.edgeType = {
+ value: $scope.linkConfig.edgeType
+ };
+ CreateSelect2({
+ element: '#workflow_node_edge_2',
+ multiple: false
+ });
+ }
+ });
+ }
+];
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
new file mode 100644
index 0000000000..00b3afc765
--- /dev/null
+++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-link-form.directive.js
@@ -0,0 +1,22 @@
+/*************************************************
+ * Copyright (c) 2018 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+import workflowLinkFormController from './workflow-link-form.controller';
+
+export default ['templateUrl',
+ function(templateUrl) {
+ return {
+ scope: {
+ linkConfig: '<',
+ cancel: '&',
+ select: '&'
+ },
+ restrict: 'E',
+ templateUrl: templateUrl('templates/workflows/workflow-maker/forms/workflow-link-form'),
+ controller: workflowLinkFormController
+ };
+ }
+];
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
new file mode 100644
index 0000000000..cf03f1624c
--- /dev/null
+++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-link-form.partial.html
@@ -0,0 +1,25 @@
+{{:: 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
new file mode 100644
index 0000000000..41e8b11142
--- /dev/null
+++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.controller.js
@@ -0,0 +1,11 @@
+/*************************************************
+ * Copyright (c) 2018 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+export default ['$scope',
+ function($scope) {
+ console.log('inside wnf controller');
+ }
+];
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
new file mode 100644
index 0000000000..197b6ae86b
--- /dev/null
+++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.directive.js
@@ -0,0 +1,21 @@
+/*************************************************
+ * Copyright (c) 2018 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+import workflowNodeFormController from './workflow-node-form.controller';
+
+export default ['templateUrl',
+ function(templateUrl) {
+ return {
+ scope: {},
+ restrict: 'E',
+ templateUrl: templateUrl('templates/workflows/workflow-maker/forms/workflow-node-form'),
+ controller: workflowNodeFormController,
+ link: function(scope) {
+ console.log('inside link function for workflow node form');
+ }
+ };
+ }
+];
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
new file mode 100644
index 0000000000..c35a26575a
--- /dev/null
+++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+ {{:: strings.get('workflows.INVALID_JOB_TEMPLATE') }}
+
+
+
+
+
+ {{:: strings.get('workflows.CREDENTIAL_WITH_PASS') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/main.js b/awx/ui/client/src/templates/workflows/workflow-maker/main.js
index 821dfe18aa..f93a952b82 100644
--- a/awx/ui/client/src/templates/workflows/workflow-maker/main.js
+++ b/awx/ui/client/src/templates/workflows/workflow-maker/main.js
@@ -1,8 +1,9 @@
import workflowMaker from './workflow-maker.directive';
import WorkflowMakerController from './workflow-maker.controller';
+import workflowMakerForms from './forms/main';
export default
- angular.module('templates.workflowMaker', [])
+ angular.module('templates.workflowMaker', [workflowMakerForms.name])
// In order to test this controller I had to expose it at the module level
// like so. Is this correct? Is there a better pattern for doing this?
.controller('WorkflowMakerController', WorkflowMakerController)
diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less
index f1e3671d1c..580d1aa231 100644
--- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less
+++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less
@@ -127,7 +127,6 @@
color: @list-title-txt;
font-size: 14px;
font-weight: bold;
- text-transform: uppercase;
margin-bottom: 20px;
}
.WorkflowMaker-formHelp {
diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js
index 8a8258220b..64b3ef3334 100644
--- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js
+++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js
@@ -203,7 +203,7 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
});
});
} else {
- if (params.node.edited || !params.node.originalParentId || (params.node.originalParentId && params.parentId !== params.node.originalParentId)) {
+ if (params.node.edited || !params.node.originalParentId || (params.node.originalParentId && (params.parentId !== params.node.originalParentId || params.node.originalEdge !== params.node.edgeType))) {
if (params.node.edited) {
@@ -446,6 +446,10 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
$scope.cancelNodeForm();
}
+ if ($scope.linkBeingEdited) {
+ $scope.cancelLinkForm();
+ }
+
$scope.workflowMakerFormConfig.nodeMode = "add";
$scope.addParent = parent;
$scope.betweenTwoNodes = betweenTwoNodes;
@@ -572,6 +576,10 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
$scope.startEditNode = function (nodeToEdit) {
$scope.editNodeHelpMessage = null;
+ if ($scope.linkBeingEdited) {
+ $scope.cancelLinkForm();
+ }
+
if (!$scope.nodeBeingEdited || ($scope.nodeBeingEdited && $scope.nodeBeingEdited.id !== nodeToEdit.id)) {
if ($scope.placeholderNode || $scope.nodeBeingEdited) {
$scope.cancelNodeForm();
@@ -893,6 +901,91 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
};
+ /* EDIT LINK FUNCTIONS */
+
+ $scope.startEditLink = (parentId, childId) => {
+ const setupLinkEdit = () => {
+ const parentNode = WorkflowService.searchTree({
+ element: $scope.treeData.data,
+ matchingId: parentId,
+ byNodeId: true
+ });
+
+ parentNode.isLinkEditParent = true;
+
+ // Loop across children looking for childId
+ const childNode = _.find(parentNode.children, {'nodeId': childId});
+
+ childNode.isLinkEditChild = true;
+
+ $scope.linkBeingEdited = {
+ parent: parentNode,
+ child: childNode
+ }
+
+ $scope.linkConfig = {
+ parent: {
+ id: parentId,
+ name: parentNode.unifiedJobTemplate.name
+ },
+ child: {
+ id: childId,
+ name: childNode.unifiedJobTemplate.name
+ },
+ edgeType: childNode.edgeType
+ }
+ $scope.editLink = true;
+
+ $scope.$broadcast("refreshWorkflowChart");
+ }
+
+ if ($scope.nodeBeingEdited || $scope.placeholderNode) {
+ $scope.cancelNodeForm();
+ }
+
+ if ($scope.linkBeingEdited) {
+ if ($scope.linkBeingEdited.parent.nodeId !== parentId || $scope.linkBeingEdited.child.nodeId !== childId) {
+ $scope.linkBeingEdited.parent.isLinkEditParent = false;
+ $scope.linkBeingEdited.child.isLinkEditChild = false;
+ setupLinkEdit()
+ }
+ } else {
+ setupLinkEdit();
+ }
+
+ };
+
+ $scope.confirmLinkForm = (parentId, childId, edgeType) => {
+ $scope.linkBeingEdited.parent.isLinkEditParent = false;
+ $scope.linkBeingEdited.child.isLinkEditChild = false;
+ const parentNode = WorkflowService.searchTree({
+ element: $scope.treeData.data,
+ matchingId: parentId,
+ byNodeId: true
+ });
+
+ // Loop across children looking for childId
+ const childNode = _.find(parentNode.children, {'nodeId': childId});
+
+ childNode.edgeType = edgeType;
+
+ $scope.linkBeingEdited = null;
+
+ $scope.editLink = false;
+
+ $scope.$broadcast("refreshWorkflowChart");
+ }
+
+ $scope.cancelLinkForm = () => {
+ $scope.linkBeingEdited.parent.isLinkEditParent = false;
+ $scope.linkBeingEdited.child.isLinkEditChild = false;
+ $scope.linkBeingEdited = null;
+
+ $scope.editLink = false;
+
+ $scope.$broadcast("refreshWorkflowChart");
+ };
+
/* DELETE NODE FUNCTIONS */
function resetDeleteNode() {
@@ -912,6 +1005,10 @@ export default ['$scope', 'WorkflowService', 'TemplatesService',
$scope.confirmDeleteNode = function () {
if ($scope.nodeToBeDeleted) {
+ if ($scope.linkBeingEdited) {
+ $scope.cancelLinkForm();
+ }
+
// TODO: turn this into a promise so that we can handle errors
WorkflowService.removeNodeFromTree({
diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html
index 7760f5b7aa..b7c18669e5 100644
--- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html
+++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html
@@ -81,68 +81,69 @@
-
+
-
{{(workflowMakerFormConfig.nodeMode === 'edit' && nodeBeingEdited) ? ((nodeBeingEdited.unifiedJobTemplate && nodeBeingEdited.unifiedJobTemplate.name) ? nodeBeingEdited.unifiedJobTemplate.name : strings.get('workflow_maker.EDIT_TEMPLATE')) : strings.get('workflow_maker.ADD_A_TEMPLATE')}}
-
-