Merge pull request #1467 from leigh-johnson/FinishJobDetails

Job Detail Tracking - finish outstanding tasks/bugs
This commit is contained in:
Leigh
2016-04-14 14:02:36 -04:00
24 changed files with 734 additions and 1100 deletions

View File

@@ -503,7 +503,7 @@
} }
#graph-section svg{ #graph-section svg{
margin-top: 15px; margin: 0 auto;
} }
path.slice{ path.slice{

View File

@@ -40,9 +40,9 @@ export default
angular.module('JobDetailHelper', ['Utilities', 'RestServices', 'ModalDialog']) angular.module('JobDetailHelper', ['Utilities', 'RestServices', 'ModalDialog'])
.factory('DigestEvent', ['$rootScope', '$log', 'UpdatePlayStatus', 'UpdateHostStatus', 'AddHostResult', .factory('DigestEvent', ['$rootScope', '$log', 'UpdatePlayStatus', 'UpdateHostStatus', 'AddHostResult',
'GetElapsed', 'UpdateTaskStatus', 'DrawGraph', 'LoadHostSummary', 'JobIsFinished', 'AddNewTask', 'AddNewPlay', 'GetElapsed', 'UpdateTaskStatus', 'JobIsFinished', 'AddNewTask', 'AddNewPlay',
function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, GetElapsed, function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, GetElapsed,
UpdateTaskStatus, DrawGraph, LoadHostSummary, JobIsFinished, AddNewTask, AddNewPlay) { UpdateTaskStatus, JobIsFinished, AddNewTask, AddNewPlay, longDateFilter) {
return function(params) { return function(params) {
var scope = params.scope, var scope = params.scope,
@@ -185,7 +185,7 @@ export default
}; };
}]) }])
.factory('GetElapsed', [ function() { .factory('GetElapsed', [function() {
return function(params) { return function(params) {
var start = params.start, var start = params.start,
end = params.end, end = params.end,
@@ -299,7 +299,7 @@ export default
}; };
}]) }])
.factory('AddNewTask', ['DrawGraph', 'UpdatePlayStatus', 'SetActivePlay', 'SetActiveTask', function(DrawGraph, UpdatePlayStatus, SetActivePlay, SetActiveTask) { .factory('AddNewTask', ['UpdatePlayStatus', 'SetActivePlay', 'SetActiveTask', function(UpdatePlayStatus, SetActivePlay, SetActiveTask) {
return function(params) { return function(params) {
var scope = params.scope, var scope = params.scope,
event = params.event, event = params.event,
@@ -363,17 +363,17 @@ export default
scope.job_status.status = 'failed'; scope.job_status.status = 'failed';
} }
if (JobIsFinished(scope) && !Empty(modified)) { if (JobIsFinished(scope) && !Empty(modified)) {
scope.job_status.finished = modified; scope.job_status.finished = longDateFilter(modified)
} }
if (!Empty(started) && Empty(scope.job_status.started)) { if (!Empty(started) && Empty(scope.job_status.started)) {
scope.job_status.started = started; scope.job_status.started = longDateFilter(modified)
} }
if (!Empty(scope.job_status.finished) && !Empty(scope.job_status.started)) { if (!Empty(scope.job_status.finished) && !Empty(scope.job_status.started)) {
scope.job_status.elapsed = GetElapsed({ scope.job_status.elapsed = GetElapsed({
start: scope.job_status.started, start: started,
end: scope.job_status.finished end: finished
}); });
} }
}; };
}]) }])
@@ -488,15 +488,6 @@ export default
counter = params.counter, counter = params.counter,
h, host; h, host;
/*
scope.host_summary.ok += (status === 'successful') ? 1 : 0;
scope.host_summary.changed += (status === 'changed') ? 1 : 0;
scope.host_summary.unreachable += (status === 'unreachable') ? 1 : 0;
scope.host_summary.failed += (status === 'failed') ? 1 : 0;
scope.host_summary.total = scope.host_summary.ok + scope.host_summary.changed + scope.host_summary.unreachable +
scope.host_summary.failed;
*/
if (scope.jobData.hostSummaries[host_id] !== undefined) { if (scope.jobData.hostSummaries[host_id] !== undefined) {
scope.jobData.hostSummaries[host_id].ok += (status === 'successful') ? 1 : 0; scope.jobData.hostSummaries[host_id].ok += (status === 'successful') ? 1 : 0;
scope.jobData.hostSummaries[host_id].changed += (status === 'changed') ? 1 : 0; scope.jobData.hostSummaries[host_id].changed += (status === 'changed') ? 1 : 0;
@@ -517,29 +508,6 @@ export default
status: (status === 'failed' || status === 'unreachable') ? 'failed' : 'successful' status: (status === 'failed' || status === 'unreachable') ? 'failed' : 'successful'
}; };
} }
scope.host_summary.ok = 0;
scope.host_summary.changed = 0;
scope.host_summary.unreachable = 0;
scope.host_summary.failed = 0;
for (h in scope.jobData.hostSummaries) {
host = scope.jobData.hostSummaries[h];
if (host.ok > 0 && host.failed === 0 && host.unreachable === 0 && host.changed === 0) {
scope.host_summary.ok++;
}
if (host.changed > 0 && host.failed === 0 && host.unreachable === 0) {
scope.host_summary.changed++;
}
if (host.failed > 0) {
scope.host_summary.failed++;
}
if (host.unreachable > 0) {
scope.host_summary.unreachable++;
}
}
scope.host_summary.total = scope.host_summary.ok + scope.host_summary.changed + scope.host_summary.unreachable +
scope.host_summary.failed;
UpdateTaskStatus({ UpdateTaskStatus({
scope: scope, scope: scope,
task_id: task_id, task_id: task_id,
@@ -822,7 +790,6 @@ export default
url += (scope.search_task_name) ? '&task__icontains=' + scope.search_task_name : ''; url += (scope.search_task_name) ? '&task__icontains=' + scope.search_task_name : '';
url += (scope.search_task_status === 'failed') ? '&failed=true' : ''; url += (scope.search_task_status === 'failed') ? '&failed=true' : '';
url += '&page_size=' + scope.tasksMaxRows + '&order=id'; url += '&page_size=' + scope.tasksMaxRows + '&order=id';
scope.plays.every(function(p, idx) { scope.plays.every(function(p, idx) {
if (p.id === scope.selectedPlay) { if (p.id === scope.selectedPlay) {
play = scope.plays[idx]; play = scope.plays[idx];
@@ -931,7 +898,7 @@ export default
}]) }])
// Call when the selected task needs to change // Call when the selected task needs to change
.factory('SelectTask', ['LoadHosts', function(LoadHosts) { .factory('SelectTask', ['JobDetailService', function(JobDetailService) {
return function(params) { return function(params) {
var scope = params.scope, var scope = params.scope,
id = params.id, id = params.id,
@@ -946,239 +913,52 @@ export default
scope.tasks[idx].taskActiveClass = ''; scope.tasks[idx].taskActiveClass = '';
} }
}); });
var params = {
LoadHosts({ parent: scope.selectedTask,
scope: scope, event__startswith: 'runner',
callback: callback, page_size: scope.hostResultsMaxRows,
clear: true order: 'host_name,counter',
};
JobDetailService.getRelatedJobEvents(scope.job.id, params).success(function(res){
scope.hostResults = JobDetailService.processHostEvents(res.results)
scope.hostResultsLoading = false;
}); });
}; };
}]) }])
// Refresh the list of hosts
.factory('LoadHosts', ['Rest', 'ProcessErrors', function(Rest, ProcessErrors) {
return function(params) {
var scope = params.scope,
callback = params.callback,
url;
scope.hostResults = [];
if (scope.selectedTask) {
// If we have a selected task, then get the list of hosts
url = scope.job.related.job_events + '?parent=' + scope.selectedTask + '&';
url += (scope.search_host_name) ? 'host__name__icontains=' + scope.search_host_name + '&' : '';
url += (scope.search_host_status === 'failed') ? 'failed=true&' : '';
url += 'event__startswith=runner&page_size=' + scope.hostResultsMaxRows + '&order=host_name,counter';
scope.hostResultsLoading = true;
Rest.setUrl(url);
Rest.get()
.success(function(data) {
scope.next_host_results = data.next;
scope.hostResults = [];
data.results.forEach(function(event) {
var status, status_text, item, msg;
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);
item = item.replace(/\"/g,'').replace(/:/g,': ').replace(/,/g,', ');
}
}
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") {
scope.hostResults.push({
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,
item: item
});
}
});
scope.hostResultsLoading = false;
if (callback) {
scope.$emit(callback);
}
})
.error(function(data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
}
else {
if (callback) {
scope.$emit(callback);
}
//$('#hosts-table-detail').mCustomScrollbar("update");
}
};
}])
// Refresh the list of hosts in the hosts summary section
.factory('ReloadHostSummaryList', ['Rest', 'ProcessErrors', function(Rest, ProcessErrors) {
return function(params) {
var scope = params.scope,
callback = params.callback,
url;
url = scope.job.related.job_host_summaries + '?';
url += (scope.search_host_summary_name) ? 'host_name__icontains=' + scope.search_host_summary_name + '&': '';
url += (scope.search_host_summary_status === 'failed') ? 'failed=true&' : '';
url += '&page_size=' + scope.hostSummariesMaxRows + '&order=host_name';
scope.hosts = [];
scope.hostSummariesLoading = true;
Rest.setUrl(url);
Rest.get()
.success(function(data) {
scope.next_host_summaries = data.next;
scope.hosts = [];
data.results.forEach(function(event) {
var name;
if (event.host_name) {
name = event.host_name;
}
else {
name = "<deleted host>";
}
scope.hosts.push({
id: name,
name: event.host_name,
ok: event.ok,
changed: event.changed,
unreachable: event.dark,
failed: event.failures,
status: (event.failed) ? 'failed' : 'successful'
});
});
scope.hostSummariesLoading = false;
if (callback) {
scope.$emit(callback);
}
})
.error(function(data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
};
}])
.factory('LoadHostSummary', [ function() {
return function(params) {
var scope = params.scope,
data = params.data,
host;
scope.host_summary.ok = 0;
for (host in data.ok) {
if (!data.changed[host] && !data.dark[host] && !data.failures[host]) {
scope.host_summary.ok += 1;
}
}
scope.host_summary.changed = 0;
for (host in data.changed) {
if (!data.dark[host] && !data.failures[host]) {
scope.host_summary.changed += 1;
}
}
scope.host_summary.unreachable = 0;
for (host in data.dark) {
scope.host_summary.unreachable += 1;
}
scope.host_summary.failed = 0;
for (host in data.failures) {
scope.host_summary.failed += 1;
}
scope.host_summary.total = scope.host_summary.ok + scope.host_summary.changed +
scope.host_summary.unreachable + scope.host_summary.failed;
};
}])
.factory('DrawGraph', ['DonutChart', function(DonutChart) { .factory('DrawGraph', ['DonutChart', function(DonutChart) {
return function(params) { return function(params) {
var scope = params.scope, var count = params.count,
graph_data = []; graph_data = [];
// Ready the data // Ready the data
if (scope.host_summary.ok) { if (count.ok.length > 0) {
graph_data.push({ graph_data.push({
label: 'OK', label: 'OK',
value: scope.host_summary.ok, value: count.ok.length,
color: '#60D66F' color: '#60D66F'
}); });
} }
if (scope.host_summary.changed) { if (count.changed.length > 0) {
graph_data.push({ graph_data.push({
label: 'CHANGED', label: 'CHANGED',
value: scope.host_summary.changed, value: count.changed.length,
color: '#FF9900' color: '#FF9900'
}); });
} }
if (scope.host_summary.unreachable) { if (count.unreachable.length > 0) {
graph_data.push({ graph_data.push({
label: 'UNREACHABLE', label: 'UNREACHABLE',
value: scope.host_summary.unreachable, value: count.unreachable.length,
color: '#FF0000' color: '#FF0000'
}); });
} }
if (scope.host_summary.failed) { if (count.failures.length > 0) {
graph_data.push({ graph_data.push({
label: 'FAILED', label: 'FAILED',
value: scope.host_summary.failed, value: count.failures.length,
color: '#ff5850' color: '#ff5850'
}); });
} }
scope.graph_data = graph_data;
var total_count = 0, gd_obj;
for (gd_obj in graph_data) {
total_count += graph_data[gd_obj].value;
}
scope.total_count_for_graph = total_count;
DonutChart({ DonutChart({
data: graph_data data: graph_data
}); });
@@ -1220,44 +1000,42 @@ export default
"font-family": 'Open Sans', "font-family": 'Open Sans',
"font-style": "normal", "font-style": "normal",
"font-weight":400, "font-weight":400,
"src": "url(/static/assets/OpenSans-Regular.ttf)" "src": "url(/static/assets/OpenSans-Regular.ttf)",
"width": 500,
"height": 300,
}); });
d3.select(element.find(".nv-label text")[0]) d3.select(element.find(".nv-label text")[0])
.attr("class", "DashboardGraphs-hostStatusLabel--successful") .attr("class", "HostSummary-graph--successful")
.style({ .style({
"font-family": 'Open Sans', "font-family": 'Open Sans',
"text-anchor": "start",
"font-size": "16px", "font-size": "16px",
"text-transform" : "uppercase", "text-transform" : "uppercase",
"fill" : colors[0], "fill" : colors[0],
"src": "url(/static/assets/OpenSans-Regular.ttf)" "src": "url(/static/assets/OpenSans-Regular.ttf)"
}); });
d3.select(element.find(".nv-label text")[1]) d3.select(element.find(".nv-label text")[1])
.attr("class", "DashboardGraphs-hostStatusLabel--failed") .attr("class", "HostSummary-graph--changed")
.style({ .style({
"font-family": 'Open Sans', "font-family": 'Open Sans',
"text-anchor" : "end !imporant",
"font-size": "16px", "font-size": "16px",
"text-transform" : "uppercase", "text-transform" : "uppercase",
"fill" : colors[1], "fill" : colors[1],
"src": "url(/static/assets/OpenSans-Regular.ttf)" "src": "url(/static/assets/OpenSans-Regular.ttf)"
}); });
d3.select(element.find(".nv-label text")[2]) d3.select(element.find(".nv-label text")[2])
.attr("class", "DashboardGraphs-hostStatusLabel--successful") .attr("class", "HostSummary-graph--failed")
.style({ .style({
"font-family": 'Open Sans', "font-family": 'Open Sans',
"text-anchor" : "end !imporant",
"font-size": "16px", "font-size": "16px",
"text-transform" : "uppercase", "text-transform" : "uppercase",
"fill" : colors[2], "fill" : colors[2],
"src": "url(/static/assets/OpenSans-Regular.ttf)" "src": "url(/static/assets/OpenSans-Regular.ttf)"
}); });
d3.select(element.find(".nv-label text")[3]) d3.select(element.find(".nv-label text")[3])
.attr("class", "DashboardGraphs-hostStatusLabel--failed") .attr("class", "HostSummary-graph--unreachable")
.style({ .style({
"font-family": 'Open Sans', "font-family": 'Open Sans',
"text-anchor" : "end !imporant",
"font-size": "16px", "font-size": "16px",
"text-transform" : "uppercase", "text-transform" : "uppercase",
"fill" : colors[3], "fill" : colors[3],
@@ -1274,7 +1052,8 @@ export default
idx = 0, idx = 0,
result = [], result = [],
newKeys = [], newKeys = [],
plays = JSON.parse(JSON.stringify(scope.jobData.plays)), //plays = JSON.parse(JSON.stringify(scope.jobData.plays)),
plays = scope.jobData.plays,
filteredListX = [], filteredListX = [],
filteredListA = [], filteredListA = [],
filteredListB = [], filteredListB = [],
@@ -1363,7 +1142,8 @@ export default
if (scope.activePlay && scope.jobData.plays[scope.activePlay]) { if (scope.activePlay && scope.jobData.plays[scope.activePlay]) {
tasks = JSON.parse(JSON.stringify(scope.jobData.plays[scope.activePlay].tasks)); //tasks = JSON.parse(JSON.stringify(scope.jobData.plays[scope.activePlay].tasks));
tasks = scope.jobData.plays[scope.activePlay].tasks;
// Only draw tasks that are in the 'active' list // Only draw tasks that are in the 'active' list
for (key in tasks) { for (key in tasks) {
@@ -1437,7 +1217,8 @@ export default
if (scope.activePlay && scope.activeTask && scope.jobData.plays[scope.activePlay] && if (scope.activePlay && scope.activeTask && scope.jobData.plays[scope.activePlay] &&
scope.jobData.plays[scope.activePlay].tasks[scope.activeTask]) { scope.jobData.plays[scope.activePlay].tasks[scope.activeTask]) {
hostResults = JSON.parse(JSON.stringify(scope.jobData.plays[scope.activePlay].tasks[scope.activeTask].hostResults)); //hostResults = JSON.parse(JSON.stringify(scope.jobData.plays[scope.activePlay].tasks[scope.activeTask].hostResults));
hostResults = scope.jobData.plays[scope.activePlay].tasks[scope.activeTask].hostResults;
if (scope.search_host_name) { if (scope.search_host_name) {
for (key in hostResults) { for (key in hostResults) {
@@ -1498,88 +1279,20 @@ export default
}; };
}]) }])
.factory('DrawHostSummaries', [ function() { .factory('UpdateDOM', ['DrawPlays', 'DrawTasks', 'DrawHostResults',
return function(params) { function(DrawPlays, DrawTasks, DrawHostResults) {
var scope = params.scope,
result = [],
filteredListA = [],
filteredListB = [],
idx = 0,
hostSummaries,
key,
keys = Object.keys(scope.jobData.hostSummaries);
if (keys.length > 0) {
hostSummaries = JSON.parse(JSON.stringify(scope.jobData.hostSummaries));
if (scope.search_host_summary_name) {
for (key in hostSummaries) {
if (hostSummaries[key].name.indexOf(scope.search_host_summary_name) > 0) {
filteredListA[key] = hostSummaries[key];
}
}
}
else {
filteredListA = hostSummaries;
}
if (scope.search_host_summary_status === 'failed') {
for (key in filteredListA) {
if (filteredListA[key].status === 'failed' || filteredListA[key].status === 'unreachable') {
filteredListB[key] = filteredListA[key];
}
}
}
else {
filteredListB = filteredListA;
}
keys = Object.keys(filteredListB);
keys.sort(function(a,b) {
if (filteredListB[a].name > filteredListB[b].name) {
return 1;
}
if (filteredListB[a].name < filteredListB[b].name) {
return -1;
}
// a must be equal to b
return 0;
});
while (idx < keys.length && result.length < scope.hostSummariesMaxRows) {
result.push(filteredListB[keys[idx]]);
idx++;
}
}
setTimeout( function() {
scope.$apply( function() {
scope.hosts = result;
});
});
};
}])
.factory('UpdateDOM', ['DrawPlays', 'DrawTasks', 'DrawHostResults', 'DrawHostSummaries', 'DrawGraph',
function(DrawPlays, DrawTasks, DrawHostResults, DrawHostSummaries, DrawGraph) {
return function(params) { return function(params) {
var scope = params.scope; var scope = params.scope;
if (!scope.pauseLiveEvents) { if (!scope.pauseLiveEvents) {
DrawPlays({ scope: scope }); DrawPlays({ scope: scope });
DrawTasks({ scope: scope }); DrawTasks({ scope: scope });
DrawHostResults({ scope: scope }); DrawHostResults({ scope: scope });
} }
DrawHostSummaries({ scope: scope });
setTimeout(function() { setTimeout(function() {
scope.playsLoading = false; scope.playsLoading = false;
scope.tasksLoading = false; scope.tasksLoading = false;
scope.hostResultsLoading = false; scope.hostResultsLoading = false;
scope.LoadHostSummaries = false;
},100); },100);
if (scope.host_summary.total > 0) {
DrawGraph({ scope: scope, resize: true });
}
}; };
}]); }]);

View File

@@ -752,9 +752,9 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm,
* *
*/ */
// Submit request to run a playbook // Submit request to run a playbook
.factory('PlaybookRun', ['$location','$stateParams', 'LaunchJob', 'PromptForPasswords', 'Rest', 'GetBasePath', 'Alert', 'ProcessErrors', 'Wait', 'Empty', .factory('PlaybookRun', ['$location', '$state', '$stateParams', 'LaunchJob', 'PromptForPasswords', 'Rest', 'GetBasePath', 'Alert', 'ProcessErrors', 'Wait', 'Empty',
'PromptForCredential', 'PromptForVars', 'PromptForSurvey' , 'CreateLaunchDialog', 'PromptForCredential', 'PromptForVars', 'PromptForSurvey' , 'CreateLaunchDialog',
function ($location, $stateParams, LaunchJob, PromptForPasswords, Rest, GetBasePath, Alert, ProcessErrors, Wait, Empty, function ($location, $state, $stateParams, LaunchJob, PromptForPasswords, Rest, GetBasePath, Alert, ProcessErrors, Wait, Empty,
PromptForCredential, PromptForVars, PromptForSurvey, CreateLaunchDialog) { PromptForCredential, PromptForVars, PromptForSurvey, CreateLaunchDialog) {
return function (params) { return function (params) {
var //parent_scope = params.scope, var //parent_scope = params.scope,
@@ -803,7 +803,8 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm,
var job = data.job || data.system_job; var job = data.job || data.system_job;
if((scope.portalMode===false || scope.$parent.portalMode===false ) && Empty(data.system_job) || if((scope.portalMode===false || scope.$parent.portalMode===false ) && Empty(data.system_job) ||
(base === 'home')){ (base === 'home')){
$location.path('/jobs/' + job); // use $state.go with reload: true option to re-instantiate sockets in
$state.go('jobDetail', {id: job}, {reload: true})
} }
}); });

View File

@@ -75,7 +75,7 @@ export default
scope.viewJobDetails = function(job) { scope.viewJobDetails = function(job) {
var goToJobDetails = function(state) { var goToJobDetails = function(state) {
$state.go(state, {id: job.id}); $state.go(state, {id: job.id}, {reload:true});
} }
switch(job.type) { switch(job.type) {

View File

@@ -1,49 +1,49 @@
<div class="HostEvent-details--left"> <div class="HostEvent-details--left">
<div class="HostEvent-field"> <div class="HostEvent-field">
<div class="HostEvent-title">EVENT</div> <div class="HostEvent-title">EVENT</div>
<span class="HostEvent-field--content"></span> <span class="HostEvent-field--content"></span>
</div> </div>
<div class="HostEvent-field"> <div class="HostEvent-field">
<span class="HostEvent-field--label">HOST</span> <span class="HostEvent-field--label">HOST</span>
<span class="HostEvent-field--content"> <span class="HostEvent-field--content">
<a ui-sref="jobDetail.host-events({hostName: event.host_name})">{{event.host_name || "No result found"}}</a></span> <a ui-sref="jobDetail.host-events({hostName: event.host_name})">{{event.host_name || "No result found"}}</a></span>
</div> </div>
<div class="HostEvent-field"> <div class="HostEvent-field">
<span class="HostEvent-field--label">STATUS</span> <span class="HostEvent-field--label">STATUS</span>
<span class="HostEvent-field--content"> <span class="HostEvent-field--content">
<a class="HostEvents-status"> <a class="HostEvents-status">
<i class="fa fa-circle" ng-class="processEventStatus"></i> <i class="fa fa-circle" ng-class="processEventStatus(event).class"></i>
</a> </a>
{{event.status || "No result found"}} {{processEventStatus(event).status || "No result found"}}
</span> </span>
</div> </div>
<div class="HostEvent-field"> <div class="HostEvent-field">
<span class="HostEvent-field--label">ID</span> <span class="HostEvent-field--label">ID</span>
<span class="HostEvent-field--content">{{event.id || "No result found"}}</span> <span class="HostEvent-field--content">{{event.id || "No result found"}}</span>
</div> </div>
<div class="HostEvent-field"> <div class="HostEvent-field">
<span class="HostEvent-field--label">CREATED</span> <span class="HostEvent-field--label">CREATED</span>
<span class="HostEvent-field--content">{{event.created || "No result found"}}</span> <span class="HostEvent-field--content">{{event.created || "No result found"}}</span>
</div> </div>
<div class="HostEvent-field"> <div class="HostEvent-field">
<span class="HostEvent-field--label">PLAY</span> <span class="HostEvent-field--label">PLAY</span>
<span class="HostEvent-field--content">{{event.play || "No result found"}}</span> <span class="HostEvent-field--content">{{event.play || "No result found"}}</span>
</div> </div>
<div class="HostEvent-field"> <div class="HostEvent-field">
<span class="HostEvent-field--label">TASK</span> <span class="HostEvent-field--label">TASK</span>
<span class="HostEvent-field--content">{{event.task || "No result found"}}</span> <span class="HostEvent-field--content">{{event.task || "No result found"}}</span>
</div> </div>
<div class="HostEvent-field"> <div class="HostEvent-field">
<span class="HostEvent-field--label">MODULE</span> <span class="HostEvent-field--label">MODULE</span>
<span class="HostEvent-field--content">{{event.event_data.res.invocation.module_name || "No result found"}}</span> <span class="HostEvent-field--content">{{event.event_data.res.invocation.module_name || "No result found"}}</span>
</div> </div>
</div> </div>
<div class="HostEvent-details--right" ng-show="event.event_data.res"> <div class="HostEvent-details--right" ng-show="event.event_data.res">
<div class="HostEvent-title">RESULTS</div> <div class="HostEvent-title">RESULTS</div>
<!-- discard any objects in the ansible response until we decide to flatten them --> <!-- discard any objects in the ansible response until we decide to flatten them -->
<div class="HostEvent-field" ng-repeat="(key, value) in results = event.event_data.res track by $index" ng-if="processResults(value)"> <div class="HostEvent-field" ng-repeat="(key, value) in results = event.event_data.res track by $index" ng-if="processResults(value)">
<span class="HostEvent-field--label">{{key}}</span> <span class="HostEvent-field--label">{{key}}</span>
<span class="HostEvent-field--content">{{value}}</span> <span class="HostEvent-field--content">{{value}}</span>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
<div id="HostEvent" class="HostEvent modal fade" role="dialog"> <div id="HostEvent" class="HostEvent modal" role="dialog">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<!-- modal body --> <!-- modal body -->
@@ -14,8 +14,7 @@
<!-- view navigation buttons --> <!-- view navigation buttons -->
<button ui-sref="jobDetail.host-event.details" type="button" class="btn btn-sm btn-default" >Details</button> <button ui-sref="jobDetail.host-event.details" type="button" class="btn btn-sm btn-default" >Details</button>
<button ui-sref="jobDetail.host-event.json" type="button" class="btn btn-sm btn-default ">JSON</button> <button ui-sref="jobDetail.host-event.json" type="button" class="btn btn-sm btn-default ">JSON</button>
<button ng-show="event.stdout" ui-sref="jobDetail.host-event.stdout" type="button" class="btn btn-sm btn-default ">Standard Out</button> <button ng-show="stdout" ui-sref="jobDetail.host-event.stdout" type="button" class="btn btn-sm btn-default ">Standard Out</button>
<button ng-show="event.timing" ui-sref="jobDetail.host-event.timing" type="button" class="btn btn-sm btn-default ">Timing</button>
</div> </div>
<div class="HostEvent-body"> <div class="HostEvent-body">

View File

@@ -1,13 +1,2 @@
<div class="EventHost-stdoutPanel Panel"> <textarea id="HostEvent-stdout" class="HostEvent-stdout">
<div class="StandardOut-panelHeader"> </textarea>
<div class="StandardOut-panelHeaderText">STANDARD OUT</div>
<div class="StandardOut-panelHeaderActions">
<a href="/api/v1/jobs/{{ job.id }}/stdout?format=txt_download&token={{ token }}">
<button class="StandardOut-actionButton" aw-tool-tip="Download Output" data-placement="top">
<i class="fa fa-download"></i>
</button>
</a>
</div>
</div>
<standard-out-log stdout-endpoint="event._stdout"></standard-out-log>
</div>

View File

@@ -49,6 +49,7 @@
width: 80px; width: 80px;
margin-right: 20px; margin-right: 20px;
font-size: 12px; font-size: 12px;
word-wrap: break-word;
} }
.HostEvent-field{ .HostEvent-field{
.OnePlusTwo-left--detailsRow; .OnePlusTwo-left--detailsRow;
@@ -58,12 +59,17 @@
} }
.HostEvent-details--left, .HostEvent-details--right{ .HostEvent-details--left, .HostEvent-details--right{
vertical-align:top; vertical-align:top;
width:270px; width:265px;
display: inline-block; display: inline-block;
}
.HostEvent-details--left{
margin-right: 10px;
} }
.HostEvent-details--right{ .HostEvent-details--right{
.HostEvent-field--label{ .HostEvent-field--label{
width: 170px; width: auto;
} }
} .HostEvent-field--content{
text-align: right;
}
}

View File

@@ -4,68 +4,76 @@
* All Rights Reserved * All Rights Reserved
*************************************************/ *************************************************/
export default export default
['$stateParams', '$scope', '$state', 'Wait', 'JobDetailService', 'moment', 'event', ['$stateParams', '$scope', '$state', 'Wait', 'JobDetailService', 'event',
function($stateParams, $scope, $state, Wait, JobDetailService, moment, event){ function($stateParams, $scope, $state, Wait, JobDetailService, event){
// Avoid rendering objects in the details fieldset
// ng-if="processResults(value)" via host-event-details.partial.html
$scope.processResults = function(value){
if (typeof value == 'object'){return false}
else {return true}
};
var codeMirror = function(){ $scope.processEventStatus = JobDetailService.processEventStatus;
var el = $('#HostEvent-json')[0]; $scope.hostResults = [];
var editor = CodeMirror.fromTextArea(el, { // Avoid rendering objects in the details fieldset
lineNumbers: true, // ng-if="processResults(value)" via host-event-details.partial.html
mode: {name: "javascript", json: true} $scope.processResults = function(value){
}); if (typeof value == 'object'){return false;}
editor.getDoc().setValue(JSON.stringify($scope.json, null, 4)); else {return true;}
}; };
$scope.getActiveHostIndex = function(){ var codeMirror = function(el, json){
var result = $scope.hostResults.filter(function( obj ) { var container = $(el)[0];
return obj.id == $scope.event.id; var editor = CodeMirror.fromTextArea(container, {
}); lineNumbers: true,
return $scope.hostResults.indexOf(result[0]) mode: {name: "javascript", json: true}
}; });
editor.setSize("100%", 300)
editor.getDoc().setValue(JSON.stringify(json, null, 4));
};
$scope.showPrev = function(){ $scope.getActiveHostIndex = function(){
return $scope.getActiveHostIndex() != 0 var result = $scope.hostResults.filter(function( obj ) {
}; return obj.id == $scope.event.id;
});
return $scope.hostResults.indexOf(result[0]);
};
$scope.showNext = function(){ $scope.showPrev = function(){
return $scope.getActiveHostIndex() < $scope.hostResults.indexOf($scope.hostResults[$scope.hostResults.length - 1]) return $scope.getActiveHostIndex() != 0;
}; };
$scope.goNext = function(){ $scope.showNext = function(){
var index = $scope.getActiveHostIndex() + 1; return $scope.getActiveHostIndex() < $scope.hostResults.indexOf($scope.hostResults[$scope.hostResults.length - 1]);
var id = $scope.hostResults[index].id; };
$state.go('jobDetail.host-event.details', {eventId: id})
};
$scope.goPrev = function(){ $scope.goNext = function(){
var index = $scope.getActiveHostIndex() - 1; var index = $scope.getActiveHostIndex() + 1;
var id = $scope.hostResults[index].id; var id = $scope.hostResults[index].id;
$state.go('jobDetail.host-event.details', {eventId: id}) $state.go('jobDetail.host-event.details', {eventId: id});
}; };
var init = function(){ $scope.goPrev = function(){
$scope.event = event.data.results[0]; var index = $scope.getActiveHostIndex() - 1;
$scope.event.created = moment($scope.event.created).format(); var id = $scope.hostResults[index].id;
$scope.processEventStatus = JobDetailService.processEventStatus($scope.event); $state.go('jobDetail.host-event.details', {eventId: id});
$scope.hostResults = $stateParams.hostResults; };
$scope.json = JobDetailService.processJson($scope.event);
if ($state.current.name == 'jobDetail.host-event.json'){ var init = function(){
codeMirror(); $scope.event = event;
} JobDetailService.getJobEventChildren($stateParams.taskId).success(function(res){
try { $scope.hostResults = res.results;
$scope.stdout = $scope.event.event_data.res.stdout });
} $scope.json = JobDetailService.processJson($scope.event);
catch(err){ if ($state.current.name == 'jobDetail.host-event.json'){
$scope.sdout = null; codeMirror('#HostEvent-json', $scope.json);
} }
$('#HostEvent').modal('show'); try {
}; $scope.stdout = JobDetailService.processJson($scope.event.event_data.res)
init(); if ($state.current.name == 'jobDetail.host-event.stdout'){
}]; codeMirror('#HostEvent-stdout', $scope.stdout);
}
}
catch(err){
$scope.sdout = null;
}
$('#HostEvent').modal('show');
};
init();
}];

View File

@@ -8,79 +8,51 @@
var hostEventModal = { var hostEventModal = {
name: 'jobDetail.host-event', name: 'jobDetail.host-event',
url: '/host-event/:eventId', url: '/task/:taskId/host-event/:eventId',
controller: 'HostEventController', controller: 'HostEventController',
params:{
hostResults: {
value: null,
squash: false,
}
},
templateUrl: templateUrl('job-detail/host-event/host-event-modal'), templateUrl: templateUrl('job-detail/host-event/host-event-modal'),
resolve: { resolve: {
features: ['FeaturesService', function(FeaturesService){ features: ['FeaturesService', function(FeaturesService){
return FeaturesService.get(); return FeaturesService.get();
}], }],
event: ['JobDetailService','$stateParams', function(JobDetailService, $stateParams) { event: ['JobDetailService','$stateParams', 'moment', function(JobDetailService, $stateParams, moment) {
return JobDetailService.getRelatedJobEvents($stateParams.id, { return JobDetailService.getRelatedJobEvents($stateParams.id, {
id: $stateParams.eventId id: $stateParams.eventId,
}).success(function(res){ return res.results[0]}) }).then(function(res){
res.data.results[0].created = moment(res.data.results[0].created).format('MMMM Do YYYY, h:mm:ss a');
return res.data.results[0];
});
}] }]
}, },
onExit: function($state){ onExit: function($state){
// close the modal // close the modal
// using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X" // using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X"
$('#HostEvent').modal('hide'); $('#HostEvent').modal('hide');
// hacky way to handle user browsing away via URL bar // hacky way to handle user browsing away via URL bar
$('.modal-backdrop').remove(); $('.modal-backdrop').remove();
$('body').removeClass('modal-open'); $('body').removeClass('modal-open');
} }
} };
var hostEventDetails = { var hostEventDetails = {
name: 'jobDetail.host-event.details', name: 'jobDetail.host-event.details',
url: '/details', url: '/details',
controller: 'HostEventController', controller: 'HostEventController',
templateUrl: templateUrl('job-detail/host-event/host-event-details'), templateUrl: templateUrl('job-detail/host-event/host-event-details'),
resolve: { };
features: ['FeaturesService', function(FeaturesService){
return FeaturesService.get();
}]
}
}
var hostEventJson = { var hostEventJson = {
name: 'jobDetail.host-event.json', name: 'jobDetail.host-event.json',
url: '/json', url: '/json',
controller: 'HostEventController', controller: 'HostEventController',
templateUrl: templateUrl('job-detail/host-event/host-event-json'), templateUrl: templateUrl('job-detail/host-event/host-event-json')
resolve: {
features: ['FeaturesService', function(FeaturesService){
return FeaturesService.get();
}]
}
};
var hostEventTiming = {
name: 'jobDetail.host-event.timing',
url: '/timing',
controller: 'HostEventController',
templateUrl: templateUrl('job-detail/host-event/host-event-timing'),
resolve: {
features: ['FeaturesService', function(FeaturesService){
return FeaturesService.get();
}]
}
}; };
var hostEventStdout = { var hostEventStdout = {
name: 'jobDetail.host-event.stdout', name: 'jobDetail.host-event.stdout',
url: '/stdout', url: '/stdout',
controller: 'HostEventController', controller: 'HostEventController',
templateUrl: templateUrl('job-detail/host-event/host-event-stdout'), templateUrl: templateUrl('job-detail/host-event/host-event-stdout')
resolve: {
features: ['FeaturesService', function(FeaturesService){
return FeaturesService.get();
}]
}
}; };
export {hostEventDetails, hostEventJson, hostEventTiming, hostEventStdout, hostEventModal} export {hostEventDetails, hostEventJson, hostEventStdout, hostEventModal}

View File

@@ -4,7 +4,7 @@
* All Rights Reserved * All Rights Reserved
*************************************************/ *************************************************/
import {hostEventModal, hostEventDetails, hostEventTiming, import {hostEventModal, hostEventDetails,
hostEventJson, hostEventStdout} from './host-event.route'; hostEventJson, hostEventStdout} from './host-event.route';
import controller from './host-event.controller'; import controller from './host-event.controller';
@@ -15,7 +15,6 @@
.run(['$stateExtender', function($stateExtender){ .run(['$stateExtender', function($stateExtender){
$stateExtender.addState(hostEventModal); $stateExtender.addState(hostEventModal);
$stateExtender.addState(hostEventDetails); $stateExtender.addState(hostEventDetails);
$stateExtender.addState(hostEventTiming);
$stateExtender.addState(hostEventJson); $stateExtender.addState(hostEventJson);
$stateExtender.addState(hostEventStdout); $stateExtender.addState(hostEventStdout);
}]); }]);

View File

@@ -1,6 +1,9 @@
@import "awx/ui/client/src/shared/branding/colors.less"; @import "awx/ui/client/src/shared/branding/colors.less";
@import "awx/ui/client/src/shared/branding/colors.default.less"; @import "awx/ui/client/src/shared/branding/colors.default.less";
.HostEvents .CodeMirror{
border: none;
}
.HostEvents .modal-footer{ .HostEvents .modal-footer{
border: 0; border: 0;
margin-top: 0px; margin-top: 0px;

View File

@@ -72,7 +72,8 @@
if (filter == 'ok'){ if (filter == 'ok'){
return JobDetailService.getRelatedJobEvents($stateParams.id, { return JobDetailService.getRelatedJobEvents($stateParams.id, {
host_name: $stateParams.hostName, host_name: $stateParams.hostName,
event: 'runner_on_ok', or__field__event: 'runner_on_ok',
or__field__event: 'runner_on_ok_async',
changed: false changed: false
}) })
.success(function(res){ .success(function(res){
@@ -92,7 +93,7 @@
} }
if (filter == 'failed'){ if (filter == 'failed'){
return JobDetailService.getRelatedJobEvents($stateParams.id, { return JobDetailService.getRelatedJobEvents($stateParams.id, {
host_name: $stateParams.hostName, host_name: $stateParams.hostName,
failed: true}) failed: true})
.success(function(res){ .success(function(res){
$scope.results = res.results; $scope.results = res.results;

View File

@@ -36,9 +36,9 @@
<td class=HostEvents-table--cell> <td class=HostEvents-table--cell>
<!-- status circles --> <!-- status circles -->
<a class="HostEvents-status"> <a class="HostEvents-status">
<i class="fa fa-circle" ng-class="processEventStatus(event)"></i> <i class="fa fa-circle" ng-class="processEventStatus(event).class"></i>
</a> </a>
{{event.status}} {{processEventStatus(event).status}}
</td> </td>
<td class=HostEvents-table--cell>{{event.play}}</td> <td class=HostEvents-table--cell>{{event.play}}</td>
<td class=HostEvents-table--cell>{{event.task}}</td> <td class=HostEvents-table--cell>{{event.task}}</td>

View File

@@ -0,0 +1,13 @@
.HostSummary-graph--successful{
text-anchor: start !important;
}
.HostSummary-graph--failed{
text-anchor: end !important;
}
.HostSummary-graph--changed{
text-anchor: end !important;
}
.HostSUmmary-graph--unreachable{}
.HostSummary-loading{
border: none;
}

View File

@@ -0,0 +1,139 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default
['$scope', '$rootScope', '$stateParams', 'Wait', 'JobDetailService', 'jobSocket', 'DrawGraph', function($scope, $rootScope, $stateParams, Wait, JobDetailService, jobSocket, DrawGraph){
var page_size = 200;
$scope.loading = $scope.hosts.length > 0 ? false : true;
$scope.filter = 'all';
$scope.search = null;
var buildTooltips = function(hosts){
// status waterfall: unreachable > failed > changed > ok > skipped
var count, grammar, text = {};
count = {
ok : _.filter(hosts, function(o){
return o.failures === 0 && o.changed === 0 && o.ok > 0;
}),
skipped : _.filter(hosts, function(o){
return o.skipped > 0;
}),
unreachable : _.filter(hosts, function(o){
return o.dark > 0;
}),
failures : _.filter(hosts, function(o){
return o.failed === true;
}),
changed : _.filter(hosts, function(o){
return o.changed > 0;
})
};
var grammar = function(n, status){
var dict = {
0: 'No host events were ',
1: ' host event was ',
2: ' host events were '
};
if (n >= 2){
return n + dict[2] + status;
}
else{
return n !== 0 ? n + dict[n] + status : dict[n] + status;
}
};
_.forIn(count, function(value, key){
text[key] = grammar(value.length, key);
});
return {count, text}
};
var socketListener = function(){
// emitted by the API in the same function used to persist host summary data
// JobEvent.update_host_summary_from_stats() from /awx/main.models.jobs.py
jobSocket.on('summary_complete', function(data) {
// discard socket msgs we don't care about in this context
if ($stateParams.id == data['unified_job_id']){
init()
}
});
// UnifiedJob.def socketio_emit_status() from /awx/main.models.unified_jobs.py
jobSocket.on('status_changed', function(data) {
if ($stateParams.id == data['unified_job_id']){
$scope.status = data['status'];
}
});
};
$scope.getNextPage = function(){
if ($scope.next){
JobDetailService.getNextPage($scope.next).success(function(res){
res.results.forEach(function(key, index){
$scope.hosts.push(res.results[index]);
})
$scope.hosts.push(res.results);
$scope.next = res.next;
});
}
};
$scope.search = function(){
Wait('start')
JobDetailService.getJobHostSummaries($stateParams.id, {
page_size: page_size,
host_name__icontains: $scope.searchTerm,
}).success(function(res){
$scope.hosts = res.results;
$scope.next = res.next;
Wait('stop')
})
};
$scope.setFilter = function(filter){
$scope.filter = filter;
var getAll = function(){
Wait('start');
JobDetailService.getJobHostSummaries($stateParams.id, {
page_size: page_size
}).success(function(res){
Wait('stop')
$scope.hosts = res.results;
$scope.next = res.next;
});
};
var getFailed = function(){
Wait('start');
JobDetailService.getJobHostSummaries($stateParams.id, {
page_size: page_size,
failed: true
}).success(function(res){
Wait('stop')
$scope.hosts = res.results;
$scope.next = res.next;
});
}
var get = filter == 'all' ? getAll() : getFailed()
};
$scope.$watchCollection('hosts', function(curr, prev){
$scope.tooltips = buildTooltips(curr);
DrawGraph({count: $scope.tooltips.count, resize:true});
});
var init = function(){
Wait('start');
JobDetailService.getJobHostSummaries($stateParams.id, {page_size: page_size})
.success(function(res){
$scope.hosts = res.results;
$scope.next = res.next;
Wait('stop');
});
JobDetailService.getJob($stateParams.id)
.success(function(res){
$scope.status = status;
});
};
socketListener();
init();
}];

View File

@@ -0,0 +1,71 @@
<div id="hosts-summary-section" class="section">
<div class="JobDetail-searchHeaderRow" ng-hide="hosts.length == 0">
<div class="JobDetail-searchContainer form-group">
<div class="search-name">
<form ng-submit="search()">
<input type="text" class="JobDetail-searchInput form-control List-searchInput" id="search_host_summary_name" ng-model="searchTerm" placeholder="Host Name" />
<a class="List-searchInputIcon search-icon" ng-click="search()"><i class="fa fa-search"></i></a>
</form>
</div>
</div>
<div class="JobDetail-tableToggleContainer form-group">
<div class="btn-group" >
<button
ng-click="setFilter('all')"
class="JobDetail-tableToggle btn btn-xs" ng-class="{'btn-default': filter == 'failed', 'btn-primary': filter == 'all'}">All</button>
<button ng-click="setFilter('failed')"
ng-class="{'btn-default': filter == 'all', 'btn-primary': filter == 'failed'}" class="JobDetail-tableToggle btn btn-xs">Failed</button>
</div>
</div>
</div>
<div class="table-header" ng-hide="hosts.length == 0">
<table class="table table-condensed">
<thead>
<tr>
<th class="List-tableHeader col-lg-6 col-md-6 col-sm-6 col-xs-6">Hosts</th>
<th class="List-tableHeader JobDetail-tableHeader col-lg-6 col-md-5 col-sm-5 col-xs-5">Completed Tasks</th>
</tr>
</thead>
</table>
</div>
<div id="hosts-summary-table" class="table-detail" lr-infinite-scroll="getNextPage" scroll-threshold="10" time-threshold="500">
<table class="table">
<tbody>
<tr class="List-tableRow" ng-repeat="host in hosts track by $index" id="{{ host.id }}" ng-class-even="'List-tableRow--evenRow'" ng-class-odd="'List-tableRow--oddRow'">
<td class="List-tableCell name col-lg-6 col-md-6 col-sm-6 col-xs-6">
<a ui-sref="jobDetail.host-events({hostName: host.host_name})" aw-tool-tip="View events" data-placement="top">{{ host.host_name }}</a>
</td>
<td class="List-tableCell col-lg-6 col-md-5 col-sm-5 col-xs-5 badge-column">
<a ui-sref="jobDetail.host-events({hostName: host.host_name, filter: 'ok'})" aw-tool-tip="{{ tooltips.text.ok }}" data-placement="top" ng-hide="host.ok == 0"><span class="badge successful-hosts">{{ host.ok - host.changed }}</span></a>
<a ui-sref="jobDetail.host-events({hostName: host.host_name, filter: 'changed'})" aw-tool-tip="{{ tooltips.text.changed }}" data-placement="top" ng-hide="host.changed == 0"><span class="badge changed-hosts">{{ host.changed }}</span></a>
<a ui-sref="jobDetail.host-events({hostName: host.host_name, filter: 'skipped'})" aw-tool-tip="{{ tooltips.text.skipped }}" data-placement="top" ng-hide="host.skipped == 0"><span class="badge skipped-hosts">{{ host.skipped }}</span></a>
<a ui-sref="jobDetail.host-events({hostName: host.host_name, filter: 'unreachable'})" aw-tool-tip="{{ tooltips.text.unreachable}}" data-placement="top" ng-hide="host.dark == 0"><span class="badge unreachable-hosts">{{ host.dark }}</span></a>
<a ui-sref="jobDetail.host-events({hostName: host.name, filter: 'failed'})" aw-tool-tip="{{ tooltips.text.failures}}" data-placement="top" ng-hide="host.failed == 0"><span class="badge failed-hosts">{{ host.failures }}</span></a>
</td>
</tr>
<tr ng-show="hosts.length === 0 && status === 'pending'">
<td colspan="5" class="col-lg-12 HostSummary-loading">Waiting...</td>
</tr>
<tr ng-show="hosts.length === 0 && status === 'running' ">
<td colspan="5" class="col-lg-12 HostSummary-loading">Loading...</td>
</tr>
<tr ng-show="status === 'failed' || status === 'successful' && hosts.length === 0 ">
<td colspan="2" class="col-lg-12 HostSummary-loading">No matching hosts</td>
</tr>
</tbody>
</table>
</div>
<div class="scroll-spinner" id="hostSummariesMoreRows">
<i class="fa fa-cog fa-spin"></i>
</div>
</div><!-- section -->
<div id="graph-section" class="JobDetail-graphSection">
<svg></svg>
</div>

View File

@@ -2,28 +2,26 @@
@import '../shared/branding/colors.less'; @import '../shared/branding/colors.less';
@import '../shared/branding/colors.default.less'; @import '../shared/branding/colors.default.less';
@import '../shared/layouts/one-plus-one.less';
@breakpoint-md: 1200px;
@breakpoint-sm: 420px;
.JobDetail{ .JobDetail{
display: flex; .OnePlusOne-container(100%, @breakpoint-md);
flex-direction: row;
} }
.JobDetail-leftSide{ .JobDetail-leftSide{
flex: 1 0 auto; .OnePlusOne-panel--left(100%, @breakpoint-md);
width: 50%;
padding-right: 20px;
} }
.JobDetail-rightSide{ .JobDetail-rightSide{
flex: 1 0 auto; .OnePlusOne-panel--right(100%, @breakpoint-md);
width: 50%;
} }
.JobDetail-panelHeader{ .JobDetail-panelHeader{
display: flex; display: flex;
height: 30px; height: 30px;
} }
.JobDetail-expandContainer{ .JobDetail-expandContainer{
flex: 1; flex: 1;
margin: 0px; margin: 0px;
@@ -62,11 +60,17 @@
flex-wrap: wrap; flex-wrap: wrap;
flex-direction: row; flex-direction: row;
padding-top: 25px; padding-top: 25px;
@media screen and(max-width: @breakpoint-sm){
flex-direction: column;
}
} }
.JobDetail-resultRow{ .JobDetail-resultRow{
width: 50%; width: 50%;
display: flex; display: flex;
@media screen and(max-width: @breakpoint-sm){
width: 100%;
}
} }
.JobDetail-resultRowLabel{ .JobDetail-resultRowLabel{
@@ -78,6 +82,9 @@
font-size: 14px; font-size: 14px;
font-weight: normal!important; font-weight: normal!important;
flex: 1 0 auto; flex: 1 0 auto;
@media screen and(max-width: @breakpoint-md){
flex: 2.5 0 auto;
}
} }
.JobDetail-resultRow--variables{ .JobDetail-resultRow--variables{
@@ -109,10 +116,16 @@
flex-direction: row; flex-direction: row;
height: 50px; height: 50px;
margin-top: 25px; margin-top: 25px;
@media screen and(max-width: @breakpoint-sm){
height: auto;
}
} }
.JobDetail-searchContainer{ .JobDetail-searchContainer{
flex: 1 0 auto; flex: 2 0 auto;
@media screen and(max-width: @breakpoint-sm){
margin-bottom: 0px;
}
} }
.JobDetail-tableToggleContainer{ .JobDetail-tableToggleContainer{

View File

@@ -13,19 +13,18 @@
export default export default
[ '$location', '$rootScope', '$filter', '$scope', '$compile', [ '$location', '$rootScope', '$filter', '$scope', '$compile',
'$stateParams', '$log', 'ClearScope', 'GetBasePath', 'Wait', '$stateParams', '$log', 'ClearScope', 'GetBasePath', 'Wait',
'ProcessErrors', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed', 'ProcessErrors', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed',
'DrawGraph', 'LoadHostSummary', 'ReloadHostSummaryList',
'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'DeleteJob', 'PlaybookRun', 'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'DeleteJob', 'PlaybookRun',
'LoadPlays', 'LoadTasks', 'LoadHosts', 'HostsEdit', 'LoadPlays', 'LoadTasks', 'HostsEdit',
'ParseVariableString', 'GetChoices', 'fieldChoices', 'fieldLabels', 'ParseVariableString', 'GetChoices', 'fieldChoices', 'fieldLabels',
'EditSchedule', 'ParseTypeChange', 'JobDetailService', 'EditSchedule', 'ParseTypeChange', 'JobDetailService',
function( function(
$location, $rootScope, $filter, $scope, $compile, $stateParams, $location, $rootScope, $filter, $scope, $compile, $stateParams,
$log, ClearScope, GetBasePath, Wait, ProcessErrors, $log, ClearScope, GetBasePath, Wait, ProcessErrors,
SelectPlay, SelectTask, Socket, GetElapsed, DrawGraph, SelectPlay, SelectTask, Socket, GetElapsed,
LoadHostSummary, ReloadHostSummaryList, JobIsFinished, JobIsFinished,
SetTaskStyles, DigestEvent, UpdateDOM, DeleteJob, SetTaskStyles, DigestEvent, UpdateDOM, DeleteJob,
PlaybookRun, LoadPlays, LoadTasks, LoadHosts, PlaybookRun, LoadPlays, LoadTasks,
HostsEdit, ParseVariableString, GetChoices, fieldChoices, HostsEdit, ParseVariableString, GetChoices, fieldChoices,
fieldLabels, EditSchedule, ParseTypeChange, JobDetailService fieldLabels, EditSchedule, ParseTypeChange, JobDetailService
) { ) {
@@ -82,38 +81,6 @@ export default
} }
}); });
scope.hosts = []; 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.tasks = [];
scope.$watch('tasks', function(tasks) { scope.$watch('tasks', function(tasks) {
for (var task in tasks) { for (var task in tasks) {
@@ -169,7 +136,6 @@ export default
scope.hostResults = []; scope.hostResults = [];
scope.hostResultsMaxRows = 200; scope.hostResultsMaxRows = 200;
scope.hostSummariesMaxRows = 200;
scope.tasksMaxRows = 200; scope.tasksMaxRows = 200;
scope.playsMaxRows = 200; scope.playsMaxRows = 200;
@@ -177,7 +143,6 @@ export default
scope.playsLoading = true; scope.playsLoading = true;
scope.tasksLoading = true; scope.tasksLoading = true;
scope.hostResultsLoading = true; scope.hostResultsLoading = true;
scope.hostSummariesLoading = true;
// Turn on the 'Waiting...' message until events begin arriving // Turn on the 'Waiting...' message until events begin arriving
scope.waiting = true; scope.waiting = true;
@@ -192,11 +157,9 @@ export default
scope.searchPlaysEnabled = true; scope.searchPlaysEnabled = true;
scope.searchTasksEnabled = true; scope.searchTasksEnabled = true;
scope.searchHostsEnabled = true; scope.searchHostsEnabled = true;
scope.searchHostSummaryEnabled = true;
scope.search_play_status = 'all'; scope.search_play_status = 'all';
scope.search_task_status = 'all'; scope.search_task_status = 'all';
scope.search_host_status = 'all'; scope.search_host_status = 'all';
scope.search_host_summary_status = 'all';
scope.haltEventQueue = false; scope.haltEventQueue = false;
scope.processing = false; scope.processing = false;
@@ -204,13 +167,6 @@ export default
scope.lessDetail = false; scope.lessDetail = false;
scope.lessEvents = true; scope.lessEvents = 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 = {};
scope.jobData.hostSummaries = {}; scope.jobData.hostSummaries = {};
@@ -230,7 +186,6 @@ export default
url: GetBasePath('unified_jobs'), url: GetBasePath('unified_jobs'),
field: 'status', field: 'status',
variable: 'status_choices', variable: 'status_choices',
// callback: 'choicesReady'
}); });
scope.eventsHelpText = "<p><i class=\"fa fa-circle successful-hosts-color\"></i> Successful</p>\n" + scope.eventsHelpText = "<p><i class=\"fa fa-circle successful-hosts-color\"></i> Successful</p>\n" +
@@ -244,6 +199,7 @@ export default
data.event = data.event_name; data.event = data.event_name;
DigestEvent({ scope: scope, event: data }); DigestEvent({ scope: scope, event: data });
} }
UpdateDOM({ scope: scope });
}); });
} }
openSocket(); openSocket();
@@ -258,9 +214,6 @@ export default
if (data.status === 'failed' || data.status === 'canceled' || if (data.status === 'failed' || data.status === 'canceled' ||
data.status === 'error' || data.status === 'successful' || data.status === 'running') { data.status === 'error' || data.status === 'successful' || data.status === 'running') {
$scope.liveEventProcessing = false; $scope.liveEventProcessing = false;
if ($rootScope.jobDetailInterval) {
window.clearInterval($rootScope.jobDetailInterval);
}
if (!scope.pauseLiveEvents) { if (!scope.pauseLiveEvents) {
$scope.$emit('LoadJob'); //this is what is used for the refresh $scope.$emit('LoadJob'); //this is what is used for the refresh
} }
@@ -274,10 +227,9 @@ export default
$rootScope.removeJobSummaryComplete = $rootScope.$on('JobSummaryComplete', function() { $rootScope.removeJobSummaryComplete = $rootScope.$on('JobSummaryComplete', function() {
// the job host summary should now be available from the API // the job host summary should now be available from the API
$log.debug('Trigging reload of job_host_summaries'); $log.debug('Trigging reload of job_host_summaries');
scope.$emit('LoadHostSummaries'); scope.$emit('InitialLoadComplete');
}); });
if (scope.removeInitialLoadComplete) { if (scope.removeInitialLoadComplete) {
scope.removeInitialLoadComplete(); scope.removeInitialLoadComplete();
} }
@@ -292,75 +244,21 @@ export default
}; };
JobDetailService.getRelatedJobEvents(scope.job.id, params) JobDetailService.getRelatedJobEvents(scope.job.id, params)
.success(function(data) { .success(function(data) {
if (data.results.length > 0) {
LoadHostSummary({
scope: scope,
data: data.results[0].event_data
});
}
UpdateDOM({ scope: scope }); UpdateDOM({ scope: scope });
}) })
.error(function(data, status) { .error(function(data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!', ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status }); msg: 'Call to ' + url + '. GET returned: ' + status });
}); });
if ($rootScope.jobDetailInterval) {
window.clearInterval($rootScope.jobDetailInterval);
}
$log.debug('Job completed!'); $log.debug('Job completed!');
$log.debug(scope.jobData); $log.debug(scope.jobData);
} }
else { else {
api_complete = true; //trigger events to start processing api_complete = true; //trigger events to start processing
if ($rootScope.jobDetailInterval) { UpdateDOM({ scope: scope})
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 params = {
page_size: scope.hostSummariesMaxRows,
order: 'host_name'
};
JobDetailService.getJobHostSummaries(scope.job.id, params)
.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 = "<deleted host>";
}
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');
});
}
});
if (scope.removeLoadHosts) { if (scope.removeLoadHosts) {
scope.removeLoadHosts(); scope.removeLoadHosts();
} }
@@ -376,81 +274,22 @@ export default
var params = { var params = {
parent: task.id, parent: task.id,
event__startswith: 'runner', event__startswith: 'runner',
page_size: scope.hostResultsMaxRows
}; };
JobDetailService.getRelatedJobEvents(scope.job.id, params) JobDetailService.getRelatedJobEvents(scope.job.id, params)
.success(function(data) { .success(function(data) {
var idx, event, status, status_text, item, msg; var event, status, status_text, item, msg;
if (data.results.length > 0) { if (data.results.length > 0) {
lastEventId = data.results[0].id; lastEventId = data.results[0].id;
} }
scope.next_host_results = data.next; scope.next_host_results = data.next;
for (idx=data.results.length - 1; idx >= 0; idx--) { task.hostResults = JobDetailService.processHostEvents(data.results);
event = data.results[idx]; scope.$emit('InitialLoadComplete');
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');
}); });
} else { } else {
scope.$emit('LoadHostSummaries'); scope.$emit('InitialLoadComplete');
} }
} else { } else {
scope.$emit('LoadHostSummaries'); scope.$emit('InitialLoadComplete');
} }
}); });
@@ -559,10 +398,10 @@ export default
msg: 'Call to ' + url + '. GET returned: ' + status }); msg: 'Call to ' + url + '. GET returned: ' + status });
}); });
} else { } else {
scope.$emit('LoadHostSummaries'); scope.$emit('InitialLoadComplete');
} }
} else { } else {
scope.$emit('LoadHostSummaries'); scope.$emit('InitialLoadComplete');
} }
}); });
@@ -570,12 +409,6 @@ export default
scope.removeLoadPlays(); scope.removeLoadPlays();
} }
scope.removeLoadPlays = scope.$on('LoadPlays', function(e, events_url) { 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 = {}; scope.jobData.plays = {};
var params = { var params = {
order_by: 'id' order_by: 'id'
@@ -659,13 +492,6 @@ export default
scope.jobData.plays[event.id].status_text = 'No matching hosts'; scope.jobData.plays[event.id].status_text = 'No matching hosts';
scope.jobData.plays[event.id].status_tip = "Event ID: " + event.id + "<br />Status: No matching hosts"; scope.jobData.plays[event.id].status_tip = "Event ID: " + event.id + "<br />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]) { if (scope.activePlay && scope.jobData.plays[scope.activePlay]) {
scope.jobData.plays[scope.activePlay].playActiveClass = 'JobDetail-tableRow--selected'; scope.jobData.plays[scope.activePlay].playActiveClass = 'JobDetail-tableRow--selected';
@@ -673,7 +499,7 @@ export default
scope.$emit('LoadTasks', events_url); scope.$emit('LoadTasks', events_url);
}); });
}); });
if (scope.removeLoadJob) { if (scope.removeLoadJob) {
scope.removeLoadJob(); scope.removeLoadJob();
@@ -685,7 +511,6 @@ export default
scope.playsLoading = true; scope.playsLoading = true;
scope.tasksLoading = true; scope.tasksLoading = true;
scope.hostResultsLoading = true; scope.hostResultsLoading = true;
scope.LoadHostSummaries = true;
// Load the job record // Load the job record
JobDetailService.getJob(job_id) JobDetailService.getJob(job_id)
@@ -769,7 +594,6 @@ export default
scope.playsLoading = false; scope.playsLoading = false;
scope.tasksLoading = false; scope.tasksLoading = false;
scope.hostResultsLoading = false; scope.hostResultsLoading = false;
scope.hostSummariesLoading = false;
} }
else { else {
scope.job_status.finished = null; scope.job_status.finished = null;
@@ -811,10 +635,6 @@ export default
// First time. User just loaded page. // First time. User just loaded page.
scope.$emit('LoadJob'); scope.$emit('LoadJob');
} }
else {
// Check if the graph needs to redraw
setTimeout(function() { DrawGraph({ scope: scope, resize: true }); }, 500);
}
}); });
scope.adjustSize = function() { scope.adjustSize = function() {
@@ -859,11 +679,6 @@ export default
$('#tasks-table-detail').height(120 + (height * 0.20)); $('#tasks-table-detail').height(120 + (height * 0.20));
$('#hosts-table-detail').height(150 + (height * 0.70)); $('#hosts-table-detail').height(150 + (height * 0.70));
} }
// Summary table height adjusting.
height = ($('#job-detail-container').height() / 2) - $('#hosts-summary-section .JobDetail-searchHeaderRow').outerHeight() -
$('#hosts-summary-section .table-header').outerHeight() - 20;
// $('#hosts-summary-table').height(height);
//$('#hosts-summary-table').mCustomScrollbar("update");
scope.$emit('RefreshCompleted'); scope.$emit('RefreshCompleted');
}; };
@@ -912,52 +727,6 @@ export default
} }
}; };
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) { scope.objectIsEmpty = function(obj) {
if (angular.isObject(obj)) { if (angular.isObject(obj)) {
return (Object.keys(obj).length > 0) ? false : true; return (Object.keys(obj).length > 0) ? false : true;
@@ -965,6 +734,17 @@ export default
return true; return true;
}; };
scope.toggleLessEvents = function() {
if (!scope.lessEvents) {
$('#events-summary').slideUp(200);
scope.lessEvents = true;
}
else {
$('#events-summary').slideDown(200);
scope.lessEvents = false;
}
};
scope.toggleLessStatus = function() { scope.toggleLessStatus = function() {
if (!scope.lessStatus) { if (!scope.lessStatus) {
$('#job-status-form').slideUp(200); $('#job-status-form').slideUp(200);
@@ -987,18 +767,6 @@ export default
} }
}; };
scope.toggleLessEvents = function() {
if (!scope.lessEvents) {
$('#events-summary').slideUp(200);
scope.lessEvents = true;
}
else {
$('#events-summary').slideDown(200);
scope.lessEvents = false;
DrawGraph({scope:scope});
}
};
scope.filterPlayStatus = function() { scope.filterPlayStatus = function() {
scope.search_play_status = (scope.search_play_status === 'all') ? 'failed' : 'all'; scope.search_play_status = (scope.search_play_status === 'all') ? 'failed' : 'all';
if (!scope.liveEventProcessing || scope.pauseLiveEvents) { if (!scope.liveEventProcessing || scope.pauseLiveEvents) {
@@ -1037,6 +805,9 @@ export default
scope.searchTasksEnabled = true; scope.searchTasksEnabled = true;
} }
if (!scope.liveEventProcessing || scope.pauseLiveEvents) { if (!scope.liveEventProcessing || scope.pauseLiveEvents) {
if (scope.search_task_status === 'failed'){
params.failed = true;
}
LoadTasks({ LoadTasks({
scope: scope scope: scope
}); });
@@ -1058,66 +829,24 @@ export default
scope.searchHostsEnabled = true; scope.searchHostsEnabled = true;
} }
if (!scope.liveEventProcessing || scope.pauseLiveEvents) { if (!scope.liveEventProcessing || scope.pauseLiveEvents) {
LoadHosts({ scope.hostResultsLoading = true;
scope: scope var params = {
parent: scope.selectedTask,
event__startswith: 'runner',
page_size: scope.hostResultsMaxRows,
order: 'host_name,counter',
host_name__icontains: scope.search_host_name
}
if (scope.search_host_status === 'failed'){
params.failed = true;
}
JobDetailService.getRelatedJobEvents(scope.job.id, params).success(function(res){
scope.hostResults = JobDetailService.processHostEvents(res.results)
scope.hostResultsLoading = false;
}); });
} }
}; };
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
});
}
};
if (scope.removeDeleteFinished) { if (scope.removeDeleteFinished) {
scope.removeDeleteFinished(); scope.removeDeleteFinished();
@@ -1354,41 +1083,6 @@ export default
} }
}; };
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;
JobDetailService.getNextPage(scope.next_host_summaries)
.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 = "<deleted host>";
}
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.refresh = function(){ scope.refresh = function(){
$scope.$emit('LoadJob'); $scope.$emit('LoadJob');
}; };

