From 9c1114591494eb660ad14fc59c831cec610861c3 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Mon, 16 Jun 2014 00:24:26 -0400 Subject: [PATCH] Job detail page refactor Reconfigured event processing and queueing. 50 no op tasks for 200 hosts seems to be working without melting the CPU and staying below 300MB. --- awx/ui/static/js/app.js | 4 + awx/ui/static/js/controllers/JobDetail.js | 72 +- awx/ui/static/js/controllers/JobDetail.js.old | 715 ++++++++++++ awx/ui/static/js/controllers/Jobs.js | 4 - awx/ui/static/js/helpers/JobDetail.js | 254 ++-- awx/ui/static/js/helpers/JobDetail.js.old | 1017 +++++++++++++++++ awx/ui/static/js/helpers/Jobs.js | 20 +- 7 files changed, 1903 insertions(+), 183 deletions(-) create mode 100644 awx/ui/static/js/controllers/JobDetail.js.old create mode 100644 awx/ui/static/js/helpers/JobDetail.js.old diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index a3ef0a3242..078983b04c 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -432,6 +432,10 @@ angular.module('Tower', [ HideStream(); } + if ($rootScope.myInterval) { + window.clearInterval($rootScope.myInterval); + } + // On each navigation request, check that the user is logged in if (!/^\/(login|logout)/.test($location.path())) { // capture most recent URL, excluding login/logout diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js index 7fee4be686..85b057e087 100644 --- a/awx/ui/static/js/controllers/JobDetail.js +++ b/awx/ui/static/js/controllers/JobDetail.js @@ -7,19 +7,27 @@ 'use strict'; -function JobDetailController ($scope, $compile, $routeParams, $log, ClearScope, Breadcrumbs, LoadBreadCrumbs, GetBasePath, Wait, Rest, ProcessErrors, - DigestEvents, SelectPlay, SelectTask, Socket, GetElapsed, SelectHost, FilterAllByHostName, DrawGraph, LoadHostSummary, ReloadHostSummaryList, +function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, ClearScope, Breadcrumbs, LoadBreadCrumbs, GetBasePath, Wait, Rest, + ProcessErrors, ProcessEventQueue, SelectPlay, SelectTask, Socket, GetElapsed, SelectHost, FilterAllByHostName, DrawGraph, LoadHostSummary, ReloadHostSummaryList, JobIsFinished) { ClearScope(); var job_id = $routeParams.id, event_socket, - event_queue = [], scope = $scope, api_complete = false, refresh_count = 0, - lastEventId = 0; + lastEventId = 0, + queue = []; + + scope.plays = {}; + scope.hosts = []; + scope.hostsMap = {}; + scope.tasks = {}; + scope.hostResults = []; + scope.hostResultsMap = {}; + api_complete = false; scope.search_all_tasks = []; scope.search_all_plays = []; @@ -28,9 +36,10 @@ function JobDetailController ($scope, $compile, $routeParams, $log, ClearScope, scope.auto_scroll = false; scope.searchTaskHostsEnabled = true; scope.searchSummaryHostsEnabled = true; - scope.hostTableRows = 300; - scope.hostSummaryTableRows = 300; + scope.hostTableRows = 150; + scope.hostSummaryTableRows = 150; scope.searchAllHostsEnabled = true; + scope.haltEventQueue = false; scope.host_summary = {}; scope.host_summary.ok = 0; @@ -54,10 +63,25 @@ function JobDetailController ($scope, $compile, $routeParams, $log, ClearScope, event_socket.on("job_events-" + job_id, function(data) { data.event = data.event_name; - $log.debug('push event: ' + data.id); - event_queue.push(data); + if (api_complete && data.id > lastEventId) { + $log.debug('received event: ' + data.id); + if (queue.length < 50) { + queue.unshift(data); + } + else { + api_complete = false; // stop more events from hitting the queue + $log.debug('queue halted. reloading in 1.'); + setTimeout(function() { + $log.debug('reloading'); + scope.haltEventQueue = true; + queue = []; + scope.$emit('LoadJob'); + }, 1000); + } + } }); + if (scope.removeAPIComplete) { scope.removeAPIComplete(); } @@ -90,10 +114,9 @@ function JobDetailController ($scope, $compile, $routeParams, $log, ClearScope, api_complete = true; Wait('stop'); - DigestEvents({ + ProcessEventQueue({ scope: scope, - queue: event_queue, - lastEventId: lastEventId + eventQueue: queue }); // Draw the graph @@ -153,23 +176,13 @@ function JobDetailController ($scope, $compile, $routeParams, $log, ClearScope, scope.removeLoadJobDetails(); } scope.removeRefreshJobDetails = scope.$on('LoadJobDetails', function(e, events_url) { - // Call to load all the job bits including, plays, tasks, hosts results and host summary var url = scope.job.url + 'job_plays/?order_by=id'; - - scope.plays = {}; - scope.tasks = {}; - scope.hostResults = []; - scope.hostResultsMap = {}; - scope.hosts = []; - scope.hostsMap = {}; - api_complete = false; - Rest.setUrl(url); Rest.get() .success( function(data) { data.forEach(function(event, idx) { - var status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'none', + var status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful', start = event.started, end, elapsed; @@ -254,7 +267,7 @@ function JobDetailController ($scope, $compile, $routeParams, $log, ClearScope, scope.removeLoadJob(); } scope.removeLoadJobRow = scope.$on('LoadJob', function() { - Wait('start'); + //Wait('start'); // Load the job record Rest.setUrl(GetBasePath('jobs') + job_id + '/'); Rest.get() @@ -275,7 +288,7 @@ function JobDetailController ($scope, $compile, $routeParams, $log, ClearScope, scope.verbosity = data.verbosity; scope.job_tags = data.job_tags; - // In the case that the job is already completed, or an error already happened, + // In the case the job is already completed, or an error already happened, // populate scope.job_status info scope.job_status.status = (data.status === 'waiting' || data.status === 'new') ? 'pending' : data.status; scope.job_status.started = data.started; @@ -292,9 +305,6 @@ function JobDetailController ($scope, $compile, $routeParams, $log, ClearScope, else { scope.job_status.elapsed = '00:00:00'; } - if (scope.myInterval) { - window.clearInterval(scope.myInterval); - } scope.setSearchAll('host'); scope.$emit('LoadJobDetails', data.related.job_events); scope.$emit('GetCredentialNames', data); @@ -315,7 +325,7 @@ function JobDetailController ($scope, $compile, $routeParams, $log, ClearScope, scope.$emit('LoadJob'); } else { - // Check if we need to redraw the group + // Check if the graph needs to redraw setTimeout(function() { DrawGraph({ scope: scope, resize: true }); }, 500); } }); @@ -459,6 +469,7 @@ function JobDetailController ($scope, $compile, $routeParams, $log, ClearScope, return true; }; + /* scope.HostDetailOnTotalScroll = _.debounce(function() { // Called when user scrolls down (or forward in time). Using _.debounce var url, mcs = arguments[0]; @@ -638,6 +649,7 @@ function JobDetailController ($scope, $compile, $routeParams, $log, ClearScope, scope.auto_scroll = false; } }; + */ scope.searchAllByHost = function() { var nxtPlay; @@ -702,7 +714,7 @@ function JobDetailController ($scope, $compile, $routeParams, $log, ClearScope, } -JobDetailController.$inject = [ '$scope', '$compile', '$routeParams', '$log', 'ClearScope', 'Breadcrumbs', 'LoadBreadCrumbs', 'GetBasePath', 'Wait', - 'Rest', 'ProcessErrors', 'DigestEvents', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed', 'SelectHost', 'FilterAllByHostName', 'DrawGraph', +JobDetailController.$inject = [ '$rootScope', '$scope', '$compile', '$routeParams', '$log', 'ClearScope', 'Breadcrumbs', 'LoadBreadCrumbs', 'GetBasePath', + 'Wait', 'Rest', 'ProcessErrors', 'ProcessEventQueue', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed', 'SelectHost', 'FilterAllByHostName', 'DrawGraph', 'LoadHostSummary', 'ReloadHostSummaryList', 'JobIsFinished' ]; diff --git a/awx/ui/static/js/controllers/JobDetail.js.old b/awx/ui/static/js/controllers/JobDetail.js.old new file mode 100644 index 0000000000..2d99d733d8 --- /dev/null +++ b/awx/ui/static/js/controllers/JobDetail.js.old @@ -0,0 +1,715 @@ +/************************************ + * Copyright (c) 2014 AnsibleWorks, Inc. + * + * JobDetail.js + * + */ + +'use strict'; + +function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, ClearScope, Breadcrumbs, LoadBreadCrumbs, GetBasePath, Wait, Rest, + ProcessErrors, DigestEvents, SelectPlay, SelectTask, Socket, GetElapsed, SelectHost, FilterAllByHostName, DrawGraph, LoadHostSummary, ReloadHostSummaryList, + JobIsFinished) { + + ClearScope(); + + var job_id = $routeParams.id, + event_socket, + event_queue = [], + scope = $scope, + api_complete = false, + refresh_count = 0, + lastEventId = 0; + + scope.plays = {}; + scope.hosts = []; + scope.hostsMap = {}; + + scope.search_all_tasks = []; + scope.search_all_plays = []; + scope.job_status = {}; + scope.job_id = job_id; + scope.auto_scroll = false; + scope.searchTaskHostsEnabled = true; + scope.searchSummaryHostsEnabled = true; + scope.hostTableRows = 300; + scope.hostSummaryTableRows = 300; + scope.searchAllHostsEnabled = true; + + scope.host_summary = {}; + scope.host_summary.ok = 0; + scope.host_summary.changed = 0; + scope.host_summary.unreachable = 0; + scope.host_summary.failed = 0; + scope.host_summary.total = 0; + + scope.eventsHelpText = "

Successful

\n" + + "

Changed

\n" + + "

Unreachable

\n" + + "

Failed

\n" + + "
esc or click to close
\n"; + + event_socket = Socket({ + scope: scope, + endpoint: "job_events" + }); + + event_socket.init(); + + event_socket.on("job_events-" + job_id, function(data) { + data.event = data.event_name; + $log.debug('push event: ' + data.id); + if (event_queue.length < 100) { + event_queue.push(data); + } + else { + // if we get too far behind, clear the queue and refresh + scope.$emit('LoadJob'); + } + }); + + if (scope.removeAPIComplete) { + scope.removeAPIComplete(); + } + scope.removeAPIComplete = scope.$on('APIComplete', function() { + // process any events sitting in the queue + var url, hostId = 0, taskId = 0, playId = 0; + + function notEmpty(x) { + return Object.keys(x).length > 0; + } + + function getMaxId(x) { + var keys = Object.keys(x); + keys.sort(); + return keys[keys.length - 1]; + } + + // Find the max event.id value in memory + if (notEmpty(scope.hostResults)) { + hostId = getMaxId(scope.hostResults); + } + else if (notEmpty(scope.tasks)) { + taskId = getMaxId(scope.tasks); + } + else if (notEmpty(scope.plays)) { + playId = getMaxId(scope.plays); + } + lastEventId = Math.max(hostId, taskId, playId); + + api_complete = true; + Wait('stop'); + + DigestEvents({ + scope: scope, + queue: event_queue, + lastEventId: lastEventId + }); + + // Draw the graph + if (JobIsFinished(scope)) { + url = scope.job.related.job_events + '?event=playbook_on_stats'; + Rest.setUrl(url); + Rest.get() + .success(function(data) { + if (data.count > 0) { + LoadHostSummary({ + scope: scope, + data: data.results[0].event_data + }); + DrawGraph({ scope: scope, resize: true }); + Wait('stop'); + } + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + } + else { + // Draw the graph based on summary values in memory + DrawGraph({ scope: scope, resize: true }); + } + }); + + if (scope.removeInitialDataLoaded) { + scope.removeInitialDataLoaded(); + } + scope.removeInitialDataLoaded = scope.$on('InitialDataLoaded', function() { + // Load data for the host summary table + if (!api_complete) { + ReloadHostSummaryList({ + scope: scope, + callback: 'APIComplete' + }); + } + }); + + if (scope.removePlaysReady) { + scope.removePlaysReady(); + } + scope.removePlaysReady = scope.$on('PlaysReady', function() { + // Select the most recent play, which will trigger tasks and hosts to load + var ids = Object.keys(scope.plays), + lastPlay = (ids.length > 0) ? ids[ids.length - 1] : null; + SelectPlay({ + scope: scope, + id: lastPlay, + callback: 'InitialDataLoaded' + }); + }); + + if (scope.removeLoadJobDetails) { + scope.removeLoadJobDetails(); + } + scope.removeRefreshJobDetails = scope.$on('LoadJobDetails', function(e, events_url) { + + // Call to load all the job bits including, plays, tasks, hosts results and host summary + var url = scope.job.url + 'job_plays/?order_by=id'; + + scope.tasks = {}; + scope.hostResults = []; + scope.hostResultsMap = {}; + api_complete = false; + + Rest.setUrl(url); + Rest.get() + .success( function(data) { + data.forEach(function(event, idx) { + var status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful', + start = event.started, + end, + elapsed; + if (idx < data.length - 1) { + // end date = starting date of the next event + end = data[idx + 1].started; + } + else if (JobIsFinished(scope)) { + // this is the last play and the job already finished + end = scope.job_status.finished; + } + if (end) { + elapsed = GetElapsed({ + start: start, + end: end + }); + } + else { + elapsed = '00:00:00'; + } + scope.plays[event.id] = { + id: event.id, + name: event.play, + created: start, + finished: end, + status: status, + elapsed: elapsed, + playActiveClass: '' + }; + scope.host_summary.ok += data.ok_count; + scope.host_summary.changed += data.changed_count; + scope.host_summary.unreachable += (data.unreachable_count) ? data.unreachable_count : 0; + scope.host_summary.failed += data.failed_count; + scope.host_summary.total = scope.host_summary.ok + scope.host_summary.changed + + scope.host_summary.unreachable + scope.host_summary.failed; + }); + + scope.$emit('PlaysReady', events_url); + }) + .error( function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. 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; + }) + .error( function(data, status) { + scope.credential_name = ''; + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + } + if (data.cloud_credential) { + url = GetBasePath('credentials') + data.credential + '/'; + Rest.setUrl(url); + Rest.get() + .success( function(data) { + scope.cloud_credential_name = data.name; + }) + .error( function(data, status) { + scope.credential_name = ''; + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + } + }); + + + if (scope.removeLoadJob) { + scope.removeLoadJob(); + } + scope.removeLoadJobRow = scope.$on('LoadJob', function() { + //Wait('start'); + // Load the job record + Rest.setUrl(GetBasePath('jobs') + job_id + '/'); + Rest.get() + .success(function(data) { + scope.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; + + // 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 === 'waiting' || data.status === 'new') ? 'pending' : 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'; + } + if ($rootScope.myInterval) { + window.clearInterval($rootScope.myInterval); + } + scope.setSearchAll('host'); + scope.$emit('LoadJobDetails', data.related.job_events); + scope.$emit('GetCredentialNames', data); + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve job: ' + $routeParams.id + '. GET returned: ' + status }); + }); + }); + + if (scope.removeRefreshCompleted) { + scope.removeRefreshCompleted(); + } + scope.removeRefreshCompleted = scope.$on('RefreshCompleted', function() { + refresh_count++; + if (refresh_count === 1) { + // First time. User just loaded page. + scope.$emit('LoadJob'); + } + else { + // Check if we need to redraw the group + setTimeout(function() { DrawGraph({ scope: scope, resize: true }); }, 500); + } + }); + + scope.adjustSize = function() { + var height, ww = $(window).width(); + if (ww < 1240) { + $('#job-summary-container').hide(); + $('#job-detail-container').css({ "width": "100%", "padding-right": "15px" }); + $('#summary-button').show(); + } + else { + $('.overlay').hide(); + $('#summary-button').hide(); + $('#hide-summary-button').hide(); + $('#job-detail-container').css({ "width": "58.33333333%", "padding-right": "7px" }); + $('#job-summary-container .job_well').css({ + 'box-shadow': 'none', + 'height': 'auto' + }); + $('#job-summary-container').css({ + "width": "41.66666667%", + "padding-left": "7px", + "padding-right": "15px", + "z-index": 0 + }); + setTimeout(function() { $('#job-summary-container .job_well').height($('#job-detail-container').height() - 18); }, 500); + $('#job-summary-container').show(); + } + // Detail table height adjusting. First, put page height back to 'normal'. + $('#plays-table-detail').height(150); + $('#plays-table-detail').mCustomScrollbar("update"); + $('#tasks-table-detail').height(150); + $('#tasks-table-detail').mCustomScrollbar("update"); + $('#hosts-table-detail').height(150); + $('#hosts-table-detail').mCustomScrollbar("update"); + height = $('#wrap').height() - $('.site-footer').outerHeight() - $('.main-container').height(); + if (height > 15) { + // there's a bunch of white space at the bottom, let's use it + $('#plays-table-detail').height(150 + (height / 3)); + $('#plays-table-detail').mCustomScrollbar("update"); + $('#tasks-table-detail').height(150 + (height / 3)); + $('#tasks-table-detail').mCustomScrollbar("update"); + $('#hosts-table-detail').height(150 + (height / 3)); + $('#hosts-table-detail').mCustomScrollbar("update"); + } + // Summary table height adjusting. + height = ($('#job-detail-container').height() / 2) - $('#hosts-summary-section .header').outerHeight() - + $('#hosts-summary-section .table-header').outerHeight() - + $('#summary-search-section').outerHeight() - 20; + $('#hosts-summary-table').height(height); + $('#hosts-summary-table').mCustomScrollbar("update"); + scope.$emit('RefreshCompleted'); + }; + + setTimeout(function() { scope.adjustSize(); }, 500); + + // Use debounce for the underscore library to adjust after user resizes window. + $(window).resize(_.debounce(function(){ + scope.adjustSize(); + }, 500)); + + scope.setSearchAll = function(search) { + if (search === 'host') { + scope.search_all_label = 'Host'; + scope.searchAllDisabled = false; + scope.search_all_placeholder = 'Search all by host name'; + } + else { + scope.search_all_label = 'Failures'; + scope.search_all_placeholder = 'Show failed events'; + scope.searchAllDisabled = true; + scope.search_all_placeholder = ''; + } + }; + + scope.selectPlay = function(id) { + SelectPlay({ + scope: scope, + id: id + }); + }; + + scope.selectTask = function(id) { + SelectTask({ + scope: scope, + id: id + }); + }; + + scope.toggleSummary = function(hide) { + var docw, doch, height = $('#job-detail-container').height(), slide_width; + if (!hide) { + docw = $(window).width(); + doch = $(window).height(); + slide_width = (docw < 840) ? '100%' : '80%'; + $('#summary-button').hide(); + $('.overlay').css({ + width: $(document).width(), + height: $(document).height() + }).show(); + + // Adjust the summary table height + $('#job-summary-container .job_well').height(height - 18).css({ + 'box-shadow': '-3px 3px 5px 0 #ccc' + }); + height = Math.floor($('#job-detail-container').height() * 0.5) - + $('#hosts-summary-section .header').outerHeight() - + $('#hosts-summary-section .table-header').outerHeight() - + $('#hide-summary-button').outerHeight() - + $('#summary-search-section').outerHeight() - + $('#hosts-summary-section .header').outerHeight() - + $('#hosts-summary-section .legend').outerHeight(); + $('#hosts-summary-table').height(height - 50); + $('#hosts-summary-table').mCustomScrollbar("update"); + + $('#hide-summary-button').show(); + + $('#job-summary-container').css({ + top: 0, + right: 0, + width: slide_width, + 'z-index': 2000, + 'padding-right': '15px', + 'padding-left': '15px' + }).show('slide', {'direction': 'right'}); + + setTimeout(function() { DrawGraph({ scope: scope, resize: true }); }, 500); + } + else { + $('.overlay').hide(); + $('#summary-button').show(); + $('#job-summary-container').hide('slide', {'direction': 'right'}); + } + }; + + scope.objectIsEmpty = function(obj) { + if (angular.isObject(obj)) { + return (Object.keys(obj).length > 0) ? false : true; + } + return true; + }; + + scope.HostDetailOnTotalScroll = _.debounce(function() { + // Called when user scrolls down (or forward in time). Using _.debounce + var url, mcs = arguments[0]; + scope.$apply(function() { + if (!scope.auto_scroll && scope.activeTask && scope.hostResults.length) { + scope.auto_scroll = true; + url = GetBasePath('jobs') + job_id + '/job_events/?parent=' + scope.activeTask + '&'; + url += (scope.search_all_hosts_name) ? 'host__name__icontains=' + scope.search_all_hosts_name + '&' : ''; + url += (scope.searchAllStatus === 'failed') ? 'failed=true&' : ''; + url += 'host__name__gt=' + scope.hostResults[scope.hostResults.length - 1].name + '&host__isnull=false&page_size=' + (scope.hostTableRows / 3) + '&order_by=host__name'; + Wait('start'); + Rest.setUrl(url); + Rest.get() + .success(function(data) { + data.results.forEach(function(row) { + scope.hostResults.push({ + id: row.id, + status: ( (row.failed) ? 'failed': (row.changed) ? 'changed' : 'successful' ), + host_id: row.host, + task_id: row.parent, + name: row.event_data.host, + created: row.created, + msg: ( (row.event_data && row.event_data.res) ? row.event_data.res.msg : '' ) + }); + if (scope.hostResults.length > scope.hostTableRows) { + scope.hostResults.splice(0,1); + } + }); + if (data.next) { + // there are more rows. move dragger up, letting user know. + setTimeout(function() { $('#hosts-table-detail .mCSB_dragger').css({ top: (mcs.draggerTop - 15) + 'px'}); }, 700); + } + scope.auto_scroll = false; + Wait('stop'); + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + } + else { + scope.auto_scroll = false; + } + }); + }, 300); + + scope.HostDetailOnTotalScrollBack = _.debounce(function() { + // Called when user scrolls up (or back in time) + var url, mcs = arguments[0]; + scope.$apply(function() { + if (!scope.auto_scroll && scope.activeTask && scope.hostResults.length) { + scope.auto_scroll = true; + url = GetBasePath('jobs') + job_id + '/job_events/?parent=' + scope.activeTask + '&'; + url += (scope.search_all_hosts_name) ? 'host__name__icontains=' + scope.search_all_hosts_name + '&' : ''; + url += (scope.searchAllStatus === 'failed') ? 'failed=true&' : ''; + url += 'host__name__lt=' + scope.hostResults[0].name + '&host__isnull=false&page_size=' + (scope.hostTableRows / 3) + '&order_by=-host__name'; + Wait('start'); + Rest.setUrl(url); + Rest.get() + .success(function(data) { + data.results.forEach(function(row) { + scope.hostResults.unshift({ + id: row.id, + status: ( (row.failed) ? 'failed': (row.changed) ? 'changed' : 'successful' ), + host_id: row.host, + task_id: row.parent, + name: row.event_data.host, + created: row.created, + msg: ( (row.event_data && row.event_data.res) ? row.event_data.res.msg : '' ) + }); + if (scope.hostResults.length > scope.hostTableRows) { + scope.hostResults.pop(); + } + }); + if (data.next) { + // there are more rows. move dragger down, letting user know. + setTimeout(function() { $('#hosts-table-detail .mCSB_dragger').css({ top: (mcs.draggerTop + 15) + 'px' }); }, 700); + } + Wait('stop'); + scope.auto_scroll = false; + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + } + else { + scope.auto_scroll = false; + } + }); + }, 300); + + scope.HostSummaryOnTotalScroll = function(mcs) { + var url; + if (!scope.auto_scroll && scope.hosts) { + url = GetBasePath('jobs') + job_id + '/job_host_summaries/?'; + url += (scope.search_all_hosts_name) ? 'host__name__icontains=' + scope.search_all_hosts_name + '&' : ''; + url += (scope.searchAllStatus === 'failed') ? 'failed=true&' : ''; + url += 'host__name__gt=' + scope.hosts[scope.hosts.length - 1].name + '&page_size=' + (scope.hostSummaryTableRows / 3) + '&order_by=host__name'; + Wait('start'); + Rest.setUrl(url); + Rest.get() + .success(function(data) { + setTimeout(function() { + scope.$apply(function() { + data.results.forEach(function(row) { + scope.hosts.push({ + id: row.host, + name: row.summary_fields.host.name, + ok: row.ok, + changed: row.changed, + unreachable: row.dark, + failed: row.failures + }); + if (scope.hosts.length > scope.hostSummaryTableRows) { + scope.hosts.splice(0,1); + } + }); + if (data.next) { + // there are more rows. move dragger up, letting user know. + setTimeout(function() { $('#hosts-summary-table .mCSB_dragger').css({ top: (mcs.draggerTop - 15) + 'px'}); }, 700); + } + }); + }, 100); + Wait('stop'); + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + } + else { + scope.auto_scroll = false; + } + }; + + scope.HostSummaryOnTotalScrollBack = function(mcs) { + var url; + if (!scope.auto_scroll && scope.hosts) { + url = GetBasePath('jobs') + job_id + '/job_host_summaries/?'; + url += (scope.search_all_hosts_name) ? 'host__name__icontains=' + scope.search_all_hosts_name + '&' : ''; + url += (scope.searchAllStatus === 'failed') ? 'failed=true&' : ''; + url += 'host__name__lt=' + scope.hosts[0].name + '&page_size=' + (scope.hostSummaryTableRows / 3) + '&order_by=-host__name'; + Wait('start'); + Rest.setUrl(url); + Rest.get() + .success(function(data) { + setTimeout(function() { + scope.$apply(function() { + data.results.forEach(function(row) { + scope.hosts.unshift({ + id: row.host, + name: row.summary_fields.host.name, + ok: row.ok, + changed: row.changed, + unreachable: row.dark, + failed: row.failures + }); + if (scope.hosts.length > scope.hostSummaryTableRows) { + scope.hosts.pop(); + } + }); + if (data.next) { + // there are more rows. move dragger down, letting user know. + setTimeout(function() { $('#hosts-summary-table .mCSB_dragger').css({ top: (mcs.draggerTop + 15) + 'px' }); }, 700); + } + }); + }, 100); + Wait('stop'); + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + } + else { + scope.auto_scroll = false; + } + }; + + scope.searchAllByHost = function() { + var nxtPlay; + if (scope.search_all_hosts_name) { + FilterAllByHostName({ + scope: scope, + host: scope.search_all_hosts_name + }); + scope.searchAllHostsEnabled = false; + } + else { + scope.search_all_tasks = []; + scope.search_all_plays = []; + scope.searchAllHostsEnabled = true; + nxtPlay = scope.plays[scope.plays.length - 1].id; + SelectPlay({ + scope: scope, + id: nxtPlay + }); + ReloadHostSummaryList({ + scope: scope + }); + } + }; + + scope.allHostNameKeyPress = function(e) { + if (e.keyCode === 13) { + scope.searchAllByHost(); + } + }; + + scope.filterByStatus = function(choice) { + var key, keys, nxtPlay; + if (choice === 'Failed') { + scope.searchAllStatus = 'failed'; + for(key in scope.plays) { + if (scope.plays[key].status === 'failed') { + nxtPlay = key; + } + } + } + else { + scope.searchAllStatus = ''; + keys = Object.keys(scope.plays); + nxtPlay = (keys.length > 0) ? keys[keys.length - 1] : null; + } + SelectPlay({ + scope: scope, + id: nxtPlay + }); + ReloadHostSummaryList({ + scope: scope + }); + //setTimeout(function() { + // SelectPlay({ scope: scope, id: scope.activePlay }); + //}, 2000); + }; + + scope.viewEvent = function(event_id) { + $log.debug(event_id); + }; + +} + +JobDetailController.$inject = [ '$rootScope', '$scope', '$compile', '$routeParams', '$log', 'ClearScope', 'Breadcrumbs', 'LoadBreadCrumbs', 'GetBasePath', + 'Wait', 'Rest', 'ProcessErrors', 'DigestEvents', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed', 'SelectHost', 'FilterAllByHostName', 'DrawGraph', + 'LoadHostSummary', 'ReloadHostSummaryList', 'JobIsFinished' +]; diff --git a/awx/ui/static/js/controllers/Jobs.js b/awx/ui/static/js/controllers/Jobs.js index 716e1952d3..7e10f69c85 100644 --- a/awx/ui/static/js/controllers/Jobs.js +++ b/awx/ui/static/js/controllers/Jobs.js @@ -92,7 +92,6 @@ function JobsListController ($scope, $compile, $routeParams, ClearScope, Breadcr var event; listCount=0; if (event_queue.length > 0) { - //console.log('found queued events'); event = event_queue[0]; processEvent(event); event_queue.splice(0,1); @@ -107,9 +106,6 @@ function JobsListController ($scope, $compile, $routeParams, ClearScope, Breadcr } }); } - //else { - //console.log('no more events'); - //} }); LoadBreadCrumbs(); diff --git a/awx/ui/static/js/helpers/JobDetail.js b/awx/ui/static/js/helpers/JobDetail.js index 614628bd41..88c9691cba 100644 --- a/awx/ui/static/js/helpers/JobDetail.js +++ b/awx/ui/static/js/helpers/JobDetail.js @@ -39,83 +39,61 @@ angular.module('JobDetailHelper', ['Utilities', 'RestServices']) -.factory('DigestEvents', ['$log', 'UpdatePlayStatus', 'UpdateHostStatus', 'AddHostResult', 'SelectPlay', 'SelectTask', +.factory('ProcessEventQueue', ['$log', 'DigestEvent', 'JobIsFinished', function ($log, DigestEvent, JobIsFinished) { + return function(params) { + var scope = params.scope, + eventQueue = params.eventQueue, + event; + function runTheQ() { + while (eventQueue.length > 0) { + event = eventQueue.pop(); + $log.debug('read event: ' + event.id); + DigestEvent({ scope: scope, event: event }); + } + if (!JobIsFinished(scope) && !scope.haltEventQueue) { + setTimeout( function() { + runTheQ(); + }, 300); + } + } + runTheQ(); + }; +}]) + +.factory('DigestEvent', ['$rootScope', '$log', 'UpdatePlayStatus', 'UpdateHostStatus', 'AddHostResult', 'SelectPlay', 'SelectTask', 'GetHostCount', 'GetElapsed', 'UpdateTaskStatus', 'DrawGraph', 'LoadHostSummary', 'JobIsFinished', -function($log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, SelectTask, GetHostCount, GetElapsed, +function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, SelectTask, GetHostCount, GetElapsed, UpdateTaskStatus, DrawGraph, LoadHostSummary, JobIsFinished) { return function(params) { var scope = params.scope, - queue = params.queue, - lastEventId = params.lastEventId; + event = params.event, + hostCount; - function popEvent() { - $log.debug('queue length: ' + queue.length); - if (queue.length > 0 && queue.length < 500) { - var event = queue.splice(0,1); - if (event[0].id > lastEventId) { - $log.debug('processing event: ' + event[0].id); - scope.$emit('ProcessEvent', event[0]); - } - } - else if (queue.length > 500) { - // if we get too far behind, clear the queue and refresh - queue = []; - scope.emit('LoadJob'); - } - } - - if (scope.removeGetNextEvent) { - scope.removeGetNextEvent(); - } - scope.removeGetNextEvent = scope.$on('GetNextEvent', function() { - if (scope.myInterval) { - window.clearInterval(scope.myInterval); - } - popEvent(); - if (!JobIsFinished(scope)) { - scope.myInterval = window.setInterval(function() { - popEvent(); - }, 1000); - } - }); - - if (scope.removeProcessEvent) { - scope.removeProcessEvent(); - } - scope.removeProcessEvent = scope.$on('ProcessEvent', function(e, event) { - var hostCount; - $log.debug('handling event: ' + event.id); - if (event.event === 'playbook_on_start') { + switch (event.event) { + case 'playbook_on_start': if (!JobIsFinished(scope)) { scope.job_status.started = event.created; scope.job_status.status = 'running'; } - scope.$emit('GetNextEvent'); - } + break; - if (event.event === 'playbook_on_play_start') { + case 'playbook_on_play_start': scope.plays[event.id] = { id: event.id, name: event.play, created: event.created, - status: (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'none', + status: (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful', elapsed: '00:00:00' }; - if (scope.plays.length > 1) { - DrawGraph({ scope: scope, resize: false }); - } - SelectPlay({ - scope: scope, - id: event.id - }); - } - if (event.event === 'playbook_on_setup') { + scope.tasks = {}; + scope.hostResults = []; + scope.hostResultsMap = {}; + scope.activePlay = event.id; + break; + + case 'playbook_on_setup': if (scope.activePlay === event.parent) { - hostCount = GetHostCount({ - scope: scope, - play_id: event.parent - }); scope.tasks[event.id] = { id: event.id, play_id: event.parent, @@ -123,7 +101,7 @@ function($log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, Se status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ), created: event.created, modified: event.modified, - hostCount: hostCount, + hostCount: 0, reportedHosts: 0, successfulCount: 0, failedCount: 0, @@ -134,10 +112,9 @@ function($log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, Se changedStyle: { display: 'none' }, skippedStyle: { display: 'none' } }; - SelectTask({ - scope: scope, - id: event.id - }); + scope.hostResults = []; + scope.hostResultsMap = {}; + scope.activeTask = event.id; } UpdatePlayStatus({ scope: scope, @@ -146,25 +123,11 @@ function($log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, Se changed: event.changed, modified: event.modified }); - } - if (event.event === 'playbook_on_no_hosts_matched') { - UpdatePlayStatus({ - scope: scope, - play_id: event.parent, - failed: true, - changed: false, - modified: event.modified, - status_text: 'failed- no hosts matched' - }); - scope.$emit('GetNextEvent'); - } - if (event.event === 'playbook_on_task_start') { - if (scope.activePlay === event.parent) { - hostCount = GetHostCount({ - scope: scope, - play_id: event.parent - }); + break; + case 'playbook_on_task_start': + if (scope.activePlay === event.parent) { + hostCount = GetHostCount({ scope: scope }); scope.tasks[event.id] = { id: event.id, name: event.task, @@ -184,10 +147,9 @@ function($log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, Se changedStyle: { display: 'none' }, skippedStyle: { display: 'none' } }; - SelectTask({ - scope: scope, - id: event.id - }); + scope.hostResults = []; + scope.hostResultsMap = {}; + scope.activeTask = event.id; } if (event.role) { scope.hasRoles = true; @@ -199,37 +161,64 @@ function($log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, Se changed: event.changed, modified: event.modified }); - } + break; - if (event.event === 'runner_on_unreachable') { + case 'runner_on_ok': + case 'runner_on_async_ok': + UpdateHostStatus({ + scope: scope, + name: event.event_data.host, + host_id: event.host, + task_id: event.parent, + status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ), + id: event.id, + created: event.created, + modified: event.modified, + message: (event.event_data && event.event_data.res) ? event.event_data.res.msg : '' + }); + break; + + case 'playbook_on_no_hosts_matched': + UpdatePlayStatus({ + scope: scope, + play_id: event.parent, + failed: true, + changed: false, + modified: event.modified, + status_text: 'failed- no hosts matched' + }); + break; + + case '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, + id: event.id, created: event.created, modified: event.modified, message: ( (event.event_data && event.event_data.res) ? event.event_data.res.msg : '' ) }); - scope.$emit('GetNextEvent'); - } - if (event.event === 'runner_on_error' || event.event === 'runner_on_async_failed') { + break; + + case 'runner_on_error': + case 'runner_on_async_failed': UpdateHostStatus({ scope: scope, name: event.event_data.host, host_id: event.host, task_id: event.parent, status: 'failed', - event_id: event.id, + id: event.id, created: event.created, modified: event.modified, message: (event.event_data && event.event_data.res) ? event.event_data.res.msg : '' }); - scope.$emit('GetNextEvent'); - } - if (event.event === 'runner_on_no_hosts') { + break; + + case 'runner_on_no_hosts': UpdateTaskStatus({ scope: scope, failed: event.failed, @@ -238,37 +227,23 @@ function($log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, Se modified: event.modified, no_hosts: true }); - scope.$emit('GetNextEvent'); - } - if (event.event === 'runner_on_skipped') { + break; + + case '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, + id: event.id, created: event.created, modified: event.modified, message: (event.event_data && event.event_data.res) ? event.event_data.res.msg : '' }); - scope.$emit('GetNextEvent'); - } - if (event.event === 'runner_on_ok' || event.event === 'runner_on_async_ok') { - UpdateHostStatus({ - scope: scope, - name: event.event_data.host, - host_id: event.host, - task_id: event.parent, - status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ), - event_id: event.id, - created: event.created, - modified: event.modified, - message: (event.event_data && event.event_data.res) ? event.event_data.res.msg : '' - }); - scope.$emit('GetNextEvent'); - } - if (event.event === 'playbook_on_stats') { + break; + + case 'playbook_on_stats': scope.job_status.finished = event.modified; scope.job_status.elapsed = GetElapsed({ start: scope.job_status.started, @@ -279,11 +254,8 @@ function($log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, Se scope.host_summary = {}; LoadHostSummary({ scope: scope, data: event.event_data }); DrawGraph({ scope: scope, resize: true }); - } - }); - - scope.$emit('GetNextEvent'); // Start checking the queue - + break; + } }; }]) @@ -455,7 +427,7 @@ function($log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, Se var scope = params.scope, status = params.status, // successful, changed, unreachable, failed, skipped name = params.name, - event_id = params.event_id, + event_id = params.id, host_id = params.host_id, task_id = params.task_id, modified = params.modified, @@ -541,8 +513,7 @@ function($log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, Se msg = params.message, play_id, first; - if (scope.activeTask === task_id && !scope.hostResultsMap[host_id]) { - // the event applies to the currently selected task + if (!scope.hostResultsMap[host_id]) { scope.hostResults.push({ id: event_id, status: status, @@ -633,8 +604,9 @@ function($log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, Se return function(params) { var scope = params.scope, id = params.id, - callback = params.callback; - + callback = params.callback, + clear; + clear = (scope.activePlay === id) ? false : true; //are we moving to a new play? if (scope.plays[scope.activePlay]) { scope.plays[scope.activePlay].playActiveClass = ''; } @@ -645,7 +617,8 @@ function($log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, Se LoadTasks({ scope: scope, - callback: callback + callback: callback, + clear: clear }); }; }]) @@ -654,6 +627,7 @@ function($log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, Se return function(params) { var scope = params.scope, callback = params.callback, + clear = params.clear, url, tIds, lastId; if (scope.activePlay) { @@ -667,7 +641,9 @@ function($log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, Se Rest.setUrl(url); Rest.get() .success(function(data) { - scope.tasks = {}; + if (clear) { + scope.tasks = {}; + } data.forEach(function(event, idx) { var end, elapsed; if ((!scope.searchAllStatus) || (scope.searchAllStatus === 'failed' && event.failed) && @@ -749,12 +725,13 @@ function($log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, Se return function(params) { var scope = params.scope, id = params.id, - callback = params.callback; + callback = params.callback, + clear; + clear = (scope.activeTask === id) ? false : true; if (scope.activeTask && scope.tasks[scope.activeTask]) { scope.tasks[scope.activeTask].taskActiveClass = ''; } - if (id) { scope.tasks[id].taskActiveClass = 'active'; scope.activeTaskName = scope.tasks[id].name; @@ -765,11 +742,13 @@ function($log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, Se setTimeout( function() { scope.auto_scroll = true; $('#tasks-table-detail').mCustomScrollbar("scrollTo", "bottom"); - }, 700); + + }, 1500); LoadHosts({ scope: scope, - callback: callback + callback: callback, + clear: clear }); }; }]) @@ -779,9 +758,12 @@ function($log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, Se return function(params) { var scope = params.scope, callback = params.callback, + clear = params.clear, url; - scope.hostResults = []; - scope.hostResultsMap = {}; + if (clear) { + scope.hostResults = []; + scope.hostResultsMap = {}; + } if (scope.activeTask) { // If we have a selected task, then get the list of hosts url = scope.job.related.job_events + '?parent=' + scope.activeTask + '&'; @@ -807,7 +789,6 @@ function($log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, Se scope.$emit(callback); } SelectHost({ scope: scope }); - scope.$emit('GetNextEvent'); }) .error(function(data, status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', @@ -819,7 +800,6 @@ function($log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, Se scope.$emit(callback); } SelectHost({ scope: scope }); - scope.$emit('GetNextEvent'); } }; }]) diff --git a/awx/ui/static/js/helpers/JobDetail.js.old b/awx/ui/static/js/helpers/JobDetail.js.old new file mode 100644 index 0000000000..35c935fe46 --- /dev/null +++ b/awx/ui/static/js/helpers/JobDetail.js.old @@ -0,0 +1,1017 @@ +/************************************ + * Copyright (c) 2014 AnsibleWorks, Inc. + * + * JobDetail.js + * + * Helper moduler for JobDetails controller + * + */ + +/* + # Playbook events will be structured to form the following hierarchy: + # - playbook_on_start (once for each playbook file) + # - playbook_on_vars_prompt (for each play, but before play starts, we + # currently don't handle responding to these prompts) + # - playbook_on_play_start (once for each play) + # - playbook_on_import_for_host + # - playbook_on_not_import_for_host + # - playbook_on_no_hosts_matched + # - playbook_on_no_hosts_remaining + # - playbook_on_setup + # - runner_on* + # - playbook_on_task_start (once for each task within a play) + # - runner_on_failed + # - runner_on_ok + # - runner_on_error + # - runner_on_skipped + # - runner_on_unreachable + # - runner_on_no_hosts + # - runner_on_async_poll + # - runner_on_async_ok + # - runner_on_async_failed + # - runner_on_file_diff + # - playbook_on_notify (once for each notification from the play) + # - playbook_on_stats + +*/ + +'use strict'; + +angular.module('JobDetailHelper', ['Utilities', 'RestServices']) + +.factory('DigestEvents', ['$rootScope', '$log', 'UpdatePlayStatus', 'UpdateHostStatus', 'AddHostResult', 'SelectPlay', 'SelectTask', + 'GetHostCount', 'GetElapsed', 'UpdateTaskStatus', 'DrawGraph', 'LoadHostSummary', 'JobIsFinished', +function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, SelectPlay, SelectTask, GetHostCount, GetElapsed, + UpdateTaskStatus, DrawGraph, LoadHostSummary, JobIsFinished) { + return function(params) { + + var scope = params.scope, + queue = params.queue, + lastEventId = params.lastEventId; + + function popEvent() { + $log.debug('queue length: ' + queue.length); + if (queue.length > 0) { + var event = queue.splice(0,1); + if (event[0].id > lastEventId) { + $log.debug('processing event: ' + event[0].id); + scope.$emit('ProcessEvent', event[0]); + } + } + } + + if (scope.removeGetNextEvent) { + scope.removeGetNextEvent(); + } + scope.removeGetNextEvent = scope.$on('GetNextEvent', function() { + if ($rootScope.myInterval) { + window.clearInterval(scope.myInterval); + } + if (!JobIsFinished(scope)) { + $rootScope.myInterval = window.setInterval(function() { + popEvent(); + }, 300); + } + }); + + if (scope.removeProcessEvent) { + scope.removeProcessEvent(); + } + scope.removeProcessEvent = scope.$on('ProcessEvent', function(e, event) { + var hostCount; + $log.debug('handling event: ' + event.id); + if (event.event === 'playbook_on_start') { + if (!JobIsFinished(scope)) { + scope.job_status.started = event.created; + scope.job_status.status = 'running'; + } + } + + if (event.event === 'playbook_on_play_start') { + scope.plays[event.id] = { + id: event.id, + name: event.play, + created: event.created, + status: (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'none', + elapsed: '00:00:00' + }; + if (scope.plays.length > 1) { + DrawGraph({ scope: scope, resize: false }); + } + SelectPlay({ + scope: scope, + id: event.id + }); + } + if (event.event === 'playbook_on_setup') { + if (scope.activePlay === event.parent) { + hostCount = GetHostCount({ + scope: scope, + play_id: event.parent + }); + scope.tasks[event.id] = { + id: event.id, + play_id: event.parent, + name: event.event_display, + status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ), + created: event.created, + modified: event.modified, + hostCount: hostCount, + reportedHosts: 0, + successfulCount: 0, + failedCount: 0, + changedCount: 0, + skippedCount: 0, + successfulStyle: { display: 'none'}, + failedStyle: { display: 'none' }, + changedStyle: { display: 'none' }, + skippedStyle: { display: 'none' } + }; + SelectTask({ + scope: scope, + id: event.id + }); + } + UpdatePlayStatus({ + scope: scope, + play_id: event.parent, + failed: event.failed, + changed: event.changed, + modified: event.modified + }); + } + if (event.event === 'playbook_on_no_hosts_matched') { + UpdatePlayStatus({ + scope: scope, + play_id: event.parent, + failed: true, + changed: false, + modified: event.modified, + status_text: 'failed- no hosts matched' + }); + } + if (event.event === 'playbook_on_task_start') { + if (scope.activePlay === event.parent) { + hostCount = GetHostCount({ + scope: scope, + play_id: event.parent + }); + + scope.tasks[event.id] = { + id: event.id, + name: event.task, + play_id: event.parent, + 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, + skippedCount: 0, + successfulStyle: { display: 'none'}, + failedStyle: { display: 'none' }, + changedStyle: { display: 'none' }, + skippedStyle: { display: 'none' } + }; + SelectTask({ + scope: scope, + id: event.id + }); + } + if (event.role) { + scope.hasRoles = true; + } + UpdatePlayStatus({ + scope: scope, + play_id: event.parent, + failed: event.failed, + changed: event.changed, + modified: event.modified + }); + } + + 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, + created: event.created, + modified: event.modified, + message: ( (event.event_data && event.event_data.res) ? event.event_data.res.msg : '' ) + }); + } + if (event.event === 'runner_on_error' || event.event === 'runner_on_async_failed') { + UpdateHostStatus({ + scope: scope, + name: event.event_data.host, + host_id: event.host, + task_id: event.parent, + status: 'failed', + event_id: event.id, + created: event.created, + modified: event.modified, + message: (event.event_data && event.event_data.res) ? event.event_data.res.msg : '' + }); + } + if (event.event === 'runner_on_no_hosts') { + UpdateTaskStatus({ + scope: scope, + failed: event.failed, + changed: event.changed, + task_id: event.parent, + modified: event.modified, + no_hosts: true + }); + } + 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, + created: event.created, + modified: event.modified, + message: (event.event_data && event.event_data.res) ? event.event_data.res.msg : '' + }); + } + if (event.event === 'runner_on_ok' || event.event === 'runner_on_async_ok') { + UpdateHostStatus({ + scope: scope, + name: event.event_data.host, + host_id: event.host, + task_id: event.parent, + status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ), + event_id: event.id, + created: event.created, + modified: event.modified, + message: (event.event_data && event.event_data.res) ? event.event_data.res.msg : '' + }); + } + 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) ? 'failed' : 'successful'; + scope.job_status.status_class = ""; + scope.host_summary = {}; + LoadHostSummary({ scope: scope, data: event.event_data }); + DrawGraph({ scope: scope, resize: true }); + } + }); + + scope.$emit('GetNextEvent'); // Start checking the queue + + }; +}]) + +.factory('JobIsFinished', [ function() { + return function(scope) { + return (scope.job_status.status === 'failed' || scope.job_status.status === 'canceled' || + scope.job_status.status === 'error' || scope.job_status.status === 'successful'); + }; +}]) + +//Get the # of expected hosts for a task by looking at the number +//on the very first task for a play +.factory('GetHostCount', [ 'FindFirstTaskofPlay', function(FindFirstTaskofPlay) { + return function(params) { + var scope = params.scope, + task_id = FindFirstTaskofPlay({ scope: scope }); + if (task_id) { + return scope.tasks[task_id].hostCount; + } + return 0; + }; +}]) + +.factory('FindFirstTaskofPlay', function() { + return function(params) { + var scope = params.scope, + taskIds; + taskIds = Object.keys(scope.tasks); + return (taskIds.length > 0) ? scope.tasks[taskIds[0]].id : null; + }; +}) + +.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'; + } + }; +}]) + +.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 = 'failed'; + } + 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)) { + scope.job_status.elapsed = GetElapsed({ + start: scope.job_status.started, + end: scope.job_status.finished + }); + } + }; +}]) + +// Update the status of a play +.factory('UpdatePlayStatus', ['GetElapsed', 'UpdateJobStatus', function(GetElapsed, UpdateJobStatus) { + return function(params) { + var scope = params.scope, + failed = params.failed, + changed = params.changed, + id = params.play_id, + modified = params.modified, + no_hosts = params.no_hosts, + status_text = params.status_text, + play = scope.plays[id]; + + if (scope.plays[id]) { + if (failed) { + scope.plays[id].status = 'failed'; + } + else if (play.status !== 'changed' && play.status !== 'failed') { + // once the status becomes 'changed' or 'failed' don't modify it + if (no_hosts) { + scope.plays[id].status = 'no-matching-hosts'; + } + else { + scope.plays[id].status = (changed) ? 'changed' : (failed) ? 'failed' : 'successful'; + } + } + scope.plays[id].finished = modified; + scope.plays[id].elapsed = GetElapsed({ + start: play.created, + end: modified + }); + scope.plays[id].status_text = (status_text) ? status_text : scope.plays[id].status; + } + + UpdateJobStatus({ + scope: scope, + failed: null, + modified: modified + }); + }; +}]) + +.factory('UpdateTaskStatus', ['UpdatePlayStatus', 'GetElapsed', function(UpdatePlayStatus, GetElapsed) { + return function(params) { + var scope = params.scope, + failed = params.failed, + changed = params.changed, + id = params.task_id, + modified = params.modified, + no_hosts = params.no_hosts, + task = scope.tasks[id]; + + if (scope.tasks[id]) { + if (no_hosts){ + task.status = 'no-matching-hosts'; + } + else if (failed) { + task.status = 'failed'; + } + else if (task.status !== 'changed' && task.status !== 'failed') { + // once the status becomes 'changed' or 'failed' don't modify it + task.status = (failed) ? 'failed' : (changed) ? 'changed' : 'successful'; + } + task.finished = params.modified; + task.elapsed = GetElapsed({ + start: task.created, + end: modified + }); + + UpdatePlayStatus({ + scope: scope, + failed: failed, + changed: changed, + play_id: task.play_id, + modified: modified, + no_hosts: no_hosts + }); + } + }; +}]) + +// Each time a runner event is received update host summary totals and the parent task +.factory('UpdateHostStatus', ['UpdateTaskStatus', 'AddHostResult', + function(UpdateTaskStatus, AddHostResult) { + return function(params) { + var scope = params.scope, + 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, + msg = params.message, + host; + + if (scope.hostsMap[host_id]) { + host = scope.hosts[scope.hostsMap[host_id]]; + host.ok += (status === 'successful') ? 1 : 0; + host.changed += (status === 'changed') ? 1 : 0; + host.unreachable += (status === 'unreachable') ? 1 : 0; + host.failed += (status === 'failed') ? 1 : 0; + } + else { + // Totals for the summary graph + scope.host_summary.total += 1; + scope.host_summary.ok += (status === 'successful') ? 1 : 0; + scope.host_summary.changed += (status === 'changed') ? 1 : 0; + scope.host_summary.unreachable += (status === 'unreachable') ? 1 : 0; + scope.host_summary.failed += (status === 'failed') ? 1 : 0; + + scope.hosts.push({ + id: host_id, + name: name, + ok: (status === 'successful') ? 1 : 0, + changed: (status === 'changed') ? 1 : 0, + unreachable: (status === 'unreachable') ? 1 : 0, + failed: (status === 'failed') ? 1 : 0 + }); + + scope.hosts.sort(function (a, b) { + if (a.name > b.name) + return 1; + if (a.name < b.name) + return -1; + // a must be equal to b + return 0; + }); + + // prune the hosts array and rebuild the map + if (scope.hosts.length > scope.hostSummaryTableRows) { + scope.hosts.pop(); + } + scope.hostsMap = {}; + scope.hosts.forEach(function(host, idx){ + scope.hostsMap[host.id] = idx; + }); + $('#tasks-table-detail').mCustomScrollbar("update"); + } + + UpdateTaskStatus({ + scope: scope, + task_id: task_id, + failed: ((status === 'failed' || status === 'unreachable') ? true :false), + changed: ((status === 'changed') ? true : false), + modified: modified + }); + + AddHostResult({ + scope: scope, + task_id: task_id, + host_id: host_id, + event_id: event_id, + status: status, + name: name, + created: created, + message: msg + }); + }; +}]) + +// Add a new host result +.factory('AddHostResult', ['FindFirstTaskofPlay', 'SetTaskStyles', function(FindFirstTaskofPlay, SetTaskStyles) { + 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, + created = params.created, + name = params.name, + msg = params.message, + play_id, first; + + if (scope.activeTask === task_id && !scope.hostResultsMap[host_id]) { + // the event applies to the currently selected task + scope.hostResults.push({ + id: event_id, + status: status, + host_id: host_id, + task_id: task_id, + name: name, + created: created, + msg: msg + }); + scope.hostResults.sort(function(a,b) { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; + } + return 0; + }); + // Keep the list pruned to a limited # of hosts + if (scope.hostResults.length > scope.hostTableRows) { + scope.hostResults.splice(0,1); + } + // Refresh the map + scope.hostResultsMap = {}; + scope.hostResults.forEach(function(result, idx) { + scope.hostResultsMap[result.id] = idx; + }); + } + + // update the task + if (scope.tasks[task_id]) { + play_id = scope.tasks[task_id].play_id; + + first = FindFirstTaskofPlay({ + scope: scope, + play_id: play_id + }); + if (task_id === first) { + scope.tasks[task_id].hostCount += 1; + } + scope.tasks[task_id].reportedHosts += 1; + scope.tasks[task_id].failedCount += (status === 'failed' || status === 'unreachable') ? 1 : 0; + scope.tasks[task_id].changedCount += (status === 'changed') ? 1 : 0; + scope.tasks[task_id].successfulCount += (status === 'successful') ? 1 : 0; + scope.tasks[task_id].skippedCount += (status === 'skipped') ? 1 : 0; + SetTaskStyles({ + scope: scope, + task_id: task_id + }); + } + }; +}]) + +.factory('SetTaskStyles', [ function() { + return function(params) { + var task_id = params.task_id, + scope = params.scope, + diff; + scope.tasks[task_id].failedPct = (scope.tasks[task_id].hostCount > 0) ? Math.ceil((100 * (scope.tasks[task_id].failedCount / scope.tasks[task_id].hostCount))) : 0; + scope.tasks[task_id].changedPct = (scope.tasks[task_id].hostCount > 0) ? Math.ceil((100 * (scope.tasks[task_id].changedCount / scope.tasks[task_id].hostCount))) : 0; + scope.tasks[task_id].skippedPct = (scope.tasks[task_id].hostCount > 0) ? Math.ceil((100 * (scope.tasks[task_id].skippedCount / scope.tasks[task_id].hostCount))) : 0; + scope.tasks[task_id].successfulPct = (scope.tasks[task_id].hostCount > 0) ? Math.ceil((100 * (scope.tasks[task_id].successfulCount / scope.tasks[task_id].hostCount))) : 0; + + diff = (scope.tasks[task_id].failedPct + scope.tasks[task_id].changedPct + scope.tasks[task_id].skippedPct + scope.tasks[task_id].successfulPct) - 100; + if (diff > 0) { + if (scope.tasks[task_id].failedPct > diff) { + scope.tasks[task_id].failedPct = scope.tasks[task_id].failedPct - diff; + } + else if (scope.tasks[task_id].changedPct > diff) { + scope.tasks[task_id].changedPct = scope.tasks[task_id].changedPct - diff; + } + else if (scope.tasks[task_id].skippedPct > diff) { + scope.tasks[task_id].skippedPct = scope.tasks[task_id].skippedPct - diff; + } + else if (scope.tasks[task_id].successfulPct > diff) { + scope.tasks[task_id].successfulPct = scope.tasks[task_id].successfulPct - diff; + } + } + scope.tasks[task_id].successfulStyle = (scope.tasks[task_id].successfulPct > 0) ? { 'display': 'inline-block', 'width': scope.tasks[task_id].successfulPct + "%" } : { 'display': 'none' }; + scope.tasks[task_id].changedStyle = (scope.tasks[task_id].changedPct > 0) ? { 'display': 'inline-block', 'width': scope.tasks[task_id].changedPct + "%" } : { 'display': 'none' }; + scope.tasks[task_id].skippedStyle = (scope.tasks[task_id].skippedPct > 0) ? { 'display': 'inline-block', 'width': scope.tasks[task_id].skippedPct + "%" } : { 'display': 'none' }; + scope.tasks[task_id].failedStyle = (scope.tasks[task_id].failedPct > 0) ? { 'display': 'inline-block', 'width': scope.tasks[task_id].failedPct + "%" } : { 'display': 'none' }; + }; +}]) + +// Call SelectPlay whenever the the activePlay needs to change +.factory('SelectPlay', ['SelectTask', 'LoadTasks', function(SelectTask, LoadTasks) { + return function(params) { + var scope = params.scope, + id = params.id, + callback = params.callback; + + if (scope.plays[scope.activePlay]) { + scope.plays[scope.activePlay].playActiveClass = ''; + } + if (id) { + scope.plays[id].playActiveClass = 'active'; + } + scope.activePlay = id; + + LoadTasks({ + scope: scope, + callback: callback + }); + }; +}]) + +.factory('LoadTasks', ['Rest', 'ProcessErrors', 'GetElapsed', 'SelectTask', 'SetTaskStyles', function(Rest, ProcessErrors, GetElapsed, SelectTask, SetTaskStyles) { + return function(params) { + var scope = params.scope, + callback = params.callback, + url, tIds, lastId; + + if (scope.activePlay) { + url = scope.job.url + 'job_tasks/?event_id=' + scope.activePlay; + // job_tasks seems to ignore all query predicates other than event_id + //+ '&'; + //url += (scope.search_all_plays.length > 0) ? 'event_id__in=' + scope.search_all_plays.join() + '&' : ''; + //url += (scope.searchAllStatus === 'failed') ? 'failed=true&' : ''; + //url += 'order_by=id'; + + Rest.setUrl(url); + Rest.get() + .success(function(data) { + scope.tasks = {}; + data.forEach(function(event, idx) { + var end, elapsed; + if ((!scope.searchAllStatus) || (scope.searchAllStatus === 'failed' && event.failed) && + ((scope.search_all_tasks.length === 0) || (scope.searchAllTasks.indexOf(event.id)))) { + + if (idx < data.length - 1) { + // end date = starting date of the next event + end = data[idx + 1].created; + } + else { + // no next event (task), get the end time of the play + end = scope.plays[scope.activePlay].finished; + } + + if (end) { + elapsed = GetElapsed({ + start: event.created, + end: end + }); + } + else { + elapsed = '00:00:00'; + } + + scope.tasks[event.id] = { + id: event.id, + play_id: scope.activePlay, + name: event.name, + status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ), + created: event.created, + modified: event.modified, + finished: end, + elapsed: elapsed, + hostCount: event.host_count, // hostCount, + reportedHosts: event.reported_hosts, + successfulCount: event.successful_count, + failedCount: event.failed_count, + changedCount: event.changed_count, + skippedCount: event.skipped_count, + taskActiveClass: '' + }; + + SetTaskStyles({ + scope: scope, + task_id: event.id + }); + } + }); + + // set the active task + tIds = Object.keys(scope.tasks); + lastId = (tIds.length > 0) ? tIds[tIds.length - 1] : null; + SelectTask({ + scope: scope, + id: lastId, + callback: callback + }); + }) + .error(function(data) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + } + else { + // set the active task + tIds = Object.keys(scope.tasks); + lastId = (tIds.length > 0) ? tIds[tIds.length - 1] : null; + SelectTask({ + scope: scope, + id: lastId, + callback: callback + }); + } + }; +}]) + +// Call SelectTask whenever the activeTask needs to change +.factory('SelectTask', ['LoadHosts', function(LoadHosts) { + return function(params) { + var scope = params.scope, + id = params.id, + callback = params.callback; + + if (scope.activeTask && scope.tasks[scope.activeTask]) { + scope.tasks[scope.activeTask].taskActiveClass = ''; + } + + if (id) { + scope.tasks[id].taskActiveClass = 'active'; + scope.activeTaskName = scope.tasks[id].name; + } + scope.activeTask = id; + + $('#tasks-table-detail').mCustomScrollbar("update"); + setTimeout( function() { + scope.auto_scroll = true; + $('#tasks-table-detail').mCustomScrollbar("scrollTo", "bottom"); + + }, 1500); + LoadHosts({ + scope: scope, + callback: callback + }); + }; +}]) + +// Refresh the list of hosts +.factory('LoadHosts', ['Rest', 'ProcessErrors', 'SelectHost', function(Rest, ProcessErrors, SelectHost) { + return function(params) { + var scope = params.scope, + callback = params.callback, + url; + scope.hostResults = []; + scope.hostResultsMap = {}; + if (scope.activeTask) { + // If we have a selected task, then get the list of hosts + url = scope.job.related.job_events + '?parent=' + scope.activeTask + '&'; + url += (scope.search_all_hosts_name) ? 'host__name__icontains=' + scope.search_all_hosts_name + '&' : ''; + url += (scope.searchAllStatus === 'failed') ? 'failed=true&' : ''; + url += 'host__isnull=false&page_size=' + scope.hostTableRows + '&order_by=host__name'; + Rest.setUrl(url); + Rest.get() + .success(function(data) { + data.results.forEach(function(event) { + scope.hostResults.push({ + id: event.id, + status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ), + host_id: event.host, + task_id: event.parent, + name: event.event_data.host, + created: event.created, + msg: ( (event.event_data && event.event_data.res) ? event.event_data.res.msg : '' ) + }); + scope.hostResultsMap[event.id] = scope.hostResults.length - 1; + }); + if (callback) { + scope.$emit(callback); + } + SelectHost({ scope: scope }); + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + } + else { + if (callback) { + scope.$emit(callback); + } + SelectHost({ scope: scope }); + } + }; +}]) + +.factory('SelectHost', [ function() { + return function(params) { + var scope = params.scope; + $('#tasks-table-detail').mCustomScrollbar("update"); + setTimeout( function() { + scope.auto_scroll = true; + $('#hosts-table-detail').mCustomScrollbar("scrollTo", "bottom"); + }, 700); + }; +}]) + +// Refresh the list of hosts in the hosts summary section +.factory('ReloadHostSummaryList', ['Rest', 'ProcessErrors', function(Rest, ProcessErrors) { + return function(params) { + var scope = params.scope, + callback = params.callback, + url; + scope.hosts = []; + scope.hostsMap = {}; + url = scope.job.related.job_host_summaries + '?'; + url += (scope.search_all_hosts_name) ? 'host__name__icontains=' + scope.search_all_hosts_name + '&': ''; + url += (scope.searchAllStatus === 'failed') ? 'failed=true&' : ''; + url += 'page_size=' + scope.hostSummaryTableRows + '&order_by=host__name'; + Rest.setUrl(url); + Rest.get() + .success(function(data) { + data.results.forEach(function(event) { + scope.hosts.push({ + id: event.host, + name: event.summary_fields.host.name, + ok: event.ok, + changed: event.changed, + unreachable: event.dark, + failed: event.failures + }); + scope.hostsMap[event.id] = scope.hosts.length - 1; + }); + $('#hosts-summary-table').mCustomScrollbar("update"); + if (callback) { + scope.$emit(callback); + } + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + }; +}]) + +.factory('LoadHostSummary', [ function() { + return function(params) { + var scope = params.scope, + data = params.data; + scope.host_summary.ok = Object.keys(data.ok).length; + scope.host_summary.changed = Object.keys(data.changed).length; + scope.host_summary.unreachable = Object.keys(data.dark).length; + scope.host_summary.failed = Object.keys(data.failures).length; + scope.host_summary.total = scope.host_summary.ok + scope.host_summary.changed + + scope.host_summary.unreachable + scope.host_summary.failed; + }; +}]) + +.factory('DrawGraph', [ function() { + return function(params) { + var scope = params.scope, + resize = params.resize, + width, height, svg_height, svg_width, svg_radius, svg, graph_data = []; + + // Ready the data + if (scope.host_summary.ok) { + graph_data.push({ + label: 'OK', + value: (scope.host_summary.ok === scope.host_summary.total) ? 1 : scope.host_summary.ok, + color: '#5bb75b' + }); + } + if (scope.host_summary.changed) { + graph_data.push({ + label: 'Changed', + value: (scope.host_summary.changed === scope.host_summary.total) ? 1 : scope.host_summary.changed, + color: '#FF9900' + }); + } + if (scope.host_summary.unreachable) { + graph_data.push({ + label: 'Unreachable', + value: (scope.host_summary.unreachable === scope.host_summary.total) ? 1 : scope.host_summary.unreachable, + color: '#A9A9A9' + }); + } + if (scope.host_summary.failed) { + graph_data.push({ + label: 'Failed', + value: (scope.host_summary.failed === scope.host_summary.total) ? 1 : scope.host_summary.failed, + color: '#DA4D49' + }); + } + + // Adjust the size + width = $('#job-summary-container .job_well').width(); + height = $('#job-summary-container .job_well').height() - $('#summary-well-top-section').height() - $('#graph-section .header').outerHeight() - 15; + svg_radius = Math.min(width, height); + svg_width = width; + svg_height = height; + if (svg_height > 0 && svg_width > 0) { + if (!resize && $('#graph-section svg').length > 0) { + Donut3D.transition("completedHostsDonut", graph_data, Math.floor(svg_radius * 0.50), Math.floor(svg_radius * 0.25), 18, 0.4); + } + else { + if ($('#graph-section svg').length > 0) { + $('#graph-section svg').remove(); + } + svg = d3.select("#graph-section").append("svg").attr("width", svg_width).attr("height", svg_height); + svg.append("g").attr("id","completedHostsDonut"); + Donut3D.draw("completedHostsDonut", graph_data, Math.floor(svg_width / 2), Math.floor(svg_height / 2), Math.floor(svg_radius * 0.50), Math.floor(svg_radius * 0.25), 18, 0.4); + $('#graph-section .header .legend').show(); + } + } + }; +}]) + +.factory('FilterAllByHostName', ['Rest', 'GetBasePath', 'ProcessErrors', 'SelectPlay', function(Rest, GetBasePath, ProcessErrors, SelectPlay) { + return function(params) { + var scope = params.scope, + host = params.host, + newActivePlay, + url = scope.job.related.job_events + '?event__icontains=runner&host_name__icontains=' + host + '&parent__isnull=false'; + + scope.search_all_tasks = []; + scope.search_all_plays = []; + + if (scope.removeAllPlaysReady) { + scope.removeAllPlaysReady(); + } + scope.removeAllPlaysReady = scope.$on('AllPlaysReady', function() { + if (scope.activePlay) { + setTimeout(function() { + SelectPlay({ + scope: scope, + id: newActivePlay + }); + }, 500); + } + else { + scope.tasks = {}; + scope.hostResults = []; + } + }); + + if (scope.removeAllTasksReady) { + scope.removeAllTasksReady(); + } + scope.removeAllTasksReady = scope.$on('AllTasksReady', function() { + url = scope.job.related.job_events + '?id__in=' + scope.search_all_tasks.join(); + Rest.setUrl(url); + Rest.get() + .success(function(data) { + if (data.count > 0) { + data.results.forEach(function(row) { + if (row.parent) { + scope.search_all_plays.push(row.parent); + } + }); + if (scope.search_all_plays.length > 0) { + scope.search_all_plays.sort(); + newActivePlay = scope.search_all_plays[scope.search_all_plays.length - 1]; + } + else { + newActivePlay = null; + } + } + else { + scope.search_all_plays.push(0); + } + scope.$emit('AllPlaysReady'); + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + }); + + Rest.setUrl(url); + Rest.get() + .success(function(data) { + if (data.count > 0) { + data.results.forEach(function(row) { + if (row.parent) { + scope.search_all_tasks.push(row.parent); + } + }); + if (scope.search_all_tasks.length > 0) { + scope.search_all_tasks.sort(); + } + } + else { + scope.search_all_tasks.push(0); + } + scope.$emit('AllTasksReady'); + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + }; +}]); \ No newline at end of file diff --git a/awx/ui/static/js/helpers/Jobs.js b/awx/ui/static/js/helpers/Jobs.js index 594b4f0732..9f0bfbef1b 100644 --- a/awx/ui/static/js/helpers/Jobs.js +++ b/awx/ui/static/js/helpers/Jobs.js @@ -14,7 +14,7 @@ angular.module('JobsHelper', ['Utilities', 'RestServices', 'FormGenerator', 'Job /** * JobsControllerInit({ scope: $scope }); - * + * * Initialize calling scope with all the bits required to support a jobs list * */ @@ -22,10 +22,9 @@ angular.module('JobsHelper', ['Utilities', 'RestServices', 'FormGenerator', 'Job function($location, Find, DeleteJob, RelaunchJob, LogViewer) { return function(params) { var scope = params.scope, - parent_scope = params.parent_scope, iterator = (params.iterator) ? params.iterator : scope.iterator, base = $location.path().replace(/^\//, '').split('/')[0]; - + scope.deleteJob = function(id) { DeleteJob({ scope: scope, id: id }); }; @@ -61,10 +60,7 @@ angular.module('JobsHelper', ['Utilities', 'RestServices', 'FormGenerator', 'Job }; scope.refreshJobs = function() { - if (base === 'jobs') { - parent_scope.refreshJobs(); - } - else { + if (base !== 'jobs') { scope.search(iterator); } @@ -169,7 +165,7 @@ angular.module('JobsHelper', ['Utilities', 'RestServices', 'FormGenerator', 'Job '
\n'; $('#inventory-modal-container').empty().append(html); - + scope = generator.inject(form, { mode: 'edit', id: 'form-container', breadCrumbs: false, related: false }); // Set modal dimensions based on viewport width @@ -321,7 +317,7 @@ angular.module('JobsHelper', ['Utilities', 'RestServices', 'FormGenerator', 'Job else if (item.type === "job") { itm.nameHref = ""; } - + if (list.name === 'completed_jobs' || list.name === 'running_jobs') { itm.status_tip = itm.status_label + '. Click for details.'; } @@ -343,7 +339,7 @@ angular.module('JobsHelper', ['Utilities', 'RestServices', 'FormGenerator', 'Job }]) /** - * + * * Called from JobsList controller to load each section or list on the page * */ @@ -390,7 +386,7 @@ angular.module('JobsHelper', ['Utilities', 'RestServices', 'FormGenerator', 'Job url: url, pageSize: pageSize }); - + scope.iterator = list.iterator; if (scope.removePostRefresh) { @@ -422,7 +418,7 @@ angular.module('JobsHelper', ['Utilities', 'RestServices', 'FormGenerator', 'Job .factory('DeleteJob', ['Find', 'GetBasePath', 'Rest', 'Wait', 'ProcessErrors', 'Prompt', 'Alert', function(Find, GetBasePath, Rest, Wait, ProcessErrors, Prompt, Alert){ return function(params) { - + var scope = params.scope, id = params.id, action, jobs, job, url, action_label, hdr;