diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/main.js b/awx/ui/client/src/templates/workflows/workflow-chart/main.js index 2b1851a972..12379f4b8f 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/main.js +++ b/awx/ui/client/src/templates/workflows/workflow-chart/main.js @@ -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); 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 cb4b392e7c..eca1f5b546 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 @@ -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); }) diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.service.js b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.service.js new file mode 100644 index 0000000000..4c51ecc1d1 --- /dev/null +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.service.js @@ -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 + }; + } + }; +}]; 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 42b159eebc..0bb164ea95 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 @@ -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, { diff --git a/awx/ui/client/src/workflow-results/workflow-results.controller.js b/awx/ui/client/src/workflow-results/workflow-results.controller.js index 3b297cc073..2e30bda4be 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.controller.js +++ b/awx/ui/client/src/workflow-results/workflow-results.controller.js @@ -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 = {