View File

@@ -62,7 +62,7 @@
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job_status.started"> <div class="form-group JobDetail-resultRow toggle-show" ng-show="job_status.started">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Started</label> <label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Started</label>
<div class="JobDetail-resultRowText">{{ job_status.started | date:'MM/dd/yy HH:mm:ss' }}</div> <div class="JobDetail-resultRowText">{{ job_status.started | date:'M/d/yy HH:mm:ss a' }}</div>
</div> </div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job_type"> <div class="form-group JobDetail-resultRow toggle-show" ng-show="job_type">
@@ -72,7 +72,7 @@
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job_status.started"> <div class="form-group JobDetail-resultRow toggle-show" ng-show="job_status.started">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Finished</label> <label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Finished</label>
<div class="JobDetail-resultRowText">{{ job_status.finished | date:'MM/dd/yy HH:mm:ss' }}</div> <div class="JobDetail-resultRowText">{{ job_status.finished | date:'M/d/yy HH:mm:ss a' }}</div>
</div> </div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="created_by"> <div class="form-group JobDetail-resultRow toggle-show" ng-show="created_by">
@@ -344,7 +344,7 @@
<tbody> <tbody>
<tr class="List-tableRow cursor-pointer" ng-class-odd="'List-tableRow--oddRow'" ng-class-even="'List-tableRow--evenRow'" ng-repeat="result in results = (hostResults) track by $index"> <tr class="List-tableRow cursor-pointer" ng-class-odd="'List-tableRow--oddRow'" ng-class-even="'List-tableRow--evenRow'" ng-repeat="result in results = (hostResults) track by $index">
<td class="List-tableCell col-lg-4 col-md-3 col-sm-3 col-xs-3 status-column"> <td class="List-tableCell col-lg-4 col-md-3 col-sm-3 col-xs-3 status-column">
<a ui-sref="jobDetail.host-event.details({eventId: result.id, hostResults: hostResults})" aw-tool-tip="Event ID: {{ result.id }}<br \>Status: {{ result.status_text }}. Click for details" data-placement="top"><i ng-show="result.status_text != 'Unreachable'" class="JobDetail-statusIcon fa icon-job-{{ result.status }}"></i><span ng-show="result.status_text != 'Unreachable'">{{ result.name }}</span><i ng-show="result.status_text == 'Unreachable'" class="JobDetail-statusIcon fa icon-job-unreachable"></i><span ng-show="result.status_text == 'Unreachable'">{{ result.name }}</span></a> <a ui-sref="jobDetail.host-event.details({eventId: result.id, taskId: selectedTask})" aw-tool-tip="Event ID: {{ result.id }}<br \>Status: {{ result.status_text }}. Click for details" data-placement="top"><i ng-show="result.status_text != 'Unreachable'" class="JobDetail-statusIcon fa icon-job-{{ result.status }}"></i><span ng-show="result.status_text != 'Unreachable'">{{ result.name }}</span><i ng-show="result.status_text == 'Unreachable'" class="JobDetail-statusIcon fa icon-job-unreachable"></i><span ng-show="result.status_text == 'Unreachable'">{{ result.name }}</span></a>
</td> </td>
<td class="List-tableCell col-lg-3 col-md-4 col-sm-3 col-xs-3 item-column">{{ result.item }}</td> <td class="List-tableCell col-lg-3 col-md-4 col-sm-3 col-xs-3 item-column">{{ result.item }}</td>
<td class="List-tableCell col-lg-3 col-md-4 col-sm-3 col-xs-3">{{ result.msg }}</td> <td class="List-tableCell col-lg-3 col-md-4 col-sm-3 col-xs-3">{{ result.msg }}</td>
@@ -388,75 +388,10 @@
</div> </div>
</div> </div>
<div id="events-summary" style="display:none"> <!-- Host Summary view -->
<div id="events-summary" ng-hide="lessEvents">
<div id="hosts-summary-section" class="section"> <div ui-view="host-summary@jobDetail"></div>
</div>
<div class="JobDetail-searchHeaderRow">
<div class="JobDetail-searchContainer form-group">
<div class="search-name">
<input type="text" class="JobDetail-searchInput form-control List-searchInput" id="search_host_summary_name" ng-model="search_host_summary_name" placeholder="Host Name" ng-keypress="searchHostSummaryKeyPress($event)" >
<a class="List-searchInputIcon search-icon" ng-show="searchHostSummaryEnabled" ng-click="searchHostSummary()"><i class="fa fa-search"></i></a>
<a class="List-searchInputIcon search-icon" ng-show="!searchHostSummaryEnabled" ng-click="search_host_summary_name=''; searchHostSummary()"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="JobDetail-tableToggleContainer form-group">
<div class="btn-group" aw-toggle-button data-after-toggle="filterHostSummaryStatus">
<button class="JobDetail-tableToggle btn btn-xs btn-primary active">All</button>
<button class="JobDetail-tableToggle btn btn-xs btn-default">Failed</button>
</div>
</div>
</div>
<div class="table-header">
<table class="table table-condensed">
<thead>
<tr>
<th class="List-tableHeader col-lg-6 col-md-6 col-sm-6 col-xs-6">Hosts</th>
<th class="List-tableHeader JobDetail-tableHeader col-lg-6 col-md-5 col-sm-5 col-xs-5">Completed Tasks</th>
</tr>
</thead>
</table>
</div>
<div id="hosts-summary-table" class="table-detail" lr-infinite-scroll="hostSummariesScrollDown" scroll-threshold="10" time-threshold="500">
<table class="table">
<tbody>
<tr class="List-tableRow" ng-repeat="host in summaryList = (hosts) track by $index" id="{{ host.id }}" ng-class-even="'List-tableRow--evenRow'" ng-class-odd="'List-tableRow--oddRow'">
<td class="List-tableCell name col-lg-6 col-md-6 col-sm-6 col-xs-6">
<a ui-sref="jobDetail.host-events({hostName: host.name})" aw-tool-tip="View events" data-placement="top">{{ host.name }}</a>
</td>
<td class="List-tableCell col-lg-6 col-md-5 col-sm-5 col-xs-5 badge-column">
<a ui-sref="jobDetail.host-events({hostName: host.name, hostId: host.id, filter: 'ok'})" aw-tool-tip="{{ host.okTip }}" data-tip-watch="host.okTip" data-placement="top" ng-hide="host.ok == 0"><span class="badge successful-hosts">{{ host.ok }}</span></a>
<a ui-sref="jobDetail.host-events({hostName: host.name, hostId: host.id, filter: 'changed'})" aw-tool-tip="{{ host.changedTip }}" data-tip-watch="host.changedTip" data-placement="top" ng-hide="host.changed == 0"><span class="badge changed-hosts">{{ host.changed }}</span></a>
<a ui-sref="jobDetail.host-events({hostName: host.name, hostId: host.id, filter: 'unreachable'})" aw-tool-tip="{{ host.unreachableTip }}" data-tip-watch="host.unreachableTip" data-placement="top" ng-hide="host.unreachable == 0"><span class="badge unreachable-hosts">{{ host.unreachable }}</span></a>
<a ui-sref="jobDetail.host-events({hostName: host.name, hostId: host.id, filter: 'failed'})" aw-tool-tip="{{ host.failedTip }}" data-tip-watch="host.failedTip" data-placement="top" ng-hide="host.failed == 0"><span class="badge failed-hosts">{{ host.failed }}</span></a>
</td>
</tr>
<tr ng-show="summaryList.length === 0 && waiting">
<td colspan="5" class="col-lg-12 loading-info">Waiting...</td>
</tr>
<tr ng-show="summaryList.length === 0 && hostSummariesLoading && !waiting">
<td colspan="5" class="col-lg-12 loading-info">Loading...</td>
</tr>
<tr ng-show="summaryList.length === 0 && !hostSummariesLoading && !waiting">
<td colspan="2" class="col-lg-12 loading-info">No matching hosts</td>
</tr>
</tbody>
</table>
</div>
<div class="scroll-spinner" id="hostSummariesMoreRows">
<i class="fa fa-cog fa-spin"></i>
</div>
</div><!-- section -->
<div id="graph-section" class="JobDetail-graphSection">
<svg width="100%" height="100%"></svg>
</div>
</div>
<!--end of events summary-->
</div> </div>
<!-- end of events summary--> <!-- end of events summary-->

