diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js
index 132c3bd1ca..4bbaf8ede5 100644
--- a/awx/ui/static/js/controllers/JobDetail.js
+++ b/awx/ui/static/js/controllers/JobDetail.js
@@ -51,6 +51,9 @@ function JobDetailController ($scope, $compile, $routeParams, ClearScope, Breadc
if (data.next) {
$scope.$emit('JobReady', data.next);
}
+ else {
+ Wait('stop');
+ }
})
.error(function(data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
@@ -91,7 +94,9 @@ function JobDetailController ($scope, $compile, $routeParams, ClearScope, Breadc
});
}
});
-
+
+ Wait('start');
+
// Load the job record
Rest.setUrl(GetBasePath('jobs') + job_id + '/');
Rest.get()
diff --git a/awx/ui/static/js/helpers/JobDetail.js b/awx/ui/static/js/helpers/JobDetail.js
index 79d9aa524c..e300e407d4 100644
--- a/awx/ui/static/js/helpers/JobDetail.js
+++ b/awx/ui/static/js/helpers/JobDetail.js
@@ -47,6 +47,7 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
events = params.events;
events.forEach(function(event) {
+ var hostCount;
if (event.event === 'playbook_on_play_start') {
scope.plays.push({
id: event.id,
@@ -60,11 +61,18 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
});
}
if (event.event === 'playbook_on_setup') {
+ hostCount = (scope.tasks.length > 0) ? scope.tasks[scope.tasks.length - 1].hostCount : 0;
scope.tasks.push({
id: event.id,
name: event.event_display,
play_id: event.parent,
- status: (event.failed) ? 'failed' : 'successful'
+ status: (event.failed) ? 'failed' : 'successful',
+ created: event.created,
+ hostCount: hostCount,
+ failedCount: 0,
+ changedCount: 0,
+ successfulCount: 0,
+ skippedCount: 0
});
UpdatePlayStatus({
scope: scope,
@@ -72,21 +80,36 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
failed: event.failed,
changed: event.changed
});
+ SelectTask({
+ scope: scope,
+ id: event.id
+ });
}
if (event.event === 'playbook_on_task_start') {
+ hostCount = (scope.tasks.length > 0) ? scope.tasks[scope.tasks.length - 1].hostCount : 0;
scope.tasks.push({
id: event.id,
name: event.task,
play_id: event.parent,
- status: (event.changed) ? 'changed' : (event.failed) ? 'failed' : 'successful'
+ status: ( (event.changed) ? 'changed' : (event.failed) ? 'failed' : 'successful' ),
+ role: event.role,
+ created: event.created,
+ hostCount: hostCount,
+ failedCount: 0,
+ changedCount: 0,
+ successfulCount: 0,
+ skippedCount: 0
});
+ if (event.role) {
+ scope.hasRoles = true;
+ }
UpdatePlayStatus({
scope: scope,
play_id: event.parent,
failed: event.failed,
changed: event.changed
});
- SelectTask({
+ SelectTask({
scope: scope,
id: event.id
});
@@ -94,9 +117,38 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
/*if (event.event === 'playbook_on_no_hosts_matched') {
UpdatePlayNoHostsMatched({ scope: scope, play_id: event.parent });
}*/
- if (event.event === 'runner_on_failed') {
+
+ if (event.event === 'runner_on_unreachable') {
+ UpdateHostStatus({
+ scope: scope,
+ name: event.event_data.host,
+ host_id: event.host,
+ task_id: event.parent,
+ status: 'unreachable',
+ event_id: event.id
+ });
}
+ if (event.event === 'runner_on_error') {
+ UpdateHostStatus({
+ scope: scope,
+ name: event.event_data.host,
+ host_id: event.host,
+ task_id: event.parent,
+ status: 'failed',
+ event_id: event.id
+ });
+ }
+ if (event.event === 'runner_on_skipped') {
+ UpdateHostStatus({
+ scope: scope,
+ name: event.event_data.host,
+ host_id: event.host,
+ task_id: event.parent,
+ status: 'skipped',
+ event_id: event.id
+ });
+ }
if (event.event === 'runner_on_ok') {
UpdateHostStatus({
scope: scope,
@@ -104,11 +156,7 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
host_id: event.host,
task_id: event.parent,
status: (event.changed) ? 'changed' : 'ok',
- results: (event.res && event.res.results) ? event.res.results : ''
- });
- AddHostResult({
- scope: scope,
- event: event
+ event_id: event.id
});
}
if (event.event === 'playbook_on_stats') {
@@ -218,15 +266,17 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
};
}])
-// Update or add a new host
-.factory('UpdateHostStatus', ['UpdateTaskStatus', function(UpdateTaskStatus) {
+// Update host summary totals and update the task
+.factory('UpdateHostStatus', ['UpdateTaskStatus', 'AddHostResult', function(UpdateTaskStatus, AddHostResult) {
return function(params) {
var scope = params.scope,
status = params.status, // ok, changed, unreachable, failed
name = params.name,
+ event_id = params.event_id,
host_id = params.host_id,
task_id = params.task_id,
host_found = false;
+
scope.hosts.every(function(host, i) {
if (host.id === host_id) {
scope.hosts[i].ok += (status === 'ok' || status === 'changed') ? 1 : 0;
@@ -238,6 +288,7 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
}
return true;
});
+
if (!host_found) {
scope.hosts.push({
id: host_id,
@@ -248,54 +299,54 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
failed: (status === 'failed') ? 1 : 0
});
}
+
UpdateTaskStatus({
scope: scope,
task_id: task_id,
failed: (status === 'failed' || status === 'unreachable') ? true :false,
changed: (status === 'changed') ? true : false
});
+
+ AddHostResult({
+ scope: scope,
+ task_id: task_id,
+ host_id: host_id,
+ event_id: event_id
+ });
};
}])
// Add a new host result
-.factory('AddHostResult', ['Empty', function(Empty) {
+.factory('AddHostResult', [ function() {
return function(params) {
var scope = params.scope,
- event = params.event,
- id, status, host_id, play_name, task_name, module_name, module_args,
- results, rc;
+ task_id = params.task_id,
+ host_id = params.host_id,
+ event_id = params.event_id,
+ status = params.status;
- id = event.id;
status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful';
host_id = event.host;
- play_name = event.play;
- task_name = event.task;
- if (event.event_data.res && event.event_data.res.invocation) {
- module_name = event.event_data.res.invocation.module_name;
- module_args = event.event_data.res.invocation.module_args;
- }
- else {
- module_name = '';
- module_args = '';
- }
- if (event.event_data.res && event.event_data.res.results) {
- results = '';
- event.event_data.res.results.forEach(function(row) {
- results += row;
- });
- }
- rc = (event.event_data.res && !Empty(event.event_data.res.rc)) ? event.event_data.res.rc : '';
+
scope.hostResults.push({
- id: id,
+ id: event_id,
status: status,
host_id: host_id,
- task_id: event.parent,
- task_name: task_name,
- host_name: event.event_data.host,
- module_name: module_name,
- module_args: module_args,
- results: results,
- rc: rc
+ task_id: event.parent
+ });
+
+ scope.tasks.forEach(function(task, idx) {
+ 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
+ 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].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);
+ }
});
};
}])
@@ -327,10 +378,15 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla
scope: scope,
id: max_task_id,
callback: function() {
+ // Scroll the task table all the way to the bottom, revealing the last row
setTimeout(function() {
- var inner_height = $('#job_tasks .job-detail-table').height();
- $('#job_tasks').scrollTop(inner_height);
- }, 100);
+ var original_height = $('#task-table-body').css('height'),
+ table_height;
+ $('#task-table-body').css('height', 'auto');
+ table_height = $('#task-table-body').height();
+ $('#task-table-body').css('height', original_height);
+ $('#task-table-body').scrollTop(table_height);
+ }, 300);
}
});
};
diff --git a/awx/ui/static/js/helpers/PaginationHelpers.js b/awx/ui/static/js/helpers/PaginationHelpers.js
index d6910f92c8..745cc2c670 100644
--- a/awx/ui/static/js/helpers/PaginationHelpers.js
+++ b/awx/ui/static/js/helpers/PaginationHelpers.js
@@ -49,7 +49,11 @@ 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/js/helpers/refresh.js b/awx/ui/static/js/helpers/refresh.js
index c83b32e178..b1a284e305 100644
--- a/awx/ui/static/js/helpers/refresh.js
+++ b/awx/ui/static/js/helpers/refresh.js
@@ -26,6 +26,8 @@ angular.module('RefreshHelper', ['RestServices', 'Utilities', 'PaginationHelpers
iterator = params.iterator,
url = params.url;
+ scope[iterator + "HidePaginator"] = true;
+
//scope[iterator + 'Loading'] = true;
scope.current_url = url;
Rest.setUrl(url);
@@ -45,7 +47,7 @@ angular.module('RefreshHelper', ['RestServices', 'Utilities', 'PaginationHelpers
}
scope[set] = data.results;
scope[iterator + 'Loading'] = false;
- scope[iterator + "HidePaginator"] = false;
+ scope[iterator + 'HidePaginator'] = false;
Wait('stop');
scope.$emit('PostRefresh');
})
diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less
index 9bfcae9a89..83de74e47b 100644
--- a/awx/ui/static/less/ansible-ui.less
+++ b/awx/ui/static/less/ansible-ui.less
@@ -996,28 +996,32 @@ input[type="checkbox"].checkbox-no-label {
.icon-job-running:before,
.icon-job-success:before,
.icon-job-successful:before,
- .icon-job-changed:before {
+ .icon-job-changed:before,
+ .icon-job-ok:before {
content: "\f111";
}
.icon-job-stopped:before,
.icon-job-error:before,
.icon-job-failed:before,
- .icon-job-canceled:before {
+ .icon-job-canceled:before,
+ .icon-job-unreachable:before {
content: "\f06a";
}
.icon-job-pending:before,
.icon-job-waiting:before,
.icon-job-new:before,
- .icon-job-none:before {
+ .icon-job-none:before,
+ .icon-job-skipped:before {
content: "\f10c";
}
.icon-job-active,
.icon-job-running,
.icon-job-success,
- .icon-job-successful {
+ .icon-job-successful,
+ .icon-job-ok {
color: @green;
}
@@ -1033,14 +1037,16 @@ input[type="checkbox"].checkbox-no-label {
.icon-job-stopped,
.icon-job-error,
.icon-job-failed,
- .icon-job-canceled {
+ .icon-job-canceled,
+ .icon-job-unreachable {
color: @red;
}
.icon-job-none,
.icon-job-pending,
.icon-job-waiting,
- .icon-job-new {
+ .icon-job-new,
+ .icon-job-skipped {
color: @grey;
opacity: 0.45;
}
diff --git a/awx/ui/static/less/job-details.less b/awx/ui/static/less/job-details.less
index 295f5e8863..a5deb52591 100644
--- a/awx/ui/static/less/job-details.less
+++ b/awx/ui/static/less/job-details.less
@@ -7,35 +7,52 @@
*
*/
-.job-detail-tables {
- .table {
- margin-bottom: 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%;
}
- .table>tbody>tr>td {
+ thead tr {
+ display: table-row;
+ width: 100%;
+ }
+ tbody {
+ display: block;
+ height: 122px;
+ width: 100%;
+ overflow-y: scroll;
+ float: left;
+ }
+ tbody tr {
+ display: table;
+ width: 100%;
+ }
+ tbody>tr>td {
border-top-color: @well;
- padding: 0;
+ padding-top: 0;
+ padding-bottom: 0;
}
- .table>thead>tr>th {
- border-bottom-color: @well;
- padding: 0;
+ thead>tr>th {
+ padding-top: 0;
+ padding-bottom: 0;
+ font-size: 14px;
}
- ul {
- list-style-type: none;
- margin: 0;
- padding: 0;
- }
- li ul {
- margin-left: 20px;
- }
- .status-column {
- width: 25px;
- font-size: 12px;
+ td.status-column {
text-align: center;
+ font-size: 12px;
i {
margin-top: 4px;
}
}
- .table>tbody>tr.active>td {
+ tbody>tr.active>td {
background-color: @active-color;
}
}
@@ -77,6 +94,23 @@
}
}
+.status-bar {
+ display: inline-block;
+ height: 15px;
+}
+.failed-hosts {
+ background-color: @red;
+}
+.successful-hosts {
+ background-color: @green;
+}
+.changed-hosts {
+ background-color: @warning;
+}
+.skipped-hosts {
+ background-color: @grey;
+}
+
.job_well {
padding: 8px;
background-color: @well;
diff --git a/awx/ui/static/lib/ansible/list-generator.js b/awx/ui/static/lib/ansible/list-generator.js
index 3d3d59bcd5..439484d71e 100644
--- a/awx/ui/static/lib/ansible/list-generator.js
+++ b/awx/ui/static/lib/ansible/list-generator.js
@@ -82,8 +82,7 @@ angular.module('ListGenerator', ['GeneratorHelpers'])
// Reset the scope to prevent displaying old data from our last visit to this list
//this.scope[list.name] = null;
this.scope[list.iterator] = [];
- this.scope[list.iterator + "HidePaginator"] = true;
-
+
// Remove any lingering tooltip and popover
elements
$('.tooltip').each(function() {
$(this).remove();
diff --git a/awx/ui/static/partials/job_detail.html b/awx/ui/static/partials/job_detail.html
index ef6d56345c..102ef54703 100644
--- a/awx/ui/static/partials/job_detail.html
+++ b/awx/ui/static/partials/job_detail.html
@@ -77,21 +77,35 @@
-
Tasks for play: {{ activePlayName }}
-
-
-
-
- |
- {{ task.name }} |
-
-
-
-
+
Tasks
+
+
+
+ | Started |
+ Role |
+ Name |
+ Status
+ |
+
+
+
+ | {{ task.created | date: 'HH:mm:ss' }} |
+ {{ task.role }} |
+ {{ task.name }} |
+
+
+
+
+ |
+
+
+
-
Hosts in task: {{ activeTaskName }}
+
Hosts