diff --git a/awx/ui/client/src/job-results/duration.filter.js b/awx/ui/client/src/job-results/duration.filter.js
new file mode 100644
index 0000000000..ba23374784
--- /dev/null
+++ b/awx/ui/client/src/job-results/duration.filter.js
@@ -0,0 +1,120 @@
+// TODO: come back and get this library
+export default function() {
+ var DURATION_FORMATS_SPLIT = /((?:[^ydhms']+)|(?:'(?:[^']|'')*')|(?:y+|d+|h+|m+|s+))(.*)/;
+ var DURATION_FORMATS = {
+ 'y': { // years
+ // "longer" years are not supported
+ value: 365 * 24 * 60 * 60 * 1000
+ },
+ 'yy': {
+ value: 'y',
+ pad: 2
+ },
+ 'd': { // days
+ value: 24 * 60 * 60 * 1000
+ },
+ 'dd': {
+ value: 'd',
+ pad: 2
+ },
+ 'h': { // hours
+ value: 60 * 60 * 1000
+ },
+ 'hh': { // padded hours
+ value: 'h',
+ pad: 2
+ },
+ 'm': { // minutes
+ value: 60 * 1000
+ },
+ 'mm': { // padded minutes
+ value: 'm',
+ pad: 2
+ },
+ 's': { // seconds
+ value: 1000
+ },
+ 'ss': { // padded seconds
+ value: 's',
+ pad: 2
+ },
+ 'sss': { // milliseconds
+ value: 1
+ },
+ 'ssss': { // padded milliseconds
+ value: 'sss',
+ pad: 4
+ }
+ };
+
+ function _parseFormat(string) {
+ // @inspiration AngularJS date filter
+ var parts = [];
+ var format = string;
+ while(format) {
+ var match = DURATION_FORMATS_SPLIT.exec(format);
+ if (match) {
+ parts = parts.concat(match.slice(1));
+
+ format = parts.pop();
+ } else {
+ parts.push(format);
+ format = null;
+ }
+ }
+ return parts;
+ }
+ function _formatDuration(timestamp, format) {
+ var text = '';
+ var values = { };
+ format.filter(function(format) { // filter only value parts of format
+ return DURATION_FORMATS.hasOwnProperty(format);
+ }).map(function(format) { // get formats with values only
+ var config = DURATION_FORMATS[format];
+ if(config.hasOwnProperty('pad')) {
+ return config.value;
+ } else {
+ return format;
+ }
+ }).filter(function(format, index, arr) { // remove duplicates
+ return (arr.indexOf(format) === index);
+ }).map(function(format) { // get format configurations with values
+ return angular.extend({
+ name: format,
+ }, DURATION_FORMATS[format]);
+ }).sort(function(a, b) { // sort formats descending by value
+ return b.value - a.value;
+ }).forEach(function(format) { // create values for format parts
+ var value = values[format.name] = Math.floor(timestamp / format.value);
+ timestamp = timestamp - (value * format.value);
+ });
+ format.forEach(function(part) {
+ var format = DURATION_FORMATS[part];
+ if(format) {
+ var value = values[format.value];
+ text += (format.hasOwnProperty('pad') ? _padNumber(value, Math.max(format.pad, value.toString().length)) : values[part]);
+ } else {
+ text += part.replace(/(^'|'$)/g, '').replace(/''/g, '\'');
+ }
+ });
+ return text;
+ }
+ function _padNumber(number, len) {
+ return ((new Array(len + 1)).join('0') + number).slice(-len);
+ }
+ return function(value, format) {
+ if (typeof value !== "number") {
+ return value;
+ }
+
+ var timestamp = parseInt(value.valueOf(), 10);
+ if(isNaN(timestamp)) {
+ return value;
+ } else {
+ return _formatDuration(
+ timestamp,
+ _parseFormat(format)
+ );
+ }
+ };
+ };
diff --git a/awx/ui/client/src/job-results/host-status-bar/host-status-bar.block.less b/awx/ui/client/src/job-results/host-status-bar/host-status-bar.block.less
new file mode 100644
index 0000000000..9e85b992a6
--- /dev/null
+++ b/awx/ui/client/src/job-results/host-status-bar/host-status-bar.block.less
@@ -0,0 +1,39 @@
+@import '../../shared/branding/colors.default.less';
+
+.JobResults-hostStatusBar{
+ display: flex;
+ flex: 1 0 auto;
+ width: 100%;
+ margin-top: 10px;
+}
+
+.JobResults-hostStatusBar--ok{
+ background-color: @default-succ;
+ display: flex;
+ flex: 1 0 auto;
+ height: 5px;
+}
+
+.JobResults-hostStatusBar--changed{
+ background-color: @default-warning;
+ flex: 1 0 auto;
+ height: 5px;
+}
+
+.JobResults-hostStatusBar--unreachable{
+ background-color: @default-unreachable;
+ flex: 1 0 auto;
+ height: 5px;
+}
+
+.JobResults-hostStatusBar--failures{
+ background-color: @default-err;
+ flex: 1 0 auto;
+ height: 5px;
+}
+
+.JobResults-hostStatusBar--skipped{
+ background-color: @default-link;
+ flex: 1 0 auto;
+ height: 5px;
+}
diff --git a/awx/ui/client/src/job-results/host-status-bar/host-status-bar.directive.js b/awx/ui/client/src/job-results/host-status-bar/host-status-bar.directive.js
new file mode 100644
index 0000000000..e1268336a4
--- /dev/null
+++ b/awx/ui/client/src/job-results/host-status-bar/host-status-bar.directive.js
@@ -0,0 +1,22 @@
+/*************************************************
+ * Copyright (c) 2016 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+// import hostStatusBarController from './host-status-bar.controller';
+export default [ 'templateUrl',
+ function(templateUrl) {
+ return {
+ scope: {
+ jobData: '='
+ },
+ templateUrl: templateUrl('job-results/host-status-bar/host-status-bar'),
+ restrict: 'E',
+ // controller: standardOutLogController,
+ link: function(scope) {
+ // All of our DOM related stuff will go in here
+
+ }
+ }
+}];
diff --git a/awx/ui/client/src/job-results/host-status-bar/host-status-bar.partial.html b/awx/ui/client/src/job-results/host-status-bar/host-status-bar.partial.html
new file mode 100644
index 0000000000..bc27f817fb
--- /dev/null
+++ b/awx/ui/client/src/job-results/host-status-bar/host-status-bar.partial.html
@@ -0,0 +1,7 @@
+
diff --git a/awx/ui/client/src/job-results/host-status-bar/main.js b/awx/ui/client/src/job-results/host-status-bar/main.js
new file mode 100644
index 0000000000..2b17a2e414
--- /dev/null
+++ b/awx/ui/client/src/job-results/host-status-bar/main.js
@@ -0,0 +1,11 @@
+/*************************************************
+ * Copyright (c) 2015 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+import hostStatusBar from './host-status-bar.directive';
+
+export default
+ angular.module('hostStatusBarDirective', [])
+ .directive('hostStatusBar', hostStatusBar);
diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js
index 843d5936a6..65b7d86080 100644
--- a/awx/ui/client/src/job-results/job-results.controller.js
+++ b/awx/ui/client/src/job-results/job-results.controller.js
@@ -1,4 +1,4 @@
-export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', function(jobData, jobDataOptions, jobLabels, $scope, ParseTypeChange, ParseVariableString, jobResultsService) {
+export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', '$rootScope', function(jobData, jobDataOptions, jobLabels, $scope, ParseTypeChange, ParseVariableString, jobResultsService, $rootScope) {
var getTowerLinks = function() {
var getTowerLink = function(key) {
if ($scope.job.related[key]) {
@@ -57,7 +57,7 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeCh
$scope.toggleStdoutFullscreen = function() {
$scope.stdoutFullScreen = !$scope.stdoutFullScreen;
};
-
+
$scope.deleteJob = function() {
jobResultsService.deleteJob($scope.job);
};
@@ -66,9 +66,23 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeCh
jobResultsService.cancelJob($scope.job);
};
- // Set the job status
- // TODO: pull from websockets
- $scope.job_status = {"status": ""};
- $scope.job_status.status = (jobData.status === 'waiting' ||
- jobData.status === 'new') ? 'pending' : jobData.status;
+ $rootScope.event_socket.on("job_events-" + $scope.job.id, function(data) {
+ console.log(data);
+ if(data.event_name === "playbook_on_stats"){
+ // get the data for populating the host status bar
+ jobResultsService.getHostStatusBarCounts(data.event_data);
+ }
+
+ });
+
+ $rootScope.$on('JobStatusChange-jobDetails', function(e, data) {
+ console.log(data);
+ if (parseInt(data.unified_job_id, 10) === parseInt($scope.job.id,10)) {
+ $scope.job.status = data.status;
+
+ // $scope.job.elapsed = data.elapsed;
+ }
+ });
+
+
}];
diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html
index 038998ef14..3300afa695 100644
--- a/awx/ui/client/src/job-results/job-results.partial.html
+++ b/awx/ui/client/src/job-results/job-results.partial.html
@@ -447,7 +447,7 @@
+ fa icon-job-{{ job.status }}">
{{ job.name }}
@@ -483,7 +483,7 @@
Elapsed
- {{ job_status.elapsed || "00:00:01"}}
+ {{ job.elapsed * 1000 | duration: "hh:mm:ss" }}
@@ -513,6 +513,8 @@
+
+
diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js
index eb6d72b1a7..5268acd0fa 100644
--- a/awx/ui/client/src/job-results/job-results.route.js
+++ b/awx/ui/client/src/job-results/job-results.route.js
@@ -74,6 +74,20 @@ export default {
val.reject(data);
});
return val.promise;
+ }],
+ jobEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) {
+ if (!$rootScope.event_socket) {
+ $rootScope.event_socket = Socket({
+ scope: $rootScope,
+ endpoint: "job_events"
+ });
+ $rootScope.event_socket.init();
+ // returns should really be providing $rootScope.event_socket
+ // otherwise, we have to inject the entire $rootScope into the controller
+ return true;
+ } else {
+ return true;
+ }
}]
},
templateUrl: templateUrl('job-results/job-results'),
diff --git a/awx/ui/client/src/job-results/job-results.service.js b/awx/ui/client/src/job-results/job-results.service.js
index fd4a10f588..57016653c4 100644
--- a/awx/ui/client/src/job-results/job-results.service.js
+++ b/awx/ui/client/src/job-results/job-results.service.js
@@ -5,8 +5,67 @@
*************************************************/
-export default ['Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', function (Prompt, $filter, Wait, Rest, $state, ProcessErrors) {
- return {
+export default ['Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', '$rootScope', function (Prompt, $filter, Wait, Rest, $state, ProcessErrors, $rootScope) {
+ var val = {
+ getHostStatusBarCounts: function(event_data) {
+ var hosts = {};
+
+ // iterate over the event_data and populate an object with hosts
+ // and their status data
+ Object.keys(event_data).forEach(key => {
+ // failed passes boolean not integer
+ if (key === "failed") {
+ // array of hosts from failed type
+ var hostsArr = Object.keys(event_data[key]);
+ hostsArr.forEach(host => {
+ if (!hosts[host]) {
+ // host has not been added to hosts object
+ // add now
+ hosts[host] = {};
+ }
+
+ hosts[host][key] = event_data[key][host];
+ });
+ } else {
+ // array of hosts from each type ("changed", "dark", etc.)
+ var hostsArr = Object.keys(event_data[key]);
+ hostsArr.forEach(host => {
+ if (!hosts[host]) {
+ // host has not been added to hosts object
+ // add now
+ hosts[host] = {};
+ }
+
+ if (!hosts[host][key]) {
+ // host doesn't have key
+ hosts[host][key] = 0;
+ }
+ hosts[host][key] += event_data[key][host];
+ });
+ }
+ });
+
+ // use the hosts data populate above to get the count
+ var count = {
+ ok : _.filter(hosts, function(o){
+ return !o.failures && !o.changed && o.ok > 0;
+ }),
+ skipped : _.filter(hosts, function(o){
+ return o.skipped > 0;
+ }),
+ unreachable : _.filter(hosts, function(o){
+ return o.dark > 0;
+ }),
+ failures : _.filter(hosts, function(o){
+ return o.failed === true;
+ }),
+ changed : _.filter(hosts, function(o){
+ return o.changed > 0;
+ })
+ };
+
+ return count;
+ },
deleteJob: function(job) {
Prompt({
hdr: 'Delete Job',
@@ -100,4 +159,5 @@ export default ['Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors',
});
}
};
+ return val;
}];
diff --git a/awx/ui/client/src/job-results/main.js b/awx/ui/client/src/job-results/main.js
index 6a67cd000f..104ca76086 100644
--- a/awx/ui/client/src/job-results/main.js
+++ b/awx/ui/client/src/job-results/main.js
@@ -6,10 +6,13 @@
import route from './job-results.route.js';
import jobResultsService from './job-results.service';
+import hostStatusBarDirective from './host-status-bar/main';
+import durationFilter from './duration.filter';
export default
- angular.module('jobResults', [])
+ angular.module('jobResults', [hostStatusBarDirective.name])
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route);
}])
- .service('jobResultsService', jobResultsService);
+ .service('jobResultsService', jobResultsService)
+ .filter('duration', durationFilter);