View File

@@ -1,16 +1,15 @@
/************************************************* /*************************************************
* Copyright (c) 2015 Ansible, Inc. * Copyright (c) 2016 Ansible, Inc.
* *
* All Rights Reserved * All Rights Reserved
*************************************************/ *************************************************/
import {templateUrl} from '../shared/template-url/template-url.factory'; import {templateUrl} from '../shared/template-url/template-url.factory';
import HostSummaryController from './host-summary/host-summary.controller';
export default { export default {
name: 'jobDetail', name: 'jobDetail',
url: '/jobs/:id', url: '/jobs/:id',
templateUrl: templateUrl('job-detail/job-detail'),
controller: 'JobDetailController',
ncyBreadcrumb: { ncyBreadcrumb: {
parent: 'jobs', parent: 'jobs',
label: "{{ job.id }} - {{ job.name }}" label: "{{ job.id }} - {{ job.name }}"
@@ -26,10 +25,32 @@ export default {
endpoint: "job_events" endpoint: "job_events"
}); });
$rootScope.event_socket.init(); $rootScope.event_socket.init();
// returns should really be providing $rootScope.event_socket
// otherwise, we have to inject the entire $rootScope into the controller
return true; return true;
} else { } else {
return true; return true;
} }
}],
jobSocket: ['Socket', '$rootScope', function(Socket, $rootScope) {
var job_socket = Socket({
scope: $rootScope,
endpoint: "jobs"
});
job_socket.init();
// returns should really be providing $rootScope.job_socket
// otherwise, we have to inject the entire $rootScope into the controller
return job_socket;
}] }]
},
views: {
'': {
templateUrl: templateUrl('job-detail/job-detail'),
controller: 'JobDetailController',
},
'host-summary@jobDetail': {
templateUrl: templateUrl('job-detail/host-summary/host-summary'),
controller: HostSummaryController
}
} }
}; };

