From e9899c278f4bea83439497c7f521a3d5c35fb868 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 17 Feb 2016 15:50:27 -0500 Subject: [PATCH 01/11] Modularized Standard Out/Job details for all jobs that are not playbook runs. This is mostly scaffolding. --- awx/ui/client/src/app.js | 56 +----- awx/ui/client/src/helpers/Jobs.js | 71 +++----- .../src/jobs/standard-out-details.block.less | 34 ---- awx/ui/client/src/lists/AllJobs.js | 13 +- awx/ui/client/src/lists/CompletedJobs.js | 19 +- awx/ui/client/src/lists/Jobs.js | 13 +- awx/ui/client/src/lists/PortalJobs.js | 2 +- awx/ui/client/src/partials/job_stdout.html | 38 ---- .../client/src/partials/job_stdout_adhoc.html | 172 ------------------ awx/ui/client/src/shared/generator-helpers.js | 3 - .../adhoc/standard-out-adhoc.partial.html | 116 ++++++++++++ .../adhoc/standard-out-adhoc.route.js | 36 ++++ .../standard-out-inventory-sync.partial.html | 40 ++++ .../standard-out-inventory-sync.route.js | 38 ++++ awx/ui/client/src/standard-out/main.js | 20 ++ .../standard-out-management-jobs.partial.html | 40 ++++ .../standard-out-management-jobs.route.js | 36 ++++ .../standard-out-scm-update.partial.html | 40 ++++ .../standard-out-scm-update.route.js | 38 ++++ .../src/standard-out/standard-out.block.less | 60 ++++++ .../standard-out.controller.js} | 68 +++---- .../src/standard-out/standardOut.block.less | 47 ----- 22 files changed, 541 insertions(+), 459 deletions(-) delete mode 100644 awx/ui/client/src/jobs/standard-out-details.block.less delete mode 100644 awx/ui/client/src/partials/job_stdout.html delete mode 100644 awx/ui/client/src/partials/job_stdout_adhoc.html create mode 100644 awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.partial.html create mode 100644 awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.route.js create mode 100644 awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html create mode 100644 awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.route.js create mode 100644 awx/ui/client/src/standard-out/main.js create mode 100644 awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html create mode 100644 awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.route.js create mode 100644 awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html create mode 100644 awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.route.js create mode 100644 awx/ui/client/src/standard-out/standard-out.block.less rename awx/ui/client/src/{controllers/JobStdout.js => standard-out/standard-out.controller.js} (85%) delete mode 100644 awx/ui/client/src/standard-out/standardOut.block.less diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index b62191f36e..79e9a0a29c 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -43,8 +43,8 @@ import templateUrl from './shared/template-url/main'; import adhoc from './adhoc/main'; import login from './login/main'; import activityStream from './activity-stream/main'; +import standardOut from './standard-out/main'; import {JobDetailController} from './controllers/JobDetail'; -import {JobStdoutController} from './controllers/JobStdout'; import {JobTemplatesList, JobTemplatesAdd, JobTemplatesEdit} from './controllers/JobTemplates'; import {LicenseController} from './controllers/License'; import {ScheduleEditController} from './controllers/Schedules'; @@ -95,6 +95,7 @@ var tower = angular.module('Tower', [ login.name, activityStream.name, footer.name, + standardOut.name, 'templates', 'Utilities', 'LicenseHelper', @@ -320,56 +321,6 @@ var tower = angular.module('Tower', [ } }). - state('jobsStdout', { - url: '/jobs/:id/stdout', - templateUrl: urlPrefix + 'partials/job_stdout.html', - controller: JobStdoutController, - ncyBreadcrumb: { - parent: 'jobDetail', - label: "STANDARD OUT" - }, - resolve: { - features: ['FeaturesService', function(FeaturesService) { - return FeaturesService.get(); - }], - jobEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { - if (!$rootScope.event_socket) { - $rootScope.event_socket = Socket({ - scope: $rootScope, - endpoint: "job_events" - }); - $rootScope.event_socket.init(); - return true; - } else { - return true; - } - }] - } - }). - - state('adHocJobStdout', { - url: '/ad_hoc_commands/:id', - templateUrl: urlPrefix + 'partials/job_stdout_adhoc.html', - controller: JobStdoutController, - resolve: { - features: ['FeaturesService', function(FeaturesService) { - return FeaturesService.get(); - }], - adhocEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { - if (!$rootScope.adhoc_event_socket) { - $rootScope.adhoc_event_socket = Socket({ - scope: $rootScope, - endpoint: "ad_hoc_command_events" - }); - $rootScope.adhoc_event_socket.init(); - return true; - } else { - return true; - } - }] - } - }). - state('jobTemplates', { url: '/job_templates', templateUrl: urlPrefix + 'partials/job_templates.html', @@ -1062,6 +1013,9 @@ var tower = angular.module('Tower', [ $rootScope.$emit('JobStatusChange-jobs', data); } else if (/\/jobs\/(\d)+\/stdout/.test(urlToCheck) || /\/ad_hoc_commands\/(\d)+/.test(urlToCheck)) { + + // TODO: something will need to change here for stdout + $log.debug("sending status to standard out"); $rootScope.$emit('JobStatusChange-jobStdout', data); } else if (/\/jobs\/(\d)+/.test(urlToCheck)) { diff --git a/awx/ui/client/src/helpers/Jobs.js b/awx/ui/client/src/helpers/Jobs.js index 7cab25fa9f..6320259ade 100644 --- a/awx/ui/client/src/helpers/Jobs.js +++ b/awx/ui/client/src/helpers/Jobs.js @@ -22,12 +22,11 @@ export default * Initialize calling scope with all the bits required to support a jobs list * */ - .factory('JobsControllerInit', ['$location', 'Find', 'DeleteJob', 'RelaunchJob', 'LogViewer', '$window', - function($location, Find, DeleteJob, RelaunchJob, LogViewer, $window) { + .factory('JobsControllerInit', ['$state', 'Find', 'DeleteJob', 'RelaunchJob', 'LogViewer', '$window', + function($state, Find, DeleteJob, RelaunchJob, LogViewer, $window) { return function(params) { var scope = params.scope, iterator = (params.iterator) ? params.iterator : scope.iterator; - //base = $location.path().replace(/^\//, '').split('/')[0]; scope.deleteJob = function(id) { DeleteJob({ scope: scope, id: id }); @@ -70,53 +69,39 @@ export default }; scope.refreshJobs = function() { - // if (base !== 'jobs') { - scope.search(iterator); - // } - + scope.search(iterator); }; - scope.viewJobLog = function(id) { - var list, job; - if (scope.completed_jobs) { - list = scope.completed_jobs; - } - else if (scope.running_jobs) { - list = scope.running_jobs; - } - else if (scope.queued_jobs) { - list = scope.queued_jobs; - } - else if (scope.jobs) { - list = scope.jobs; - } - else if(scope.all_jobs){ - list = scope.all_jobs; - } - else if(scope.portal_jobs){ - list=scope.portal_jobs; - } - job = Find({ list: list, key: 'id', val: id }); - if (job.type === 'job') { + scope.viewJobDetails = function(job) { + + var goToJobDetails = function(state) { if(scope.$parent.portalMode===true){ - $window.open('/#/jobs/' + job.id, '_blank'); + var url = $state.href(state, {id: job.id}); + $window.open(url, '_blank'); } else { - $location.url('/jobs/' + job.id); + $state.go(state, {id: job.id}); } - } else if (job.type === 'ad_hoc_command') { - if(scope.$parent.portalMode===true){ - $window.open('/#/ad_hoc_commands/' + job.id, '_blank'); - } - else { - $location.url('/ad_hoc_commands/' + job.id); - } - } else { - LogViewer({ - scope: scope, - url: job.url - }); } + + switch(job.type) { + case 'job': + goToJobDetails('jobDetail'); + break; + case 'ad_hoc_command': + goToJobDetails('adHocJobStdout'); + break; + case 'system_job': + goToJobDetails('managementJobStdout'); + break; + case 'project_update': + goToJobDetails('scmUpdateStdout'); + break; + case 'inventory_update': + goToJobDetails('inventorySyncStdout'); + break; + } + }; }; } diff --git a/awx/ui/client/src/jobs/standard-out-details.block.less b/awx/ui/client/src/jobs/standard-out-details.block.less deleted file mode 100644 index 99958e9808..0000000000 --- a/awx/ui/client/src/jobs/standard-out-details.block.less +++ /dev/null @@ -1,34 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** @define StandardOutDetails */ - -// Some of these are left empty as a helpful measure so that you can see how the new -// SuitCSS styling should work. They can be removed once we've done more -// SuitCSS - -.StandardOutDetails { -} - -.StandardOutDetails-detailRow { - margin-bottom: 15px; -} - -.StandardOutDetails-detailRow--closable { - display: none; -} - -.StandardOutDetails-detailLabel { -} - -.StandardOutDetails-detailContent { -} - -.StandardOutDetails-closedToggle { -} - -.StandardOutDetails-closedToggleLink { -} diff --git a/awx/ui/client/src/lists/AllJobs.js b/awx/ui/client/src/lists/AllJobs.js index 3a99817c99..4cc7a995c6 100644 --- a/awx/ui/client/src/lists/AllJobs.js +++ b/awx/ui/client/src/lists/AllJobs.js @@ -25,7 +25,7 @@ export default dataTitle: "{{ all_job.status_popover_title }}", icon: 'icon-job-{{ all_job.status }}', iconOnly: true, - ngClick:"viewJobLog(all_job.id)", + ngClick:"viewJobDetails(all_job)", searchable: true, searchType: 'select', nosort: true, @@ -38,7 +38,7 @@ export default }, id: { label: 'ID', - ngClick:"viewJobLog(all_job.id)", + ngClick:"viewJobDetails(all_job)", searchType: 'int', columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumnAdjacent', awToolTip: "{{ all_job.status_tip }}", @@ -47,7 +47,7 @@ export default name: { label: 'Name', columnClass: 'col-lg-3 col-md-3 col-sm-4 col-xs-6', - ngClick: "viewJobLog(all_job.id, all_job.nameHref)", + ngClick: "viewJobDetails(all_job)", defaultSearchField: true, awToolTip: "{{ all_job.name | sanitize }}", dataPlacement: 'top' @@ -87,13 +87,6 @@ export default columnClass: 'col-lg-2 col-md-2 col-sm-3 col-xs-4', - stdout: { - mode: 'all', - href: '/#/jobs/{{ all_job.id }}/stdout', - awToolTip: 'View standard output', - dataPlacement: 'top', - ngShow: "all_job.type == 'job'" - }, submit: { icon: 'icon-rocket', mode: 'all', diff --git a/awx/ui/client/src/lists/CompletedJobs.js b/awx/ui/client/src/lists/CompletedJobs.js index 4ed0d96dbf..a40a48b0ce 100644 --- a/awx/ui/client/src/lists/CompletedJobs.js +++ b/awx/ui/client/src/lists/CompletedJobs.js @@ -27,7 +27,7 @@ export default dataTitle: "{{ completed_job.status_popover_title }}", icon: 'icon-job-{{ completed_job.status }}', iconOnly: true, - ngClick:"viewJobLog(completed_job.id)", + ngClick:"viewJobDetails(completed_job)", searchable: true, searchType: 'select', nosort: true, @@ -40,7 +40,7 @@ export default }, id: { label: 'ID', - ngClick:"viewJobLog(completed_job.id)", + ngClick:"viewJobDetails(completed_job)", searchType: 'int', columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumnAdjacent', awToolTip: "{{ completed_job.status_tip }}", @@ -49,7 +49,7 @@ export default name: { label: 'Name', columnClass: 'col-lg-4 col-md-4 col-sm-4 col-xs-6', - ngClick: "viewJobLog(completed_job.id, completed_job.nameHref)", + ngClick: "viewJobDetails(completed_job)", defaultSearchField: true, awToolTip: "{{ completed_job.name | sanitize }}", dataPlacement: 'top' @@ -89,13 +89,6 @@ export default columnClass: 'col-lg-2 col-md-2 col-sm-3 col-xs-4', - stdout: { - mode: 'all', - href: '/#/jobs/{{ completed_job.id }}/stdout', - awToolTip: 'View standard output', - dataPlacement: 'top', - ngShow: "completed_job.type == 'job'" - }, submit: { icon: 'icon-rocket', mode: 'all', @@ -110,11 +103,5 @@ export default awToolTip: 'Delete the job', dataPlacement: 'top' } - // job_details: { - // mode: 'all', - // ngClick: "viewJobLog(completed_job.id)", - // awToolTip: 'View job details', - // dataPlacement: 'top' - // } } }); diff --git a/awx/ui/client/src/lists/Jobs.js b/awx/ui/client/src/lists/Jobs.js index 53b7c74669..0441d1da23 100644 --- a/awx/ui/client/src/lists/Jobs.js +++ b/awx/ui/client/src/lists/Jobs.js @@ -20,7 +20,7 @@ export default fields: { id: { label: 'ID', - ngClick:"viewJobLog(job.id)", + ngClick:"viewJobDetails(job)", key: true, desc: true, searchType: 'int', @@ -36,7 +36,7 @@ export default dataTitle: "{{ job.status_popover_title }}", icon: 'icon-job-{{ job.status }}', iconOnly: true, - ngClick:"viewJobLog(job.id)", + ngClick:"viewJobDetails(job)", searchable: true, nosort: true, searchType: 'select', @@ -66,7 +66,7 @@ export default name: { label: 'Name', columnClass: 'col-md-3 col-xs-5', - ngClick: "viewJobLog(job.id, job.nameHref)", + ngClick: "viewJobDetails(job)", defaultSearchField: true } }, @@ -74,13 +74,6 @@ export default actions: { }, fieldActions: { - stdout: { - mode: 'all', - href: '/#/jobs/{{ job.id }}/stdout', - awToolTip: 'View standard output', - dataPlacement: 'top', - ngShow: "job.type == 'job'" - }, submit: { mode: 'all', icon: 'icon-rocket', diff --git a/awx/ui/client/src/lists/PortalJobs.js b/awx/ui/client/src/lists/PortalJobs.js index 645eb90e32..753ef764fc 100644 --- a/awx/ui/client/src/lists/PortalJobs.js +++ b/awx/ui/client/src/lists/PortalJobs.js @@ -64,7 +64,7 @@ export default job_details: { mode: 'all', - ngClick: "viewJobLog(portal_job.id)", + ngClick: "viewJobDetails(portal_job)", awToolTip: 'View job details', dataPlacement: 'top' } diff --git a/awx/ui/client/src/partials/job_stdout.html b/awx/ui/client/src/partials/job_stdout.html deleted file mode 100644 index 6412480fcb..0000000000 --- a/awx/ui/client/src/partials/job_stdout.html +++ /dev/null @@ -1,38 +0,0 @@ -
-
- -
-
- -
-
-
-
-
-
-
{{ job.status }}
-
-
-
-
-
-

Standard Output - - Download - -

-
-
-
-
-
- -
-
-
-
-
-
diff --git a/awx/ui/client/src/partials/job_stdout_adhoc.html b/awx/ui/client/src/partials/job_stdout_adhoc.html deleted file mode 100644 index 78637571e9..0000000000 --- a/awx/ui/client/src/partials/job_stdout_adhoc.html +++ /dev/null @@ -1,172 +0,0 @@ -
-
-
-
-
-
- -
-
- -
-
- -
- {{ job.status }} -
-
- -
- -
-
- Started  {{ job.started | date:'MM/dd/yy HH:mm:ss' }} -
-
- Finished  {{ job.finished | date:'MM/dd/yy HH:mm:ss' }} -
-
- Elapsed  {{ job.elapsed }} seconds -
-
-
- -
- -
{{ job.module_name }} -
-
- -
- -
{{ job.module_args }} -
-
- -
- - -
- -
- - -
- -
- - -
- - -
- -
{{ forks }}
-
- -
- -
{{ limit }}
-
- - -
- -
{{ verbosity }}
-
- - -
-
- -
-
-

Standard Output - Download -

-
-
-
-
-
-
- -
-
-
- -
-
-
diff --git a/awx/ui/client/src/shared/generator-helpers.js b/awx/ui/client/src/shared/generator-helpers.js index b355cdcd57..1fc8880e63 100644 --- a/awx/ui/client/src/shared/generator-helpers.js +++ b/awx/ui/client/src/shared/generator-helpers.js @@ -172,9 +172,6 @@ angular.module('GeneratorHelpers', [systemStatus.name]) case 'schedule': icon = "fa-calendar"; break; - case 'stdout': - icon = "fa-external-link"; - break; case 'question_cancel': icon = 'fa-times'; break; diff --git a/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.partial.html b/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.partial.html new file mode 100644 index 0000000000..9d6d6ad4ac --- /dev/null +++ b/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.partial.html @@ -0,0 +1,116 @@ +
+
+
+
+
+
+ RESULTS +
+
+ +
+
Name
+
{{ job.module_name }}
+
+ +
+
STATUS
+
+ + {{ job.status }} +
+
+ +
+
STARTED
+
+ {{ job.started | date:'MM/dd/yy HH:mm:ss' }} +
+
+ +
+
FINISHED
+
+ {{ job.finished | date:'MM/dd/yy HH:mm:ss' }} +
+
+ +
+
ELAPSED
+
+ {{ job.elapsed }} seconds +
+
+ +
+
Module Args
+
{{ job.module_args }}
+
+ +
+
Inventory
+ +
+ +
+
Credential
+ +
+ +
+
Launched By
+ +
+ + +
+
Forks
+
{{ forks }}
+
+ +
+
Limit
+
{{ limit }}
+
+ + +
+
Verbosity
+
{{ verbosity }}
+
+
+
+
+
+
+
+ STANDARD OUT +
+
+
+
+
+
+ +
+
+
+
+
+
+
diff --git a/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.route.js b/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.route.js new file mode 100644 index 0000000000..85c281876f --- /dev/null +++ b/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.route.js @@ -0,0 +1,36 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import {templateUrl} from '../../shared/template-url/template-url.factory'; + +export default { + name: 'adHocJobStdout', + route: '/ad_hoc_commands/:id/stdout', + templateUrl: templateUrl('standard-out/adhoc/standard-out-adhoc'), + controller: 'JobStdoutController', + data: { + jobType: 'ad_hoc_commands' + }, + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }], + adhocEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { + // if (!$rootScope.adhoc_event_socket) { + // $rootScope.adhoc_event_socket = Socket({ + // scope: $rootScope, + // endpoint: "ad_hoc_command_events" + // }); + // $rootScope.adhoc_event_socket.init(); + // return true; + // } else { + // return true; + // } + + return true; + }] + } +}; diff --git a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html new file mode 100644 index 0000000000..747bfc9bfc --- /dev/null +++ b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html @@ -0,0 +1,40 @@ +
+
+
+
+
+
+ RESULTS +
+
+ +
+
STATUS
+
+ + {{ job.status }} +
+
+ +
+
+
+
+
+
+ STANDARD OUT +
+
+
+
+
+
+ +
+
+
+
+
+
+
diff --git a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.route.js b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.route.js new file mode 100644 index 0000000000..aaa92bd2d7 --- /dev/null +++ b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.route.js @@ -0,0 +1,38 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import {templateUrl} from '../../shared/template-url/template-url.factory'; + +// TODO: figure out what this route should be - should it be inventory_sync? + +export default { + name: 'inventorySyncStdout', + route: '/inventory_sync/:id/stdout', + templateUrl: templateUrl('standard-out/inventory-sync/standard-out-inventory-sync'), + controller: 'JobStdoutController', + data: { + jobType: 'inventory_updates' + }, + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }], + adhocEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { + // if (!$rootScope.adhoc_event_socket) { + // $rootScope.adhoc_event_socket = Socket({ + // scope: $rootScope, + // endpoint: "ad_hoc_command_events" + // }); + // $rootScope.adhoc_event_socket.init(); + // return true; + // } else { + // return true; + // } + + return true; + }] + } +}; diff --git a/awx/ui/client/src/standard-out/main.js b/awx/ui/client/src/standard-out/main.js new file mode 100644 index 0000000000..e8a0946e82 --- /dev/null +++ b/awx/ui/client/src/standard-out/main.js @@ -0,0 +1,20 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import stdoutAdhocRoute from './adhoc/standard-out-adhoc.route'; +import stdoutManagementJobsRoute from './management-jobs/standard-out-management-jobs.route'; +import stdoutInventorySyncRoute from './inventory-sync/standard-out-inventory-sync.route'; +import stdoutScmUpdateRoute from './scm-update/standard-out-scm-update.route'; +import {JobStdoutController} from './standard-out.controller'; + +export default angular.module('standardOut', []) + .controller('JobStdoutController', JobStdoutController) + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(stdoutAdhocRoute); + $stateExtender.addState(stdoutManagementJobsRoute); + $stateExtender.addState(stdoutInventorySyncRoute); + $stateExtender.addState(stdoutScmUpdateRoute); + }]); diff --git a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html b/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html new file mode 100644 index 0000000000..747bfc9bfc --- /dev/null +++ b/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html @@ -0,0 +1,40 @@ +
+
+
+
+
+
+ RESULTS +
+
+ +
+
STATUS
+
+ + {{ job.status }} +
+
+ +
+
+
+
+
+
+ STANDARD OUT +
+
+
+
+
+
+ +
+
+
+
+
+
+
diff --git a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.route.js b/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.route.js new file mode 100644 index 0000000000..a1f3464ed2 --- /dev/null +++ b/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.route.js @@ -0,0 +1,36 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import {templateUrl} from '../../shared/template-url/template-url.factory'; + +export default { + name: 'managementJobStdout', + route: '/management_jobs/:id/stdout', + templateUrl: templateUrl('standard-out/management-jobs/standard-out-management-jobs'), + controller: 'JobStdoutController', + data: { + jobType: 'system_jobs' + }, + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }], + adhocEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { + // if (!$rootScope.adhoc_event_socket) { + // $rootScope.adhoc_event_socket = Socket({ + // scope: $rootScope, + // endpoint: "ad_hoc_command_events" + // }); + // $rootScope.adhoc_event_socket.init(); + // return true; + // } else { + // return true; + // } + + return true; + }] + } +}; diff --git a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html new file mode 100644 index 0000000000..747bfc9bfc --- /dev/null +++ b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html @@ -0,0 +1,40 @@ +
+
+
+
+
+
+ RESULTS +
+
+ +
+
STATUS
+
+ + {{ job.status }} +
+
+ +
+
+
+
+
+
+ STANDARD OUT +
+
+
+
+
+
+ +
+
+
+
+
+
+
diff --git a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.route.js b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.route.js new file mode 100644 index 0000000000..6ffc812160 --- /dev/null +++ b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.route.js @@ -0,0 +1,38 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import {templateUrl} from '../../shared/template-url/template-url.factory'; + +// TODO: figure out what this route should be - should it be scm_update? + +export default { + name: 'scmUpdateStdout', + route: '/scm_update/:id/stdout', + templateUrl: templateUrl('standard-out/scm-update/standard-out-scm-update'), + controller: 'JobStdoutController', + data: { + jobType: 'project_updates' + }, + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }], + adhocEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { + // if (!$rootScope.adhoc_event_socket) { + // $rootScope.adhoc_event_socket = Socket({ + // scope: $rootScope, + // endpoint: "ad_hoc_command_events" + // }); + // $rootScope.adhoc_event_socket.init(); + // return true; + // } else { + // return true; + // } + + return true; + }] + } +}; diff --git a/awx/ui/client/src/standard-out/standard-out.block.less b/awx/ui/client/src/standard-out/standard-out.block.less new file mode 100644 index 0000000000..7fcd0aa9b6 --- /dev/null +++ b/awx/ui/client/src/standard-out/standard-out.block.less @@ -0,0 +1,60 @@ +@import "../shared/branding/colors.default.less"; + +/** @define StandardOut */ + +.StandardOut { + height: 100%; + display: flex; + flex-direction: row; +} + +.StandardOut-leftPanel { + flex: 0 0 400px; +} + +.StandardOut-rightPanel { + flex: 1 0; + margin-left: 20px; +} + +.StandardOut-panelHeader { + color: @default-interface-txt; + font-size: 14px; + font-weight: bold; + margin-right: 10px; + text-transform: uppercase; +} + +.StandardOut-consoleOutput { + margin-top: 25px; + min-height: 200px; + background-color: @default-secondary-bg; + border-radius: 5px; +} + +.StandardOut-details { + margin-top: 25px; +} + +.StandardOut-detailsRow { + display: flex; +} + +.StandardOut-detailsRow:not(:last-child) { + margin-bottom: 20px; +} + +.StandardOut-detailsLabel { + width: 130px; + flex: 0 0 130px; + color: @default-interface-txt; + text-transform: uppercase; +} + +.StandardOut-detailsContent { + flex: 1 0; +} + +.StandardOut-statusText { + margin-left: 6px; +} diff --git a/awx/ui/client/src/controllers/JobStdout.js b/awx/ui/client/src/standard-out/standard-out.controller.js similarity index 85% rename from awx/ui/client/src/controllers/JobStdout.js rename to awx/ui/client/src/standard-out/standard-out.controller.js index 91c0f3f322..efca57eeb1 100644 --- a/awx/ui/client/src/controllers/JobStdout.js +++ b/awx/ui/client/src/standard-out/standard-out.controller.js @@ -3,7 +3,7 @@ * * All Rights Reserved *************************************************/ - + /** * @ngdoc function * @name controllers.function:JobStdout @@ -11,11 +11,12 @@ */ -export function JobStdoutController ($location, $log, $rootScope, $scope, $compile, $stateParams, ClearScope, GetBasePath, Wait, Rest, ProcessErrors) { +export function JobStdoutController ($location, $log, $rootScope, $scope, $compile, $state, $stateParams, ClearScope, GetBasePath, Wait, Rest, ProcessErrors, ModelToBasePathKey) { ClearScope(); var job_id = $stateParams.id, + jobType = $state.current.data.jobType, api_complete = false, stdout_url, current_range, @@ -32,26 +33,27 @@ export function JobStdoutController ($location, $log, $rootScope, $scope, $compi $scope.isClosed = true; - function openSockets() { - if (/\/jobs\/(\d)+\/stdout/.test($location.$$url)) { - $log.debug("socket watching on job_events-" + job_id); - $rootScope.event_socket.on("job_events-" + job_id, function() { - $log.debug("socket fired on job_events-" + job_id); - if (api_complete) { - event_queue++; - } - }); - } else if (/\/ad_hoc_commands\/(\d)+/.test($location.$$url)) { - $log.debug("socket watching on ad_hoc_command_events-" + job_id); - $rootScope.adhoc_event_socket.on("ad_hoc_command_events-" + job_id, function() { - $log.debug("socket fired on ad_hoc_command_events-" + job_id); - if (api_complete) { - event_queue++; - } - }); - } - } - openSockets(); + // function openSockets() { + // if (/\/jobs\/(\d)+\/stdout/.test($location.$$url)) { + // $log.debug("socket watching on job_events-" + job_id); + // $rootScope.event_socket.on("job_events-" + job_id, function() { + // $log.debug("socket fired on job_events-" + job_id); + // if (api_complete) { + // event_queue++; + // } + // }); + // } else if (/\/ad_hoc_commands\/(\d)+/.test($location.$$url)) { + // $log.debug("socket watching on ad_hoc_command_events-" + job_id); + // $rootScope.adhoc_event_socket.on("ad_hoc_command_events-" + job_id, function() { + // $log.debug("socket fired on ad_hoc_command_events-" + job_id); + // if (api_complete) { + // event_queue++; + // } + // }); + // } + // } + // + // openSockets(); if ($rootScope.removeJobStatusChange) { $rootScope.removeJobStatusChange(); @@ -158,9 +160,7 @@ export function JobStdoutController ($location, $log, $rootScope, $scope, $compi $(".StandardOut").height($("body").height() - 60); - // Note: could be ad_hoc_commands or jobs - var jobType = $location.path().replace(/^\//, '').split('/')[0]; - Rest.setUrl(GetBasePath(jobType) + job_id + '/'); + Rest.setUrl(GetBasePath('base') + jobType + '/' + job_id + '/'); Rest.get() .success(function(data) { $scope.job = data; @@ -182,13 +182,15 @@ export function JobStdoutController ($location, $log, $rootScope, $scope, $compi $scope.verbosity = data.verbosity; $scope.job_tags = data.job_tags; stdout_url = data.related.stdout; - if (data.status === 'successful' || data.status === 'failed' || data.status === 'error' || data.status === 'canceled') { - live_event_processing = false; - if ($rootScope.jobStdOutInterval) { - window.clearInterval($rootScope.jobStdOutInterval); - } + // if (data.status === 'successful' || data.status === 'failed' || data.status === 'error' || data.status === 'canceled') { + // live_event_processing = false; + // if ($rootScope.jobStdOutInterval) { + // window.clearInterval($rootScope.jobStdOutInterval); + // } + // } + if(stdout_url) { + $scope.$emit('LoadStdout'); } - $scope.$emit('LoadStdout'); }) .error(function(data, status) { ProcessErrors($scope, data, status, null, { hdr: 'Error!', @@ -197,11 +199,9 @@ export function JobStdoutController ($location, $log, $rootScope, $scope, $compi $scope.refresh = function(){ if (loaded_sections.length === 0) { ////this if statement for refresh - $log.debug('calling LoadStdout'); $scope.$emit('LoadStdout'); } else if (live_event_processing) { - $log.debug('calling getNextSection'); getNextSection(); } }; @@ -281,4 +281,4 @@ export function JobStdoutController ($location, $log, $rootScope, $scope, $compi } -JobStdoutController.$inject = [ '$location', '$log', '$rootScope', '$scope', '$compile', '$stateParams', 'ClearScope', 'GetBasePath', 'Wait', 'Rest', 'ProcessErrors']; +JobStdoutController.$inject = [ '$location', '$log', '$rootScope', '$scope', '$compile', '$state', '$stateParams', 'ClearScope', 'GetBasePath', 'Wait', 'Rest', 'ProcessErrors', 'ModelToBasePathKey']; diff --git a/awx/ui/client/src/standard-out/standardOut.block.less b/awx/ui/client/src/standard-out/standardOut.block.less deleted file mode 100644 index 497471a7eb..0000000000 --- a/awx/ui/client/src/standard-out/standardOut.block.less +++ /dev/null @@ -1,47 +0,0 @@ -/** @define StandardOut */ - -.StandardOut { - height: 100%; - display: flex; - flex-direction: column; -} - -.StandardOut-header { - flex: initial; -} - -.StandardOut-breadcrumbs { - padding-left: 15px; -} - -.StandardOut-form { - padding-left: 15px; -} - -.StandardOut-panel { - flex: 1 0 0; - display: flex; - flex-direction: column; - margin-bottom: -41px; -} - -.StandardOut-panelHeading { - flex: initial; -} - -.StandardOut-panelBody { - flex: 1 0 auto; - padding: 0; - position: relative; -} - -.StandardOut-preContainer { - position: absolute; - height: 100%; - padding-left: 15px; - padding-right: 15px; -} - -.StandardOut-preContent { - position: absolute; -} From 8ec19f9226bcf948ee5dbec8829fa640fdbe36d3 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Wed, 17 Feb 2016 13:53:18 -0800 Subject: [PATCH 02/11] Initial restructuring of Job Detail code To use our new modular approach to organizing code by feature --- awx/ui/client/src/app.js | 30 +- awx/ui/client/src/controllers/JobDetail.js | 1442 ---------------- .../src/job-detail/job-detail.controller.js | 1452 +++++++++++++++++ .../src/job-detail/job-detail.factory.js | 0 .../job-detail.partial.html} | 0 .../client/src/job-detail/job-detail.route.js | 35 + awx/ui/client/src/job-detail/main.js | 15 + 7 files changed, 1504 insertions(+), 1470 deletions(-) delete mode 100644 awx/ui/client/src/controllers/JobDetail.js create mode 100644 awx/ui/client/src/job-detail/job-detail.controller.js create mode 100644 awx/ui/client/src/job-detail/job-detail.factory.js rename awx/ui/client/src/{partials/job_detail.html => job-detail/job-detail.partial.html} (100%) create mode 100644 awx/ui/client/src/job-detail/job-detail.route.js create mode 100644 awx/ui/client/src/job-detail/main.js diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index b62191f36e..ff9d38e8aa 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -31,6 +31,7 @@ import systemTracking from './system-tracking/main'; import inventoryScripts from './inventory-scripts/main'; import permissions from './permissions/main'; import managementJobs from './management-jobs/main'; +import jobDetail from './job-detail/main'; // modules import setupMenu from './setup-menu/main'; @@ -43,7 +44,6 @@ import templateUrl from './shared/template-url/main'; import adhoc from './adhoc/main'; import login from './login/main'; import activityStream from './activity-stream/main'; -import {JobDetailController} from './controllers/JobDetail'; import {JobStdoutController} from './controllers/JobStdout'; import {JobTemplatesList, JobTemplatesAdd, JobTemplatesEdit} from './controllers/JobTemplates'; import {LicenseController} from './controllers/License'; @@ -95,6 +95,7 @@ var tower = angular.module('Tower', [ login.name, activityStream.name, footer.name, + jobDetail.name, 'templates', 'Utilities', 'LicenseHelper', @@ -293,33 +294,6 @@ var tower = angular.module('Tower', [ } }). - state('jobDetail', { - url: '/jobs/:id', - templateUrl: urlPrefix + 'partials/job_detail.html', - controller: JobDetailController, - ncyBreadcrumb: { - parent: 'jobs', - label: "{{ job.id }} - {{ job.name }}" - }, - resolve: { - features: ['FeaturesService', function(FeaturesService) { - return FeaturesService.get(); - }], - jobEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { - if (!$rootScope.event_socket) { - $rootScope.event_socket = Socket({ - scope: $rootScope, - endpoint: "job_events" - }); - $rootScope.event_socket.init(); - return true; - } else { - return true; - } - }] - } - }). - state('jobsStdout', { url: '/jobs/:id/stdout', templateUrl: urlPrefix + 'partials/job_stdout.html', diff --git a/awx/ui/client/src/controllers/JobDetail.js b/awx/ui/client/src/controllers/JobDetail.js deleted file mode 100644 index af399872f4..0000000000 --- a/awx/ui/client/src/controllers/JobDetail.js +++ /dev/null @@ -1,1442 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:JobDetail - * @description This controller's for the Job Detail Page -*/ - - -export function JobDetailController ($location, $rootScope, $filter, $scope, $compile, $stateParams, $log, ClearScope, GetBasePath, Wait, Rest, - ProcessErrors, SelectPlay, SelectTask, Socket, GetElapsed, DrawGraph, LoadHostSummary, ReloadHostSummaryList, JobIsFinished, SetTaskStyles, DigestEvent, - UpdateDOM, EventViewer, DeleteJob, PlaybookRun, HostEventsViewer, LoadPlays, LoadTasks, LoadHosts, HostsEdit, ParseVariableString, GetChoices, fieldChoices, fieldLabels, EditSchedule) { - - ClearScope(); - - var job_id = $stateParams.id, - scope = $scope, - api_complete = false, - refresh_count = 0, - lastEventId = 0, - verbosity_options, - job_type_options; - - scope.plays = []; - - scope.previousTaskFailed = false; - - scope.$watch('job_status', function(job_status) { - if (job_status && job_status.explanation && job_status.explanation.split(":")[0] === "Previous Task Failed") { - scope.previousTaskFailed = true; - var taskObj = JSON.parse(job_status.explanation.substring(job_status.explanation.split(":")[0].length + 1)); - // return a promise from the options request with the permission type choices (including adhoc) as a param - var fieldChoice = fieldChoices({ - scope: $scope, - url: 'api/v1/unified_jobs/', - field: 'type' - }); - - // manipulate the choices from the options request to be set on - // scope and be usable by the list form - fieldChoice.then(function (choices) { - choices = - fieldLabels({ - choices: choices - }); - scope.explanation_fail_type = choices[taskObj.job_type]; - scope.explanation_fail_name = taskObj.job_name; - scope.explanation_fail_id = taskObj.job_id; - scope.task_detail = scope.explanation_fail_type + " failed for " + scope.explanation_fail_name + " with ID " + scope.explanation_fail_id + "."; - }); - } else { - scope.previousTaskFailed = false; - } - }, true); - - scope.$watch('plays', function(plays) { - for (var play in plays) { - if (plays[play].elapsed) { - plays[play].finishedTip = "Play completed at " + $filter("longDate")(plays[play].finished) + "."; - } else { - plays[play].finishedTip = "Play not completed."; - } - } - }); - scope.hosts = []; - scope.$watch('hosts', function(hosts) { - for (var host in hosts) { - if (hosts[host].ok) { - hosts[host].okTip = hosts[host].ok; - hosts[host].okTip += (hosts[host].ok === 1) ? " host event was" : " host events were"; - hosts[host].okTip += " ok."; - } else { - hosts[host].okTip = "No host events were ok."; - } - if (hosts[host].changed) { - hosts[host].changedTip = hosts[host].changed; - hosts[host].changedTip += (hosts[host].changed === 1) ? " host event" : " host events"; - hosts[host].changedTip += " changed."; - } else { - hosts[host].changedTip = "No host events changed."; - } - if (hosts[host].failed) { - hosts[host].failedTip = hosts[host].failed; - hosts[host].failedTip += (hosts[host].failed === 1) ? " host event" : " host events"; - hosts[host].failedTip += " failed."; - } else { - hosts[host].failedTip = "No host events failed."; - } - if (hosts[host].unreachable) { - hosts[host].unreachableTip = hosts[host].unreachable; - hosts[host].unreachableTip += (hosts[host].unreachable === 1) ? " host event was" : " hosts events were"; - hosts[host].unreachableTip += " unreachable"; - } else { - hosts[host].unreachableTip = "No host events were unreachable."; - } - } - }); - scope.tasks = []; - scope.$watch('tasks', function(tasks) { - for (var task in tasks) { - if (tasks[task].elapsed) { - tasks[task].finishedTip = "Task completed at " + $filter("longDate")(tasks[task].finished) + "."; - } else { - tasks[task].finishedTip = "Task not completed."; - } - if (tasks[task].successfulCount) { - tasks[task].successfulCountTip = tasks[task].successfulCount; - tasks[task].successfulCountTip += (tasks[task].successfulCount === 1) ? " host event was" : " host events were"; - tasks[task].successfulCountTip += " ok."; - } else { - tasks[task].successfulCountTip = "No host events were ok."; - } - if (tasks[task].changedCount) { - tasks[task].changedCountTip = tasks[task].changedCount; - tasks[task].changedCountTip += (tasks[task].changedCount === 1) ? " host event" : " host events"; - tasks[task].changedCountTip += " changed."; - } else { - tasks[task].changedCountTip = "No host events changed."; - } - if (tasks[task].skippedCount) { - tasks[task].skippedCountTip = tasks[task].skippedCount; - tasks[task].skippedCountTip += (tasks[task].skippedCount === 1) ? " host event was" : " hosts events were"; - tasks[task].skippedCountTip += " skipped."; - } else { - tasks[task].skippedCountTip = "No host events were skipped."; - } - if (tasks[task].failedCount) { - tasks[task].failedCountTip = tasks[task].failedCount; - tasks[task].failedCountTip += (tasks[task].failedCount === 1) ? " host event" : " host events"; - tasks[task].failedCountTip += " failed."; - } else { - tasks[task].failedCountTip = "No host events failed."; - } - if (tasks[task].unreachableCount) { - tasks[task].unreachableCountTip = tasks[task].unreachableCount; - tasks[task].unreachableCountTip += (tasks[task].unreachableCount === 1) ? " host event was" : " hosts events were"; - tasks[task].unreachableCountTip += " unreachable."; - } else { - tasks[task].unreachableCountTip = "No host events were unreachable."; - } - if (tasks[task].missingCount) { - tasks[task].missingCountTip = tasks[task].missingCount; - tasks[task].missingCountTip += (tasks[task].missingCount === 1) ? " host event was" : " host events were"; - tasks[task].missingCountTip += " missing."; - } else { - tasks[task].missingCountTip = "No host events were missing."; - } - } - }); - scope.hostResults = []; - - scope.hostResultsMaxRows = 200; - scope.hostSummariesMaxRows = 200; - scope.tasksMaxRows = 200; - scope.playsMaxRows = 200; - - // Set the following to true when 'Loading...' message desired - scope.playsLoading = true; - scope.tasksLoading = true; - scope.hostResultsLoading = true; - scope.hostSummariesLoading = true; - - // Turn on the 'Waiting...' message until events begin arriving - scope.waiting = true; - - scope.liveEventProcessing = true; // true while job is active and live events are arriving - scope.pauseLiveEvents = false; // control play/pause state of event processing - - scope.job_status = {}; - scope.job_id = job_id; - scope.auto_scroll = false; - - scope.searchPlaysEnabled = true; - scope.searchTasksEnabled = true; - scope.searchHostsEnabled = true; - scope.searchHostSummaryEnabled = true; - scope.search_play_status = 'all'; - scope.search_task_status = 'all'; - scope.search_host_status = 'all'; - scope.search_host_summary_status = 'all'; - - scope.haltEventQueue = false; - scope.processing = false; - scope.lessStatus = 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.jobData = {}; - scope.jobData.hostSummaries = {}; - - verbosity_options = [ - { value: 0, label: 'Default' }, - { value: 1, label: 'Verbose' }, - { value: 3, label: 'Debug' } - ]; - - job_type_options = [ - { value: 'run', label: 'Run' }, - { value: 'check', label: 'Check' } - ]; - - GetChoices({ - scope: scope, - url: GetBasePath('unified_jobs'), - field: 'status', - variable: 'status_choices', - // callback: 'choicesReady' - }); - - scope.eventsHelpText = "

Successful

\n" + - "

Changed

\n" + - "

Unreachable

\n" + - "

Failed

\n"; - function openSocket() { - $rootScope.event_socket.on("job_events-" + job_id, function(data) { - if (api_complete && data.id > lastEventId) { - scope.waiting = false; - data.event = data.event_name; - DigestEvent({ scope: scope, event: data }); - } - }); - } - openSocket(); - - if ($rootScope.removeJobStatusChange) { - $rootScope.removeJobStatusChange(); - } - $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange-jobDetails', function(e, data) { - // if we receive a status change event for the current job indicating the job - // is finished, stop event queue processing and reload - if (parseInt(data.unified_job_id, 10) === parseInt(job_id,10)) { - if (data.status === 'failed' || data.status === 'canceled' || - data.status === 'error' || data.status === 'successful' || data.status === 'running') { - $scope.liveEventProcessing = false; - if ($rootScope.jobDetailInterval) { - window.clearInterval($rootScope.jobDetailInterval); - } - if (!scope.pauseLiveEvents) { - $scope.$emit('LoadJob'); //this is what is used for the refresh - } - } - } - }); - - if ($rootScope.removeJobSummaryComplete) { - $rootScope.removeJobSummaryComplete(); - } - $rootScope.removeJobSummaryComplete = $rootScope.$on('JobSummaryComplete', function() { - // the job host summary should now be available from the API - $log.debug('Trigging reload of job_host_summaries'); - scope.$emit('LoadHostSummaries'); - }); - - - if (scope.removeInitialLoadComplete) { - scope.removeInitialLoadComplete(); - } - scope.removeInitialLoadComplete = scope.$on('InitialLoadComplete', function() { - var url; - Wait('stop'); - - if (JobIsFinished(scope)) { - scope.liveEventProcessing = false; // signal that event processing is over and endless scroll - scope.pauseLiveEvents = false; // should be enabled - url = scope.job.related.job_events + '?event=playbook_on_stats'; - Rest.setUrl(url); - Rest.get() - .success(function(data) { - if (data.results.length > 0) { - LoadHostSummary({ - scope: scope, - data: data.results[0].event_data - }); - } - UpdateDOM({ scope: scope }); - }) - .error(function(data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + '. GET returned: ' + status }); - }); - if ($rootScope.jobDetailInterval) { - window.clearInterval($rootScope.jobDetailInterval); - } - $log.debug('Job completed!'); - $log.debug(scope.jobData); - } - else { - api_complete = true; //trigger events to start processing - if ($rootScope.jobDetailInterval) { - window.clearInterval($rootScope.jobDetailInterval); - } - $rootScope.jobDetailInterval = setInterval(function() { - UpdateDOM({ scope: scope }); - }, 2000); - } - }); - - if (scope.removeLoadHostSummaries) { - scope.removeLoadHostSummaries(); - } - scope.removeHostSummaries = scope.$on('LoadHostSummaries', function() { - if(scope.job){ - var url = scope.job.related.job_host_summaries + '?'; - url += '&page_size=' + scope.hostSummariesMaxRows + '&order=host_name'; - - Rest.setUrl(url); - Rest.get() - .success(function(data) { - scope.next_host_summaries = data.next; - if (data.results.length > 0) { - // only dump what's in memory when job_host_summaries is available. - scope.jobData.hostSummaries = {}; - } - data.results.forEach(function(event) { - var name; - if (event.host_name) { - name = event.host_name; - } - else { - name = ""; - } - scope.jobData.hostSummaries[event.host] = { - id: event.host, - name: name, - ok: event.ok, - changed: event.changed, - unreachable: event.dark, - failed: event.failures, - status: (event.failed) ? 'failed' : 'successful' - }; - }); - scope.$emit('InitialLoadComplete'); - }) - .error(function(data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + '. GET returned: ' + status }); - }); - } - - }); - - if (scope.removeLoadHosts) { - scope.removeLoadHosts(); - } - scope.removeLoadHosts = scope.$on('LoadHosts', function() { - if (scope.activeTask) { - - var play = scope.jobData.plays[scope.activePlay], - task, // = play.tasks[scope.activeTask], - url; - if(play){ - task = play.tasks[scope.activeTask]; - } - if (play && task) { - url = scope.job.related.job_events + '?parent=' + task.id + '&'; - url += 'event__startswith=runner&page_size=' + scope.hostResultsMaxRows + '&order=host_name,counter'; - - Rest.setUrl(url); - Rest.get() - .success(function(data) { - var idx, event, status, status_text, item, msg; - if (data.results.length > 0) { - lastEventId = data.results[0].id; - } - scope.next_host_results = data.next; - for (idx=data.results.length - 1; idx >= 0; idx--) { - event = data.results[idx]; - if (event.event === "runner_on_skipped") { - status = 'skipped'; - } - else if (event.event === "runner_on_unreachable") { - status = 'unreachable'; - } - else { - status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful'; - } - switch(status) { - case "successful": - status_text = 'OK'; - break; - case "changed": - status_text = "Changed"; - break; - case "failed": - status_text = "Failed"; - break; - case "unreachable": - status_text = "Unreachable"; - break; - case "skipped": - status_text = "Skipped"; - } - - if (event.event_data && event.event_data.res) { - item = event.event_data.res.item; - if (typeof item === "object") { - item = JSON.stringify(item); - } - } - - msg = ''; - if (event.event_data && event.event_data.res) { - if (typeof event.event_data.res === 'object') { - msg = event.event_data.res.msg; - } else { - msg = event.event_data.res; - } - } - - if (event.event !== "runner_on_no_hosts") { - task.hostResults[event.id] = { - id: event.id, - status: status, - status_text: status_text, - host_id: event.host, - task_id: event.parent, - name: event.event_data.host, - created: event.created, - msg: msg, - counter: event.counter, - item: item - }; - } - } - scope.$emit('LoadHostSummaries'); - }) - .error(function(data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + '. GET returned: ' + status }); - }); - } else { - scope.$emit('LoadHostSummaries'); - } - } else { - scope.$emit('LoadHostSummaries'); - } - }); - - if (scope.removeLoadTasks) { - scope.removeLoadTasks(); - } - scope.removeLoadTasks = scope.$on('LoadTasks', function() { - if (scope.activePlay) { - var play = scope.jobData.plays[scope.activePlay], url; - - if (play) { - url = scope.job.url + 'job_tasks/?event_id=' + play.id; - url += '&page_size=' + scope.tasksMaxRows + '&order=id'; - - Rest.setUrl(url); - Rest.get() - .success(function(data) { - scope.next_tasks = data.next; - if (data.results.length > 0) { - lastEventId = data.results[data.results.length - 1].id; - if (scope.liveEventProcessing) { - scope.activeTask = data.results[data.results.length - 1].id; - } - else { - scope.activeTask = data.results[0].id; - } - scope.selectedTask = scope.activeTask; - } - data.results.forEach(function(event, idx) { - var end, elapsed, status, status_text; - - if (play.firstTask === undefined || play.firstTask === null) { - play.firstTask = event.id; - play.hostCount = (event.host_count) ? event.host_count : 0; - } - - if (idx < data.results.length - 1) { - // end date = starting date of the next event - end = data.results[idx + 1].created; - } - else { - // no next event (task), get the end time of the play - if(scope.jobData.plays[scope.activePlay]){ - end = scope.jobData.plays[scope.activePlay].finished; - } - } - - if (end) { - elapsed = GetElapsed({ - start: event.created, - end: end - }); - } - else { - elapsed = '00:00:00'; - } - - status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful'; - status_text = (event.failed) ? 'Failed' : (event.changed) ? 'Changed' : 'OK'; - - play.tasks[event.id] = { - id: event.id, - play_id: scope.activePlay, - name: event.name, - status: status, - status_text: status_text, - status_tip: "Event ID: " + event.id + "
Status: " + status_text, - created: event.created, - modified: event.modified, - finished: end, - elapsed: elapsed, - hostCount: (event.host_count) ? event.host_count : 0, - reportedHosts: (event.reported_hosts) ? event.reported_hosts : 0, - successfulCount: (event.successful_count) ? event.successful_count : 0, - failedCount: (event.failed_count) ? event.failed_count : 0, - changedCount: (event.changed_count) ? event.changed_count : 0, - skippedCount: (event.skipped_count) ? event.skipped_count : 0, - unreachableCount: (event.unreachable_count) ? event.unreachable_count : 0, - taskActiveClass: '', - hostResults: {} - }; - if (play.firstTask !== event.id) { - // this is not the first task - play.tasks[event.id].hostCount = play.tasks[play.firstTask].hostCount; - } - if (play.tasks[event.id].reportedHosts === 0 && play.tasks[event.id].successfulCount === 0 && - play.tasks[event.id].failedCount === 0 && play.tasks[event.id].changedCount === 0 && - play.tasks[event.id].skippedCount === 0 && play.tasks[event.id].unreachableCount === 0) { - play.tasks[event.id].status = 'no-matching-hosts'; - play.tasks[event.id].status_text = 'No matching hosts'; - play.tasks[event.id].status_tip = "Event ID: " + event.id + "
Status: No matching hosts"; - } - play.taskCount++; - SetTaskStyles({ - task: play.tasks[event.id] - }); - }); - if (scope.activeTask && scope.jobData.plays[scope.activePlay] && scope.jobData.plays[scope.activePlay].tasks[scope.activeTask]) { - scope.jobData.plays[scope.activePlay].tasks[scope.activeTask].taskActiveClass = 'active'; - } - scope.$emit('LoadHosts'); - }) - .error(function(data) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + '. GET returned: ' + status }); - }); - } else { - scope.$emit('LoadHostSummaries'); - } - } else { - scope.$emit('LoadHostSummaries'); - } - }); - - if (scope.removeLoadPlays) { - scope.removeLoadPlays(); - } - scope.removeLoadPlays = scope.$on('LoadPlays', function(e, events_url) { - - 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.jobData.plays = {}; - - var url = scope.job.url + 'job_plays/?order_by=id'; - url += '&page_size=' + scope.playsMaxRows + '&order_by=id'; - - Rest.setUrl(url); - Rest.get() - .success( function(data) { - scope.next_plays = data.next; - if (data.results.length > 0) { - lastEventId = data.results[data.results.length - 1].id; - if (scope.liveEventProcessing) { - scope.activePlay = data.results[data.results.length - 1].id; - } - else { - scope.activePlay = data.results[0].id; - } - scope.selectedPlay = scope.activePlay; - } else { - // if we are here, there are no plays and the job has failed, let the user know they may want to consult stdout - if ( (scope.job_status.status === 'failed' || scope.job_status.status === 'error') && - (!scope.job_status.explanation)) { - scope.job_status.explanation = "View stdout for more detail "; - } - } - data.results.forEach(function(event, idx) { - var status, status_text, start, end, elapsed, ok, changed, failed, skipped; - - status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful'; - status_text = (event.failed) ? 'Failed' : (event.changed) ? 'Changed' : 'OK'; - start = event.started; - - if (idx < data.results.length - 1) { - // end date = starting date of the next event - end = data.results[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.jobData.plays[event.id] = { - id: event.id, - name: event.play, - created: start, - finished: end, - status: status, - status_text: status_text, - status_tip: "Event ID: " + event.id + "
Status: " + status_text, - elapsed: elapsed, - hostCount: 0, - fistTask: null, - taskCount: 0, - playActiveClass: '', - unreachableCount: (event.unreachable_count) ? event.unreachable_count : 0, - tasks: {} - }; - - ok = (event.ok_count) ? event.ok_count : 0; - changed = (event.changed_count) ? event.changed_count : 0; - failed = (event.failed_count) ? event.failed_count : 0; - skipped = (event.skipped_count) ? event.skipped_count : 0; - - scope.jobData.plays[event.id].hostCount = ok + changed + failed + skipped; - - if (scope.jobData.plays[event.id].hostCount > 0 || event.unreachable_count > 0 || scope.job_status.status === 'successful' || - scope.job_status.status === 'failed' || scope.job_status.status === 'error' || scope.job_status.status === 'canceled') { - // force the play to be on the 'active' list - scope.jobData.plays[event.id].taskCount = 1; - } - - if (scope.jobData.plays[event.id].hostCount === 0 && event.unreachable_count === 0) { - scope.jobData.plays[event.id].status = 'no-matching-hosts'; - scope.jobData.plays[event.id].status_text = 'No matching hosts'; - scope.jobData.plays[event.id].status_tip = "Event ID: " + event.id + "
Status: No matching hosts"; - } - - scope.host_summary.ok += ok; - scope.host_summary.changed += changed; - scope.host_summary.unreachable += (event.unreachable_count) ? event.unreachable_count : 0; - scope.host_summary.failed += failed; - scope.host_summary.total = scope.host_summary.ok + scope.host_summary.changed + scope.host_summary.unreachable + - scope.host_summary.failed; - }); - if (scope.activePlay && scope.jobData.plays[scope.activePlay]) { - scope.jobData.plays[scope.activePlay].playActiveClass = 'active'; - } - scope.$emit('LoadTasks', events_url); - }) - .error( function(data, status) { - 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'); - scope.job_status = {}; - - scope.playsLoading = true; - scope.tasksLoading = true; - scope.hostResultsLoading = true; - scope.LoadHostSummaries = true; - - // Load the job record - Rest.setUrl(GetBasePath('jobs') + job_id + '/'); - Rest.get() - .success(function(data) { - var i; - 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.credential_url = (data.credential) ? '/#/credentials/' + data.credential : ''; - scope.cloud_credential_url = (data.cloud_credential) ? '/#/credentials/' + data.cloud_credential : ''; - 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.variables = ParseVariableString(data.extra_vars); - - // If we get created_by back from the server then use it. This means that the job was kicked - // off by a user and not a schedule AND that the user still exists in the system. - if(data.summary_fields.created_by) { - scope.users_url = '/#/users/' + data.summary_fields.created_by.id; - scope.created_by = data.summary_fields.created_by.username; - } - else { - if(data.summary_fields.schedule) { - // Build the Launched By link to point to the schedule that kicked it off - scope.scheduled_by = (data.summary_fields.schedule.name) ? data.summary_fields.schedule.name.toString() : ''; - } - // If there is no schedule or created_by then we can assume that the job was - // created by a deleted user - } - - if (data.summary_fields.credential) { - scope.credential_name = data.summary_fields.credential.name; - scope.credential_url = data.related.credential - .replace('api/v1', '#'); - } else { - scope.credential_name = ""; - } - - if (data.summary_fields.cloud_credential) { - scope.cloud_credential_name = data.summary_fields.cloud_credential.name; - scope.cloud_credential_url = data.related.cloud_credential - .replace('api/v1', '#'); - } else { - scope.cloud_credential_name = ""; - } - - for (i=0; i < verbosity_options.length; i++) { - if (verbosity_options[i].value === data.verbosity) { - scope.verbosity = verbosity_options[i].label; - } - } - - for (i=0; i < job_type_options.length; i++) { - if (job_type_options[i].value === data.job_type) { - scope.job_type = job_type_options[i].label; - } - } - - // 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; - scope.job_status.status_class = ((data.status === 'error' || data.status === 'failed') && data.job_explanation) ? "alert alert-danger" : ""; - scope.job_status.explanation = data.job_explanation; - if(data.result_traceback) { - scope.job_status.traceback = data.result_traceback.trim().split('\n').join('
'); - } - if (data.status === 'successful' || data.status === 'failed' || data.status === 'error' || data.status === 'canceled') { - scope.job_status.finished = data.finished; - scope.liveEventProcessing = false; - scope.pauseLiveEvents = false; - scope.waiting = false; - scope.playsLoading = false; - scope.tasksLoading = false; - scope.hostResultsLoading = false; - scope.hostSummariesLoading = false; - } - else { - scope.job_status.finished = null; - } - - 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.status_choices.every(function(status) { - if (status.value === scope.job.status) { - scope.job_status.status_label = status.label; - return false; - } - return true; - }); - //scope.setSearchAll('host'); - scope.$emit('LoadPlays', data.related.job_events); - }) - .error(function(data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve job: ' + $stateParams.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 the graph needs to redraw - setTimeout(function() { DrawGraph({ scope: scope, resize: true }); }, 500); - } - }); - - scope.adjustSize = function() { - var height, ww = $(window).width(); - if (ww < 1024) { - $('#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(); - } - - scope.lessStatus = true; // close the view more status option - - // Detail table height adjusting. First, put page height back to 'normal'. - $('#plays-table-detail').height(80); - //$('#plays-table-detail').mCustomScrollbar("update"); - $('#tasks-table-detail').height(120); - //$('#tasks-table-detail').mCustomScrollbar("update"); - $('#hosts-table-detail').height(150); - //$('#hosts-table-detail').mCustomScrollbar("update"); - height = $(window).height() - $('#main-menu-container .navbar').outerHeight() - - $('#job-detail-container').outerHeight() - $('#job-detail-footer').outerHeight() - 20; - if (height > 15) { - // there's a bunch of white space at the bottom, let's use it - $('#plays-table-detail').height(80 + (height * 0.10)); - $('#tasks-table-detail').height(120 + (height * 0.20)); - $('#hosts-table-detail').height(150 + (height * 0.70)); - } - // 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)); - - function flashPlayTip() { - setTimeout(function(){ - $('#play-help').popover('show'); - },500); - setTimeout(function() { - $('#play-help').popover('hide'); - }, 5000); - } - - scope.selectPlay = function(id) { - if (scope.liveEventProcessing && !scope.pauseLiveEvents) { - scope.pauseLiveEvents = true; - flashPlayTip(); - } - SelectPlay({ - scope: scope, - id: id - }); - }; - - scope.selectTask = function(id) { - if (scope.liveEventProcessing && !scope.pauseLiveEvents) { - scope.pauseLiveEvents = true; - flashPlayTip(); - } - SelectTask({ - scope: scope, - id: id - }); - }; - - scope.togglePlayButton = function() { - if (scope.pauseLiveEvents) { - scope.pauseLiveEvents = false; - scope.$emit('LoadJob'); - } - }; - - 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': 1090, - '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.toggleLessStatus = function() { - if (!scope.lessStatus) { - $('#job-status-form .toggle-show').slideUp(200); - scope.lessStatus = true; - } - else { - $('#job-status-form .toggle-show').slideDown(200); - scope.lessStatus = false; - } - }; - - scope.filterPlayStatus = function() { - scope.search_play_status = (scope.search_play_status === 'all') ? 'failed' : 'all'; - if (!scope.liveEventProcessing || scope.pauseLiveEvents) { - LoadPlays({ - scope: scope - }); - } - }; - - scope.searchPlays = function() { - if (scope.search_play_name) { - scope.searchPlaysEnabled = false; - } - else { - scope.searchPlaysEnabled = true; - } - if (!scope.liveEventProcessing || scope.pauseLiveEvents) { - LoadPlays({ - scope: scope - }); - } - }; - - scope.searchPlaysKeyPress = function(e) { - if (e.keyCode === 13) { - scope.searchPlays(); - e.stopPropagation(); - } - }; - - scope.searchTasks = function() { - if (scope.search_task_name) { - scope.searchTasksEnabled = false; - } - else { - scope.searchTasksEnabled = true; - } - if (!scope.liveEventProcessing || scope.pauseLiveEvents) { - LoadTasks({ - scope: scope - }); - } - }; - - scope.searchTasksKeyPress = function(e) { - if (e.keyCode === 13) { - scope.searchTasks(); - e.stopPropagation(); - } - }; - - scope.searchHosts = function() { - if (scope.search_host_name) { - scope.searchHostsEnabled = false; - } - else { - scope.searchHostsEnabled = true; - } - if (!scope.liveEventProcessing || scope.pauseLiveEvents) { - LoadHosts({ - scope: scope - }); - } - }; - - scope.searchHostsKeyPress = function(e) { - if (e.keyCode === 13) { - scope.searchHosts(); - e.stopPropagation(); - } - }; - - scope.searchHostSummary = function() { - if (scope.search_host_summary_name) { - scope.searchHostSummaryEnabled = false; - } - else { - scope.searchHostSummaryEnabled = true; - } - if (!scope.liveEventProcessing || scope.pauseLiveEvents) { - ReloadHostSummaryList({ - scope: scope - }); - } - }; - - scope.searchHostSummaryKeyPress = function(e) { - if (e.keyCode === 13) { - scope.searchHostSummary(); - e.stopPropagation(); - } - }; - - scope.filterTaskStatus = function() { - scope.search_task_status = (scope.search_task_status === 'all') ? 'failed' : 'all'; - if (!scope.liveEventProcessing || scope.pauseLiveEvents) { - LoadTasks({ - scope: scope - }); - } - }; - - scope.filterHostStatus = function() { - scope.search_host_status = (scope.search_host_status === 'all') ? 'failed' : 'all'; - if (!scope.liveEventProcessing || scope.pauseLiveEvents) { - LoadHosts({ - scope: scope - }); - } - }; - - scope.filterHostSummaryStatus = function() { - scope.search_host_summary_status = (scope.search_host_summary_status === 'all') ? 'failed' : 'all'; - if (!scope.liveEventProcessing || scope.pauseLiveEvents) { - ReloadHostSummaryList({ - scope: scope - }); - } - }; - - scope.viewHostResults = function(id) { - EventViewer({ - scope: scope, - url: scope.job.related.job_events, - parent_id: scope.selectedTask, - event_id: id, - index: this.$index, - title: 'Host Event' - }); - }; - - if (scope.removeDeleteFinished) { - scope.removeDeleteFinished(); - } - scope.removeDeleteFinished = scope.$on('DeleteFinished', function(e, action) { - Wait('stop'); - if (action !== 'cancel') { - Wait('stop'); - $location.url('/jobs'); - } - }); - - scope.deleteJob = function() { - DeleteJob({ - scope: scope, - id: scope.job.id, - job: scope.job, - callback: 'DeleteFinished' - }); - }; - - scope.relaunchJob = function() { - PlaybookRun({ - scope: scope, - id: scope.job.id - }); - }; - - scope.playsScrollDown = function() { - // check for more plays when user scrolls to bottom of play list... - if (((!scope.liveEventProcessing) || (scope.liveEventProcessing && scope.pauseLiveEvents)) && scope.next_plays) { - $('#playsMoreRows').fadeIn(); - scope.playsLoading = true; - Rest.setUrl(scope.next_plays); - Rest.get() - .success( function(data) { - scope.next_plays = data.next; - data.results.forEach(function(event, idx) { - var status, status_text, start, end, elapsed, ok, changed, failed, skipped; - - status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful'; - status_text = (event.failed) ? 'Failed' : (event.changed) ? 'Changed' : 'OK'; - start = event.started; - - if (idx < data.results.length - 1) { - // end date = starting date of the next event - end = data.results[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.push({ - id: event.id, - name: event.play, - created: start, - finished: end, - status: status, - status_text: status_text, - status_tip: "Event ID: " + event.id + "
Status: " + status_text, - elapsed: elapsed, - hostCount: 0, - fistTask: null, - playActiveClass: '', - unreachableCount: (event.unreachable_count) ? event.unreachable_count : 0, - }); - - ok = (event.ok_count) ? event.ok_count : 0; - changed = (event.changed_count) ? event.changed_count : 0; - failed = (event.failed_count) ? event.failed_count : 0; - skipped = (event.skipped_count) ? event.skipped_count : 0; - - scope.plays[scope.plays.length - 1].hostCount = ok + changed + failed + skipped; - scope.playsLoading = false; - }); - $('#playsMoreRows').fadeOut(400); - }) - .error( function(data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + scope.next_plays + '. GET returned: ' + status }); - }); - } - }; - - scope.tasksScrollDown = function() { - // check for more tasks when user scrolls to bottom of task list... - if (((!scope.liveEventProcessing) || (scope.liveEventProcessing && scope.pauseLiveEvents)) && scope.next_tasks) { - $('#tasksMoreRows').fadeIn(); - scope.tasksLoading = true; - Rest.setUrl(scope.next_tasks); - Rest.get() - .success(function(data) { - scope.next_tasks = data.next; - data.results.forEach(function(event, idx) { - var end, elapsed, status, status_text; - if (idx < data.results.length - 1) { - // end date = starting date of the next event - end = data.results[idx + 1].created; - } - else { - // no next event (task), get the end time of the play - scope.plays.every(function(p, j) { - if (p.id === scope.selectedPlay) { - end = scope.plays[j].finished; - return false; - } - return true; - }); - } - if (end) { - elapsed = GetElapsed({ - start: event.created, - end: end - }); - } - else { - elapsed = '00:00:00'; - } - - status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful'; - status_text = (event.failed) ? 'Failed' : (event.changed) ? 'Changed' : 'OK'; - - scope.tasks.push({ - id: event.id, - play_id: scope.selectedPlay, - name: event.name, - status: status, - status_text: status_text, - status_tip: "Event ID: " + event.id + "
Status: " + status_text, - 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({ - task: scope.tasks[scope.tasks.length - 1] - }); - }); - $('#tasksMoreRows').fadeOut(400); - scope.tasksLoading = false; - }) - .error(function(data, status) { - $('#tasksMoreRows').fadeOut(400); - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + scope.next_tasks + '. GET returned: ' + status }); - }); - } - }; - - scope.hostResultsScrollDown = function() { - // check for more hosts when user scrolls to bottom of host results list... - if (((!scope.liveEventProcessing) || (scope.liveEventProcessing && scope.pauseLiveEvents)) && scope.next_host_results) { - $('#hostResultsMoreRows').fadeIn(); - scope.hostResultsLoading = true; - Rest.setUrl(scope.next_host_results); - Rest.get() - .success(function(data) { - scope.next_host_results = data.next; - data.results.forEach(function(row) { - var status, status_text, item, msg; - if (row.event === "runner_on_skipped") { - status = 'skipped'; - } - else if (row.event === "runner_on_unreachable") { - status = 'unreachable'; - } - else { - status = (row.failed) ? 'failed' : (row.changed) ? 'changed' : 'successful'; - } - switch(status) { - case "successful": - status_text = 'OK'; - break; - case "changed": - status_text = "Changed"; - break; - case "failed": - status_text = "Failed"; - break; - case "unreachable": - status_text = "Unreachable"; - break; - case "skipped": - status_text = "Skipped"; - } - if (row.event_data && row.event_data.res) { - item = row.event_data.res.item; - if (typeof item === "object") { - item = JSON.stringify(item); - } - } - msg = ''; - if (row.event_data && row.event_data.res) { - if (typeof row.event_data.res === 'object') { - msg = row.event_data.res.msg; - } else { - msg = row.event_data.res; - } - } - scope.hostResults.push({ - id: row.id, - status: status, - status_text: status_text, - 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 : '', - item: item - }); - scope.hostResultsLoading = false; - }); - $('#hostResultsMoreRows').fadeOut(400); - }) - .error(function(data, status) { - $('#hostResultsMoreRows').fadeOut(400); - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + scope.next_host_results + '. GET returned: ' + status }); - }); - } - }; - - scope.hostSummariesScrollDown = function() { - // check for more hosts when user scrolls to bottom of host summaries list... - if (((!scope.liveEventProcessing) || (scope.liveEventProcessing && scope.pauseLiveEvents)) && scope.next_host_summaries) { - scope.hostSummariesLoading = true; - Rest.setUrl(scope.next_host_summaries); - Rest.get() - .success(function(data) { - scope.next_host_summaries = data.next; - data.results.forEach(function(row) { - var name; - if (row.host_name) { - name = row.host_name; - } - else { - name = ""; - } - scope.hosts.push({ - id: row.id, - name: name, - ok: row.ok, - changed: row.changed, - unreachable: row.dark, - failed: row.failures - }); - }); - $('#hostSummariesMoreRows').fadeOut(); - scope.hostSummariesLoading = false; - }) - .error(function(data, status) { - $('#hostSummariesMoreRows').fadeOut(); - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + scope.next_host_summaries + '. GET returned: ' + status }); - }); - } - }; - - scope.hostEventsViewer = function(id, name, status) { - HostEventsViewer({ - scope: scope, - id: id, - name: name, - url: scope.job.related.job_events, - job_id: scope.job.id, - status: status - }); - }; - - scope.refresh = function(){ - $scope.$emit('LoadJob'); - }; - - scope.editHost = function(id) { - HostsEdit({ - host_scope: scope, - group_scope: null, - host_id: id, - inventory_id: scope.job.inventory, - mode: 'edit', // 'add' or 'edit' - selected_group_id: null - }); - }; - - scope.editSchedule = function() { - // We need to get the schedule's ID out of the related links - // An example of the related schedule link looks like /api/v1/schedules/5 - // where 5 is the ID we are trying to capture - var regex = /\/api\/v1\/schedules\/(\d+)\//; - var id = scope.job.related.schedule.match(regex)[1]; - if (id) { - // If we get an ID from the regular expression go ahead and open up the - // modal via the EditSchedule service - EditSchedule({ - scope: scope, - id: parseInt(id), - callback: 'SchedulesRefresh' - }); - } - }; - - // SchedulesRefresh is the callback string that we passed to the edit schedule modal - // When the modal successfully updates the schedule it will emit this event and pass - // the updated schedule object - if (scope.removeSchedulesRefresh) { - scope.removeSchedulesRefresh(); - } - scope.$on('SchedulesRefresh', function(e, data) { - if (data) { - scope.scheduled_by = data.name; - } - }); -} - -JobDetailController.$inject = [ '$location', '$rootScope', '$filter', '$scope', '$compile', '$stateParams', '$log', 'ClearScope', 'GetBasePath', - 'Wait', 'Rest', 'ProcessErrors', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed', 'DrawGraph', 'LoadHostSummary', 'ReloadHostSummaryList', - 'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'EventViewer', 'DeleteJob', 'PlaybookRun', 'HostEventsViewer', 'LoadPlays', 'LoadTasks', - 'LoadHosts', 'HostsEdit', 'ParseVariableString', 'GetChoices', 'fieldChoices', 'fieldLabels', 'EditSchedule' -]; diff --git a/awx/ui/client/src/job-detail/job-detail.controller.js b/awx/ui/client/src/job-detail/job-detail.controller.js new file mode 100644 index 0000000000..604179d622 --- /dev/null +++ b/awx/ui/client/src/job-detail/job-detail.controller.js @@ -0,0 +1,1452 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/** + * @ngdoc function + * @name controllers.function:JobDetail + * @description This controller's for the Job Detail Page +*/ + +export default + [ '$location', '$rootScope', '$filter', '$scope', '$compile', + '$stateParams', '$log', 'ClearScope', 'GetBasePath', 'Wait', 'Rest', + 'ProcessErrors', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed', + 'DrawGraph', 'LoadHostSummary', 'ReloadHostSummaryList', + 'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', + 'EventViewer', 'DeleteJob', 'PlaybookRun', 'HostEventsViewer', + 'LoadPlays', 'LoadTasks', 'LoadHosts', 'HostsEdit', + 'ParseVariableString', 'GetChoices', 'fieldChoices', 'fieldLabels', + 'EditSchedule', + function( + $location, $rootScope, $filter, $scope, $compile, $stateParams, + $log, ClearScope, GetBasePath, Wait, Rest, ProcessErrors, + SelectPlay, SelectTask, Socket, GetElapsed, DrawGraph, + LoadHostSummary, ReloadHostSummaryList, JobIsFinished, + SetTaskStyles, DigestEvent, UpdateDOM, EventViewer, DeleteJob, + PlaybookRun, HostEventsViewer, LoadPlays, LoadTasks, LoadHosts, + HostsEdit, ParseVariableString, GetChoices, fieldChoices, + fieldLabels, EditSchedule + ) { + ClearScope(); + + var job_id = $stateParams.id, + scope = $scope, + api_complete = false, + refresh_count = 0, + lastEventId = 0, + verbosity_options, + job_type_options; + + scope.plays = []; + + scope.previousTaskFailed = false; + + scope.$watch('job_status', function(job_status) { + if (job_status && job_status.explanation && job_status.explanation.split(":")[0] === "Previous Task Failed") { + scope.previousTaskFailed = true; + var taskObj = JSON.parse(job_status.explanation.substring(job_status.explanation.split(":")[0].length + 1)); + // return a promise from the options request with the permission type choices (including adhoc) as a param + var fieldChoice = fieldChoices({ + scope: $scope, + url: 'api/v1/unified_jobs/', + field: 'type' + }); + + // manipulate the choices from the options request to be set on + // scope and be usable by the list form + fieldChoice.then(function (choices) { + choices = + fieldLabels({ + choices: choices + }); + scope.explanation_fail_type = choices[taskObj.job_type]; + scope.explanation_fail_name = taskObj.job_name; + scope.explanation_fail_id = taskObj.job_id; + scope.task_detail = scope.explanation_fail_type + " failed for " + scope.explanation_fail_name + " with ID " + scope.explanation_fail_id + "."; + }); + } else { + scope.previousTaskFailed = false; + } + }, true); + + scope.$watch('plays', function(plays) { + for (var play in plays) { + if (plays[play].elapsed) { + plays[play].finishedTip = "Play completed at " + $filter("longDate")(plays[play].finished) + "."; + } else { + plays[play].finishedTip = "Play not completed."; + } + } + }); + scope.hosts = []; + scope.$watch('hosts', function(hosts) { + for (var host in hosts) { + if (hosts[host].ok) { + hosts[host].okTip = hosts[host].ok; + hosts[host].okTip += (hosts[host].ok === 1) ? " host event was" : " host events were"; + hosts[host].okTip += " ok."; + } else { + hosts[host].okTip = "No host events were ok."; + } + if (hosts[host].changed) { + hosts[host].changedTip = hosts[host].changed; + hosts[host].changedTip += (hosts[host].changed === 1) ? " host event" : " host events"; + hosts[host].changedTip += " changed."; + } else { + hosts[host].changedTip = "No host events changed."; + } + if (hosts[host].failed) { + hosts[host].failedTip = hosts[host].failed; + hosts[host].failedTip += (hosts[host].failed === 1) ? " host event" : " host events"; + hosts[host].failedTip += " failed."; + } else { + hosts[host].failedTip = "No host events failed."; + } + if (hosts[host].unreachable) { + hosts[host].unreachableTip = hosts[host].unreachable; + hosts[host].unreachableTip += (hosts[host].unreachable === 1) ? " host event was" : " hosts events were"; + hosts[host].unreachableTip += " unreachable"; + } else { + hosts[host].unreachableTip = "No host events were unreachable."; + } + } + }); + scope.tasks = []; + scope.$watch('tasks', function(tasks) { + for (var task in tasks) { + if (tasks[task].elapsed) { + tasks[task].finishedTip = "Task completed at " + $filter("longDate")(tasks[task].finished) + "."; + } else { + tasks[task].finishedTip = "Task not completed."; + } + if (tasks[task].successfulCount) { + tasks[task].successfulCountTip = tasks[task].successfulCount; + tasks[task].successfulCountTip += (tasks[task].successfulCount === 1) ? " host event was" : " host events were"; + tasks[task].successfulCountTip += " ok."; + } else { + tasks[task].successfulCountTip = "No host events were ok."; + } + if (tasks[task].changedCount) { + tasks[task].changedCountTip = tasks[task].changedCount; + tasks[task].changedCountTip += (tasks[task].changedCount === 1) ? " host event" : " host events"; + tasks[task].changedCountTip += " changed."; + } else { + tasks[task].changedCountTip = "No host events changed."; + } + if (tasks[task].skippedCount) { + tasks[task].skippedCountTip = tasks[task].skippedCount; + tasks[task].skippedCountTip += (tasks[task].skippedCount === 1) ? " host event was" : " hosts events were"; + tasks[task].skippedCountTip += " skipped."; + } else { + tasks[task].skippedCountTip = "No host events were skipped."; + } + if (tasks[task].failedCount) { + tasks[task].failedCountTip = tasks[task].failedCount; + tasks[task].failedCountTip += (tasks[task].failedCount === 1) ? " host event" : " host events"; + tasks[task].failedCountTip += " failed."; + } else { + tasks[task].failedCountTip = "No host events failed."; + } + if (tasks[task].unreachableCount) { + tasks[task].unreachableCountTip = tasks[task].unreachableCount; + tasks[task].unreachableCountTip += (tasks[task].unreachableCount === 1) ? " host event was" : " hosts events were"; + tasks[task].unreachableCountTip += " unreachable."; + } else { + tasks[task].unreachableCountTip = "No host events were unreachable."; + } + if (tasks[task].missingCount) { + tasks[task].missingCountTip = tasks[task].missingCount; + tasks[task].missingCountTip += (tasks[task].missingCount === 1) ? " host event was" : " host events were"; + tasks[task].missingCountTip += " missing."; + } else { + tasks[task].missingCountTip = "No host events were missing."; + } + } + }); + scope.hostResults = []; + + scope.hostResultsMaxRows = 200; + scope.hostSummariesMaxRows = 200; + scope.tasksMaxRows = 200; + scope.playsMaxRows = 200; + + // Set the following to true when 'Loading...' message desired + scope.playsLoading = true; + scope.tasksLoading = true; + scope.hostResultsLoading = true; + scope.hostSummariesLoading = true; + + // Turn on the 'Waiting...' message until events begin arriving + scope.waiting = true; + + scope.liveEventProcessing = true; // true while job is active and live events are arriving + scope.pauseLiveEvents = false; // control play/pause state of event processing + + scope.job_status = {}; + scope.job_id = job_id; + scope.auto_scroll = false; + + scope.searchPlaysEnabled = true; + scope.searchTasksEnabled = true; + scope.searchHostsEnabled = true; + scope.searchHostSummaryEnabled = true; + scope.search_play_status = 'all'; + scope.search_task_status = 'all'; + scope.search_host_status = 'all'; + scope.search_host_summary_status = 'all'; + + scope.haltEventQueue = false; + scope.processing = false; + scope.lessStatus = 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.jobData = {}; + scope.jobData.hostSummaries = {}; + + verbosity_options = [ + { value: 0, label: 'Default' }, + { value: 1, label: 'Verbose' }, + { value: 3, label: 'Debug' } + ]; + + job_type_options = [ + { value: 'run', label: 'Run' }, + { value: 'check', label: 'Check' } + ]; + + GetChoices({ + scope: scope, + url: GetBasePath('unified_jobs'), + field: 'status', + variable: 'status_choices', + // callback: 'choicesReady' + }); + + scope.eventsHelpText = "

Successful

\n" + + "

Changed

\n" + + "

Unreachable

\n" + + "

Failed

\n"; + function openSocket() { + $rootScope.event_socket.on("job_events-" + job_id, function(data) { + if (api_complete && data.id > lastEventId) { + scope.waiting = false; + data.event = data.event_name; + DigestEvent({ scope: scope, event: data }); + } + }); + } + openSocket(); + + if ($rootScope.removeJobStatusChange) { + $rootScope.removeJobStatusChange(); + } + $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange-jobDetails', function(e, data) { + // if we receive a status change event for the current job indicating the job + // is finished, stop event queue processing and reload + if (parseInt(data.unified_job_id, 10) === parseInt(job_id,10)) { + if (data.status === 'failed' || data.status === 'canceled' || + data.status === 'error' || data.status === 'successful' || data.status === 'running') { + $scope.liveEventProcessing = false; + if ($rootScope.jobDetailInterval) { + window.clearInterval($rootScope.jobDetailInterval); + } + if (!scope.pauseLiveEvents) { + $scope.$emit('LoadJob'); //this is what is used for the refresh + } + } + } + }); + + if ($rootScope.removeJobSummaryComplete) { + $rootScope.removeJobSummaryComplete(); + } + $rootScope.removeJobSummaryComplete = $rootScope.$on('JobSummaryComplete', function() { + // the job host summary should now be available from the API + $log.debug('Trigging reload of job_host_summaries'); + scope.$emit('LoadHostSummaries'); + }); + + + if (scope.removeInitialLoadComplete) { + scope.removeInitialLoadComplete(); + } + scope.removeInitialLoadComplete = scope.$on('InitialLoadComplete', function() { + var url; + Wait('stop'); + + if (JobIsFinished(scope)) { + scope.liveEventProcessing = false; // signal that event processing is over and endless scroll + scope.pauseLiveEvents = false; // should be enabled + url = scope.job.related.job_events + '?event=playbook_on_stats'; + Rest.setUrl(url); + Rest.get() + .success(function(data) { + if (data.results.length > 0) { + LoadHostSummary({ + scope: scope, + data: data.results[0].event_data + }); + } + UpdateDOM({ scope: scope }); + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + if ($rootScope.jobDetailInterval) { + window.clearInterval($rootScope.jobDetailInterval); + } + $log.debug('Job completed!'); + $log.debug(scope.jobData); + } + else { + api_complete = true; //trigger events to start processing + if ($rootScope.jobDetailInterval) { + window.clearInterval($rootScope.jobDetailInterval); + } + $rootScope.jobDetailInterval = setInterval(function() { + UpdateDOM({ scope: scope }); + }, 2000); + } + }); + + if (scope.removeLoadHostSummaries) { + scope.removeLoadHostSummaries(); + } + scope.removeHostSummaries = scope.$on('LoadHostSummaries', function() { + if(scope.job){ + var url = scope.job.related.job_host_summaries + '?'; + url += '&page_size=' + scope.hostSummariesMaxRows + '&order=host_name'; + + Rest.setUrl(url); + Rest.get() + .success(function(data) { + scope.next_host_summaries = data.next; + if (data.results.length > 0) { + // only dump what's in memory when job_host_summaries is available. + scope.jobData.hostSummaries = {}; + } + data.results.forEach(function(event) { + var name; + if (event.host_name) { + name = event.host_name; + } + else { + name = ""; + } + scope.jobData.hostSummaries[event.host] = { + id: event.host, + name: name, + ok: event.ok, + changed: event.changed, + unreachable: event.dark, + failed: event.failures, + status: (event.failed) ? 'failed' : 'successful' + }; + }); + scope.$emit('InitialLoadComplete'); + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + } + + }); + + if (scope.removeLoadHosts) { + scope.removeLoadHosts(); + } + scope.removeLoadHosts = scope.$on('LoadHosts', function() { + if (scope.activeTask) { + + var play = scope.jobData.plays[scope.activePlay], + task, // = play.tasks[scope.activeTask], + url; + if(play){ + task = play.tasks[scope.activeTask]; + } + if (play && task) { + url = scope.job.related.job_events + '?parent=' + task.id + '&'; + url += 'event__startswith=runner&page_size=' + scope.hostResultsMaxRows + '&order=host_name,counter'; + + Rest.setUrl(url); + Rest.get() + .success(function(data) { + var idx, event, status, status_text, item, msg; + if (data.results.length > 0) { + lastEventId = data.results[0].id; + } + scope.next_host_results = data.next; + for (idx=data.results.length - 1; idx >= 0; idx--) { + event = data.results[idx]; + if (event.event === "runner_on_skipped") { + status = 'skipped'; + } + else if (event.event === "runner_on_unreachable") { + status = 'unreachable'; + } + else { + status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful'; + } + switch(status) { + case "successful": + status_text = 'OK'; + break; + case "changed": + status_text = "Changed"; + break; + case "failed": + status_text = "Failed"; + break; + case "unreachable": + status_text = "Unreachable"; + break; + case "skipped": + status_text = "Skipped"; + } + + if (event.event_data && event.event_data.res) { + item = event.event_data.res.item; + if (typeof item === "object") { + item = JSON.stringify(item); + } + } + + msg = ''; + if (event.event_data && event.event_data.res) { + if (typeof event.event_data.res === 'object') { + msg = event.event_data.res.msg; + } else { + msg = event.event_data.res; + } + } + + if (event.event !== "runner_on_no_hosts") { + task.hostResults[event.id] = { + id: event.id, + status: status, + status_text: status_text, + host_id: event.host, + task_id: event.parent, + name: event.event_data.host, + created: event.created, + msg: msg, + counter: event.counter, + item: item + }; + } + } + scope.$emit('LoadHostSummaries'); + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + } else { + scope.$emit('LoadHostSummaries'); + } + } else { + scope.$emit('LoadHostSummaries'); + } + }); + + if (scope.removeLoadTasks) { + scope.removeLoadTasks(); + } + scope.removeLoadTasks = scope.$on('LoadTasks', function() { + if (scope.activePlay) { + var play = scope.jobData.plays[scope.activePlay], url; + + if (play) { + url = scope.job.url + 'job_tasks/?event_id=' + play.id; + url += '&page_size=' + scope.tasksMaxRows + '&order=id'; + + Rest.setUrl(url); + Rest.get() + .success(function(data) { + scope.next_tasks = data.next; + if (data.results.length > 0) { + lastEventId = data.results[data.results.length - 1].id; + if (scope.liveEventProcessing) { + scope.activeTask = data.results[data.results.length - 1].id; + } + else { + scope.activeTask = data.results[0].id; + } + scope.selectedTask = scope.activeTask; + } + data.results.forEach(function(event, idx) { + var end, elapsed, status, status_text; + + if (play.firstTask === undefined || play.firstTask === null) { + play.firstTask = event.id; + play.hostCount = (event.host_count) ? event.host_count : 0; + } + + if (idx < data.results.length - 1) { + // end date = starting date of the next event + end = data.results[idx + 1].created; + } + else { + // no next event (task), get the end time of the play + if(scope.jobData.plays[scope.activePlay]){ + end = scope.jobData.plays[scope.activePlay].finished; + } + } + + if (end) { + elapsed = GetElapsed({ + start: event.created, + end: end + }); + } + else { + elapsed = '00:00:00'; + } + + status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful'; + status_text = (event.failed) ? 'Failed' : (event.changed) ? 'Changed' : 'OK'; + + play.tasks[event.id] = { + id: event.id, + play_id: scope.activePlay, + name: event.name, + status: status, + status_text: status_text, + status_tip: "Event ID: " + event.id + "
Status: " + status_text, + created: event.created, + modified: event.modified, + finished: end, + elapsed: elapsed, + hostCount: (event.host_count) ? event.host_count : 0, + reportedHosts: (event.reported_hosts) ? event.reported_hosts : 0, + successfulCount: (event.successful_count) ? event.successful_count : 0, + failedCount: (event.failed_count) ? event.failed_count : 0, + changedCount: (event.changed_count) ? event.changed_count : 0, + skippedCount: (event.skipped_count) ? event.skipped_count : 0, + unreachableCount: (event.unreachable_count) ? event.unreachable_count : 0, + taskActiveClass: '', + hostResults: {} + }; + if (play.firstTask !== event.id) { + // this is not the first task + play.tasks[event.id].hostCount = play.tasks[play.firstTask].hostCount; + } + if (play.tasks[event.id].reportedHosts === 0 && play.tasks[event.id].successfulCount === 0 && + play.tasks[event.id].failedCount === 0 && play.tasks[event.id].changedCount === 0 && + play.tasks[event.id].skippedCount === 0 && play.tasks[event.id].unreachableCount === 0) { + play.tasks[event.id].status = 'no-matching-hosts'; + play.tasks[event.id].status_text = 'No matching hosts'; + play.tasks[event.id].status_tip = "Event ID: " + event.id + "
Status: No matching hosts"; + } + play.taskCount++; + SetTaskStyles({ + task: play.tasks[event.id] + }); + }); + if (scope.activeTask && scope.jobData.plays[scope.activePlay] && scope.jobData.plays[scope.activePlay].tasks[scope.activeTask]) { + scope.jobData.plays[scope.activePlay].tasks[scope.activeTask].taskActiveClass = 'active'; + } + scope.$emit('LoadHosts'); + }) + .error(function(data) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + } else { + scope.$emit('LoadHostSummaries'); + } + } else { + scope.$emit('LoadHostSummaries'); + } + }); + + if (scope.removeLoadPlays) { + scope.removeLoadPlays(); + } + scope.removeLoadPlays = scope.$on('LoadPlays', function(e, events_url) { + + 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.jobData.plays = {}; + + var url = scope.job.url + 'job_plays/?order_by=id'; + url += '&page_size=' + scope.playsMaxRows + '&order_by=id'; + + Rest.setUrl(url); + Rest.get() + .success( function(data) { + scope.next_plays = data.next; + if (data.results.length > 0) { + lastEventId = data.results[data.results.length - 1].id; + if (scope.liveEventProcessing) { + scope.activePlay = data.results[data.results.length - 1].id; + } + else { + scope.activePlay = data.results[0].id; + } + scope.selectedPlay = scope.activePlay; + } else { + // if we are here, there are no plays and the job has failed, let the user know they may want to consult stdout + if ( (scope.job_status.status === 'failed' || scope.job_status.status === 'error') && + (!scope.job_status.explanation)) { + scope.job_status.explanation = "View stdout for more detail "; + } + } + data.results.forEach(function(event, idx) { + var status, status_text, start, end, elapsed, ok, changed, failed, skipped; + + status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful'; + status_text = (event.failed) ? 'Failed' : (event.changed) ? 'Changed' : 'OK'; + start = event.started; + + if (idx < data.results.length - 1) { + // end date = starting date of the next event + end = data.results[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.jobData.plays[event.id] = { + id: event.id, + name: event.play, + created: start, + finished: end, + status: status, + status_text: status_text, + status_tip: "Event ID: " + event.id + "
Status: " + status_text, + elapsed: elapsed, + hostCount: 0, + fistTask: null, + taskCount: 0, + playActiveClass: '', + unreachableCount: (event.unreachable_count) ? event.unreachable_count : 0, + tasks: {} + }; + + ok = (event.ok_count) ? event.ok_count : 0; + changed = (event.changed_count) ? event.changed_count : 0; + failed = (event.failed_count) ? event.failed_count : 0; + skipped = (event.skipped_count) ? event.skipped_count : 0; + + scope.jobData.plays[event.id].hostCount = ok + changed + failed + skipped; + + if (scope.jobData.plays[event.id].hostCount > 0 || event.unreachable_count > 0 || scope.job_status.status === 'successful' || + scope.job_status.status === 'failed' || scope.job_status.status === 'error' || scope.job_status.status === 'canceled') { + // force the play to be on the 'active' list + scope.jobData.plays[event.id].taskCount = 1; + } + + if (scope.jobData.plays[event.id].hostCount === 0 && event.unreachable_count === 0) { + scope.jobData.plays[event.id].status = 'no-matching-hosts'; + scope.jobData.plays[event.id].status_text = 'No matching hosts'; + scope.jobData.plays[event.id].status_tip = "Event ID: " + event.id + "
Status: No matching hosts"; + } + + scope.host_summary.ok += ok; + scope.host_summary.changed += changed; + scope.host_summary.unreachable += (event.unreachable_count) ? event.unreachable_count : 0; + scope.host_summary.failed += failed; + scope.host_summary.total = scope.host_summary.ok + scope.host_summary.changed + scope.host_summary.unreachable + + scope.host_summary.failed; + }); + if (scope.activePlay && scope.jobData.plays[scope.activePlay]) { + scope.jobData.plays[scope.activePlay].playActiveClass = 'active'; + } + scope.$emit('LoadTasks', events_url); + }) + .error( function(data, status) { + 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'); + scope.job_status = {}; + + scope.playsLoading = true; + scope.tasksLoading = true; + scope.hostResultsLoading = true; + scope.LoadHostSummaries = true; + + // Load the job record + Rest.setUrl(GetBasePath('jobs') + job_id + '/'); + Rest.get() + .success(function(data) { + var i; + 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.credential_url = (data.credential) ? '/#/credentials/' + data.credential : ''; + scope.cloud_credential_url = (data.cloud_credential) ? '/#/credentials/' + data.cloud_credential : ''; + 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.variables = ParseVariableString(data.extra_vars); + + // If we get created_by back from the server then use it. This means that the job was kicked + // off by a user and not a schedule AND that the user still exists in the system. + if(data.summary_fields.created_by) { + scope.users_url = '/#/users/' + data.summary_fields.created_by.id; + scope.created_by = data.summary_fields.created_by.username; + } + else { + if(data.summary_fields.schedule) { + // Build the Launched By link to point to the schedule that kicked it off + scope.scheduled_by = (data.summary_fields.schedule.name) ? data.summary_fields.schedule.name.toString() : ''; + } + // If there is no schedule or created_by then we can assume that the job was + // created by a deleted user + } + + if (data.summary_fields.credential) { + scope.credential_name = data.summary_fields.credential.name; + scope.credential_url = data.related.credential + .replace('api/v1', '#'); + } else { + scope.credential_name = ""; + } + + if (data.summary_fields.cloud_credential) { + scope.cloud_credential_name = data.summary_fields.cloud_credential.name; + scope.cloud_credential_url = data.related.cloud_credential + .replace('api/v1', '#'); + } else { + scope.cloud_credential_name = ""; + } + + for (i=0; i < verbosity_options.length; i++) { + if (verbosity_options[i].value === data.verbosity) { + scope.verbosity = verbosity_options[i].label; + } + } + + for (i=0; i < job_type_options.length; i++) { + if (job_type_options[i].value === data.job_type) { + scope.job_type = job_type_options[i].label; + } + } + + // 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; + scope.job_status.status_class = ((data.status === 'error' || data.status === 'failed') && data.job_explanation) ? "alert alert-danger" : ""; + scope.job_status.explanation = data.job_explanation; + if(data.result_traceback) { + scope.job_status.traceback = data.result_traceback.trim().split('\n').join('
'); + } + if (data.status === 'successful' || data.status === 'failed' || data.status === 'error' || data.status === 'canceled') { + scope.job_status.finished = data.finished; + scope.liveEventProcessing = false; + scope.pauseLiveEvents = false; + scope.waiting = false; + scope.playsLoading = false; + scope.tasksLoading = false; + scope.hostResultsLoading = false; + scope.hostSummariesLoading = false; + } + else { + scope.job_status.finished = null; + } + + 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.status_choices.every(function(status) { + if (status.value === scope.job.status) { + scope.job_status.status_label = status.label; + return false; + } + return true; + }); + //scope.setSearchAll('host'); + scope.$emit('LoadPlays', data.related.job_events); + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve job: ' + $stateParams.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 the graph needs to redraw + setTimeout(function() { DrawGraph({ scope: scope, resize: true }); }, 500); + } + }); + + scope.adjustSize = function() { + var height, ww = $(window).width(); + if (ww < 1024) { + $('#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(); + } + + scope.lessStatus = true; // close the view more status option + + // Detail table height adjusting. First, put page height back to 'normal'. + $('#plays-table-detail').height(80); + //$('#plays-table-detail').mCustomScrollbar("update"); + $('#tasks-table-detail').height(120); + //$('#tasks-table-detail').mCustomScrollbar("update"); + $('#hosts-table-detail').height(150); + //$('#hosts-table-detail').mCustomScrollbar("update"); + height = $(window).height() - $('#main-menu-container .navbar').outerHeight() - + $('#job-detail-container').outerHeight() - $('#job-detail-footer').outerHeight() - 20; + if (height > 15) { + // there's a bunch of white space at the bottom, let's use it + $('#plays-table-detail').height(80 + (height * 0.10)); + $('#tasks-table-detail').height(120 + (height * 0.20)); + $('#hosts-table-detail').height(150 + (height * 0.70)); + } + // 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)); + + function flashPlayTip() { + setTimeout(function(){ + $('#play-help').popover('show'); + },500); + setTimeout(function() { + $('#play-help').popover('hide'); + }, 5000); + } + + scope.selectPlay = function(id) { + if (scope.liveEventProcessing && !scope.pauseLiveEvents) { + scope.pauseLiveEvents = true; + flashPlayTip(); + } + SelectPlay({ + scope: scope, + id: id + }); + }; + + scope.selectTask = function(id) { + if (scope.liveEventProcessing && !scope.pauseLiveEvents) { + scope.pauseLiveEvents = true; + flashPlayTip(); + } + SelectTask({ + scope: scope, + id: id + }); + }; + + scope.togglePlayButton = function() { + if (scope.pauseLiveEvents) { + scope.pauseLiveEvents = false; + scope.$emit('LoadJob'); + } + }; + + 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': 1090, + '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.toggleLessStatus = function() { + if (!scope.lessStatus) { + $('#job-status-form .toggle-show').slideUp(200); + scope.lessStatus = true; + } + else { + $('#job-status-form .toggle-show').slideDown(200); + scope.lessStatus = false; + } + }; + + scope.filterPlayStatus = function() { + scope.search_play_status = (scope.search_play_status === 'all') ? 'failed' : 'all'; + if (!scope.liveEventProcessing || scope.pauseLiveEvents) { + LoadPlays({ + scope: scope + }); + } + }; + + scope.searchPlays = function() { + if (scope.search_play_name) { + scope.searchPlaysEnabled = false; + } + else { + scope.searchPlaysEnabled = true; + } + if (!scope.liveEventProcessing || scope.pauseLiveEvents) { + LoadPlays({ + scope: scope + }); + } + }; + + scope.searchPlaysKeyPress = function(e) { + if (e.keyCode === 13) { + scope.searchPlays(); + e.stopPropagation(); + } + }; + + scope.searchTasks = function() { + if (scope.search_task_name) { + scope.searchTasksEnabled = false; + } + else { + scope.searchTasksEnabled = true; + } + if (!scope.liveEventProcessing || scope.pauseLiveEvents) { + LoadTasks({ + scope: scope + }); + } + }; + + scope.searchTasksKeyPress = function(e) { + if (e.keyCode === 13) { + scope.searchTasks(); + e.stopPropagation(); + } + }; + + scope.searchHosts = function() { + if (scope.search_host_name) { + scope.searchHostsEnabled = false; + } + else { + scope.searchHostsEnabled = true; + } + if (!scope.liveEventProcessing || scope.pauseLiveEvents) { + LoadHosts({ + scope: scope + }); + } + }; + + scope.searchHostsKeyPress = function(e) { + if (e.keyCode === 13) { + scope.searchHosts(); + e.stopPropagation(); + } + }; + + scope.searchHostSummary = function() { + if (scope.search_host_summary_name) { + scope.searchHostSummaryEnabled = false; + } + else { + scope.searchHostSummaryEnabled = true; + } + if (!scope.liveEventProcessing || scope.pauseLiveEvents) { + ReloadHostSummaryList({ + scope: scope + }); + } + }; + + scope.searchHostSummaryKeyPress = function(e) { + if (e.keyCode === 13) { + scope.searchHostSummary(); + e.stopPropagation(); + } + }; + + scope.filterTaskStatus = function() { + scope.search_task_status = (scope.search_task_status === 'all') ? 'failed' : 'all'; + if (!scope.liveEventProcessing || scope.pauseLiveEvents) { + LoadTasks({ + scope: scope + }); + } + }; + + scope.filterHostStatus = function() { + scope.search_host_status = (scope.search_host_status === 'all') ? 'failed' : 'all'; + if (!scope.liveEventProcessing || scope.pauseLiveEvents) { + LoadHosts({ + scope: scope + }); + } + }; + + scope.filterHostSummaryStatus = function() { + scope.search_host_summary_status = (scope.search_host_summary_status === 'all') ? 'failed' : 'all'; + if (!scope.liveEventProcessing || scope.pauseLiveEvents) { + ReloadHostSummaryList({ + scope: scope + }); + } + }; + + scope.viewHostResults = function(id) { + EventViewer({ + scope: scope, + url: scope.job.related.job_events, + parent_id: scope.selectedTask, + event_id: id, + index: this.$index, + title: 'Host Event' + }); + }; + + if (scope.removeDeleteFinished) { + scope.removeDeleteFinished(); + } + scope.removeDeleteFinished = scope.$on('DeleteFinished', function(e, action) { + Wait('stop'); + if (action !== 'cancel') { + Wait('stop'); + $location.url('/jobs'); + } + }); + + scope.deleteJob = function() { + DeleteJob({ + scope: scope, + id: scope.job.id, + job: scope.job, + callback: 'DeleteFinished' + }); + }; + + scope.relaunchJob = function() { + PlaybookRun({ + scope: scope, + id: scope.job.id + }); + }; + + scope.playsScrollDown = function() { + // check for more plays when user scrolls to bottom of play list... + if (((!scope.liveEventProcessing) || (scope.liveEventProcessing && scope.pauseLiveEvents)) && scope.next_plays) { + $('#playsMoreRows').fadeIn(); + scope.playsLoading = true; + Rest.setUrl(scope.next_plays); + Rest.get() + .success( function(data) { + scope.next_plays = data.next; + data.results.forEach(function(event, idx) { + var status, status_text, start, end, elapsed, ok, changed, failed, skipped; + + status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful'; + status_text = (event.failed) ? 'Failed' : (event.changed) ? 'Changed' : 'OK'; + start = event.started; + + if (idx < data.results.length - 1) { + // end date = starting date of the next event + end = data.results[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.push({ + id: event.id, + name: event.play, + created: start, + finished: end, + status: status, + status_text: status_text, + status_tip: "Event ID: " + event.id + "
Status: " + status_text, + elapsed: elapsed, + hostCount: 0, + fistTask: null, + playActiveClass: '', + unreachableCount: (event.unreachable_count) ? event.unreachable_count : 0, + }); + + ok = (event.ok_count) ? event.ok_count : 0; + changed = (event.changed_count) ? event.changed_count : 0; + failed = (event.failed_count) ? event.failed_count : 0; + skipped = (event.skipped_count) ? event.skipped_count : 0; + + scope.plays[scope.plays.length - 1].hostCount = ok + changed + failed + skipped; + scope.playsLoading = false; + }); + $('#playsMoreRows').fadeOut(400); + }) + .error( function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + scope.next_plays + '. GET returned: ' + status }); + }); + } + }; + + scope.tasksScrollDown = function() { + // check for more tasks when user scrolls to bottom of task list... + if (((!scope.liveEventProcessing) || (scope.liveEventProcessing && scope.pauseLiveEvents)) && scope.next_tasks) { + $('#tasksMoreRows').fadeIn(); + scope.tasksLoading = true; + Rest.setUrl(scope.next_tasks); + Rest.get() + .success(function(data) { + scope.next_tasks = data.next; + data.results.forEach(function(event, idx) { + var end, elapsed, status, status_text; + if (idx < data.results.length - 1) { + // end date = starting date of the next event + end = data.results[idx + 1].created; + } + else { + // no next event (task), get the end time of the play + scope.plays.every(function(p, j) { + if (p.id === scope.selectedPlay) { + end = scope.plays[j].finished; + return false; + } + return true; + }); + } + if (end) { + elapsed = GetElapsed({ + start: event.created, + end: end + }); + } + else { + elapsed = '00:00:00'; + } + + status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful'; + status_text = (event.failed) ? 'Failed' : (event.changed) ? 'Changed' : 'OK'; + + scope.tasks.push({ + id: event.id, + play_id: scope.selectedPlay, + name: event.name, + status: status, + status_text: status_text, + status_tip: "Event ID: " + event.id + "
Status: " + status_text, + 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({ + task: scope.tasks[scope.tasks.length - 1] + }); + }); + $('#tasksMoreRows').fadeOut(400); + scope.tasksLoading = false; + }) + .error(function(data, status) { + $('#tasksMoreRows').fadeOut(400); + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + scope.next_tasks + '. GET returned: ' + status }); + }); + } + }; + + scope.hostResultsScrollDown = function() { + // check for more hosts when user scrolls to bottom of host results list... + if (((!scope.liveEventProcessing) || (scope.liveEventProcessing && scope.pauseLiveEvents)) && scope.next_host_results) { + $('#hostResultsMoreRows').fadeIn(); + scope.hostResultsLoading = true; + Rest.setUrl(scope.next_host_results); + Rest.get() + .success(function(data) { + scope.next_host_results = data.next; + data.results.forEach(function(row) { + var status, status_text, item, msg; + if (row.event === "runner_on_skipped") { + status = 'skipped'; + } + else if (row.event === "runner_on_unreachable") { + status = 'unreachable'; + } + else { + status = (row.failed) ? 'failed' : (row.changed) ? 'changed' : 'successful'; + } + switch(status) { + case "successful": + status_text = 'OK'; + break; + case "changed": + status_text = "Changed"; + break; + case "failed": + status_text = "Failed"; + break; + case "unreachable": + status_text = "Unreachable"; + break; + case "skipped": + status_text = "Skipped"; + } + if (row.event_data && row.event_data.res) { + item = row.event_data.res.item; + if (typeof item === "object") { + item = JSON.stringify(item); + } + } + msg = ''; + if (row.event_data && row.event_data.res) { + if (typeof row.event_data.res === 'object') { + msg = row.event_data.res.msg; + } else { + msg = row.event_data.res; + } + } + scope.hostResults.push({ + id: row.id, + status: status, + status_text: status_text, + 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 : '', + item: item + }); + scope.hostResultsLoading = false; + }); + $('#hostResultsMoreRows').fadeOut(400); + }) + .error(function(data, status) { + $('#hostResultsMoreRows').fadeOut(400); + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + scope.next_host_results + '. GET returned: ' + status }); + }); + } + }; + + scope.hostSummariesScrollDown = function() { + // check for more hosts when user scrolls to bottom of host summaries list... + if (((!scope.liveEventProcessing) || (scope.liveEventProcessing && scope.pauseLiveEvents)) && scope.next_host_summaries) { + scope.hostSummariesLoading = true; + Rest.setUrl(scope.next_host_summaries); + Rest.get() + .success(function(data) { + scope.next_host_summaries = data.next; + data.results.forEach(function(row) { + var name; + if (row.host_name) { + name = row.host_name; + } + else { + name = ""; + } + scope.hosts.push({ + id: row.id, + name: name, + ok: row.ok, + changed: row.changed, + unreachable: row.dark, + failed: row.failures + }); + }); + $('#hostSummariesMoreRows').fadeOut(); + scope.hostSummariesLoading = false; + }) + .error(function(data, status) { + $('#hostSummariesMoreRows').fadeOut(); + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + scope.next_host_summaries + '. GET returned: ' + status }); + }); + } + }; + + scope.hostEventsViewer = function(id, name, status) { + HostEventsViewer({ + scope: scope, + id: id, + name: name, + url: scope.job.related.job_events, + job_id: scope.job.id, + status: status + }); + }; + + scope.refresh = function(){ + $scope.$emit('LoadJob'); + }; + + scope.editHost = function(id) { + HostsEdit({ + host_scope: scope, + group_scope: null, + host_id: id, + inventory_id: scope.job.inventory, + mode: 'edit', // 'add' or 'edit' + selected_group_id: null + }); + }; + + scope.editSchedule = function() { + // We need to get the schedule's ID out of the related links + // An example of the related schedule link looks like /api/v1/schedules/5 + // where 5 is the ID we are trying to capture + var regex = /\/api\/v1\/schedules\/(\d+)\//; + var id = scope.job.related.schedule.match(regex)[1]; + if (id) { + // If we get an ID from the regular expression go ahead and open up the + // modal via the EditSchedule service + EditSchedule({ + scope: scope, + id: parseInt(id), + callback: 'SchedulesRefresh' + }); + } + }; + + // SchedulesRefresh is the callback string that we passed to the edit schedule modal + // When the modal successfully updates the schedule it will emit this event and pass + // the updated schedule object + if (scope.removeSchedulesRefresh) { + scope.removeSchedulesRefresh(); + } + scope.$on('SchedulesRefresh', function(e, data) { + if (data) { + scope.scheduled_by = data.name; + } + }); + } +]; diff --git a/awx/ui/client/src/job-detail/job-detail.factory.js b/awx/ui/client/src/job-detail/job-detail.factory.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/awx/ui/client/src/partials/job_detail.html b/awx/ui/client/src/job-detail/job-detail.partial.html similarity index 100% rename from awx/ui/client/src/partials/job_detail.html rename to awx/ui/client/src/job-detail/job-detail.partial.html diff --git a/awx/ui/client/src/job-detail/job-detail.route.js b/awx/ui/client/src/job-detail/job-detail.route.js new file mode 100644 index 0000000000..3494845759 --- /dev/null +++ b/awx/ui/client/src/job-detail/job-detail.route.js @@ -0,0 +1,35 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import {templateUrl} from '../shared/template-url/template-url.factory'; + +export default { + name: 'jobDetail', + url: '/jobs/:id', + templateUrl: templateUrl('job-detail/job-detail'), + controller: 'JobDetailController', + ncyBreadcrumb: { + parent: 'jobs', + label: "{{ job.id }} - {{ job.name }}" + }, + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }], + jobEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { + if (!$rootScope.event_socket) { + $rootScope.event_socket = Socket({ + scope: $rootScope, + endpoint: "job_events" + }); + $rootScope.event_socket.init(); + return true; + } else { + return true; + } + }] + } +}; diff --git a/awx/ui/client/src/job-detail/main.js b/awx/ui/client/src/job-detail/main.js new file mode 100644 index 0000000000..42e9cae45c --- /dev/null +++ b/awx/ui/client/src/job-detail/main.js @@ -0,0 +1,15 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import route from './job-detail.route'; +import controller from './job-detail.controller'; + +export default + angular.module('jobDetail', []) + .controller('JobDetailController', controller) + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(route); + }]); From 7dbb69dd6d3e3e79f3e3701bc58ad7c0e72ed58c Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 18 Feb 2016 10:04:12 -0500 Subject: [PATCH 03/11] Accidentally included JobDetailController in app.js during my last merge. This is now pulled in via the job detail module. --- awx/ui/client/src/app.js | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 9846c273a7..43ad6627fb 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -45,7 +45,6 @@ import adhoc from './adhoc/main'; import login from './login/main'; import activityStream from './activity-stream/main'; import standardOut from './standard-out/main'; -import {JobDetailController} from './controllers/JobDetail'; import {JobTemplatesList, JobTemplatesAdd, JobTemplatesEdit} from './controllers/JobTemplates'; import {LicenseController} from './controllers/License'; import {ScheduleEditController} from './controllers/Schedules'; From 6aeaec2b76de790d31735c9c8f05aef4359fec99 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 18 Feb 2016 11:19:27 -0500 Subject: [PATCH 04/11] Switch to `resolve` in api test fixtures to automatically resolve View's and their arguments Also changed the signature of the post/get methods to better align with libraries like `requests` --- awx/main/tests/functional/conftest.py | 29 ++++++++++++------- .../tests/functional/test_activity_streams.py | 16 ++++------ 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 7e8ae5f1a0..f9fe915d4b 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -1,5 +1,8 @@ import pytest +from django.core.urlresolvers import resolve +from django.utils.six.moves.urllib.parse import urlparse + from awx.main.models.organization import Organization from awx.main.models.ha import Instance from django.contrib.auth.models import User @@ -21,13 +24,16 @@ def user(): @pytest.fixture def post(): - def rf(_cls, _user, _url, pk=None, kwargs={}, middleware=None): - view = _cls.as_view() - request = APIRequestFactory().post(_url, kwargs, format='json') + def rf(url, data, user=None, middleware=None, **kwargs): + view, view_args, view_kwargs = resolve(urlparse(url)[2]) + if 'format' not in kwargs: + kwargs['format'] = 'json' + request = APIRequestFactory().post(url, data, **kwargs) if middleware: middleware.process_request(request) - force_authenticate(request, user=_user) - response = view(request, pk=pk) + if user: + force_authenticate(request, user=user) + response = view(request, *view_args, **view_kwargs) if middleware: middleware.process_response(request, response) return response @@ -35,13 +41,16 @@ def post(): @pytest.fixture def get(): - def rf(_cls, _user, _url, pk=None, middleware=None): - view = _cls.as_view() - request = APIRequestFactory().get(_url, format='json') + def rf(url, user=None, middleware=None, **kwargs): + view, view_args, view_kwargs = resolve(urlparse(url)[2]) + if 'format' not in kwargs: + kwargs['format'] = 'json' + request = APIRequestFactory().get(url, **kwargs) if middleware: middleware.process_request(request) - force_authenticate(request, user=_user) - response = view(request, pk=pk) + if user: + force_authenticate(request, user=user) + response = view(request, *view_args, **view_kwargs) if middleware: middleware.process_response(request, response) return response diff --git a/awx/main/tests/functional/test_activity_streams.py b/awx/main/tests/functional/test_activity_streams.py index cf2f81c9d9..64a2fcb61f 100644 --- a/awx/main/tests/functional/test_activity_streams.py +++ b/awx/main/tests/functional/test_activity_streams.py @@ -1,11 +1,6 @@ import mock import pytest -from awx.api.views import ( - ActivityStreamList, - ActivityStreamDetail, - OrganizationList, -) from awx.main.middleware import ActivityStreamMiddleware from awx.main.models.activity_stream import ActivityStream from django.core.urlresolvers import reverse @@ -17,7 +12,7 @@ def mock_feature_enabled(feature, bypass_database=None): @pytest.mark.django_db def test_get_activity_stream_list(monkeypatch, organization, get, user): url = reverse('api:activity_stream_list') - response = get(ActivityStreamList, user('admin', True), url) + response = get(url, user('admin', True)) assert response.status_code == 200 @@ -31,7 +26,7 @@ def test_basic_fields(monkeypatch, organization, get, user): aspk = activity_stream.pk url = reverse('api:activity_stream_detail', args=(aspk,)) - response = get(ActivityStreamDetail, user('admin', True), url, pk=aspk) + response = get(url, user('admin', True)) assert response.status_code == 200 assert 'related' in response.data @@ -46,8 +41,9 @@ def test_middleware_actor_added(monkeypatch, post, get, user): u = user('admin-poster', True) url = reverse('api:organization_list') - response = post(OrganizationList, u, url, - kwargs=dict(name='test-org', description='test-desc'), + response = post(url, + dict(name='test-org', description='test-desc'), + u, middleware=ActivityStreamMiddleware()) assert response.status_code == 201 @@ -55,7 +51,7 @@ def test_middleware_actor_added(monkeypatch, post, get, user): activity_stream = ActivityStream.objects.filter(organization__pk=org_id).first() url = reverse('api:activity_stream_detail', args=(activity_stream.pk,)) - response = get(ActivityStreamDetail, u, url, pk=activity_stream.pk) + response = get(url, u) assert response.status_code == 200 assert response.data['summary_fields']['actor']['username'] == 'admin-poster' From 0051464525da3f0f3bf2eb3492d9b36b2760df37 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 18 Feb 2016 11:23:31 -0500 Subject: [PATCH 05/11] Added put,head,options,delete, & patch test fixtures for api related tests --- awx/main/tests/functional/conftest.py | 85 +++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index f9fe915d4b..2f5f879193 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -56,6 +56,91 @@ def get(): return response return rf +@pytest.fixture +def put(): + def rf(url, data, user=None, middleware=None, **kwargs): + view, view_args, view_kwargs = resolve(urlparse(url)[2]) + if 'format' not in kwargs: + kwargs['format'] = 'json' + request = APIRequestFactory().put(url, data, **kwargs) + if middleware: + middleware.process_request(request) + if user: + force_authenticate(request, user=user) + response = view(request, *view_args, **view_kwargs) + if middleware: + middleware.process_response(request, response) + return response + return rf + +@pytest.fixture +def patch(): + def rf(url, data, user=None, middleware=None, **kwargs): + view, view_args, view_kwargs = resolve(urlparse(url)[2]) + if 'format' not in kwargs: + kwargs['format'] = 'json' + request = APIRequestFactory().patch(url, data, **kwargs) + if middleware: + middleware.process_request(request) + if user: + force_authenticate(request, user=user) + response = view(request, *view_args, **view_kwargs) + if middleware: + middleware.process_response(request, response) + return response + return rf + +@pytest.fixture +def delete(): + def rf(url, user=None, middleware=None, **kwargs): + view, view_args, view_kwargs = resolve(urlparse(url)[2]) + if 'format' not in kwargs: + kwargs['format'] = 'json' + request = APIRequestFactory().delete(url, **kwargs) + if middleware: + middleware.process_request(request) + if user: + force_authenticate(request, user=user) + response = view(request, *view_args, **view_kwargs) + if middleware: + middleware.process_response(request, response) + return response + return rf + +@pytest.fixture +def head(): + def rf(url, user=None, middleware=None, **kwargs): + view, view_args, view_kwargs = resolve(urlparse(url)[2]) + if 'format' not in kwargs: + kwargs['format'] = 'json' + request = APIRequestFactory().head(url, **kwargs) + if middleware: + middleware.process_request(request) + if user: + force_authenticate(request, user=user) + response = view(request, *view_args, **view_kwargs) + if middleware: + middleware.process_response(request, response) + return response + return rf + +@pytest.fixture +def options(): + def rf(url, data, user=None, middleware=None, **kwargs): + view, view_args, view_kwargs = resolve(urlparse(url)[2]) + if 'format' not in kwargs: + kwargs['format'] = 'json' + request = APIRequestFactory().options(url, data, **kwargs) + if middleware: + middleware.process_request(request) + if user: + force_authenticate(request, user=user) + response = view(request, *view_args, **view_kwargs) + if middleware: + middleware.process_response(request, response) + return response + return rf + @pytest.fixture def instance(settings): return Instance.objects.create(uuid=settings.SYSTEM_UUID, primary=True, hostname="instance.example.org") From 8f1c88e35d44b42460cd9b941fbbcb65412d355e Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Thu, 18 Feb 2016 12:26:19 -0800 Subject: [PATCH 06/11] adding less file --- awx/ui/client/src/job-detail/job-detail.block.less | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 awx/ui/client/src/job-detail/job-detail.block.less diff --git a/awx/ui/client/src/job-detail/job-detail.block.less b/awx/ui/client/src/job-detail/job-detail.block.less new file mode 100644 index 0000000000..0e7ca9f79d --- /dev/null +++ b/awx/ui/client/src/job-detail/job-detail.block.less @@ -0,0 +1,8 @@ +/** @define SetupItem */ + +@import '../shared/branding/colors.less'; +@import '../shared/branding/colors.default.less'; + +.JobDetail-results{ + background-color: #848992; +} From 95467a6b974a399960cf2bd80b523cca63b0b593 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Thu, 18 Feb 2016 19:38:49 -0800 Subject: [PATCH 07/11] Restyling of job detail results panel --- .../src/job-detail/job-detail.block.less | 56 +++++++- .../src/job-detail/job-detail.controller.js | 4 +- .../src/job-detail/job-detail.partial.html | 133 +++++++++--------- 3 files changed, 120 insertions(+), 73 deletions(-) diff --git a/awx/ui/client/src/job-detail/job-detail.block.less b/awx/ui/client/src/job-detail/job-detail.block.less index 0e7ca9f79d..e4aa02d308 100644 --- a/awx/ui/client/src/job-detail/job-detail.block.less +++ b/awx/ui/client/src/job-detail/job-detail.block.less @@ -3,6 +3,58 @@ @import '../shared/branding/colors.less'; @import '../shared/branding/colors.default.less'; -.JobDetail-results{ - background-color: #848992; +.JobDetail-panelHeader{ + height: 50px; + display: flex; +} + +.JobDetail-panelHeaderText{ + color: #848992; + flex: 1 0 auto; + font-size: 14px; + font-weight: bold; + margin-right: 10px; + text-transform: uppercase; +} + +.JobDetail-panelHeaderText:hover{ + color: #848992; + font-size: 14px; + font-weight: bold; + margin-right: 10px; + text-transform: uppercase; +} + +.JobDetail-expandArrow{ + color: #848992; + font-size: 14px; + font-weight: bold; + margin-right: 10px; + text-transform: uppercase; + margin-left: 5px; +} + +.JobDetail-resultsDetails{ + display: flex; + flex-wrap: wrap; + flex-direction: row; +} + +.JobDetail-resultRow{ + flex: 1 0 auto; + width: 50%; + display: flex; +} + +.JobDetail-resultRow label{ + color: #848992; + font-size: 14px; + font-weight: normal!important; + // width: 40%; + flex: 1 0 auto; +} + +.JobDetail-resultRowText{ + width: 40%; + flex: 1 0 auto; } diff --git a/awx/ui/client/src/job-detail/job-detail.controller.js b/awx/ui/client/src/job-detail/job-detail.controller.js index 604179d622..6432e68849 100644 --- a/awx/ui/client/src/job-detail/job-detail.controller.js +++ b/awx/ui/client/src/job-detail/job-detail.controller.js @@ -200,7 +200,7 @@ export default scope.haltEventQueue = false; scope.processing = false; - scope.lessStatus = true; + scope.lessStatus = false; scope.host_summary = {}; scope.host_summary.ok = 0; @@ -854,7 +854,7 @@ export default $('#job-summary-container').show(); } - scope.lessStatus = true; // close the view more status option + scope.lessStatus = false; // close the view more status option // Detail table height adjusting. First, put page height back to 'normal'. $('#plays-table-detail').height(80); diff --git a/awx/ui/client/src/job-detail/job-detail.partial.html b/awx/ui/client/src/job-detail/job-detail.partial.html index e4aba4ec3b..fc174f182b 100644 --- a/awx/ui/client/src/job-detail/job-detail.partial.html +++ b/awx/ui/client/src/job-detail/job-detail.partial.html @@ -1,29 +1,25 @@
-
-
-
- -
{{ job_status.status_label }}
- -
- - - - - - - - -
+
+ -
- -
+
+
+ +
{{ job_status.status_label }}
+
+
@@ -44,117 +40,116 @@
-
+
-
+
-
- -
-
Started  {{ job_status.started | date:'MM/dd/yy HH:mm:ss' }}
-
Finished  {{ job_status.finished | date:'MM/dd/yy HH:mm:ss' }}
-
Elapsed  {{ job_status.elapsed }}
-
+
+ +
{{ job_status.started | date:'MM/dd/yy HH:mm:ss' }}
-