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 @@
- | Started |
+ Started |
Name |
- Elapsed |
+ Elapsed |
- | {{ play.created | date: 'HH:mm:ss' }} |
+ {{ play.created | date: 'HH:mm:ss' }} |
{{ play.name }} |
- {{ play.elapsed }} |
@@ -56,15 +58,15 @@
- | Started |
+ Started |
Name |
Host Status |
- Elapsed |
+ Elapsed |
- | {{ task.created | date: 'HH:mm:ss' }} |
+ {{ task.created | date: 'HH:mm:ss' }} |
{{ task.role }} {{ task.name }}
|
@@ -76,7 +78,7 @@
{{ task.failedCount }}
- {{ task.elapsed }} |
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 @@
-
+
+