View File

@@ -1,18 +1,17 @@
export default export default
['$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors', function($rootScope, Rest, GetBasePath, ProcessErrors){ ['$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors', function($rootScope, Rest, GetBasePath, ProcessErrors){
return { return {
/* /*
For ES6 * For ES6
it might be useful to set some default params here, e.g. * it might be useful to set some default params here, e.g.
getJobHostSummaries: function(id, page_size=200, order='host_name'){} * getJobHostSummaries: function(id, page_size=200, order='host_name'){}
without ES6, we'd have to supply defaults like this: * without ES6, we'd have to supply defaults like this:
this.page_size = params.page_size ? params.page_size : 200; * this.page_size = params.page_size ? params.page_size : 200;
*/ */
// the the API passes through Ansible's event_data response // the the API passes through Ansible's event_data response
// we need to massage away the verbose and redundant properties // we need to massage away the verbose and redundant properties
processJson: function(data){ processJson: function(data){
// a deep copy // a deep copy
var result = $.extend(true, {}, data); var result = $.extend(true, {}, data);
@@ -28,7 +27,7 @@ export default
// remove ignored properties // remove ignored properties
Object.keys(result).forEach(function(key, index){ Object.keys(result).forEach(function(key, index){
if (ignored.indexOf(key) > -1) { if (ignored.indexOf(key) > -1) {
delete result[key] delete result[key];
} }
}); });
@@ -37,154 +36,209 @@ export default
result.event_data = {}; result.event_data = {};
Object.keys(data.event_data.res).forEach(function(key, index){ Object.keys(data.event_data.res).forEach(function(key, index){
if (ignored.indexOf(key) > -1) { if (ignored.indexOf(key) > -1) {
return return;
} }
else{ else{
result.event_data[key] = data.event_data.res[key]; result.event_data[key] = data.event_data.res[key];
} }
}); });
} }
catch(err){result.event_data = null;} catch(err){result.event_data = undefined;}
return result return result;
}, },
// Return Ansible's passed-through response msg on a job_event
processEventStatus: function(event){ processEventMsg: function(event){
// Generate a helper class for job_event statuses return typeof event.event_data.res === 'object' ? event.event_data.res.msg : event.event_data.res;
// the stack for which status to display is },
// unreachable > failed > changed > ok // Return only Ansible's passed-through response item on a job_event
// uses the API's runner events and convenience properties .failed .changed to determine status. processEventItem: function(event){
// see: job_event_callback.py try{
if (event.event == 'runner_on_unreachable'){ var item = event.event_data.res.item;
event.status = 'Unreachable'; return typeof item === 'object' ? JSON.stringify(item) : item;
return 'HostEvents-status--unreachable'
}
// equiv to 'runner_on_error' && 'runner on failed'
if (event.failed){
event.status = 'Failed';
return 'HostEvents-status--failed'
}
// catch the changed case before ok, because both can be true
if (event.changed){
event.status = 'Changed';
return 'HostEvents-status--changed'
}
if (event.event == 'runner_on_ok'){
event.status = 'OK';
return 'HostEvents-status--ok'
}
if (event.event == 'runner_on_skipped'){
event.status = 'Skipped';
return 'HostEvents-status--skipped'
}
else{
// study a case where none of these apply
}
},
// GET events related to a job run
// e.g.
// ?event=playbook_on_stats
// ?parent=206&event__startswith=runner&page_size=200&order=host_name,counter
getRelatedJobEvents: function(id, params){
var url = GetBasePath('jobs');
url = url + id + '/job_events/?';
Object.keys(params).forEach(function(key, index) {
// the API is tolerant of extra ampersands
// ?&event=playbook_on_start == ?event=playbook_on_stats
url = url + '&' + key + '=' + params[key];
});
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
},
// GET job host summaries related to a job run
// e.g. ?page_size=200&order=host_name
getJobHostSummaries: function(id, params){
var url = GetBasePath('jobs');
url = url + id + '/job_host_summaries/?'
Object.keys(params).forEach(function(key, index) {
// the API is tolerant of extra ampersands
url = url + '&' + key + '=' + params[key];
});
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
},
// GET job plays related to a job run
// e.g. ?page_size=200
getJobPlays: function(id, params){
var url = GetBasePath('jobs');
url = url + id + '/job_plays/?';
Object.keys(params).forEach(function(key, index) {
// the API is tolerant of extra ampersands
url = url + '&' + key + '=' + params[key];
});
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
},
getJobTasks: function(id, params){
var url = GetBasePath('jobs');
url = url + id + '/job_tasks/?';
Object.keys(params).forEach(function(key, index) {
// the API is tolerant of extra ampersands
url = url + '&' + key + '=' + params[key];
});
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
},
getJob: function(id){
var url = GetBasePath('jobs');
url = url + id;
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
},
// GET next set of paginated results
// expects 'next' param returned by the API e.g.
// "/api/v1/jobs/51/job_plays/?order_by=id&page=2&page_size=1"
getNextPage: function(url){
return Rest.get()
.success(function(data){
return data
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
} }
} catch(err){return;}
} },
]; // Generate a helper class for job_event statuses
// the stack for which status to display is
// unreachable > failed > changed > ok
// uses the API's runner events and convenience properties .failed .changed to determine status.
// see: job_event_callback.py for more filters to support
processEventStatus: function(event){
if (event.event === 'runner_on_unreachable'){
return {
class: 'HostEvents-status--unreachable',
status: 'unreachable'
};
}
// equiv to 'runner_on_error' && 'runner on failed'
if (event.failed){
return {
class: 'HostEvents-status--failed',
status: 'failed'
}
}
// catch the changed case before ok, because both can be true
if (event.changed){
return {
class: 'HostEvents-status--changed',
status: 'changed'
};
}
if (event.event === 'runner_on_ok' || event.event === 'runner_on_async_ok'){
return {
class: 'HostEvents-status--ok',
status: 'ok'
};
}
if (event.event === 'runner_on_skipped'){
return {
class: 'HostEvents-status--skipped',
status: 'skipped'
};
}
},
// Consumes a response from this.getRelatedJobEvents(id, params)
// returns an array for view logic to iterate over to build host result rows
processHostEvents: function(data){
var self = this;
var results = [];
data.forEach(function(event){
if (event.event !== 'runner_on_no_hosts'){
var status = self.processEventStatus(event);
var msg = self.processEventMsg(event);
var item = self.processEventItem(event);
results.push({
id: event.id,
status: status.status,
status_text: _.head(status.status).toUpperCase() + _.tail(status.status),
host_id: event.host,
task_id: event.parent,
name: event.event_data.host,
created: event.created,
msg: typeof msg === 'undefined' ? undefined : msg,
item: typeof item === 'undefined' ? undefined : item
});
}
});
return results;
},
// GET events related to a job run
// e.g.
// ?event=playbook_on_stats
// ?parent=206&event__startswith=runner&page_size=200&order=host_name,counter
getRelatedJobEvents: function(id, params){
var url = GetBasePath('jobs');
url = url + id + '/job_events/?';
Object.keys(params).forEach(function(key, index) {
// the API is tolerant of extra ampersands
// ?&event=playbook_on_start == ?event=playbook_on_stats
url = url + '&' + key + '=' + params[key];
});
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data;
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
},
getJobEventChildren: function(id){
var url = GetBasePath('job_events');
url = url + id + '/children/';
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
},
// GET job host summaries related to a job run
// e.g. ?page_size=200&order=host_name
getJobHostSummaries: function(id, params){
var url = GetBasePath('jobs');
url = url + id + '/job_host_summaries/?';
Object.keys(params).forEach(function(key, index) {
// the API is tolerant of extra ampersands
url = url + '&' + key + '=' + params[key];
});
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data;
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
},
// GET job plays related to a job run
// e.g. ?page_size=200
getJobPlays: function(id, params){
var url = GetBasePath('jobs');
url = url + id + '/job_plays/?';
Object.keys(params).forEach(function(key, index) {
// the API is tolerant of extra ampersands
url = url + '&' + key + '=' + params[key];
});
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data;
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
},
getJobTasks: function(id, params){
var url = GetBasePath('jobs');
url = url + id + '/job_tasks/?';
Object.keys(params).forEach(function(key, index) {
// the API is tolerant of extra ampersands
url = url + '&' + key + '=' + params[key];
});
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data;
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
},
getJob: function(id){
var url = GetBasePath('jobs');
url = url + id;
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data;
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
},
// GET next set of paginated results
// expects 'next' param returned by the API e.g.
// "/api/v1/jobs/51/job_plays/?order_by=id&page=2&page_size=1"
getNextPage: function(url){
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data;
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
}
};
}];

