1
0
mirror of https://github.com/ansible/awx.git synced 2024-11-01 16:51:11 +03:00

First pass at implementing better node placement in the workflow graph

This commit is contained in:
mabashian 2018-11-08 11:32:05 -05:00
parent 7b95d2114d
commit 61fb3eb390
5 changed files with 207 additions and 183 deletions

View File

@ -5,7 +5,9 @@
*************************************************/
import workflowChart from './workflow-chart.directive';
import workflowChartService from './workflow-chart.service';
export default
angular.module('workflowChart', [])
.directive('workflowChart', workflowChart);
.directive('workflowChart', workflowChart)
.service('WorkflowChartService', workflowChartService);

View File

@ -56,7 +56,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
function init() {
force = d3.layout.force()
.gravity(0)
.charge(-60)
.charge(-300)
.linkDistance(300)
.size([windowHeight, windowWidth]);
@ -1003,6 +1003,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
}
});
// TODO: this
// if(scope.treeState.arrayOfNodesForChart && scope.treeState.arrayOfNodesForChart > 1 && !graphLoaded) {
// zoomToFitChart();
// }
@ -1010,14 +1011,14 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
graphLoaded = true;
// 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();
let tick = (e) => {
var k = 6 * e.alpha;
// TODO: replace hard-coded 60 here
let tick = () => {
linkLines
.each(function(d) { d.source.y -= k; d.target.y += k; })
.each(function(d) {
d.target.y = scope.treeState.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); })

View File

@ -0,0 +1,144 @@
export default [function(){
return {
generateDepthMap: (arrayOfLinks) => {
let depthMap = {};
let nodesWithChildren = {};
let walkBranch = (nodeId, depth) => {
depthMap[nodeId] = depthMap[nodeId] ? (depth > depthMap[nodeId] ? depth : depthMap[nodeId]) : depth;
if (nodesWithChildren[nodeId]) {
_.forEach(nodesWithChildren[nodeId].children, (childNodeId) => {
walkBranch(childNodeId, depth+1);
});
}
};
let rootNodeIds = [];
arrayOfLinks.forEach(link => {
// link.source.index of 0 is our artificial start node
if (link.source.index !== 0) {
if (!nodesWithChildren[link.source.id]) {
nodesWithChildren[link.source.id] = {
children: []
};
}
nodesWithChildren[link.source.id].children.push(link.target.id);
} else {
// Store the fact that might be a root node
rootNodeIds.push(link.target.id);
}
});
_.forEach(rootNodeIds, function(rootNodeId) {
walkBranch(rootNodeId, 1);
depthMap[rootNodeId] = 1;
});
return depthMap;
},
generateArraysOfNodesAndLinks: function(allNodes) {
let nonRootNodeIds = [];
let allNodeIds = [];
let arrayOfLinksForChart = [];
let nodeIdToChartNodeIdMapping = {};
let chartNodeIdToIndexMapping = {};
let nodeRef = {};
let nodeIdCounter = 1;
let arrayOfNodesForChart = [
{
index: 0,
id: nodeIdCounter,
isStartNode: true,
unifiedJobTemplate: {
name: "START"
},
fixed: true,
x: 0,
y: 0
}
];
nodeIdCounter++;
// Assign each node an ID - 0 is reserved for the start node. We need to
// make sure that we have an ID on every node including new nodes so the
// ID returned by the api won't do
allNodes.forEach((node) => {
node.workflowMakerNodeId = nodeIdCounter;
nodeRef[nodeIdCounter] = {
originalNodeObject: node
};
const nodeObj = {
index: nodeIdCounter-1,
id: nodeIdCounter
};
if(node.summary_fields.job) {
nodeObj.job = node.summary_fields.job;
}
if(node.summary_fields.unified_job_template) {
nodeObj.unifiedJobTemplate = node.summary_fields.unified_job_template;
}
arrayOfNodesForChart.push(nodeObj);
allNodeIds.push(node.id);
nodeIdToChartNodeIdMapping[node.id] = node.workflowMakerNodeId;
chartNodeIdToIndexMapping[nodeIdCounter] = nodeIdCounter-1;
nodeIdCounter++;
});
allNodes.forEach((node) => {
const sourceIndex = chartNodeIdToIndexMapping[node.workflowMakerNodeId];
node.success_nodes.forEach((nodeId) => {
const targetIndex = chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[nodeId]];
arrayOfLinksForChart.push({
source: arrayOfNodesForChart[sourceIndex],
target: arrayOfNodesForChart[targetIndex],
edgeType: "success"
});
nonRootNodeIds.push(nodeId);
});
node.failure_nodes.forEach((nodeId) => {
const targetIndex = chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[nodeId]];
arrayOfLinksForChart.push({
source: arrayOfNodesForChart[sourceIndex],
target: arrayOfNodesForChart[targetIndex],
edgeType: "failure"
});
nonRootNodeIds.push(nodeId);
});
node.always_nodes.forEach((nodeId) => {
const targetIndex = chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[nodeId]];
arrayOfLinksForChart.push({
source: arrayOfNodesForChart[sourceIndex],
target: arrayOfNodesForChart[targetIndex],
edgeType: "always"
});
nonRootNodeIds.push(nodeId);
});
});
let uniqueNonRootNodeIds = Array.from(new Set(nonRootNodeIds));
let rootNodes = _.difference(allNodeIds, uniqueNonRootNodeIds);
rootNodes.forEach((rootNodeId) => {
const targetIndex = chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[rootNodeId]];
arrayOfLinksForChart.push({
source: arrayOfNodesForChart[0],
target: arrayOfNodesForChart[targetIndex],
edgeType: "always"
});
});
return {
arrayOfNodesForChart,
arrayOfLinksForChart,
chartNodeIdToIndexMapping,
nodeIdToChartNodeIdMapping,
nodeRef,
workflowMakerNodeIdCounter: nodeIdCounter
};
}
};
}];

