diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js
index 4bbaf8ede5..4521970a7d 100644
--- a/awx/ui/static/js/controllers/JobDetail.js
+++ b/awx/ui/static/js/controllers/JobDetail.js
@@ -26,7 +26,9 @@ function JobDetailController ($scope, $compile, $routeParams, ClearScope, Breadc
$scope.tasks = [];
$scope.hosts = [];
$scope.hostResults = [];
-
+ $scope.job_status = {};
+ $scope.job_id = job_id;
+
// Apply each event to the view
if ($scope.removeEventsReady) {
$scope.removeEventsReady();
@@ -116,10 +118,10 @@ function JobDetailController ($scope, $compile, $routeParams, ClearScope, Breadc
$scope.limit = data.limit;
$scope.verbosity = data.verbosity;
$scope.job_tags = data.job_tags;
- $scope.started = data.started;
- $scope.finished = data.finished;
- $scope.elapsed = data.elapsed;
- $scope.job_status = data.status;
+ //$scope.started = data.started;
+ //$scope.finished = data.finished;
+ //$scope.elapsed = data.elapsed;
+ //$scope.job_status = data.status;
$scope.$emit('JobReady', data.related.job_events + '?page_size=50&order_by=id');
$scope.$emit('GetCredentialNames', data);
})
@@ -141,6 +143,17 @@ function JobDetailController ($scope, $compile, $routeParams, ClearScope, Breadc
id: id
});
};
+
+ $( "#hosts-slider-vertical" ).slider({
+ orientation: "vertical",
+ range: "min",
+ min: 0,
+ max: 100,
+ value: 60,
+ slide: function( event, ui ) {
+ $( "#amount" ).val( ui.value );
+ }
+ });
}
JobDetailController.$inject = [ '$scope', '$compile', '$routeParams', 'ClearScope', 'Breadcrumbs', 'LoadBreadCrumbs', 'GetBasePath', 'Wait',
diff --git a/awx/ui/static/js/helpers/JobDetail.js b/awx/ui/static/js/helpers/JobDetail.js
index e300e407d4..0b163602c6 100644
--- a/awx/ui/static/js/helpers/JobDetail.js
+++ b/awx/ui/static/js/helpers/JobDetail.js
@@ -40,7 +40,8 @@
angular.module('JobDetailHelper', ['Utilities', 'RestServices'])
.factory('DigestEvents', ['UpdatePlayStatus', 'UpdatePlayNoHostsMatched', 'UpdateHostStatus', 'UpdatePlayChild', 'AddHostResult', 'SelectPlay', 'SelectTask',
-function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePlayChild, AddHostResult, SelectPlay, SelectTask) {
+'GetHostCount', 'GetElapsed',
+function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePlayChild, AddHostResult, SelectPlay, SelectTask, GetHostCount, GetElapsed) {
return function(params) {
var scope = params.scope,
@@ -48,10 +49,17 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
events.forEach(function(event) {
var hostCount;
+
+ if (event.event === 'playbook_on_start') {
+ scope.job_status.started = event.created;
+ scope.job_status.status = 'running';
+ }
+
if (event.event === 'playbook_on_play_start') {
scope.plays.push({
id: event.id,
name: event.play,
+ created: event.created,
status: (event.changed) ? 'changed' : (event.failed) ? 'failed' : 'none',
children: []
});
@@ -61,24 +69,30 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
});
}
if (event.event === 'playbook_on_setup') {
- hostCount = (scope.tasks.length > 0) ? scope.tasks[scope.tasks.length - 1].hostCount : 0;
+ hostCount = GetHostCount({
+ scope: scope,
+ play_id: event.parent
+ });
scope.tasks.push({
id: event.id,
name: event.event_display,
play_id: event.parent,
status: (event.failed) ? 'failed' : 'successful',
created: event.created,
+ modified: event.modified,
hostCount: hostCount,
failedCount: 0,
changedCount: 0,
successfulCount: 0,
- skippedCount: 0
+ skippedCount: 0,
+ reportedHosts: 0
});
UpdatePlayStatus({
scope: scope,
play_id: event.parent,
failed: event.failed,
- changed: event.changed
+ changed: event.changed,
+ modified: event.modified
});
SelectTask({
scope: scope,
@@ -86,7 +100,10 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
});
}
if (event.event === 'playbook_on_task_start') {
- hostCount = (scope.tasks.length > 0) ? scope.tasks[scope.tasks.length - 1].hostCount : 0;
+ hostCount = GetHostCount({
+ scope: scope,
+ play_id: event.parent
+ });
scope.tasks.push({
id: event.id,
name: event.task,
@@ -94,11 +111,13 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
status: ( (event.changed) ? 'changed' : (event.failed) ? 'failed' : 'successful' ),
role: event.role,
created: event.created,
+ modified: event.modified,
hostCount: hostCount,
failedCount: 0,
changedCount: 0,
successfulCount: 0,
- skippedCount: 0
+ skippedCount: 0,
+ reportedHosts: 0
});
if (event.role) {
scope.hasRoles = true;
@@ -107,7 +126,8 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
scope: scope,
play_id: event.parent,
failed: event.failed,
- changed: event.changed
+ changed: event.changed,
+ modified: event.modified
});
SelectTask({
scope: scope,
@@ -125,7 +145,9 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
host_id: event.host,
task_id: event.parent,
status: 'unreachable',
- event_id: event.id
+ event_id: event.id,
+ created: event.created,
+ modified: event.modified
});
}
@@ -136,7 +158,9 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
host_id: event.host,
task_id: event.parent,
status: 'failed',
- event_id: event.id
+ event_id: event.id,
+ created: event.created,
+ modified: event.modified
});
}
if (event.event === 'runner_on_skipped') {
@@ -146,7 +170,9 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
host_id: event.host,
task_id: event.parent,
status: 'skipped',
- event_id: event.id
+ event_id: event.id,
+ created: event.created,
+ modified: event.modified
});
}
if (event.event === 'runner_on_ok') {
@@ -155,17 +181,72 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
name: event.event_data.host,
host_id: event.host,
task_id: event.parent,
- status: (event.changed) ? 'changed' : 'ok',
- event_id: event.id
+ status: ( (event.changed) ? 'changed' : (event.failed) ? 'failed' : 'successful' ),
+ event_id: event.id,
+ created: event.created,
+ modified: event.modified
});
}
if (event.event === 'playbook_on_stats') {
-
+ scope.job_status.finished = event.modified;
+ scope.job_status.elapsed = GetElapsed({
+ start: scope.job_status.started,
+ end: scope.job_status.finished
+ });
+ scope.job_status.status = (event.failed) ? 'error' : 'successful';
}
});
};
}])
+.factory('GetHostCount', function() {
+ return function(params) {
+ var scope = params.scope,
+ play_id = params.play_id,
+ tasks = [];
+ // Get the known set of tasks for a given play
+ if (scope.tasks.length > 0) {
+ scope.tasks.forEach(function(task) {
+ if (task.play_id === play_id) {
+ tasks.push(task);
+ }
+ });
+ // sort by ascending event.id
+ if (tasks.length > 0) {
+ tasks.sort(function(a, b) {
+ return a.id - b.id;
+ });
+ return tasks[0].hostCount;
+ }
+ }
+ return 0;
+ };
+})
+
+.factory('FindFirstTaskofPlay', function() {
+ return function(params) {
+ var scope = params.scope,
+ play_id = params.play_id,
+ tasks = [];
+ // Get the known set of tasks for a given play
+ if (scope.tasks.length > 0) {
+ scope.tasks.forEach(function(task) {
+ if (task.play_id === play_id) {
+ tasks.push(task);
+ }
+ });
+ // sort by ascending event.id
+ if (tasks.length > 0) {
+ tasks.sort(function(a, b) {
+ return a.id - b.id;
+ });
+ return tasks[0].id;
+ }
+ }
+ return 0;
+ };
+})
+
.factory('MakeLastRowActive', [ function() {
return function(params) {
var scope = params.scope,
@@ -215,16 +296,51 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
};
}])
+.factory('GetElapsed', [ function() {
+ return function(params) {
+ var start = params.start,
+ end = params.end,
+ dt1, dt2, sec, hours, min;
+ dt1 = new Date(start);
+ dt2 = new Date(end);
+ if ( dt2.getTime() !== dt1.getTime() ) {
+ sec = Math.floor( (dt2.getTime() - dt1.getTime()) / 1000 );
+ hours = Math.floor(sec / 3600);
+ sec = sec - (hours * 3600);
+ if (('' + hours).length < 2) {
+ hours = ('00' + hours).substr(-2, 2);
+ }
+ min = Math.floor(sec / 60);
+ sec = sec - (min * 60);
+ min = ('00' + min).substr(-2,2);
+ sec = ('00' + sec).substr(-2,2);
+ return hours + ':' + min + ':' + sec;
+ }
+ else {
+ return '00:00:00';
+ }
+ };
+}])
+
// Update the status of a play
-.factory('UpdatePlayStatus', [ function() {
+.factory('UpdatePlayStatus', ['GetElapsed', function(GetElapsed) {
return function(params) {
var scope = params.scope,
failed = params.failed,
changed = params.changed,
- id = params.play_id;
+ id = params.play_id,
+ modified = params.modified;
scope.plays.every(function(play,idx) {
if (play.id === id) {
- scope.plays[idx].status = (changed) ? 'changed' : (failed) ? 'failed' : 'successful';
+ if (play.status !== 'changed' && play.status !== 'failed') {
+ // once the status becomes 'changed' or 'failed' don't modify it
+ scope.plays[idx].status = (changed) ? 'changed' : (failed) ? 'failed' : 'successful';
+ }
+ scope.plays[idx].finished = modified;
+ scope.plays[idx].elapsed = GetElapsed({
+ start: play.created,
+ end: modified
+ });
return false;
}
return true;
@@ -232,22 +348,34 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
};
}])
-.factory('UpdateTaskStatus', ['UpdatePlayStatus', function(UpdatePlayStatus) {
+.factory('UpdateTaskStatus', ['UpdatePlayStatus', 'GetElapsed', function(UpdatePlayStatus, GetElapsed) {
return function(params) {
var scope = params.scope,
failed = params.failed,
changed = params.changed,
- id = params.task_id;
+ id = params.task_id,
+ modified = params.modified;
scope.tasks.every(function (task, i) {
if (task.id === id) {
- scope.tasks[i].status = (changed) ? 'changed' : (failed) ? 'failed' : 'successful';
+ if (task.status !== 'changed' && task.status !== 'failed') {
+ // once the status becomes 'changed' or 'failed' don't modify it
+ scope.tasks[i].status = (changed) ? 'changed' : (failed) ? 'failed' : 'successful';
+ }
+ scope.tasks[i].finished = params.modified;
+ scope.tasks[i].elapsed = GetElapsed({
+ start: task.created,
+ end: modified
+ });
UpdatePlayStatus({
scope: scope,
failed: failed,
changed: changed,
- play_id: task.play_id
+ play_id: task.play_id,
+ modified: modified
});
+ return false;
}
+ return true;
});
};
}])
@@ -270,16 +398,18 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
.factory('UpdateHostStatus', ['UpdateTaskStatus', 'AddHostResult', function(UpdateTaskStatus, AddHostResult) {
return function(params) {
var scope = params.scope,
- status = params.status, // ok, changed, unreachable, failed
+ status = params.status, // successful, changed, unreachable, failed, skipped
name = params.name,
event_id = params.event_id,
host_id = params.host_id,
task_id = params.task_id,
+ modified = params.modified,
+ created = params.created,
host_found = false;
scope.hosts.every(function(host, i) {
if (host.id === host_id) {
- scope.hosts[i].ok += (status === 'ok' || status === 'changed') ? 1 : 0;
+ scope.hosts[i].ok += (status === 'successful') ? 1 : 0;
scope.hosts[i].changed += (status === 'changed') ? 1 : 0;
scope.hosts[i].unreachable += (status === 'unreachable') ? 1 : 0;
scope.hosts[i].failed += (status === 'failed') ? 1 : 0;
@@ -293,7 +423,7 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
scope.hosts.push({
id: host_id,
name: name,
- ok: (status === 'ok' || status === 'changed') ? 1 : 0,
+ ok: (status === 'successful') ? 1 : 0,
changed: (status === 'changed') ? 1 : 0,
unreachable: (status === 'unreachable') ? 1 : 0,
failed: (status === 'failed') ? 1 : 0
@@ -304,49 +434,78 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
scope: scope,
task_id: task_id,
failed: (status === 'failed' || status === 'unreachable') ? true :false,
- changed: (status === 'changed') ? true : false
+ changed: (status === 'changed') ? true : false,
+ modified: modified
});
AddHostResult({
scope: scope,
task_id: task_id,
host_id: host_id,
- event_id: event_id
+ event_id: event_id,
+ status: status,
+ name: name,
+ created: created
});
};
}])
// Add a new host result
-.factory('AddHostResult', [ function() {
+.factory('AddHostResult', ['FindFirstTaskofPlay', function(FindFirstTaskofPlay) {
return function(params) {
var scope = params.scope,
task_id = params.task_id,
host_id = params.host_id,
event_id = params.event_id,
- status = params.status;
-
- status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful';
- host_id = event.host;
+ status = params.status,
+ created = params.created,
+ name = params.name,
+ play_id, first;
scope.hostResults.push({
id: event_id,
status: status,
host_id: host_id,
- task_id: event.parent
+ task_id: task_id,
+ name: name,
+ created: created
});
- scope.tasks.forEach(function(task, idx) {
+ scope.tasks.every(function(task) {
if (task.id === task_id) {
- scope.tasks[idx].hostCount += (idx === 0) ? 1 : 0; // we only need to count hosts for the first task in a play
+ play_id = task.play_id;
+ return false;
+ }
+ return true;
+ });
+
+ first = FindFirstTaskofPlay({
+ scope: scope,
+ play_id: play_id
+ });
+
+ scope.tasks.every(function(task, idx) {
+ if (task.id === task_id) {
+ scope.tasks[idx].hostCount += (task.id === first) ? 1 : 0; // we only need to count hosts for the first task in a play
+ scope.tasks[idx].reportedHosts++;
scope.tasks[idx].failedCount += (status === 'failed' || status === 'unreachable') ? 1 : 0;
scope.tasks[idx].changedCount += (status === 'changed') ? 1 : 0;
- scope.tasks[idx].successfulCount += (status === 'successful' || status === 'changed') ? 1 : 0;
+ scope.tasks[idx].successfulCount += (status === 'successful') ? 1 : 0;
scope.tasks[idx].skippedCount += (status === 'skipped') ? 1 : 0;
- scope.tasks[idx].failedPct = 100 * Math.round(scope.tasks[idx].failedCount / scope.tasks[idx].hostCount);
- scope.tasks[idx].changedPct = 100 * (scope.tasks[idx].successfulCount) ? Math.round(scope.tasks[idx].changedCount / scope.tasks[idx].successfulCount) : 0;
- scope.tasks[idx].skippedPct = 100 * Math.round(scope.tasks[idx].skippedCount / scope.tasks[idx].hostCount);
- scope.tasks[idx].successfulPct = 100 * Math.round(scope.tasks[idx].successfulCount / scope.tasks[idx].hostCount);
+
+ scope.tasks[idx].failedPct = (scope.tasks[idx].hostCount > 0) ? 100 * Math.round(scope.tasks[idx].failedCount / scope.tasks[idx].hostCount) : 0;
+ scope.tasks[idx].changedPct = (scope.tasks[idx].hostCount > 0) ? 100 * Math.round(scope.tasks[idx].changedCount / scope.tasks[idx].hostCount) : 0;
+ scope.tasks[idx].skippedPct = (scope.tasks[idx].hostCount > 0) ? 100 * Math.round(scope.tasks[idx].skippedCount / scope.tasks[idx].hostCount) : 0;
+ scope.tasks[idx].successfulPct = (scope.tasks[idx].hostCount > 0) ? 100 * Math.round(scope.tasks[idx].successfulCount / scope.tasks[idx].hostCount) : 0;
+
+ scope.tasks[idx].successfulStyle = (scope.tasks[idx].successfulPct > 0) ? { width: scope.tasks[idx].successfulPct + '%' } : { display: 'none' };
+ scope.tasks[idx].changedStyle = (scope.tasks[idx].changedPct > 0) ? { width: scope.tasks[idx].changedPct + '%' } : { display: 'none' };
+ scope.tasks[idx].skippedStyle = (scope.tasks[idx].skippedPct > 0) ? { width: scope.tasks[idx].skippedPct + '%' } : { display: 'none' };
+ scope.tasks[idx].failedStyle = (scope.tasks[idx].failedPct > 0) ? { width: scope.tasks[idx].failedPct + '%' } : { display: 'none' };
+
+ return false;
}
+ return true;
});
};
}])
diff --git a/awx/ui/static/js/helpers/PaginationHelpers.js b/awx/ui/static/js/helpers/PaginationHelpers.js
index 745cc2c670..22022c16b4 100644
--- a/awx/ui/static/js/helpers/PaginationHelpers.js
+++ b/awx/ui/static/js/helpers/PaginationHelpers.js
@@ -49,11 +49,6 @@ angular.module('PaginationHelpers', ['Utilities', 'RefreshHelper', 'RefreshRelat
for (i = first; i <= last; i++) {
scope[iterator + '_page_range'].push(i);
}
- console.log('first: ' + first);
- console.log('last: ' + last);
- console.log('range: ');
- console.log(scope[iterator + '_page_range']);
- console.log('num_pages: ' + scope[iterator + '_num_pages']);
};
}
])
diff --git a/awx/ui/static/less/job-details.less b/awx/ui/static/less/job-details.less
index a5deb52591..1b89b8d346 100644
--- a/awx/ui/static/less/job-details.less
+++ b/awx/ui/static/less/job-details.less
@@ -7,34 +7,68 @@
*
*/
+#jobs-detail {
+ .nav-path {
+ margin-bottom: 20px;
+ }
+}
+
+.inline-block {
+ display: inline-block;
+ vertical-align: top;
+}
+
+#job-status {
+ margin: 8px 0 15px 0;
+
+ ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ }
+ li {
+ display: inline-block;
+ margin-right: 15px;
+ }
+ i {
+ font-size: 12px;
+ }
+ .label {
+ font-size: 12px;
+ color: @black;
+ padding-left: 0;
+ }
+}
+
.job-detail-table {
margin-bottom: 0;
- border: 1px solid @well;
-
- /**
- The thing that makes the table body scrollable:
- http://kamlekar.wordpress.com/2013/06/17/table-tbody-scroll-cross-browser/comment-page-1/
- **/
- thead {
- display: table;
- float: left;
- width: 100%;
- }
- thead tr {
- display: table-row;
- width: 100%;
+ border: 1px solid @grey;
+ background-color: @white;
+
+ /* http://stackoverflow.com/questions/21168521/scrollable-table-with-fixed-header-in-bootstrap */
+ width: 100%;
+ thead, tbody, tr, td, th { display: block; }
+ tr:after {
+ content: ' ';
+ display: block;
+ visibility: hidden;
+ clear: both;
}
tbody {
- display: block;
- height: 122px;
- width: 100%;
- overflow-y: scroll;
+ overflow-y: auto;
+ height: 150px;
+ }
+ thead {
+ /* fallback */
+ }
+ thead>tr>th {
+ height: 22px;
+ }
+ tbody td, thead th {
+ height: auto;
float: left;
}
- tbody tr {
- display: table;
- width: 100%;
- }
+
tbody>tr>td {
border-top-color: @well;
padding-top: 0;
@@ -45,70 +79,75 @@
padding-bottom: 0;
font-size: 14px;
}
- td.status-column {
- text-align: center;
- font-size: 12px;
- i {
- margin-top: 4px;
- }
+ tbody>tr.active, tbody>tr.active>td {
+ background-color: #EDF2F2;
}
- tbody>tr.active>td {
- background-color: @active-color;
+
+ .status-column i {
+ font-size: 12px;
}
}
.section {
- margin-top: 20px;
+ margin-bottom: 20px;
h5 {
margin-top: 0;
- margin-bottom: 5px;
- }
- .small-title {
- font-weight: normal;
+ margin-bottom: 12px;
}
}
-.job_summary, .job_status {
+.section:last-child {
+ margin-bottom: 0;
+}
+
+.job_summary {
.table {
margin-bottom: 0;
+ border: 1px solid @grey;
+ background-color: @white;
}
.table>tbody>tr>td {
border-top-color: @well;
+ padding-bottom: 0;
}
.table>thead>tr>th {
border-bottom-color: @well;
+ padding-bottom: 0;
+ height: 22px;
}
}
-.job_status {
- margin-bottom: 25px;
-
- .label_column {
- width: 80px;
- }
- .table>tbody>tr>td {
- padding-bottom: 3px;
- }
- .status-column i {
- font-size: 12px;
- }
-}
-
.status-bar {
- display: inline-block;
- height: 15px;
+ height: 16px;
+ overflow: hidden;
+ border-radius: 4px;
+ -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);
+ width: 98%;
+ margin-top: 2px;
}
+
+.inner-bar {
+ display: inline-block;
+ overflow: hidden;
+ height: 16px;
+ text-align: center;
+ font-size: 12px;
+ font-weight: bold;
+ line-height: normal;
+}
+
.failed-hosts {
- background-color: @red;
+ background-color: #DA4D49;
}
.successful-hosts {
- background-color: @green;
+ background-color: #9ED89E;
}
.changed-hosts {
- background-color: @warning;
+ background-color: #FFC773;
}
.skipped-hosts {
- background-color: @grey;
+ background-color: #D4D4D4;
}
.job_well {
@@ -126,8 +165,34 @@
overflow-x: none;
}
-#job_plays, #job_tasks, #host_details {
+#job_plays, #job_tasks {
height: 150px;
overflow-y: auto;
overflow-x: none;
}
+
+#hosts-section {
+ border: 1px solid @grey;
+ border-top: 2px solid #ddd;
+ padding: 5px;
+ height: 150px;
+ background-color: @white;
+}
+
+#host-details {
+ ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ i {
+ font-size: 12px;
+ }
+ }
+ li {
+ border-bottom: 1px solid @well;
+ padding-bottom: 2px;
+ padding-top: 0;
+ margin: 0;
+ line-height: normal;
+ }
+}
diff --git a/awx/ui/static/lib/d3js/.bower.json b/awx/ui/static/lib/d3js/.bower.json
new file mode 100644
index 0000000000..6e2680e72d
--- /dev/null
+++ b/awx/ui/static/lib/d3js/.bower.json
@@ -0,0 +1,14 @@
+{
+ "name": "d3js",
+ "homepage": "https://github.com/henrytao-me/d3js",
+ "_release": "2745db14e3",
+ "_resolution": {
+ "type": "branch",
+ "branch": "master",
+ "commit": "2745db14e37540fc5a9c4ac3b44152bfd7f92e9a"
+ },
+ "_source": "git://github.com/henrytao-me/d3js.git",
+ "_target": "*",
+ "_originalSource": "d3js",
+ "_direct": true
+}
\ No newline at end of file
diff --git a/awx/ui/static/lib/d3js/.gitignore b/awx/ui/static/lib/d3js/.gitignore
new file mode 100644
index 0000000000..a72b52ebe8
--- /dev/null
+++ b/awx/ui/static/lib/d3js/.gitignore
@@ -0,0 +1,15 @@
+lib-cov
+*.seed
+*.log
+*.csv
+*.dat
+*.out
+*.pid
+*.gz
+
+pids
+logs
+results
+
+npm-debug.log
+node_modules
diff --git a/awx/ui/static/lib/d3js/LICENSE b/awx/ui/static/lib/d3js/LICENSE
new file mode 100644
index 0000000000..63e9564f3a
--- /dev/null
+++ b/awx/ui/static/lib/d3js/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2013 Henry Tao
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/awx/ui/static/lib/d3js/README.md b/awx/ui/static/lib/d3js/README.md
new file mode 100644
index 0000000000..9ad31ded1c
--- /dev/null
+++ b/awx/ui/static/lib/d3js/README.md
@@ -0,0 +1,2 @@
+d3js
+====
diff --git a/awx/ui/static/partials/job_detail.html b/awx/ui/static/partials/job_detail.html
index 102ef54703..0efeab2496 100644
--- a/awx/ui/static/partials/job_detail.html
+++ b/awx/ui/static/partials/job_detail.html
@@ -1,168 +1,136 @@
-
+
-
-
-
Job
-
-
+
+
+
+
+ - Status {{ job_status.status }}
+ - Start {{ job_status.started | date:'MM/dd/yy HH:mm:ss' }}
+ - Finish {{ job_status.finished | date:'MM/dd/yy HH:mm:ss' }}
-
+
- Elapsed {{ job_status.elapsed }}
+
+
+
+
-
-
Plays
-
-
-
-
- |
- {{ play.name }} |
+
+
Tasks
+
+
+
+ | Started |
+ Name |
+ Host Status |
+ Elapsed |
+
+
+
+
+ | {{ task.created | date: 'HH:mm:ss' }} |
+
+ {{ task.role }} {{ task.name }}
+ |
+
+
+ {{ task.successfulCount }}
+ {{ task.changedCount }}
+ {{ task.skippedCount }}
+ {{ task.failedCount }}
+
+ |
+ {{ task.elapsed }} |
-
-
+
-
-
Tasks
-
+
+
+
+
+
+
+
+
+
+
+
+
Host Summary
+
- | Started |
- Role |
- Name |
- Status
+ | Host |
+ OK |
+ Changed |
+ Dark |
+ Failed |
-
-
- | {{ task.created | date: 'HH:mm:ss' }} |
- {{ task.role }} |
- {{ task.name }} |
-
-
-
-
- |
+
+
+ | {{ host.name }} |
+ {{ host.ok }} |
+ {{ host.changed }} |
+ {{ host.unreachable }} |
+ {{ host.failed }} |
+
+
-
-
-
-
-
-
-
Summary
-
-
-
-
-
- | Status | {{ job_status }} |
- | Started | {{ started | date:'MM/dd/yy HH:mm:ss' }} |
- | Finished | {{ finished | date:'MM/dd/yy HH:mm:ss' }} |
- | Elapsed | {{ elapsed }} seconds |
-
-
-
-
-
-
-
-
- | Host |
- OK |
- Changed |
- Unreachable |
- Failed |
-
-
-
-
- | {{ host.name }} |
- {{ host.ok }} |
- {{ host.changed }} |
- {{ host.unreachable }} |
- {{ host.failed }} |
-
-
-
-
-
-
-
-
-
diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html
index fbe3a1cbf1..8c48dd1633 100644
--- a/awx/ui/templates/ui/index.html
+++ b/awx/ui/templates/ui/index.html
@@ -399,6 +399,132 @@
+
+