View File

@@ -9,7 +9,7 @@ export default
angular.module('AllJobsDefinition', ['sanitizeFilter', 'capitalizeFilter']) angular.module('AllJobsDefinition', ['sanitizeFilter', 'capitalizeFilter'])
.value( 'AllJobsList', { .value( 'AllJobsList', {
name: 'all_jobs', name: 'jobs',
basePath: 'unified_jobs', basePath: 'unified_jobs',
iterator: 'all_job', iterator: 'all_job',
editTitle: 'All Jobs', editTitle: 'All Jobs',
@@ -18,8 +18,9 @@ export default
well: false, well: false,
fields: { fields: {
status: { status: {
label: 'Status', label: '',
columnClass: 'List-staticColumn--smallStatus', searchLabel: 'Status',
columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumn--smallStatus',
awToolTip: "{{ all_job.status_tip }}", awToolTip: "{{ all_job.status_tip }}",
awTipPlacement: "right", awTipPlacement: "right",
dataTitle: "{{ all_job.status_popover_title }}", dataTitle: "{{ all_job.status_popover_title }}",
@@ -30,13 +31,10 @@ export default
searchType: 'select', searchType: 'select',
nosort: true, nosort: true,
searchOptions: [ searchOptions: [
{ name: "Success", value: "successful" },
{ name: "Error", value: "error" },
{ name: "Failed", value: "failed" },
{ name: "Canceled", value: "canceled" }
] ]
}, },
id: { id: {
key: true,
label: 'ID', label: 'ID',
ngClick:"viewJobDetails(all_job)", ngClick:"viewJobDetails(all_job)",
searchType: 'int', searchType: 'int',
@@ -45,6 +43,7 @@ export default
dataPlacement: 'top' dataPlacement: 'top'
}, },
name: { name: {
key: true,
label: 'Name', label: 'Name',
columnClass: 'col-lg-3 col-md-3 col-sm-4 col-xs-6', columnClass: 'col-lg-3 col-md-3 col-sm-4 col-xs-6',
ngClick: "viewJobDetails(all_job)", ngClick: "viewJobDetails(all_job)",

View File

@@ -35,6 +35,10 @@ angular.module('ApiLoader', ['Utilities'])
.success(function (data) { .success(function (data) {
data.base = base; data.base = base;
$rootScope.defaultUrls = data; $rootScope.defaultUrls = data;
// tiny hack to side-step api/v1/job_events not being a visible endpoint @ GET api/v1/
if (!$rootScope.defaultUrls['job_events']){
$rootScope.defaultUrls['job_events'] = '/api/v1/job_events/';
}
Store('api', data); Store('api', data);
}) })
.error(function (data, status) { .error(function (data, status) {