diff --git a/awx/ui/client/src/shared/socket/socket.service.js b/awx/ui/client/src/shared/socket/socket.service.js index b636cc1db8..d38a6e8e66 100644 --- a/awx/ui/client/src/shared/socket/socket.service.js +++ b/awx/ui/client/src/shared/socket/socket.service.js @@ -93,6 +93,12 @@ export default // ex: 'ws-jobs-' str = `ws-${data.group_name}-${data.job}`; } + else if(data.group_name==="workflow_events"){ + // The naming scheme is "ws" then a + // dash (-) and the group_name, then the job ID + // ex: 'ws-jobs-' + str = `ws-${data.group_name}-${data.workflow_job_id}`; + } else if(data.group_name==="ad_hoc_command_events"){ // The naming scheme is "ws" then a // dash (-) and the group_name, then the job ID 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 c6ddb85702..1ee7d1b444 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 @@ -67,3 +67,12 @@ .WorkflowChart-nodeTypeLetter { fill: @default-bg; } +.workflowChart-nodeStatus--running { + fill: @default-icon; +} +.workflowChart-nodeStatus--success { + fill: @default-succ; +} +.workflowChart-nodeStatus--failed { + fill: @default-err; +} 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 fe58bd7eed..aad3fe4034 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 @@ -282,6 +282,36 @@ export default [ d3.select("#node-" + d.id + "-remove") .classed("removeHovering", false); }); + + thisNode.append("circle") + .attr("class", function(d) { + + let statusClass = "WorkflowChart-nodeStatus "; + + switch(d.jobStatus) { + 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; + } + + return statusClass; + }) + .style("display", function(d) { return d.jobStatus ? null : "none"; }) + .attr("cy", 10) + .attr("cx", 10) + .attr("r", 6); } }); @@ -407,7 +437,7 @@ export default [ }); t.selectAll(".linkCircle") - .style("display", function(d) { return (d.source.placeholder || d.target.placeholder) ? "none" : null; }) + .style("display", function(d) { return (d.source.placeholder || d.target.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; }) .attr("cx", function(d) { return (d.target.y + d.source.y + rectW) / 2; }) @@ -416,7 +446,7 @@ export default [ }); t.selectAll(".linkCross") - .style("display", function(d) { return (d.source.placeholder || d.target.placeholder) ? "none" : null; }) + .style("display", function(d) { return (d.source.placeholder || d.target.placeholder || scope.canAddWorkflowJobTemplate === false) ? "none" : null; }) .attr("transform", function(d) { return "translate(" + (d.target.y + d.source.y + rectW) / 2 + "," + (d.target.x + d.source.x + rectH) / 2 + ")"; }); t.selectAll(".rect") @@ -442,6 +472,53 @@ export default [ return (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update")) ? "P" : (d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? "I" : ""); }) .style("display", function(d) { return d.unifiedJobTemplate && (d.unifiedJobTemplate.type === "project" || d.unifiedJobTemplate.unified_job_type === "project_update" || d.unifiedJobTemplate.type === "inventory_source" || d.unifiedJobTemplate.unified_job_type === "inventory_update") ? null : "none"; }); + + t.selectAll(".WorkflowChart-nodeStatus") + .attr("class", function(d) { + + let statusClass = "WorkflowChart-nodeStatus "; + + switch(d.jobStatus) { + 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; + } + + return statusClass; + }) + .style("display", function(d) { return d.jobStatus ? null : "none"; }) + .transition() + .duration(0) + .attr("r", 6) + .each(function(d) { + if(d.jobStatus && (d.jobStatus === "pending" || d.jobStatus === "waiting" || d.jobStatus === "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() { diff --git a/awx/ui/client/src/templates/workflows/workflow.service.js b/awx/ui/client/src/templates/workflows/workflow.service.js index 1d2a102b1a..dff656e17d 100644 --- a/awx/ui/client/src/templates/workflows/workflow.service.js +++ b/awx/ui/client/src/templates/workflows/workflow.service.js @@ -3,8 +3,11 @@ export default [function(){ searchTree: function(params) { // params.element // params.matchingId + // params.byNodeId - if(params.element.id === params.matchingId){ + let thisNodeId = params.byNodeId ? params.element.nodeId : params.element.id; + + if(thisNodeId === params.matchingId){ return params.element; }else if (params.element.children && params.element.children.length > 0){ let result = null; @@ -12,7 +15,8 @@ export default [function(){ _.forEach(params.element.children, function(child) { result = thisService.searchTree({ element: child, - matchingId: params.matchingId + matchingId: params.matchingId, + byNodeId: params.byNodeId ? params.byNodeId : false }); if(result) { return false; @@ -205,7 +209,8 @@ export default [function(){ originalEdge: params.edgeType, originalNodeObj: _.clone(params.nodesObj[params.nodeId]), promptValues: {}, - isRoot: params.isRoot ? params.isRoot : false + isRoot: params.isRoot ? params.isRoot : false, + //jobStatus: 'failed' successful waiting running pending }; params.treeData.data.totalNodes++; @@ -216,6 +221,10 @@ export default [function(){ treeNode.originalParentId = params.parentId; } + if(params.nodesObj[params.nodeId].summary_fields.job) { + treeNode.jobStatus = params.nodesObj[params.nodeId].summary_fields.job.status; + } + // Loop across the success nodes and add them recursively _.forEach(params.nodesObj[params.nodeId].success_nodes, function(successNodeId) { treeNode.children.push(_this.buildBranch({ @@ -250,6 +259,22 @@ export default [function(){ }); return treeNode; + }, + updateStatusOfNode: function(params) { + // params.treeData + // params.nodeId + // params.status + + let matchingNode = this.searchTree({ + element: params.treeData.data, + matchingId: params.nodeId, + byNodeId: true + }); + + if(matchingNode) { + matchingNode.jobStatus = params.status; + } + } }; }]; 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 164ef5d35b..1c4aee2ade 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.controller.js +++ b/awx/ui/client/src/workflow-results/workflow-results.controller.js @@ -74,8 +74,6 @@ export default ['workflowData', // Click binding for the expand/collapse button on the standard out log $scope.stdoutFullScreen = false; - $scope.stdoutArr = []; - $scope.treeData = WorkflowService.buildTree({ workflowNodes: workflowNodes }); @@ -113,92 +111,18 @@ export default ['workflowData', workflowResultsService.relaunchJob($scope); }; - // EVENT STUFF BELOW + init(); - // just putting the event queue on scope so it can be inspected in the - // console - // $scope.event_queue = eventQueue.queue; - // $scope.defersArr = eventQueue.populateDefers; + $scope.$on(`ws-workflow_events-${$scope.workflow.id}`, function(e, data) { - // This is where the async updates to the UI actually happen. - // Flow is event queue munging in the service -> $scope setting in here - // var processEvent = function(event) { - // // put the event in the queue - // eventQueue.populate(event).then(mungedEvent => { - // // make changes to ui based on the event returned from the queue - // if (mungedEvent.changes) { - // mungedEvent.changes.forEach(change => { - // // we've got a change we need to make to the UI! - // // update the necessary scope and make the change - // if (change === 'startTime' && !$scope.workflow.start) { - // $scope.workflow.start = mungedEvent.startTime; - // } - // - // if (change === 'count' && !$scope.countFinished) { - // // for all events that affect the host count, - // // update the status bar as well as the host - // // count badge - // $scope.count = mungedEvent.count; - // $scope.hostCount = getTotalHostCount(mungedEvent - // .count); - // } - // - // if (change === 'playCount') { - // $scope.playCount = mungedEvent.playCount; - // } - // - // if (change === 'taskCount') { - // $scope.taskCount = mungedEvent.taskCount; - // } - // - // if (change === 'finishedTime' && !$scope.workflow.finished) { - // $scope.workflow.finished = mungedEvent.finishedTime; - // } - // - // if (change === 'countFinished') { - // // the playbook_on_stats event actually lets - // // us know that we don't need to iteratively - // // look at event to update the host counts - // // any more. - // $scope.countFinished = true; - // } - // - // if(change === 'stdout'){ - // angular - // .element(".JobResultsStdOut-stdoutContainer") - // .append($compile(mungedEvent - // .stdout)($scope)); - // } - // }); - // } - // - // // the changes have been processed in the ui, mark it in the queue - // eventQueue.markProcessed(event); - // }); - // }; + WorkflowService.updateStatusOfNode({ + treeData: $scope.treeData, + nodeId: data.workflow_node_id, + status: data.status + }); - // PULL! grab completed event data and process each event - // TODO: implement retry logic in case one of these requests fails - // var getEvents = function(url) { - // workflowResultsService.getEvents(url) - // .then(events => { - // events.results.forEach(event => { - // // get the name in the same format as the data - // // coming over the websocket - // event.event_name = event.event; - // processEvent(event); - // }); - // if (events.next) { - // getEvents(events.next); - // } - // }); - // }; - // getEvents($scope.job.related.job_events); - - // // Processing of job_events messages from the websocket - // $scope.$on(`ws-job_events-${$scope.workflow.id}`, function(e, data) { - // processEvent(data); - // }); + $scope.$broadcast("refreshWorkflowChart"); + }); // Processing of job-status messages from the websocket $scope.$on(`ws-jobs`, function(e, data) { @@ -206,6 +130,4 @@ export default ['workflowData', $scope.workflow.status = data.status; } }); - - init(); }]; diff --git a/awx/ui/client/src/workflow-results/workflow-results.partial.html b/awx/ui/client/src/workflow-results/workflow-results.partial.html index 616b035d02..f8266f74f8 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.partial.html +++ b/awx/ui/client/src/workflow-results/workflow-results.partial.html @@ -177,7 +177,7 @@
+ fa icon-job-{{ workflow.status }}"> {{ workflow.name }}
@@ -213,18 +213,6 @@ - - - - -
diff --git a/awx/ui/client/src/workflow-results/workflow-results.route.js b/awx/ui/client/src/workflow-results/workflow-results.route.js index 8e70daf17a..70b54c940d 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.route.js +++ b/awx/ui/client/src/workflow-results/workflow-results.route.js @@ -13,11 +13,12 @@ export default { url: '/workflows/:id', ncyBreadcrumb: { parent: 'jobs', - label: '{{ job.id }} - {{ job.name }}' + label: '{{ workflow.id }} - {{ workflow.name }}' }, data: { socket: { "groups":{ + "jobs": ["status_changed"], "workflow_events": [] } }