mirror of
https://github.com/ansible/awx.git
synced 2026-05-11 11:27:36 -02:30
Added dagre to handle our workflow graph layout. Fixed various workflow related bugs.
This commit is contained in:
@@ -23,8 +23,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
link: function(scope, element) {
|
link: function(scope, element) {
|
||||||
|
|
||||||
let marginLeft = 20,
|
let nodeW = 180,
|
||||||
nodeW = 180,
|
|
||||||
nodeH = 60,
|
nodeH = 60,
|
||||||
rootW = 60,
|
rootW = 60,
|
||||||
rootH = 40,
|
rootH = 40,
|
||||||
@@ -32,11 +31,12 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
maxNodeTextLength = 27,
|
maxNodeTextLength = 27,
|
||||||
windowHeight,
|
windowHeight,
|
||||||
windowWidth,
|
windowWidth,
|
||||||
|
line,
|
||||||
zoomObj,
|
zoomObj,
|
||||||
baseSvg,
|
baseSvg,
|
||||||
svgGroup,
|
svgGroup,
|
||||||
graphLoaded,
|
graphLoaded,
|
||||||
force;
|
nodePositionMap = {};
|
||||||
|
|
||||||
scope.dimensionsSet = false;
|
scope.dimensionsSet = false;
|
||||||
|
|
||||||
@@ -54,16 +54,13 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
});
|
});
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
force = d3.layout.force()
|
line = d3.svg.line()
|
||||||
// .gravity(0)
|
.x(function (d) {
|
||||||
// .linkStrength(2)
|
return d.x;
|
||||||
// .friction(0.4)
|
})
|
||||||
// .charge(-4000)
|
.y(function (d) {
|
||||||
// .linkDistance(300)
|
return d.y;
|
||||||
.gravity(0)
|
});
|
||||||
.charge(-300)
|
|
||||||
.linkDistance(300)
|
|
||||||
.size([windowHeight, windowWidth]);
|
|
||||||
|
|
||||||
zoomObj = d3.behavior.zoom().scaleExtent([0.5, 2]);
|
zoomObj = d3.behavior.zoom().scaleExtent([0.5, 2]);
|
||||||
|
|
||||||
@@ -75,7 +72,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
|
|
||||||
svgGroup = baseSvg.append("g")
|
svgGroup = baseSvg.append("g")
|
||||||
.attr("id", "aw-workflow-chart-g")
|
.attr("id", "aw-workflow-chart-g")
|
||||||
.attr("transform", "translate(" + marginLeft + "," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")");
|
.attr("transform", "translate(0," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
function calcAvailableScreenSpace() {
|
function calcAvailableScreenSpace() {
|
||||||
@@ -102,6 +99,37 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
return dimensions;
|
return dimensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dagre is going to shift the root node around as nodes are added/removed
|
||||||
|
// This function ensures that the user doesn't experience that
|
||||||
|
let normalizeY = ((y) => {
|
||||||
|
return y - nodePositionMap[1].y;
|
||||||
|
});
|
||||||
|
|
||||||
|
function lineData(d) {
|
||||||
|
|
||||||
|
let sourceX = nodePositionMap[d.source.id].x + (nodePositionMap[d.source.id].width);
|
||||||
|
let sourceY = normalizeY(nodePositionMap[d.source.id].y) + (nodePositionMap[d.source.id].height/2);
|
||||||
|
let targetX = nodePositionMap[d.target.id].x;
|
||||||
|
let targetY = normalizeY(nodePositionMap[d.target.id].y) + (nodePositionMap[d.target.id].height/2);
|
||||||
|
|
||||||
|
// There's something off with the math on the root node...
|
||||||
|
if (d.source.id === 1) {
|
||||||
|
sourceY = sourceY + 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// 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
|
// see: http://stackoverflow.com/questions/15975440/add-ellipses-to-overflowing-text-in-svg#answer-27723752
|
||||||
function wrap(text) {
|
function wrap(text) {
|
||||||
@@ -137,7 +165,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
let scale = d3.event.scale,
|
let scale = d3.event.scale,
|
||||||
translation = d3.event.translate;
|
translation = d3.event.translate;
|
||||||
|
|
||||||
translation = [translation[0] + (marginLeft*scale), translation[1] + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)];
|
translation = [translation[0], translation[1] + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)];
|
||||||
|
|
||||||
svgGroup.attr("transform", "translate(" + translation + ")scale(" + scale + ")");
|
svgGroup.attr("transform", "translate(" + translation + ")scale(" + scale + ")");
|
||||||
|
|
||||||
@@ -156,7 +184,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
translateX = unscaledOffsetX*scale - ((scale*windowWidth)-windowWidth)/2,
|
translateX = unscaledOffsetX*scale - ((scale*windowWidth)-windowWidth)/2,
|
||||||
translateY = unscaledOffsetY*scale - ((scale*windowHeight)-windowHeight)/2;
|
translateY = unscaledOffsetY*scale - ((scale*windowHeight)-windowHeight)/2;
|
||||||
|
|
||||||
svgGroup.attr("transform", "translate(" + [translateX + (marginLeft*scale), translateY + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)] + ")scale(" + scale + ")");
|
svgGroup.attr("transform", "translate(" + [translateX, translateY + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)] + ")scale(" + scale + ")");
|
||||||
zoomObj.scale(scale);
|
zoomObj.scale(scale);
|
||||||
zoomObj.translate([translateX, translateY]);
|
zoomObj.translate([translateX, translateY]);
|
||||||
}
|
}
|
||||||
@@ -179,7 +207,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetZoomAndPan() {
|
function resetZoomAndPan() {
|
||||||
svgGroup.attr("transform", "translate(" + marginLeft + "," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")scale(" + 1 + ")");
|
svgGroup.attr("transform", "translate(0," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")scale(" + 1 + ")");
|
||||||
// Update the zoomObj
|
// Update the zoomObj
|
||||||
zoomObj.scale(1);
|
zoomObj.scale(1);
|
||||||
zoomObj.translate([0,0]);
|
zoomObj.translate([0,0]);
|
||||||
@@ -187,16 +215,14 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
|
|
||||||
function zoomToFitChart() {
|
function zoomToFitChart() {
|
||||||
let graphDimensions = d3.select('#aw-workflow-chart-g')[0][0].getBoundingClientRect(),
|
let graphDimensions = d3.select('#aw-workflow-chart-g')[0][0].getBoundingClientRect(),
|
||||||
startNodeDimensions = d3.select('.WorkflowChart-rootNode')[0][0].getBoundingClientRect(),
|
|
||||||
availableScreenSpace = calcAvailableScreenSpace(),
|
availableScreenSpace = calcAvailableScreenSpace(),
|
||||||
currentZoomValue = zoomObj.scale(),
|
currentZoomValue = zoomObj.scale(),
|
||||||
unscaledH = graphDimensions.height/currentZoomValue,
|
unscaledH = graphDimensions.height/currentZoomValue,
|
||||||
unscaledW = graphDimensions.width/currentZoomValue,
|
unscaledW = graphDimensions.width/currentZoomValue,
|
||||||
scaleNeededForMaxHeight = (availableScreenSpace.height)/unscaledH,
|
scaleNeededForMaxHeight = (availableScreenSpace.height)/unscaledH,
|
||||||
scaleNeededForMaxWidth = (availableScreenSpace.width - marginLeft)/unscaledW,
|
scaleNeededForMaxWidth = (availableScreenSpace.width)/unscaledW,
|
||||||
lowerScale = Math.min(scaleNeededForMaxHeight, scaleNeededForMaxWidth),
|
lowerScale = Math.min(scaleNeededForMaxHeight, scaleNeededForMaxWidth),
|
||||||
scaleToFit = lowerScale < 0.5 ? 0.5 : (lowerScale > 2 ? 2 : Math.floor(lowerScale * 10)/10),
|
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);
|
manualZoom(scaleToFit*100);
|
||||||
|
|
||||||
@@ -204,12 +230,42 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
zoom: scaleToFit
|
zoom: scaleToFit
|
||||||
});
|
});
|
||||||
|
|
||||||
svgGroup.attr("transform", "translate(" + marginLeft + "," + (windowHeight/2 - (nodeH*scaleToFit/2) + startNodeOffsetFromGraphCenter) + ")scale(" + scaleToFit + ")");
|
svgGroup.attr("transform", "translate(0," + (windowHeight/2 - (nodeH*scaleToFit/2)) + ")scale(" + scaleToFit + ")");
|
||||||
zoomObj.translate([marginLeft - scaleToFit*marginLeft, windowHeight/2 - (nodeH*scaleToFit/2) + startNodeOffsetFromGraphCenter - ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scaleToFit)]);
|
zoomObj.translate([0, windowHeight/2 - (nodeH*scaleToFit/2) - ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scaleToFit)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
if(scope.dimensionsSet) {
|
if(scope.dimensionsSet) {
|
||||||
|
var g = new dagre.graphlib.Graph();
|
||||||
|
|
||||||
|
g.setGraph({rankdir: 'LR', nodesep: 30, ranksep: 120});
|
||||||
|
|
||||||
|
g.setDefaultEdgeLabel(function() { return {}; });
|
||||||
|
|
||||||
|
scope.graphState.arrayOfNodesForChart.forEach((node) => {
|
||||||
|
if (node.id === 1) {
|
||||||
|
if (scope.mode === "details") {
|
||||||
|
g.setNode(node.id, { label: "", width: 25, height: 25 });
|
||||||
|
} else {
|
||||||
|
g.setNode(node.id, { label: "", width: rootW, height: rootH });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
g.setNode(node.id, { label: "", width: nodeW, height: nodeH });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.graphState.arrayOfLinksForChart.forEach((link) => {
|
||||||
|
g.setEdge(link.source.id, link.target.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
dagre.layout(g);
|
||||||
|
|
||||||
|
nodePositionMap = {};
|
||||||
|
|
||||||
|
g.nodes().forEach((node) => {
|
||||||
|
nodePositionMap[node] = g.node(node);
|
||||||
|
});
|
||||||
|
|
||||||
let links = svgGroup.selectAll(".WorkflowChart-link")
|
let links = svgGroup.selectAll(".WorkflowChart-link")
|
||||||
.data(scope.graphState.arrayOfLinksForChart, function(d) { return `${d.source.id}-${d.target.id}`; });
|
.data(scope.graphState.arrayOfLinksForChart, function(d) { return `${d.source.id}-${d.target.id}`; });
|
||||||
|
|
||||||
@@ -221,9 +277,11 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
.attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id;});
|
.attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id;});
|
||||||
|
|
||||||
baseSvg.selectAll(".WorkflowChart-linkPath")
|
baseSvg.selectAll(".WorkflowChart-linkPath")
|
||||||
|
.transition()
|
||||||
.attr("class", function(d) {
|
.attr("class", function(d) {
|
||||||
return (d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded) ? "WorkflowChart-linkPath WorkflowChart-isNodeBeingAdded" : "WorkflowChart-linkPath";
|
return (d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded) ? "WorkflowChart-linkPath WorkflowChart-isNodeBeingAdded" : "WorkflowChart-linkPath";
|
||||||
})
|
})
|
||||||
|
.attr("d", lineData)
|
||||||
.attr('stroke', function(d) {
|
.attr('stroke', function(d) {
|
||||||
let edgeType = d.edgeType;
|
let edgeType = d.edgeType;
|
||||||
if(edgeType) {
|
if(edgeType) {
|
||||||
@@ -254,15 +312,64 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
linkClasses.push("WorkflowChart-link--active");
|
linkClasses.push("WorkflowChart-link--active");
|
||||||
}
|
}
|
||||||
return linkClasses.join(' ');
|
return linkClasses.join(' ');
|
||||||
|
})
|
||||||
|
.attr("points",function(d) {
|
||||||
|
let x1 = nodePositionMap[d.target.id].x;
|
||||||
|
let y1 = normalizeY(nodePositionMap[d.target.id].y) + (nodePositionMap[d.target.id].height/2);
|
||||||
|
let x2 = nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].width;
|
||||||
|
let y2 = normalizeY(nodePositionMap[d.source.id].y) + (nodePositionMap[d.source.id].height/2);
|
||||||
|
let slope = (y2 - y1)/(x2-x1);
|
||||||
|
let yIntercept = y1 - slope*x1;
|
||||||
|
let orthogonalDistance = 8;
|
||||||
|
|
||||||
|
const pt1 = [x1, slope*x1 + yIntercept + orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
|
||||||
|
const pt2 = [x2, slope*x2 + yIntercept + orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
|
||||||
|
const pt3 = [x2, slope*x2 + yIntercept - orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
|
||||||
|
const pt4 = [x1, slope*x1 + yIntercept - orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
|
||||||
|
|
||||||
|
return [pt1, pt2, pt3, pt4].join(" ");
|
||||||
});
|
});
|
||||||
|
|
||||||
baseSvg.selectAll(".WorkflowChart-circleBetweenNodes")
|
baseSvg.selectAll(".WorkflowChart-circleBetweenNodes")
|
||||||
.attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-add";})
|
.attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-add";})
|
||||||
.style("display", function(d) { return (scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; });
|
.style("display", function(d) { return (scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; })
|
||||||
|
.attr("cx", function(d) {
|
||||||
|
return (nodePositionMap[d.source.id].x + nodePositionMap[d.source.id].width + nodePositionMap[d.target.id].x)/2;
|
||||||
|
})
|
||||||
|
.attr("cy", function(d) {
|
||||||
|
const normalizedSourceY = normalizeY(nodePositionMap[d.source.id].y);
|
||||||
|
const halfSourceHeight = nodePositionMap[d.source.id].height/2;
|
||||||
|
const normalizedTargetY = normalizeY(nodePositionMap[d.target.id].y);
|
||||||
|
const halfTargetHeight = nodePositionMap[d.target.id].height/2;
|
||||||
|
|
||||||
|
let yPos = (normalizedSourceY + halfSourceHeight + normalizedTargetY + halfTargetHeight)/2;
|
||||||
|
|
||||||
|
if (d.source.id === 1) {
|
||||||
|
yPos = yPos + 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
return yPos;
|
||||||
|
});
|
||||||
|
|
||||||
baseSvg.selectAll(".WorkflowChart-betweenNodesIcon")
|
baseSvg.selectAll(".WorkflowChart-betweenNodesIcon")
|
||||||
.style("display", function(d) { return (scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; });
|
.style("display", function(d) { return (scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; })
|
||||||
|
.attr("transform", function(d) {
|
||||||
|
let translate;
|
||||||
|
|
||||||
|
const normalizedSourceY = normalizeY(nodePositionMap[d.source.id].y);
|
||||||
|
const halfSourceHeight = nodePositionMap[d.source.id].height/2;
|
||||||
|
const normalizedTargetY = normalizeY(nodePositionMap[d.target.id].y);
|
||||||
|
const halfTargetHeight = nodePositionMap[d.target.id].height/2;
|
||||||
|
|
||||||
|
let yPos = (normalizedSourceY + halfSourceHeight + normalizedTargetY + halfTargetHeight)/2;
|
||||||
|
|
||||||
|
if (d.source.id === 1) {
|
||||||
|
yPos = yPos + 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
translate = "translate(" + (nodePositionMap[d.source.id].x + nodePositionMap[d.source.id].width + nodePositionMap[d.target.id].x)/2 + "," + yPos + ")";
|
||||||
|
return translate;
|
||||||
|
});
|
||||||
|
|
||||||
// Add any new links
|
// Add any new links
|
||||||
let linkEnter = links.enter().append("g")
|
let linkEnter = links.enter().append("g")
|
||||||
@@ -283,6 +390,22 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
})
|
})
|
||||||
.attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-overlay";})
|
.attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-overlay";})
|
||||||
.call(edit_link)
|
.call(edit_link)
|
||||||
|
.attr("points",function(d) {
|
||||||
|
let x1 = nodePositionMap[d.target.id].x;
|
||||||
|
let y1 = normalizeY(nodePositionMap[d.target.id].y) + (nodePositionMap[d.target.id].height/2);
|
||||||
|
let x2 = nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].width;
|
||||||
|
let y2 = normalizeY(nodePositionMap[d.source.id].y) + (nodePositionMap[d.source.id].height/2);
|
||||||
|
let slope = (y2 - y1)/(x2-x1);
|
||||||
|
let yIntercept = y1 - slope*x1;
|
||||||
|
let orthogonalDistance = 8;
|
||||||
|
|
||||||
|
const pt1 = [x1, slope*x1 + yIntercept + orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
|
||||||
|
const pt2 = [x2, slope*x2 + yIntercept + orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
|
||||||
|
const pt3 = [x2, slope*x2 + yIntercept - orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
|
||||||
|
const pt4 = [x1, slope*x1 + yIntercept - orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
|
||||||
|
|
||||||
|
return [pt1, pt2, pt3, pt4].join(" ");
|
||||||
|
})
|
||||||
.on("mouseover", function(d) {
|
.on("mouseover", function(d) {
|
||||||
if(!scope.graphState.isLinkMode && !d.source.isStartNode && d.source.id !== scope.graphState.nodeBeingAdded && d.target.id !== scope.graphState.nodeBeingAdded && scope.mode !== 'details') {
|
if(!scope.graphState.isLinkMode && !d.source.isStartNode && d.source.id !== scope.graphState.nodeBeingAdded && d.target.id !== scope.graphState.nodeBeingAdded && scope.mode !== 'details') {
|
||||||
$(`#link-${d.source.id}-${d.target.id}`).appendTo(`#aw-workflow-chart-g`);
|
$(`#link-${d.source.id}-${d.target.id}`).appendTo(`#aw-workflow-chart-g`);
|
||||||
@@ -290,13 +413,13 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
.classed("WorkflowChart-linkHovering", true);
|
.classed("WorkflowChart-linkHovering", true);
|
||||||
|
|
||||||
let xPos, yPos, arrowClass;
|
let xPos, yPos, arrowClass;
|
||||||
if (d.source.x === d.target.x) {
|
if (nodePositionMap[d.source.id].y === nodePositionMap[d.target.id].y) {
|
||||||
xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - (100/2);
|
xPos = (nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].x)/2 + 45;
|
||||||
yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 100;
|
yPos = (nodePositionMap[d.source.id].y + nodePositionMap[d.target.id].y)/2 - 107;
|
||||||
arrowClass = 'WorkflowChart-tooltipArrow--down';
|
arrowClass = 'WorkflowChart-tooltipArrow--down';
|
||||||
} else {
|
} else {
|
||||||
xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - 115;
|
xPos = (nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].x)/2 - 30;
|
||||||
yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 50;
|
yPos = (nodePositionMap[d.source.id].y + nodePositionMap[d.target.id].y)/2 - 70;
|
||||||
arrowClass = 'WorkflowChart-tooltipArrow--right';
|
arrowClass = 'WorkflowChart-tooltipArrow--right';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,10 +460,11 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add entering links in the parent’s old position.
|
// Add entering links in the parent’s old position.
|
||||||
linkEnter.append("line")
|
linkEnter.insert("path", "g")
|
||||||
.attr("class", function(d) {
|
.attr("class", function(d) {
|
||||||
return (d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded) ? "WorkflowChart-linkPath WorkflowChart-isNodeBeingAdded" : "WorkflowChart-linkPath";
|
return (d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded) ? "WorkflowChart-linkPath WorkflowChart-isNodeBeingAdded" : "WorkflowChart-linkPath";
|
||||||
})
|
})
|
||||||
|
.attr("d", lineData)
|
||||||
.call(edit_link)
|
.call(edit_link)
|
||||||
.on("mouseenter", function(d) {
|
.on("mouseenter", function(d) {
|
||||||
if(!scope.graphState.isLinkMode && !d.source.isStartNode && d.source.id !== scope.graphState.nodeBeingAdded && d.target.id !== scope.graphState.nodeBeingAdded && scope.mode !== 'details') {
|
if(!scope.graphState.isLinkMode && !d.source.isStartNode && d.source.id !== scope.graphState.nodeBeingAdded && d.target.id !== scope.graphState.nodeBeingAdded && scope.mode !== 'details') {
|
||||||
@@ -349,13 +473,13 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
.classed("WorkflowChart-linkHovering", true);
|
.classed("WorkflowChart-linkHovering", true);
|
||||||
|
|
||||||
let xPos, yPos, arrowClass;
|
let xPos, yPos, arrowClass;
|
||||||
if (d.source.x === d.target.x) {
|
if (nodePositionMap[d.source.id].y === nodePositionMap[d.target.id].y) {
|
||||||
xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - (100/2);
|
xPos = (nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].x)/2 + 45;
|
||||||
yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 100;
|
yPos = (nodePositionMap[d.source.id].y + nodePositionMap[d.target.id].y)/2 - 107;
|
||||||
arrowClass = 'WorkflowChart-tooltipArrow--down';
|
arrowClass = 'WorkflowChart-tooltipArrow--down';
|
||||||
} else {
|
} else {
|
||||||
xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - 115;
|
xPos = (nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].x)/2 - 30;
|
||||||
yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 50;
|
yPos = (nodePositionMap[d.source.id].y + nodePositionMap[d.target.id].y)/2 - 70;
|
||||||
arrowClass = 'WorkflowChart-tooltipArrow--right';
|
arrowClass = 'WorkflowChart-tooltipArrow--right';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,6 +540,23 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
.attr("r", 10)
|
.attr("r", 10)
|
||||||
.attr("class", "WorkflowChart-addCircle WorkflowChart-circleBetweenNodes")
|
.attr("class", "WorkflowChart-addCircle WorkflowChart-circleBetweenNodes")
|
||||||
.style("display", function(d) { return (scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; })
|
.style("display", function(d) { return (scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; })
|
||||||
|
.attr("cx", function(d) {
|
||||||
|
return (nodePositionMap[d.source.id].x + nodePositionMap[d.source.id].width + nodePositionMap[d.target.id].x)/2;
|
||||||
|
})
|
||||||
|
.attr("cy", function(d) {
|
||||||
|
const normalizedSourceY = normalizeY(nodePositionMap[d.source.id].y);
|
||||||
|
const halfSourceHeight = nodePositionMap[d.source.id].height/2;
|
||||||
|
const normalizedTargetY = normalizeY(nodePositionMap[d.target.id].y);
|
||||||
|
const halfTargetHeight = nodePositionMap[d.target.id].height/2;
|
||||||
|
|
||||||
|
let yPos = (normalizedSourceY + halfSourceHeight + normalizedTargetY + halfTargetHeight)/2;
|
||||||
|
|
||||||
|
if (d.source.id === 1) {
|
||||||
|
yPos = yPos + 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
return yPos;
|
||||||
|
})
|
||||||
.call(add_node_with_child)
|
.call(add_node_with_child)
|
||||||
.on("mouseover", function(d) {
|
.on("mouseover", function(d) {
|
||||||
$(`#link-${d.source.id}-${d.target.id}`).appendTo(`#aw-workflow-chart-g`);
|
$(`#link-${d.source.id}-${d.target.id}`).appendTo(`#aw-workflow-chart-g`);
|
||||||
@@ -436,6 +577,23 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
.type("cross")
|
.type("cross")
|
||||||
)
|
)
|
||||||
.style("display", function(d) { return (scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; })
|
.style("display", function(d) { return (scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; })
|
||||||
|
.attr("transform", function(d) {
|
||||||
|
let translate;
|
||||||
|
|
||||||
|
const normalizedSourceY = normalizeY(nodePositionMap[d.source.id].y);
|
||||||
|
const halfSourceHeight = nodePositionMap[d.source.id].height/2;
|
||||||
|
const normalizedTargetY = normalizeY(nodePositionMap[d.target.id].y);
|
||||||
|
const halfTargetHeight = nodePositionMap[d.target.id].height/2;
|
||||||
|
|
||||||
|
let yPos = (normalizedSourceY + halfSourceHeight + normalizedTargetY + halfTargetHeight)/2;
|
||||||
|
|
||||||
|
if (d.source.id === 1) {
|
||||||
|
yPos = yPos + 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
translate = "translate(" + (nodePositionMap[d.source.id].x + nodePositionMap[d.source.id].width + nodePositionMap[d.target.id].x)/2 + "," + yPos + ")";
|
||||||
|
return translate;
|
||||||
|
})
|
||||||
.call(add_node_with_child)
|
.call(add_node_with_child)
|
||||||
.on("mouseover", function(d) {
|
.on("mouseover", function(d) {
|
||||||
$(`#link-${d.source.id}-${d.target.id}`).appendTo(`#aw-workflow-chart-g`);
|
$(`#link-${d.source.id}-${d.target.id}`).appendTo(`#aw-workflow-chart-g`);
|
||||||
@@ -448,13 +606,6 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
.classed("WorkflowChart-addHovering", false);
|
.classed("WorkflowChart-addHovering", false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create references to all the link elements so that they can be transitioned
|
|
||||||
// properly in the tick function
|
|
||||||
let linkLines = svgGroup.selectAll(".WorkflowChart-link line");
|
|
||||||
let linkPolygons = svgGroup.selectAll(".WorkflowChart-link polygon");
|
|
||||||
let linkAddBetweenCircle = svgGroup.selectAll(".WorkflowChart-link circle");
|
|
||||||
let linkAddBetweenIcon = svgGroup.selectAll(".WorkflowChart-betweenNodesIcon");
|
|
||||||
|
|
||||||
let nodes = svgGroup.selectAll('.WorkflowChart-node')
|
let nodes = svgGroup.selectAll('.WorkflowChart-node')
|
||||||
.data(scope.graphState.arrayOfNodesForChart, function(d) { return d.id; });
|
.data(scope.graphState.arrayOfNodesForChart, function(d) { return d.id; });
|
||||||
|
|
||||||
@@ -462,6 +613,15 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
nodes.exit().remove();
|
nodes.exit().remove();
|
||||||
|
|
||||||
// Update existing nodes
|
// Update existing nodes
|
||||||
|
baseSvg.selectAll(".WorkflowChart-node")
|
||||||
|
.transition()
|
||||||
|
.attr("transform", function (d) {
|
||||||
|
// Update prior x and prior y
|
||||||
|
d.px = d.x;
|
||||||
|
d.py = d.y;
|
||||||
|
return "translate(" + nodePositionMap[d.id].x + "," + normalizeY(nodePositionMap[d.id].y) + ")";
|
||||||
|
});
|
||||||
|
|
||||||
baseSvg.selectAll(".WorkflowChart-nodeAddCircle")
|
baseSvg.selectAll(".WorkflowChart-nodeAddCircle")
|
||||||
.style("display", function(d) { return scope.graphState.isLinkMode || d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; });
|
.style("display", function(d) { return scope.graphState.isLinkMode || d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; });
|
||||||
|
|
||||||
@@ -640,7 +800,10 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
.enter()
|
.enter()
|
||||||
.append('g')
|
.append('g')
|
||||||
.attr("class", "WorkflowChart-node")
|
.attr("class", "WorkflowChart-node")
|
||||||
.attr("id", function(d){return "node-" + d.id;});
|
.attr("id", function(d){return "node-" + d.id;})
|
||||||
|
.attr("transform", function (d) {
|
||||||
|
return "translate(" + nodePositionMap[d.id].x + "," + normalizeY(nodePositionMap[d.id].y) + ")";
|
||||||
|
});
|
||||||
|
|
||||||
nodeEnter.each(function(d) {
|
nodeEnter.each(function(d) {
|
||||||
let thisNode = d3.select(this);
|
let thisNode = d3.select(this);
|
||||||
@@ -1064,75 +1227,14 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: this
|
if(scope.graphState.arrayOfNodesForChart && scope.graphState.arrayOfNodesForChart.length > 1 && !graphLoaded) {
|
||||||
// if(scope.graphState.arrayOfNodesForChart && scope.graphState.arrayOfNodesForChart > 1 && !graphLoaded) {
|
zoomToFitChart();
|
||||||
// zoomToFitChart();
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
graphLoaded = true;
|
graphLoaded = true;
|
||||||
|
|
||||||
// This will make sure that all the link elements appear before the nodes in the dom
|
// This will make sure that all the link elements appear before the nodes in the dom
|
||||||
// TODO: i don't think this is working...
|
|
||||||
svgGroup.selectAll(".WorkflowChart-node").order();
|
svgGroup.selectAll(".WorkflowChart-node").order();
|
||||||
|
|
||||||
let tick = () => {
|
|
||||||
linkLines
|
|
||||||
.each(function(d) {
|
|
||||||
d.target.y = scope.graphState.depthMap[d.target.id] * 300;
|
|
||||||
})
|
|
||||||
.attr("x1", function(d) { return d.target.y; })
|
|
||||||
.attr("y1", function(d) { return d.target.x + (nodeH/2); })
|
|
||||||
.attr("x2", function(d) { return d.source.index === 0 ? (scope.mode === 'details' ? d.source.y + 25 : d.source.y + 60) : (d.source.y + nodeW); })
|
|
||||||
.attr("y2", function(d) { return d.source.x + (nodeH/2); });
|
|
||||||
|
|
||||||
linkPolygons
|
|
||||||
.attr("points",function(d) {
|
|
||||||
let x1 = d.target.y;
|
|
||||||
let y1 = d.target.x + (nodeH/2);
|
|
||||||
let x2 = d.source.index === 0 ? (d.source.y + 60) : (d.source.y + nodeW);
|
|
||||||
let y2 = d.source.x + (nodeH/2);
|
|
||||||
let slope = (y2 - y1)/(x2-x1);
|
|
||||||
let yIntercept = y1 - slope*x1;
|
|
||||||
let orthogonalDistance = 8;
|
|
||||||
|
|
||||||
const pt1 = [x1, slope*x1 + yIntercept + orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
|
|
||||||
const pt2 = [x2, slope*x2 + yIntercept + orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
|
|
||||||
const pt3 = [x2, slope*x2 + yIntercept - orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
|
|
||||||
const pt4 = [x1, slope*x1 + yIntercept - orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
|
|
||||||
|
|
||||||
return [pt1, pt2, pt3, pt4].join(" ");
|
|
||||||
});
|
|
||||||
|
|
||||||
linkAddBetweenCircle
|
|
||||||
.attr("cx", function(d) {
|
|
||||||
return (d.source.isStartNode) ? (d.target.y + d.source.y + rootW) / 2 : (d.target.y + d.source.y + nodeW) / 2;
|
|
||||||
})
|
|
||||||
.attr("cy", function(d) {
|
|
||||||
return (d.source.isStartNode) ? ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 : (d.target.x + d.source.x + nodeH) / 2;
|
|
||||||
});
|
|
||||||
|
|
||||||
linkAddBetweenIcon
|
|
||||||
.attr("transform", function(d) {
|
|
||||||
let translate;
|
|
||||||
if(d.source.isStartNode) {
|
|
||||||
translate = "translate(" + (d.target.y + d.source.y + rootW) / 2 + "," + ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 + ")";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
translate = "translate(" + (d.target.y + d.source.y + nodeW) / 2 + "," + (d.target.x + d.source.x + nodeH) / 2 + ")";
|
|
||||||
}
|
|
||||||
return translate;
|
|
||||||
});
|
|
||||||
|
|
||||||
nodes
|
|
||||||
.attr("transform", function(d) {
|
|
||||||
return "translate(" + d.y + "," + d.x + ")"; });
|
|
||||||
};
|
|
||||||
|
|
||||||
force
|
|
||||||
.nodes(scope.graphState.arrayOfNodesForChart)
|
|
||||||
.links(scope.graphState.arrayOfLinksForChart)
|
|
||||||
.on("tick", tick)
|
|
||||||
.start();
|
|
||||||
}
|
}
|
||||||
else if(!scope.watchDimensionsSet){
|
else if(!scope.watchDimensionsSet){
|
||||||
scope.watchDimensionsSet = scope.$watch('dimensionsSet', function(){
|
scope.watchDimensionsSet = scope.$watch('dimensionsSet', function(){
|
||||||
@@ -1178,7 +1280,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
|||||||
function node_click() {
|
function node_click() {
|
||||||
this.on("click", function(d) {
|
this.on("click", function(d) {
|
||||||
if(d.id !== scope.graphState.nodeBeingAdded && !scope.readOnly){
|
if(d.id !== scope.graphState.nodeBeingAdded && !scope.readOnly){
|
||||||
if(scope.graphState.isLinkMode && !d.isInvalidLinkTarget) {
|
if(scope.graphState.isLinkMode && !d.isInvalidLinkTarget && scope.graphState.addLinkSource !== d.id) {
|
||||||
$('.WorkflowChart-potentialLink').remove();
|
$('.WorkflowChart-potentialLink').remove();
|
||||||
scope.selectNodeForLinking({
|
scope.selectNodeForLinking({
|
||||||
nodeToStartLink: d
|
nodeToStartLink: d
|
||||||
|
|||||||
@@ -571,6 +571,9 @@ export default ['$scope', 'TemplatesService',
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.selectNodeForLinking = (node) => {
|
$scope.selectNodeForLinking = (node) => {
|
||||||
|
if ($scope.nodeConfig) {
|
||||||
|
$scope.cancelNodeForm();
|
||||||
|
}
|
||||||
if ($scope.linkConfig) {
|
if ($scope.linkConfig) {
|
||||||
// This is the second node selected
|
// This is the second node selected
|
||||||
$scope.linkConfig.child = {
|
$scope.linkConfig.child = {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ require('moment');
|
|||||||
require('rrule');
|
require('rrule');
|
||||||
require('sprintf-js');
|
require('sprintf-js');
|
||||||
require('reconnectingwebsocket');
|
require('reconnectingwebsocket');
|
||||||
|
global.dagre = require('dagre');
|
||||||
|
|
||||||
// D3 + extensions
|
// D3 + extensions
|
||||||
require('d3');
|
require('d3');
|
||||||
|
|||||||
17
awx/ui/package-lock.json
generated
17
awx/ui/package-lock.json
generated
@@ -3195,6 +3195,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz",
|
"resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz",
|
||||||
"integrity": "sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g="
|
"integrity": "sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g="
|
||||||
},
|
},
|
||||||
|
"dagre": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-TEOOGZOkCOgCG7AoUIq64sJ3d21SMv8tyoqteLpX+UsUsS9Qw8iap4hhogXY4oB3r0bbZuAjO0atAilgCmsE0Q==",
|
||||||
|
"requires": {
|
||||||
|
"graphlib": "^2.1.5",
|
||||||
|
"lodash": "^4.17.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"dashdash": {
|
"dashdash": {
|
||||||
"version": "1.14.1",
|
"version": "1.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||||
@@ -6185,6 +6194,14 @@
|
|||||||
"integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=",
|
"integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"graphlib": {
|
||||||
|
"version": "2.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.5.tgz",
|
||||||
|
"integrity": "sha512-XvtbqCcw+EM5SqQrIetIKKD+uZVNQtDPD1goIg7K73RuRZtVI5rYMdcCVSHm/AS1sCBZ7vt0p5WgXouucHQaOA==",
|
||||||
|
"requires": {
|
||||||
|
"lodash": "^4.11.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"growl": {
|
"growl": {
|
||||||
"version": "1.9.2",
|
"version": "1.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz",
|
||||||
|
|||||||
@@ -117,6 +117,7 @@
|
|||||||
"codemirror": "^5.17.0",
|
"codemirror": "^5.17.0",
|
||||||
"components-font-awesome": "^4.6.1",
|
"components-font-awesome": "^4.6.1",
|
||||||
"d3": "^3.5.4",
|
"d3": "^3.5.4",
|
||||||
|
"dagre": "^0.8.2",
|
||||||
"hamsterjs": "^1.1.2",
|
"hamsterjs": "^1.1.2",
|
||||||
"html-entities": "^1.2.1",
|
"html-entities": "^1.2.1",
|
||||||
"inherits": "^1.0.2",
|
"inherits": "^1.0.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user