From f6cc351f7fc075b4e8590f2c6fbff1ddd6ccd3c7 Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Sat, 6 Oct 2018 17:28:34 -0400 Subject: [PATCH] Format workflow-chart directive code --- .../workflow-chart.directive.js | 1954 +++++++++-------- 1 file changed, 1036 insertions(+), 918 deletions(-) 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 75defa010f..8880c86321 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,994 +4,1112 @@ * 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: '&', + 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("width", windowWidth); + $('.WorkflowMaker-chart').css("height", windowHeight); + $('.WorkflowMaker-chart').css("width", windowWidth); - 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;}); - - 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); + line = d3.svg.line() + .x(function (d) { + return d.x; + }) + .y(function (d) { + return d.y; }); - 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 + ")"; }); + zoomObj = d3.behavior.zoom().scaleExtent([0.5, 2]); - 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"); + 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 } - 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 { - 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"; }); + return line(points); + } - 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); + // 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; + } + } - 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; }); + 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; + } - 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") ? null : "none"; }); + // This is the zoom function called by using the mousewheel/click and drag + function naturalZoom() { + let scale = d3.event.scale, + translation = d3.event.translate; - thisNode.append("text") - .attr("y", nodeH) - .attr("dy", ".35em") - .attr("text-anchor", "middle") - .attr("class", "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"; }); + translation = [translation[0] + (marginLeft * scale), translation[1] + ((windowHeight / 2 - rootH / 2 - startNodeOffsetY) * scale)]; - 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 = $(""); - $('body').append(tooltipDimensionChecker); - let tipWidth = $(tooltipDimensionChecker).outerWidth(); - let tipHeight = $(tooltipDimensionChecker).outerHeight(); - $(tooltipDimensionChecker).remove(); + svgGroup.attr("transform", "translate(" + translation + ")scale(" + scale + ")"); - 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); - }); + scope.workflowZoomed({ + zoom: scale + }); + } - thisNode.append("circle") - .attr("class", function(d) { + // 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; - let statusClass = "WorkflowChart-nodeStatus "; + svgGroup.attr("transform", "translate(" + [translateX + (marginLeft * scale), translateY + ((windowHeight / 2 - rootH / 2 - startNodeOffsetY) * scale)] + ")scale(" + scale + ")"); + zoomObj.scale(scale); + zoomObj.translate([translateX, translateY]); + } - 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; - } - } + 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]); + } - return statusClass; - }) - .style("display", function(d) { return d.job && d.job.status ? null : "none"; }) - .attr("cy", 10) - .attr("cx", 10) - .attr("r", 6); + function resetZoomAndPan() { + svgGroup.attr("transform", "translate(" + marginLeft + "," + (windowHeight / 2 - rootH / 2 - startNodeOffsetY) + ")scale(" + 1 + ")"); + // Update the zoomObj + zoomObj.scale(1); + zoomObj.translate([0, 0]); + } - 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"; }); - } + 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 }); - node.exit().remove(); + 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)]); - if(nodes && nodes.length > 1 && !graphLoaded) { - zoomToFitChart(); - } + } - graphLoaded = true; + 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 link = svgGroup.selectAll("g.link") - .data(links, function(d) { - return d.source.id + "-" + d.target.id; + 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 { + 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") ? null : "none"; + }); + + thisNode.append("text") + .attr("y", nodeH) + .attr("dy", ".35em") + .attr("text-anchor", "middle") + .attr("class", "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"; + }); + + 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 = $(""); + $('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"; + }); + } }); - let linkEnter = link.enter().append("g") - .attr("class", "link") - .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id;}); + node.exit().remove(); - // 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"; - } - }); + if (nodes && nodes.length > 1 && !graphLoaded) { + zoomToFitChart(); + } - 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); - }); + graphLoaded = true; - 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); - }); + let link = svgGroup.selectAll("g.link") + .data(links, function (d) { + return d.source.id + "-" + d.target.id; + }); - link.exit().remove(); + let linkEnter = link.enter().append("g") + .attr("class", "link") + .attr("id", function (d) { + return "link-" + d.source.id + "-" + d.target.id; + }); - // 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) { + // 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") { + .attr('stroke', function (d) { + if (d.target.edgeType) { + if (d.target.edgeType === "failure") { return "#d9534f"; - } - else if(d.target.edgeType === "success") { + } else if (d.target.edgeType === "success") { return "#5cb85c"; - } - else if(d.target.edgeType === "always"){ + } else if (d.target.edgeType === "always") { return "#337ab7"; } - } - else { + } 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; - }); + 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); + }); - 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; - }); + 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); + }); - 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 { + 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"; } - } - 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(".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(".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"; }); - - t.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"; }); - - 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(".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; + }); - 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"; }); - } - 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 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); + 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"; } }) - .catch(({data, status}) => { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Unable to get job: ' + status }); + .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") ? null : "none"; + }); + + t.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"; + }); + + 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-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(); + } + }); + } + } + + 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' + }); + } + }; + + 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.$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(); - }); + scope.$on('refreshWorkflowChart', function () { + if (scope.treeData) { + update(); + } }); - } - else { - scope.$on('workflowMakerModalResized', function(){ + + 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); + }); + } } - } - }; -}]; + }; + } +];