From 0b0207e20e2b8f12b2ee69a12ce183598a931ac3 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 22 Apr 2014 00:15:25 -0400 Subject: [PATCH] Latest job detail page changes. --- awx/ui/static/js/controllers/JobDetail.js | 255 +++++++++++++++------- awx/ui/static/js/helpers/JobDetail.js | 86 ++++++-- awx/ui/static/js/helpers/refresh.js | 2 +- awx/ui/static/less/ansible-ui.less | 7 + awx/ui/static/less/job-details.less | 7 +- awx/ui/static/partials/job_detail.html | 32 +-- awx/ui/templates/ui/index.html | 3 +- 7 files changed, 279 insertions(+), 113 deletions(-) diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js index 4521970a7d..166817435c 100644 --- a/awx/ui/static/js/controllers/JobDetail.js +++ b/awx/ui/static/js/controllers/JobDetail.js @@ -8,77 +8,162 @@ 'use strict'; function JobDetailController ($scope, $compile, $routeParams, ClearScope, Breadcrumbs, LoadBreadCrumbs, GetBasePath, Wait, Rest, ProcessErrors, DigestEvents, - SelectPlay, SelectTask) { + SelectPlay, SelectTask, Socket, GetElapsed) { ClearScope(); var job_id = $routeParams.id, - job; + event_socket, job, + event_queue = [], + processed_events = [], + scope = $scope, + api_complete = false; + + scope.plays = []; + scope.tasks = []; + scope.hosts = []; + scope.hostResults = []; + scope.job_status = {}; + scope.job_id = job_id; + + event_socket = Socket({ + scope: scope, + endpoint: "job_events" + }); + + event_socket.init(); - /*LoadBreadCrumbs(); - - e = angular.element(document.getElementById('breadcrumbs')); - e.html(Breadcrumbs({ list: { editTitle: 'Jobs' } , mode: 'edit' })); - $compile(e)($scope); - */ - - $scope.plays = []; - $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(); + // Evaluate elements of an array, returning the set of elements that + // match a condition as expressed in a function + // + // matches = myarray.find(function(x) { return x.id === 5 }); + // + Array.prototype.find = function(parameterFunction) { + var results = []; + this.forEach(function(row) { + if (parameterFunction(row)) { + results.push(row); + } + }); + return results; } - $scope.removeEventsReady = $scope.$on('EventsReady', function(e, events) { + + // Reduce an array of objects down to just the bits we want from each object by + // passing in a function that returns just those parts. + // + // new_array = myarray.reduce(function(x) { return { blah: x.blah, foo: x.foo } }); + // + Array.prototype.reduce = function(parameterFunction) { + var results= []; + this.forEach(function(row) { + results.push(parameterFunction(row)); + }); + return results; + } + + + // Apply each event to the view + if (scope.removeEventsReady) { + scope.removeEventsReady(); + } + scope.removeEventsReady = scope.$on('EventsReady', function(e, events) { + console.log('Inside EventsReady!'); + console.log(events); DigestEvents({ - scope: $scope, + scope: scope, events: events }); }); - - // Get events, page size 50 - if ($scope.removeJobReady) { - $scope.removeJobReady(); - } - $scope.removeJobReady = $scope.$on('JobReady', function(e, next) { - if (next) { - Rest.setUrl(next); - Rest.get() - .success(function(data) { - $scope.$emit('EventsReady', data.results); - if (data.next) { - $scope.$emit('JobReady', data.next); - } - else { - Wait('stop'); - } - }) - .error(function(data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve job events: ' + next + ' GET returned: ' + status }); - }); + + event_socket.on("job_events-" + job_id, function(data) { + var matches; + data.id = data.event_id; + console.log(data); + if (api_complete) { + matches = processed_events.find(function(x) { return x === data.id }); + if (matches.length === 0) { + // event not processed + console.log('process event: ' + data.id); + scope.$emit('EventsReady', [ data ]); + } + } + else { + console.log('queue event: ' + data.id); + event_queue.push(data); } }); - if ($scope.removeGetCredentialNames) { - $scope.removeGetCredentialNames(); + + // + if (scope.removeAPIComplete) { + scope.removeAPIComplete(); } - $scope.removeGetCredentialNames = $scope.$on('GetCredentialNames', function(e, data) { + scope.removeAPIComplete = scope.$on('APIComplete', function() { + var events; + if (event_queue.length > 0) { + // Events arrived while we were processing API results + events = event_queue.find(function(event) { + var matched = false; + processed_events.every(function(event_id) { + if (event_id === event.id) { + matched = true; + return false; + } + return true; + }); + return (!matched); //return true when event.id not in the list of processed_events + }); + console.log('processing queued events: '); + console.log(events.reduce(function(x) { return x.id })); + if (events.length > 0) { + scope.$emit('EventsReady', events); + api_complete = true; + } + } + else { + api_complete = true; + } + }); + + // Get events, 50 at a time. When done, emit APIComplete + if (scope.removeJobReady) { + scope.removeJobReady(); + } + scope.removeJobReady = scope.$on('JobReady', function(e, next) { + Rest.setUrl(next); + Rest.get() + .success(function(data) { + processed_events = processed_events.concat( data.results.reduce(function(x) { return x.id }) ); + scope.$emit('EventsReady', data.results); + if (data.next) { + scope.$emit('JobReady', data.next); + } + else { + Wait('stop'); + scope.$emit('APIComplete'); + } + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve job events: ' + next + ' GET returned: ' + status }); + }); + }); + + if (scope.removeGetCredentialNames) { + scope.removeGetCredentialNames(); + } + scope.removeGetCredentialNames = scope.$on('GetCredentialNames', function(e, data) { var url; if (data.credential) { url = GetBasePath('credentials') + data.credential + '/'; Rest.setUrl(url); Rest.get() .success( function(data) { - $scope.credential_name = data.name; + scope.credential_name = data.name; }) .error( function(data, status) { - $scope.credential_name = ''; - ProcessErrors($scope, data, status, null, { hdr: 'Error!', + scope.credential_name = ''; + ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + '. GET returned: ' + status }); }); } @@ -87,11 +172,11 @@ function JobDetailController ($scope, $compile, $routeParams, ClearScope, Breadc Rest.setUrl(url); Rest.get() .success( function(data) { - $scope.cloud_credential_name = data.name; + scope.cloud_credential_name = data.name; }) .error( function(data, status) { - $scope.credential_name = ''; - ProcessErrors($scope, data, status, null, { hdr: 'Error!', + scope.credential_name = ''; + ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + '. GET returned: ' + status }); }); } @@ -104,42 +189,56 @@ function JobDetailController ($scope, $compile, $routeParams, ClearScope, Breadc Rest.get() .success(function(data) { job = data; - $scope.job_template_name = data.name; - $scope.project_name = (data.summary_fields.project) ? data.summary_fields.project.name : ''; - $scope.inventory_name = (data.summary_fields.inventory) ? data.summary_fields.inventory.name : ''; - $scope.job_template_url = '/#/job_templates/' + data.unified_job_template; - $scope.inventory_url = ($scope.inventory_name && data.inventory) ? '/#/inventories/' + data.inventory : ''; - $scope.project_url = ($scope.project_name && data.project) ? '/#/projects/' + data.project : ''; - $scope.job_type = data.job_type; - $scope.playbook = data.playbook; - $scope.credential = data.credential; - $scope.cloud_credential = data.cloud_credential; - $scope.forks = data.forks; - $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.$emit('JobReady', data.related.job_events + '?page_size=50&order_by=id'); - $scope.$emit('GetCredentialNames', data); + scope.job_template_name = data.name; + scope.project_name = (data.summary_fields.project) ? data.summary_fields.project.name : ''; + scope.inventory_name = (data.summary_fields.inventory) ? data.summary_fields.inventory.name : ''; + scope.job_template_url = '/#/job_templates/' + data.unified_job_template; + scope.inventory_url = (scope.inventory_name && data.inventory) ? '/#/inventories/' + data.inventory : ''; + scope.project_url = (scope.project_name && data.project) ? '/#/projects/' + data.project : ''; + scope.job_type = data.job_type; + scope.playbook = data.playbook; + scope.credential = data.credential; + scope.cloud_credential = data.cloud_credential; + scope.forks = data.forks; + scope.limit = data.limit; + scope.verbosity = data.verbosity; + scope.job_tags = data.job_tags; + + // In the case that the job is already completed, or an error already happened, + // populate scope.job_status info + scope.job_status.status = data.status; + scope.job_status.started = data.started; + scope.job_status.status_class = ((data.status === 'error' || data.status === 'failed') && data.job_explanation) ? "alert alert-danger" : ""; + scope.job_status.finished = data.finished; + scope.job_status.explanation = data.job_explanation; + if (data.started && data.finished) { + scope.job_status.elapsed = GetElapsed({ + start: data.started, + end: data.finished + }); + } + else { + scope.job_status.elapsed = '00:00:00'; + } + + scope.$emit('JobReady', data.related.job_events + '?page_size=50&order_by=id'); + scope.$emit('GetCredentialNames', data); }) .error(function(data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', + ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve job: ' + $routeParams.id + '. GET returned: ' + status }); }); - $scope.selectPlay = function(id) { + scope.selectPlay = function(id) { SelectPlay({ - scope: $scope, + scope: scope, id: id }); }; - $scope.selectTask = function(id) { + scope.selectTask = function(id) { SelectTask({ - scope: $scope, + scope: scope, id: id }); }; @@ -157,5 +256,5 @@ function JobDetailController ($scope, $compile, $routeParams, ClearScope, Breadc } JobDetailController.$inject = [ '$scope', '$compile', '$routeParams', 'ClearScope', 'Breadcrumbs', 'LoadBreadCrumbs', 'GetBasePath', 'Wait', - 'Rest', 'ProcessErrors', 'DigestEvents', 'SelectPlay', 'SelectTask' + 'Rest', 'ProcessErrors', 'DigestEvents', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed' ]; diff --git a/awx/ui/static/js/helpers/JobDetail.js b/awx/ui/static/js/helpers/JobDetail.js index 0b163602c6..f1de3a9230 100644 --- a/awx/ui/static/js/helpers/JobDetail.js +++ b/awx/ui/static/js/helpers/JobDetail.js @@ -40,19 +40,22 @@ angular.module('JobDetailHelper', ['Utilities', 'RestServices']) .factory('DigestEvents', ['UpdatePlayStatus', 'UpdatePlayNoHostsMatched', 'UpdateHostStatus', 'UpdatePlayChild', 'AddHostResult', 'SelectPlay', 'SelectTask', -'GetHostCount', 'GetElapsed', -function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePlayChild, AddHostResult, SelectPlay, SelectTask, GetHostCount, GetElapsed) { + 'GetHostCount', 'GetElapsed', 'UpdateJobStatus', +function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePlayChild, AddHostResult, SelectPlay, SelectTask, GetHostCount, GetElapsed, + UpdateJobStatus) { return function(params) { var scope = params.scope, events = params.events; - events.forEach(function(event) { var hostCount; if (event.event === 'playbook_on_start') { - scope.job_status.started = event.created; - scope.job_status.status = 'running'; + if (scope.job_status.status!== 'failed' && scope.job_status.status !== 'canceled' && + scope.job_status.status !== 'error') { + scope.job_status.started = event.created; + scope.job_status.status = 'running'; + } } if (event.event === 'playbook_on_play_start') { @@ -60,7 +63,7 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla id: event.id, name: event.play, created: event.created, - status: (event.changed) ? 'changed' : (event.failed) ? 'failed' : 'none', + status: (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'none', children: [] }); SelectPlay({ @@ -77,15 +80,19 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla id: event.id, name: event.event_display, play_id: event.parent, - status: (event.failed) ? 'failed' : 'successful', + status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ), created: event.created, modified: event.modified, hostCount: hostCount, + reportedHosts: 0, + successfulCount: 0, failedCount: 0, changedCount: 0, - successfulCount: 0, skippedCount: 0, - reportedHosts: 0 + successfulStyle: { display: 'none'}, + failedStyle: { display: 'none' }, + changedStyle: { display: 'none' }, + skippedStyle: { display: 'none' } }); UpdatePlayStatus({ scope: scope, @@ -108,16 +115,20 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla id: event.id, name: event.task, play_id: event.parent, - status: ( (event.changed) ? 'changed' : (event.failed) ? 'failed' : 'successful' ), + status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ), role: event.role, created: event.created, modified: event.modified, hostCount: hostCount, + reportedHosts: 0, + successfulCount: 0, failedCount: 0, changedCount: 0, - successfulCount: 0, skippedCount: 0, - reportedHosts: 0 + successfulStyle: { display: 'none'}, + failedStyle: { display: 'none' }, + changedStyle: { display: 'none' }, + skippedStyle: { display: 'none' } }); if (event.role) { scope.hasRoles = true; @@ -181,7 +192,7 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla name: event.event_data.host, host_id: event.host, task_id: event.parent, - status: ( (event.changed) ? 'changed' : (event.failed) ? 'failed' : 'successful' ), + status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ), event_id: event.id, created: event.created, modified: event.modified @@ -194,6 +205,7 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla end: scope.job_status.finished }); scope.job_status.status = (event.failed) ? 'error' : 'successful'; + scope.job_status.status_class = ""; } }); }; @@ -322,8 +334,37 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla }; }]) +.factory('UpdateJobStatus', ['GetElapsed', 'Empty', function(GetElapsed, Empty) { + return function(params) { + var scope = params.scope, + failed = params.failed, + modified = params.modified; + started = params.started; + + if (failed && scope.job_status.status !== 'failed' && scope.job_status.status !== 'error' + && scope.job_status.status !== 'canceled') { + scope.job_status.status = 'error'; + } + if (!Empty(modified)) { + scope.job_status.finished = modified; + } + if (!Empty(started) && Empty(scope.job_status.started)) { + scope.job_status.started = started; + } + if (!Empty(scope.job_status.finished) && !Empty(scope.job_status.started)) { + console.log('scope.job_status.started: ' + scope.job_status.started); + console.log('scope.job_status.finished: ' + scope.job_status.finished); + scope.job_status.elapsed = GetElapsed({ + start: scope.job_status.started, + end: scope.job_status.finished + }); + console.log('elapsed: ' + scope.job_status.elapsed); + } + }; +}]) + // Update the status of a play -.factory('UpdatePlayStatus', ['GetElapsed', function(GetElapsed) { +.factory('UpdatePlayStatus', ['GetElapsed', 'UpdateJobStatus', function(GetElapsed, UpdateJobStatus) { return function(params) { var scope = params.scope, failed = params.failed, @@ -332,7 +373,10 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla modified = params.modified; scope.plays.every(function(play,idx) { if (play.id === id) { - if (play.status !== 'changed' && play.status !== 'failed') { + if (failed) { + scope.plays[idx].status = 'failed'; + } + else 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'; } @@ -341,6 +385,11 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla start: play.created, end: modified }); + /*UpdateJobStatus({ + scope: scope, + failed: failed, + modified: modified + });*/ return false; } return true; @@ -357,9 +406,12 @@ function(UpdatePlayStatus, UpdatePlayNoHostsMatched, UpdateHostStatus, UpdatePla modified = params.modified; scope.tasks.every(function (task, i) { if (task.id === id) { - if (task.status !== 'changed' && task.status !== 'failed') { + if (failed) { + scope.tasks[i].status = 'failed'; + } + else 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].status = (failed) ? 'failed' : (changed) ? 'changed' : 'successful'; } scope.tasks[i].finished = params.modified; scope.tasks[i].elapsed = GetElapsed({ diff --git a/awx/ui/static/js/helpers/refresh.js b/awx/ui/static/js/helpers/refresh.js index b1a284e305..18a1e2bfef 100644 --- a/awx/ui/static/js/helpers/refresh.js +++ b/awx/ui/static/js/helpers/refresh.js @@ -26,7 +26,7 @@ angular.module('RefreshHelper', ['RestServices', 'Utilities', 'PaginationHelpers iterator = params.iterator, url = params.url; - scope[iterator + "HidePaginator"] = true; + //scope[iterator + "HidePaginator"] = true; //scope[iterator + 'Loading'] = true; scope.current_url = url; diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 83de74e47b..218b35f6fd 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -950,6 +950,13 @@ input[type="checkbox"].checkbox-no-label { border-top: none; } +/* Less padding on .table-condensed */ +.table-condensed>tbody>tr>td, +.table-condensed>thead>tr>th { + padding-top: 3px; + padding-bottom: 3px; +} + /* Table info rows */ .loading-info { diff --git a/awx/ui/static/less/job-details.less b/awx/ui/static/less/job-details.less index 1b89b8d346..1ec9c090d3 100644 --- a/awx/ui/static/less/job-details.less +++ b/awx/ui/static/less/job-details.less @@ -28,12 +28,15 @@ } li { display: inline-block; - margin-right: 15px; + margin-right: 20px; } i { font-size: 12px; } .label { + display: inline-block; + text-align: left; + width: 50px; font-size: 12px; color: @black; padding-left: 0; @@ -177,6 +180,8 @@ padding: 5px; height: 150px; background-color: @white; + overflow-y: hide; + overflow-x: auto; } #host-details { diff --git a/awx/ui/static/partials/job_detail.html b/awx/ui/static/partials/job_detail.html index 0efeab2496..4c9646ae44 100644 --- a/awx/ui/static/partials/job_detail.html +++ b/awx/ui/static/partials/job_detail.html @@ -5,9 +5,8 @@
@@ -21,11 +20,14 @@
+
@@ -34,17 +36,17 @@ - + - + - + - @@ -56,15 +58,15 @@
Name
{{ play.name }}
- + - + - + @@ -76,7 +78,7 @@
{{ task.failedCount }}
- diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index 8c48dd1633..d982ceeeb8 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -399,7 +399,8 @@ - + +
Name Host Status
{{ task.role }} {{ task.name }}