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);
+ }]);