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 = $(""); - $('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 = $(""); + $('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('workflow_maker.JOBS')}}
+
{{strings.get('workflow_maker.PROJECT_SYNC')}}
+
{{strings.get('workflow_maker.INVENTORY_SYNC')}}
+
+
+
+
+
+
+ +
+
+ + {{:: 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')}}
-
-
-
-
{{strings.get('workflow_maker.JOBS')}}
-
{{strings.get('workflow_maker.PROJECT_SYNC')}}
-
{{strings.get('workflow_maker.INVENTORY_SYNC')}}
+ +
{{(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')}}
+
+
+
+
{{strings.get('workflow_maker.JOBS')}}
+
{{strings.get('workflow_maker.PROJECT_SYNC')}}
+
{{strings.get('workflow_maker.INVENTORY_SYNC')}}
+
+
+
+
+
+
+ +
+
+ + {{:: strings.get('workflows.INVALID_JOB_TEMPLATE') }} +
+
+
+
+ + {{:: strings.get('workflows.CREDENTIAL_WITH_PASS') }} +
+
+
+ +
+ +
+
+
+ + + + +
+
-
-
-
-
-
- -
-
- - {{:: strings.get('workflows.INVALID_JOB_TEMPLATE') }} -
-
-
-
- - {{:: strings.get('workflows.CREDENTIAL_WITH_PASS') }} -
-
-
- -
- -
-
-
-
-
- - - - -
-
-
+ + + +