From a895c6beea9ac609f2c833ac5dca5f7c23957b94 Mon Sep 17 00:00:00 2001 From: chouseknecht Date: Thu, 15 May 2014 03:51:34 -0400 Subject: [PATCH] First attempt at adding a graph to job detail page. --- .jshintrc | 2 +- awx/ui/static/js/controllers/JobDetail.js | 9 +- awx/ui/static/js/helpers/JobDetail.js | 114 ++++++++++++++----- awx/ui/static/less/job-details.less | 35 ++++++ awx/ui/static/lib/d3Donut/d3Donut.js | 123 +++++++++++++++++++++ awx/ui/static/partials/job_detail.html | 6 +- awx/ui/templates/ui/index.html | 127 +--------------------- 7 files changed, 260 insertions(+), 156 deletions(-) create mode 100644 awx/ui/static/lib/d3Donut/d3Donut.js diff --git a/.jshintrc b/.jshintrc index 7f9c13c67f..85f82b51c7 100644 --- a/.jshintrc +++ b/.jshintrc @@ -7,7 +7,7 @@ "jquery": true, "esnext": true, "globalstrict": true, - "globals": { "angular":false, "alert":false, "$AnsibleConfig":true, "$basePath":true, "jsyaml":false, "_":true }, + "globals": { "angular":false, "alert":false, "$AnsibleConfig":true, "$basePath":true, "jsyaml":false, "_":false, "d3":false, "Donut3D":false }, "strict": false, "quotmark": false, "smarttabs": true, diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js index 4b23b3a76b..341543da4a 100644 --- a/awx/ui/static/js/controllers/JobDetail.js +++ b/awx/ui/static/js/controllers/JobDetail.js @@ -269,7 +269,14 @@ function JobDetailController ($scope, $compile, $routeParams, ClearScope, Breadc 'box-shadow': 'none', 'height': 'auto' }); - $('#job-summary-container').css({ "width": "41.66666667%", "padding-right": "15px", "z-index": 0 }).show(); + $('#job-summary-container').css({ + "width": "41.66666667%", + "padding-left": "7px", + "padding-right": "15px", + "z-index": 0 + }); + setTimeout(function() { $('#job-summary-container .job_well').height($('#job-detail-container').height() - 18); }, 500); + $('#job-summary-container').show(); } // Detail table height adjusting. First, put page height back to 'normal'. $('#plays-table-detail').height(150); diff --git a/awx/ui/static/js/helpers/JobDetail.js b/awx/ui/static/js/helpers/JobDetail.js index 3ab92bbd30..5afbe80366 100644 --- a/awx/ui/static/js/helpers/JobDetail.js +++ b/awx/ui/static/js/helpers/JobDetail.js @@ -40,9 +40,9 @@ angular.module('JobDetailHelper', ['Utilities', 'RestServices']) .factory('DigestEvents', ['UpdatePlayStatus', 'UpdateHostStatus', 'UpdatePlayChild', 'AddHostResult', 'SelectPlay', 'SelectTask', - 'GetHostCount', 'GetElapsed', 'UpdateTaskStatus', + 'GetHostCount', 'GetElapsed', 'UpdateTaskStatus', 'DrawGraph', function(UpdatePlayStatus, UpdateHostStatus, UpdatePlayChild, AddHostResult, SelectPlay, SelectTask, GetHostCount, GetElapsed, - UpdateTaskStatus) { + UpdateTaskStatus, DrawGraph) { return function(params) { var scope = params.scope, @@ -156,6 +156,7 @@ function(UpdatePlayStatus, UpdateHostStatus, UpdatePlayChild, AddHostResult, Sel scope: scope, id: event.id }); + DrawGraph({ scope: scope }); } if (event.event === 'runner_on_unreachable') { @@ -229,6 +230,7 @@ function(UpdatePlayStatus, UpdateHostStatus, UpdatePlayChild, AddHostResult, Sel }); scope.job_status.status = (event.failed) ? 'failed' : 'successful'; scope.job_status.status_class = ""; + DrawGraph({ scope: scope }); } }); }; @@ -768,18 +770,66 @@ function(UpdatePlayStatus, UpdateHostStatus, UpdatePlayChild, AddHostResult, Sel }; }]) -.factory('GetHostSummary', [ function() { +.factory('DrawGraph', ['Rest', 'GetBasePath', 'ProcessErrors', function(Rest, GetBasePath, ProcessErrors) { + return function(params) { + var scope = params.scope, + dark = 0, failed = 0, changed = 0, ok = 0, + svg_height, svg_width, graph_data, svg, url; -}]) + svg_width = $('#graph-section').width(); + svg_height = 300; + if ($('#graph-section svg').length === 0) { + svg = d3.select("#graph-section").append("svg").attr("width", svg_width).attr("height", svg_height); + } + else { + svg = d3.select("#graph-section svg"); + } + svg.append("g").attr("id","completedHostsDonutNew"); + $('#completedHostsDonutNew').hide(); + + if (scope.removeRenderGraph) { + scope.removeRenderGraph(); + } + scope.removeRenderGraph = scope.$on('RenderGraph', function() { + Donut3D.draw("completedHostsDonutNew", graph_data, Math.floor(svg_width / 2), 150, 130, 100, 15, 0.4); + $('#completedHostsDonut').remove(); + $('#completedHostsDonutNew').attr('id','completedHostsDonut'); + $('#completedHostsDonut').show(); + }); -.factory('DrawGraph', [ function() { - /*var salesData=[ - {label:"OK", color:"#9ED89E"}, - {label:"Changed", color:"#DC3912"}, - {label:"Failed", color:"#DA4D49;"}, - {label:"Skipped", color:"#D4D4D4"}, - {label:"Unreachable", color:""} - ];*/ + url = GetBasePath('jobs') + scope.job_id + '/job_host_summaries/'; + Rest.setUrl(url); + Rest.get() + .success(function(data) { + if (data.count) { + data.results.forEach(function(row) { + if (row.dark) { + dark ++; + } + else if (row.failures) { + failed++; + } + else if (row.changed) { + changed++; + } + else if (row.ok) { + ok++; + } + }); + graph_data = [ + { label: 'OK', value: ok, color: '#9ED89E' }, + { label: 'Changed', value: changed, color: '#FFC773' }, + { label: 'Failed', value: failed, color: '#DA4D49' }, + { label: 'Unreachable', value: dark, color: '#A9A9A9' } + ]; + scope.$emit('RenderGraph'); + } + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + }; }]) .factory('FilterAllByHostName', ['Rest', 'GetBasePath', 'ProcessErrors', 'SelectPlay', function(Rest, GetBasePath, ProcessErrors, SelectPlay) { @@ -787,7 +837,7 @@ function(UpdatePlayStatus, UpdateHostStatus, UpdatePlayChild, AddHostResult, Sel var scope = params.scope, host = params.host, job_id = scope.job_id, - url = GetBasePath('jobs') + job_id + '/job_events/?event__icontains=runner&host_name__icontains=' + host; + url = GetBasePath('jobs') + job_id + '/job_events/?event__icontains=runner&host_name__icontains=' + host + '&parent__isnull=false'; scope.search_all_tasks = []; scope.search_all_plays = []; @@ -811,17 +861,22 @@ function(UpdatePlayStatus, UpdateHostStatus, UpdatePlayChild, AddHostResult, Sel Rest.setUrl(url); Rest.get() .success(function(data) { - data.results.forEach(function(row) { - if (row.parent) { - scope.search_all_plays.push(row.parent); + if (data.count > 0) { + data.results.forEach(function(row) { + if (row.parent) { + scope.search_all_plays.push(row.parent); + } + }); + if (scope.search_all_plays.length > 0) { + scope.search_all_plays.sort(); + scope.activePlay = scope.search_all_plays[scope.search_all_plays.length - 1]; + } + else { + scope.activePlay = null; } - }); - if (scope.search_all_plays.length > 0) { - scope.search_all_plays.sort(); - scope.activePlay = scope.search_all_plays[scope.search_all_plays.length - 1]; } else { - scope.activePlay = null; + scope.search_all_plays.push(0); } scope.$emit('AllPlaysReady'); }) @@ -834,13 +889,18 @@ function(UpdatePlayStatus, UpdateHostStatus, UpdatePlayChild, AddHostResult, Sel Rest.setUrl(url); Rest.get() .success(function(data) { - data.results.forEach(function(row) { - if (row.parent) { - scope.search_all_tasks.push(row.parent); + if (data.count > 0) { + data.results.forEach(function(row) { + if (row.parent) { + scope.search_all_tasks.push(row.parent); + } + }); + if (scope.search_all_tasks.length > 0) { + scope.search_all_tasks.sort(); } - }); - if (scope.search_all_tasks.length > 0) { - scope.search_all_tasks.sort(); + } + else { + scope.search_all_tasks.push(0); } scope.$emit('AllTasksReady'); }) diff --git a/awx/ui/static/less/job-details.less b/awx/ui/static/less/job-details.less index 8f026738ed..1107c2b5ba 100644 --- a/awx/ui/static/less/job-details.less +++ b/awx/ui/static/less/job-details.less @@ -142,6 +142,9 @@ padding-left: 15px; padding-right: 7px; width: 58.33333333%; + .well { + overflow: hidden; + } } #job-summary-container { @@ -303,3 +306,35 @@ label.small-label { .mCSB_container { margin-right: 18px; } + + +#graph-section { + position: relative; + .legend { + font-size: 12px; + margin-top: 10px; + } + i { + margin-left: 5px; + } + i:first-child { + margin-left: 0; + } +} + +path.slice{ + stroke-width:2px; +} + +polyline{ + opacity: .3; + stroke: black; + stroke-width: 2px; + fill: none; +} + +svg text.percent{ + fill:white; + text-anchor:middle; + font-size:12px; +} diff --git a/awx/ui/static/lib/d3Donut/d3Donut.js b/awx/ui/static/lib/d3Donut/d3Donut.js new file mode 100644 index 0000000000..b31f205abb --- /dev/null +++ b/awx/ui/static/lib/d3Donut/d3Donut.js @@ -0,0 +1,123 @@ +!function(){ + var Donut3D={}; + + function pieTop(d, rx, ry, ir ){ + if(d.endAngle - d.startAngle == 0 ) return "M 0 0"; + var sx = rx*Math.cos(d.startAngle), + sy = ry*Math.sin(d.startAngle), + ex = rx*Math.cos(d.endAngle), + ey = ry*Math.sin(d.endAngle); + + var ret =[]; + ret.push("M",sx,sy,"A",rx,ry,"0",(d.endAngle-d.startAngle > Math.PI? 1: 0),"1",ex,ey,"L",ir*ex,ir*ey); + ret.push("A",ir*rx,ir*ry,"0",(d.endAngle-d.startAngle > Math.PI? 1: 0), "0",ir*sx,ir*sy,"z"); + return ret.join(" "); + } + + function pieOuter(d, rx, ry, h ){ + var startAngle = (d.startAngle > Math.PI ? Math.PI : d.startAngle); + var endAngle = (d.endAngle > Math.PI ? Math.PI : d.endAngle); + + var sx = rx*Math.cos(startAngle), + sy = ry*Math.sin(startAngle), + ex = rx*Math.cos(endAngle), + ey = ry*Math.sin(endAngle); + + var ret =[]; + ret.push("M",sx,h+sy,"A",rx,ry,"0 0 1",ex,h+ey,"L",ex,ey,"A",rx,ry,"0 0 0",sx,sy,"z"); + return ret.join(" "); + } + + function pieInner(d, rx, ry, h, ir ){ + var startAngle = (d.startAngle < Math.PI ? Math.PI : d.startAngle); + var endAngle = (d.endAngle < Math.PI ? Math.PI : d.endAngle); + + var sx = ir*rx*Math.cos(startAngle), + sy = ir*ry*Math.sin(startAngle), + ex = ir*rx*Math.cos(endAngle), + ey = ir*ry*Math.sin(endAngle); + + var ret =[]; + ret.push("M",sx, sy,"A",ir*rx,ir*ry,"0 0 1",ex,ey, "L",ex,h+ey,"A",ir*rx, ir*ry,"0 0 0",sx,h+sy,"z"); + return ret.join(" "); + } + + function getPercent(d){ + return (d.endAngle-d.startAngle > 0.2 ? + Math.round(1000*(d.endAngle-d.startAngle)/(Math.PI*2))/10+'%' : ''); + } + + Donut3D.transition = function(id, data, rx, ry, h, ir){ + function arcTweenInner(a) { + var i = d3.interpolate(this._current, a); + this._current = i(0); + return function(t) { return pieInner(i(t), rx+0.5, ry+0.5, h, ir); }; + } + function arcTweenTop(a) { + var i = d3.interpolate(this._current, a); + this._current = i(0); + return function(t) { return pieTop(i(t), rx, ry, ir); }; + } + function arcTweenOuter(a) { + var i = d3.interpolate(this._current, a); + this._current = i(0); + return function(t) { return pieOuter(i(t), rx-.5, ry-.5, h); }; + } + function textTweenX(a) { + var i = d3.interpolate(this._current, a); + this._current = i(0); + return function(t) { return 0.6*rx*Math.cos(0.5*(i(t).startAngle+i(t).endAngle)); }; + } + function textTweenY(a) { + var i = d3.interpolate(this._current, a); + this._current = i(0); + return function(t) { return 0.6*rx*Math.sin(0.5*(i(t).startAngle+i(t).endAngle)); }; + } + + var _data = d3.layout.pie().sort(null).value(function(d) {return d.value;})(data); + + d3.select("#"+id).selectAll(".innerSlice").data(_data) + .transition().duration(750).attrTween("d", arcTweenInner); + + d3.select("#"+id).selectAll(".topSlice").data(_data) + .transition().duration(750).attrTween("d", arcTweenTop); + + d3.select("#"+id).selectAll(".outerSlice").data(_data) + .transition().duration(750).attrTween("d", arcTweenOuter); + + d3.select("#"+id).selectAll(".percent").data(_data).transition().duration(750) + .attrTween("x",textTweenX).attrTween("y",textTweenY).text(getPercent); + } + + Donut3D.draw=function(id, data, x /*center x*/, y/*center y*/, + rx/*radius x*/, ry/*radius y*/, h/*height*/, ir/*inner radius*/){ + + var _data = d3.layout.pie().sort(null).value(function(d) {return d.value;})(data); + + var slices = d3.select("#"+id).append("g").attr("transform", "translate(" + x + "," + y + ")") + .attr("class", "slices"); + + slices.selectAll(".innerSlice").data(_data).enter().append("path").attr("class", "innerSlice") + .style("fill", function(d) { return d3.hsl(d.data.color).darker(0.7); }) + .attr("d",function(d){ return pieInner(d, rx+0.5,ry+0.5, h, ir);}) + .each(function(d){this._current=d;}); + + slices.selectAll(".topSlice").data(_data).enter().append("path").attr("class", "topSlice") + .style("fill", function(d) { return d.data.color; }) + .style("stroke", function(d) { return d.data.color; }) + .attr("d",function(d){ return pieTop(d, rx, ry, ir);}) + .each(function(d){this._current=d;}); + + slices.selectAll(".outerSlice").data(_data).enter().append("path").attr("class", "outerSlice") + .style("fill", function(d) { return d3.hsl(d.data.color).darker(0.7); }) + .attr("d",function(d){ return pieOuter(d, rx-.5,ry-.5, h);}) + .each(function(d){this._current=d;}); + + slices.selectAll(".percent").data(_data).enter().append("text").attr("class", "percent") + .attr("x",function(d){ return 0.6*rx*Math.cos(0.5*(d.startAngle+d.endAngle));}) + .attr("y",function(d){ return 0.6*ry*Math.sin(0.5*(d.startAngle+d.endAngle));}) + .text(getPercent).each(function(d){this._current=d;}); + } + + this.Donut3D = Donut3D; +}(); \ No newline at end of file diff --git a/awx/ui/static/partials/job_detail.html b/awx/ui/static/partials/job_detail.html index f6666bf984..320d06dc2f 100644 --- a/awx/ui/static/partials/job_detail.html +++ b/awx/ui/static/partials/job_detail.html @@ -204,7 +204,11 @@
- +
+
Host Status Overview
+
Successful Changed + Unreachable Failed
+
diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index 491fff95ef..7fb0c2ed1d 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -405,132 +405,7 @@ - - +