View File

@ -6,10 +6,10 @@
export default ['$scope', 'TemplatesService',
'ProcessErrors', 'CreateSelect2', '$q', 'JobTemplateModel',
'Empty', 'PromptService', 'Rest', 'TemplatesStrings',
'Empty', 'PromptService', 'Rest', 'TemplatesStrings', 'WorkflowChartService',
function ($scope, TemplatesService,
ProcessErrors, CreateSelect2, $q, JobTemplate,
Empty, PromptService, Rest, TemplatesStrings) {
Empty, PromptService, Rest, TemplatesStrings, WorkflowChartService) {
$scope.strings = TemplatesStrings;
// TODO: I don't think this needs to be on scope but changing it will require changes to
@ -19,13 +19,10 @@ export default ['$scope', 'TemplatesService',
let credentialRequests = [];
let deletedNodeIds = [];
let workflowMakerNodeIdCounter = 1;
let nodeIdToMakerIdMapping = {};
let nodeIdToChartNodeIdMapping = {};
let chartNodeIdToIndexMapping = {};
let nodeRef = {};
// TODO: fix this
$scope.totalNodes = 0;
$scope.showKey = false;
$scope.toggleKey = () => $scope.showKey = !$scope.showKey;
$scope.keyClassList = `{ 'Key-menuIcon--active': showKey }`;
@ -108,7 +105,7 @@ export default ['$scope', 'TemplatesService',
}).then(({data}) => {
nodeRef[workflowMakerNodeId].originalNodeObject = data;
// TODO: do we need this?
nodeIdToMakerIdMapping[data.id] = parseInt(workflowMakerNodeId);
nodeIdToChartNodeIdMapping[data.id] = parseInt(workflowMakerNodeId);
// if (_.get(params, 'node.promptData.launchConf.ask_credential_on_launch')) {
// // This finds the credentials that were selected in the prompt but don't occur
// // in the template defaults
@ -222,8 +219,8 @@ export default ['$scope', 'TemplatesService',
Object.keys(linkMap).map((sourceNodeId) => {
Object.keys(linkMap[sourceNodeId]).map((targetNodeId) => {
const foo = nodeIdToMakerIdMapping[sourceNodeId];
const bar = nodeIdToMakerIdMapping[targetNodeId];
const foo = nodeIdToChartNodeIdMapping[sourceNodeId];
const bar = nodeIdToChartNodeIdMapping[targetNodeId];
switch(linkMap[sourceNodeId][targetNodeId]) {
case "success":
if (
@ -340,6 +337,8 @@ export default ['$scope', 'TemplatesService',
workflowMakerNodeIdCounter++;
$scope.treeState.depthMap = WorkflowChartService.generateDepthMap($scope.treeState.arrayOfLinksForChart);
$scope.$broadcast("refreshWorkflowChart");
$scope.formState.showNodeForm = true;
@ -381,6 +380,8 @@ export default ['$scope', 'TemplatesService',
workflowMakerNodeIdCounter++;
$scope.treeState.depthMap = WorkflowChartService.generateDepthMap($scope.treeState.arrayOfLinksForChart);
$scope.$broadcast("refreshWorkflowChart");
$scope.formState.showNodeForm = true;
@ -476,6 +477,8 @@ export default ['$scope', 'TemplatesService',
chartNodeIdToIndexMapping[key]--;
}
}
$scope.treeState.depthMap = WorkflowChartService.generateDepthMap($scope.treeState.arrayOfLinksForChart);
} else if ($scope.nodeConfig.mode === "edit") {
$scope.treeState.arrayOfNodesForChart.map( (node) => {
if (node.index === $scope.nodeConfig.nodeId) {
@ -562,6 +565,7 @@ export default ['$scope', 'TemplatesService',
// User is going from editing one link to editing another
if ($scope.linkConfig.mode === "add") {
$scope.treeState.arrayOfLinksForChart.splice($scope.treeState.arrayOfLinksForChart.length-1, 1);
$scope.treeState.depthMap = WorkflowChartService.generateDepthMap($scope.treeState.arrayOfLinksForChart);
}
$scope.treeState.arrayOfLinksForChart.forEach((link) => {
link.isLinkBeingEdited = false;
@ -575,7 +579,6 @@ export default ['$scope', 'TemplatesService',
};
$scope.selectNodeForLinking = (node) => {
// start here
if ($scope.linkConfig) {
// This is the second node selected
$scope.linkConfig.child = {
@ -595,6 +598,14 @@ export default ['$scope', 'TemplatesService',
isLinkBeingEdited: true
});
$scope.treeState.arrayOfLinksForChart.forEach((link, index) => {
if (link.source.id === 1 && link.target.id === node.id) {
$scope.treeState.arrayOfLinksForChart.splice(index, 1);
}
});
$scope.treeState.depthMap = WorkflowChartService.generateDepthMap($scope.treeState.arrayOfLinksForChart);
$scope.treeState.isLinkMode = false;
} else {
// This is the first node selected
@ -681,6 +692,8 @@ export default ['$scope', 'TemplatesService',
}
}
$scope.treeState.depthMap = WorkflowChartService.generateDepthMap($scope.treeState.arrayOfLinksForChart);
$scope.formState.showLinkForm = false;
$scope.linkConfig = null;
$scope.$broadcast("refreshWorkflowChart");
@ -689,6 +702,21 @@ export default ['$scope', 'TemplatesService',
$scope.cancelLinkForm = () => {
if ($scope.linkConfig.mode === "add" && $scope.linkConfig.child) {
$scope.treeState.arrayOfLinksForChart.splice($scope.treeState.arrayOfLinksForChart.length-1, 1);
let targetIsOrphaned = true;
$scope.treeState.arrayOfLinksForChart.forEach((link) => {
if (link.target.id === $scope.linkConfig.child.id) {
targetIsOrphaned = false;
}
});
if (targetIsOrphaned) {
// Link it to the start node
$scope.treeState.arrayOfLinksForChart.push({
source: $scope.treeState.arrayOfNodesForChart[0],
target: $scope.treeState.arrayOfNodesForChart[chartNodeIdToIndexMapping[$scope.linkConfig.child.id]],
edgeType: "always"
});
}
$scope.treeState.depthMap = WorkflowChartService.generateDepthMap($scope.treeState.arrayOfLinksForChart);
}
$scope.treeState.addLinkSource = null;
$scope.treeState.isLinkMode = false;
@ -779,6 +807,8 @@ export default ['$scope', 'TemplatesService',
}
}
$scope.treeState.depthMap = WorkflowChartService.generateDepthMap($scope.treeState.arrayOfLinksForChart);
$scope.nodeToBeDeleted = null;
$scope.deleteOverlayVisible = false;
@ -832,87 +862,14 @@ export default ['$scope', 'TemplatesService',
page++;
getNodes();
} else {
let nonRootNodeIds = [];
let allNodeIds = [];
let arrayOfLinksForChart = [];
let arrayOfNodesForChart = [
{
index: 0,
id: workflowMakerNodeIdCounter,
isStartNode: true,
unifiedJobTemplate: {
name: "START"
},
fixed: true,
x: 0,
y: 0
}
];
workflowMakerNodeIdCounter++;
// Assign each node an ID - 0 is reserved for the start node. We need to
// make sure that we have an ID on every node including new nodes so the
// ID returned by the api won't do
allNodes.forEach((node) => {
node.workflowMakerNodeId = workflowMakerNodeIdCounter;
nodeRef[workflowMakerNodeIdCounter] = {
originalNodeObject: node
};
arrayOfNodesForChart.push({
index: workflowMakerNodeIdCounter-1,
id: workflowMakerNodeIdCounter,
unifiedJobTemplate: node.summary_fields.unified_job_template
});
allNodeIds.push(node.id);
nodeIdToMakerIdMapping[node.id] = node.workflowMakerNodeId;
chartNodeIdToIndexMapping[workflowMakerNodeIdCounter] = workflowMakerNodeIdCounter-1;
workflowMakerNodeIdCounter++;
});
let arrayOfNodesForChart = [];
allNodes.forEach((node) => {
const sourceIndex = chartNodeIdToIndexMapping[node.workflowMakerNodeId];
node.success_nodes.forEach((nodeId) => {
const targetIndex = chartNodeIdToIndexMapping[nodeIdToMakerIdMapping[nodeId]];
arrayOfLinksForChart.push({
source: arrayOfNodesForChart[sourceIndex],
target: arrayOfNodesForChart[targetIndex],
edgeType: "success"
});
nonRootNodeIds.push(nodeId);
});
node.failure_nodes.forEach((nodeId) => {
const targetIndex = chartNodeIdToIndexMapping[nodeIdToMakerIdMapping[nodeId]];
arrayOfLinksForChart.push({
source: arrayOfNodesForChart[sourceIndex],
target: arrayOfNodesForChart[targetIndex],
edgeType: "failure"
});
nonRootNodeIds.push(nodeId);
});
node.always_nodes.forEach((nodeId) => {
const targetIndex = chartNodeIdToIndexMapping[nodeIdToMakerIdMapping[nodeId]];
arrayOfLinksForChart.push({
source: arrayOfNodesForChart[sourceIndex],
target: arrayOfNodesForChart[targetIndex],
edgeType: "always"
});
nonRootNodeIds.push(nodeId);
});
});
({arrayOfNodesForChart, arrayOfLinksForChart, chartNodeIdToIndexMapping, nodeIdToChartNodeIdMapping, nodeRef, workflowMakerNodeIdCounter} = WorkflowChartService.generateArraysOfNodesAndLinks(allNodes));
let uniqueNonRootNodeIds = Array.from(new Set(nonRootNodeIds));
let depthMap = WorkflowChartService.generateDepthMap(arrayOfLinksForChart);
let rootNodes = _.difference(allNodeIds, uniqueNonRootNodeIds);
rootNodes.forEach((rootNodeId) => {
const targetIndex = chartNodeIdToIndexMapping[nodeIdToMakerIdMapping[rootNodeId]];
arrayOfLinksForChart.push({
source: arrayOfNodesForChart[0],
target: arrayOfNodesForChart[targetIndex],
edgeType: "always"
});
});
$scope.treeState = { arrayOfNodesForChart, arrayOfLinksForChart };
$scope.treeState = { arrayOfNodesForChart, arrayOfLinksForChart, depthMap };
}
}, function ({ data, status, config }) {
ProcessErrors($scope, data, status, null, {

View File

@ -1,12 +1,12 @@
export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
'jobLabels', 'workflowNodes', '$scope', 'ParseTypeChange',
'ParseVariableString', 'count', '$state', 'i18n',
'ParseVariableString', 'count', '$state', 'i18n', 'WorkflowChartService',
'moment', function(workflowData, workflowResultsService,
workflowDataOptions, jobLabels, workflowNodes, $scope, ParseTypeChange,
ParseVariableString, count, $state, i18n, moment) {
ParseVariableString, count, $state, i18n, WorkflowChartService,
moment) {
var runTimeElapsedTimer = null;
let workflowMakerNodeIdCounter = 1;
let nodeIdToMakerIdMapping = {};
let nodeIdToChartNodeIdMapping = {};
let chartNodeIdToIndexMapping = {};
var getLinks = function() {
@ -170,93 +170,14 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
// Click binding for the expand/collapse button on the standard out log
$scope.stdoutFullScreen = false;
let nonRootNodeIds = [];
let allNodeIds = [];
let arrayOfLinksForChart = [];
let arrayOfNodesForChart = [
{
index: 0,
id: workflowMakerNodeIdCounter,
isStartNode: true,
unifiedJobTemplate: {
name: "START"
},
fixed: true,
x: 0,
y: 0
}
];
let arrayOfNodesForChart = [];
workflowMakerNodeIdCounter++;
// Assign each node an ID - 0 is reserved for the start node. We need to
// make sure that we have an ID on every node including new nodes so the
// ID returned by the api won't do
workflowNodes.forEach((node) => {
node.workflowMakerNodeId = workflowMakerNodeIdCounter;
const nodeObj = {
index: workflowMakerNodeIdCounter-1,
id: workflowMakerNodeIdCounter,
unifiedJobTemplate: node.summary_fields.unified_job_template
};
if(node.summary_fields.job) {
nodeObj.job = node.summary_fields.job;
}
if(node.summary_fields.unified_job_template) {
nodeObj.unifiedJobTemplate = node.summary_fields.unified_job_template;
}
arrayOfNodesForChart.push(nodeObj);
allNodeIds.push(node.id);
nodeIdToMakerIdMapping[node.id] = node.workflowMakerNodeId;
chartNodeIdToIndexMapping[workflowMakerNodeIdCounter] = workflowMakerNodeIdCounter-1;
workflowMakerNodeIdCounter++;
});
({arrayOfNodesForChart, arrayOfLinksForChart, chartNodeIdToIndexMapping, nodeIdToChartNodeIdMapping} = WorkflowChartService.generateArraysOfNodesAndLinks(workflowNodes));
workflowNodes.forEach((node) => {
const sourceIndex = chartNodeIdToIndexMapping[node.workflowMakerNodeId];
node.success_nodes.forEach((nodeId) => {
const targetIndex = chartNodeIdToIndexMapping[nodeIdToMakerIdMapping[nodeId]];
arrayOfLinksForChart.push({
source: arrayOfNodesForChart[sourceIndex],
target: arrayOfNodesForChart[targetIndex],
edgeType: "success"
});
nonRootNodeIds.push(nodeId);
});
node.failure_nodes.forEach((nodeId) => {
const targetIndex = chartNodeIdToIndexMapping[nodeIdToMakerIdMapping[nodeId]];
arrayOfLinksForChart.push({
source: arrayOfNodesForChart[sourceIndex],
target: arrayOfNodesForChart[targetIndex],
edgeType: "failure"
});
nonRootNodeIds.push(nodeId);
});
node.always_nodes.forEach((nodeId) => {
const targetIndex = chartNodeIdToIndexMapping[nodeIdToMakerIdMapping[nodeId]];
arrayOfLinksForChart.push({
source: arrayOfNodesForChart[sourceIndex],
target: arrayOfNodesForChart[targetIndex],
edgeType: "always"
});
nonRootNodeIds.push(nodeId);
});
});
let uniqueNonRootNodeIds = Array.from(new Set(nonRootNodeIds));
let rootNodes = _.difference(allNodeIds, uniqueNonRootNodeIds);
rootNodes.forEach((rootNodeId) => {
const targetIndex = chartNodeIdToIndexMapping[nodeIdToMakerIdMapping[rootNodeId]];
arrayOfLinksForChart.push({
source: arrayOfNodesForChart[0],
target: arrayOfNodesForChart[targetIndex],
edgeType: "always"
});
});
$scope.treeState = { arrayOfNodesForChart, arrayOfLinksForChart };
let depthMap = WorkflowChartService.generateDepthMap(arrayOfLinksForChart);
$scope.treeState = { arrayOfNodesForChart, arrayOfLinksForChart, depthMap };
}
$scope.toggleStdoutFullscreen = function() {
@ -356,12 +277,11 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
runTimeElapsedTimer = workflowResultsService.createOneSecondTimer(moment(), updateWorkflowJobElapsedTimer);
}
$scope.treeState.arrayOfNodesForChart[chartNodeIdToIndexMapping[nodeIdToMakerIdMapping[data.workflow_node_id]]].job = {
$scope.treeState.arrayOfNodesForChart[chartNodeIdToIndexMapping[nodeIdToChartNodeIdMapping[data.workflow_node_id]]].job = {
id: data.unified_job_id,
status: data.status
};
$scope.workflow_nodes.forEach(node => {
if(parseInt(node.id) === parseInt(data.workflow_node_id)){
node.summary_fields.job = {