Merge branch 'devel' of github.com:ansible/ansible-tower into 11th-hour

This commit is contained in:
Akita Noek
2016-04-16 18:35:25 -04:00
40 changed files with 1502 additions and 1817 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,
@@ -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,15 +363,15 @@ 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)
// 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; 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

@@ -173,8 +173,8 @@ export default
} }
$state.go("^"); $state.go("^");
}); });
scope.saveSchedule = function() { scope.saveSchedule = function() {
schedule.extra_data = scope.serializedExtraVars;
SchedulePost({ SchedulePost({
scope: scope, scope: scope,
url: url, url: url,
@@ -192,6 +192,7 @@ export default
Rest.get() Rest.get()
.success(function(data) { .success(function(data) {
schedule = data; schedule = data;
scope.serializedExtraVars = schedule.extra_data;
if(schedule.extra_data.hasOwnProperty('granularity')){ if(schedule.extra_data.hasOwnProperty('granularity')){
scope.isFactCleanup = true; scope.isFactCleanup = true;
} }
@@ -312,7 +313,6 @@ export default
schedule = (params.schedule) ? params.schedule : {}, schedule = (params.schedule) ? params.schedule : {},
callback = params.callback, callback = params.callback,
newSchedule, rrule, extra_vars; newSchedule, rrule, extra_vars;
if (scheduler.isValid()) { if (scheduler.isValid()) {
Wait('start'); Wait('start');
newSchedule = scheduler.getValue(); newSchedule = scheduler.getValue();
@@ -326,14 +326,16 @@ export default
"older_than": scope.scheduler_form.keep_amount.$viewValue + scope.scheduler_form.keep_unit.$viewValue.value, "older_than": scope.scheduler_form.keep_amount.$viewValue + scope.scheduler_form.keep_unit.$viewValue.value,
"granularity": scope.scheduler_form.granularity_keep_amount.$viewValue + scope.scheduler_form.granularity_keep_unit.$viewValue.value "granularity": scope.scheduler_form.granularity_keep_amount.$viewValue + scope.scheduler_form.granularity_keep_unit.$viewValue.value
}; };
schedule.extra_data = JSON.stringify(extra_vars);
} else if (scope.cleanupJob) { } else if (scope.cleanupJob) {
extra_vars = { extra_vars = {
"days" : scope.scheduler_form.schedulerPurgeDays.$viewValue "days" : scope.scheduler_form.schedulerPurgeDays.$viewValue
}; };
}
schedule.extra_data = JSON.stringify(extra_vars); schedule.extra_data = JSON.stringify(extra_vars);
}
else if (scope.serializedExtraVars){
schedule.extra_data = scope.serializedExtraVars;
}
Rest.setUrl(url); Rest.setUrl(url);
if (mode === 'add') { if (mode === 'add') {
Rest.post(schedule) Rest.post(schedule)

View File

@@ -12,9 +12,9 @@
<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">

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

View File

@@ -8,23 +8,20 @@
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){
@@ -35,52 +32,27 @@ var hostEventModal = {
$('.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){

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

@@ -14,18 +14,17 @@ 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,73 +244,19 @@ 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) {
@@ -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';
@@ -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,76 +388,11 @@
</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 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> </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>
<!-- 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

@@ -3,16 +3,15 @@ export default
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,51 +36,93 @@ 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
processEventMsg: function(event){
return typeof event.event_data.res === 'object' ? event.event_data.res.msg : event.event_data.res;
},
// Return only Ansible's passed-through response item on a job_event
processEventItem: function(event){
try{
var item = event.event_data.res.item;
return typeof item === 'object' ? JSON.stringify(item) : item;
}
catch(err){return;}
}, },
processEventStatus: function(event){
// Generate a helper class for job_event statuses // Generate a helper class for job_event statuses
// the stack for which status to display is // the stack for which status to display is
// unreachable > failed > changed > ok // unreachable > failed > changed > ok
// uses the API's runner events and convenience properties .failed .changed to determine status. // uses the API's runner events and convenience properties .failed .changed to determine status.
// see: job_event_callback.py // see: job_event_callback.py for more filters to support
if (event.event == 'runner_on_unreachable'){ processEventStatus: function(event){
event.status = 'Unreachable'; if (event.event === 'runner_on_unreachable'){
return 'HostEvents-status--unreachable' return {
class: 'HostEvents-status--unreachable',
status: 'unreachable'
};
} }
// equiv to 'runner_on_error' && 'runner on failed' // equiv to 'runner_on_error' && 'runner on failed'
if (event.failed){ if (event.failed){
event.status = 'Failed'; return {
return 'HostEvents-status--failed' class: 'HostEvents-status--failed',
status: 'failed'
}
} }
// catch the changed case before ok, because both can be true // catch the changed case before ok, because both can be true
if (event.changed){ if (event.changed){
event.status = 'Changed'; return {
return 'HostEvents-status--changed' class: 'HostEvents-status--changed',
status: 'changed'
};
} }
if (event.event == 'runner_on_ok'){ if (event.event === 'runner_on_ok' || event.event === 'runner_on_async_ok'){
event.status = 'OK'; return {
return 'HostEvents-status--ok' class: 'HostEvents-status--ok',
status: 'ok'
};
} }
if (event.event == 'runner_on_skipped'){ if (event.event === 'runner_on_skipped'){
event.status = 'Skipped'; return {
return 'HostEvents-status--skipped' class: 'HostEvents-status--skipped',
} status: 'skipped'
else{ };
// study a case where none of these apply
} }
}, },
// 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 // GET events related to a job run
// e.g. // e.g.
// ?event=playbook_on_stats // ?event=playbook_on_stats
@@ -95,6 +136,19 @@ export default
url = url + '&' + key + '=' + params[key]; url = url + '&' + key + '=' + params[key];
}); });
Rest.setUrl(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 });
});
},
getJobEventChildren: function(id){
var url = GetBasePath('job_events');
url = url + id + '/children/';
Rest.setUrl(url);
return Rest.get() return Rest.get()
.success(function(data){ .success(function(data){
return data return data
@@ -108,7 +162,7 @@ export default
// e.g. ?page_size=200&order=host_name // e.g. ?page_size=200&order=host_name
getJobHostSummaries: function(id, params){ getJobHostSummaries: function(id, params){
var url = GetBasePath('jobs'); var url = GetBasePath('jobs');
url = url + id + '/job_host_summaries/?' url = url + id + '/job_host_summaries/?';
Object.keys(params).forEach(function(key, index) { Object.keys(params).forEach(function(key, index) {
// the API is tolerant of extra ampersands // the API is tolerant of extra ampersands
url = url + '&' + key + '=' + params[key]; url = url + '&' + key + '=' + params[key];
@@ -116,7 +170,7 @@ export default
Rest.setUrl(url); Rest.setUrl(url);
return Rest.get() return Rest.get()
.success(function(data){ .success(function(data){
return data return data;
}) })
.error(function(data, status) { .error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
@@ -135,7 +189,7 @@ export default
Rest.setUrl(url); Rest.setUrl(url);
return Rest.get() return Rest.get()
.success(function(data){ .success(function(data){
return data return data;
}) })
.error(function(data, status) { .error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
@@ -152,7 +206,7 @@ export default
Rest.setUrl(url); Rest.setUrl(url);
return Rest.get() return Rest.get()
.success(function(data){ .success(function(data){
return data return data;
}) })
.error(function(data, status) { .error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
@@ -165,7 +219,7 @@ export default
Rest.setUrl(url); Rest.setUrl(url);
return Rest.get() return Rest.get()
.success(function(data){ .success(function(data){
return data return data;
}) })
.error(function(data, status) { .error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
@@ -176,15 +230,15 @@ export default
// expects 'next' param returned by the API e.g. // expects 'next' param returned by the API e.g.
// "/api/v1/jobs/51/job_plays/?order_by=id&page=2&page_size=1" // "/api/v1/jobs/51/job_plays/?order_by=id&page=2&page_size=1"
getNextPage: function(url){ getNextPage: function(url){
Rest.setUrl(url);
return Rest.get() return Rest.get()
.success(function(data){ .success(function(data){
return data return data;
}) })
.error(function(data, status) { .error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status }); 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

@@ -11,14 +11,12 @@ export default
name: 'job_templates', name: 'job_templates',
iterator: 'job_template', iterator: 'job_template',
// selectTitle: 'Add Job Template',
editTitle: 'Job Templates', editTitle: 'Job Templates',
listTitle: 'Job Templates', listTitle: 'Job Templates',
// selectInstructions: "Click on a row to select it, and click Finished when done. Use the <i class=\"icon-plus\"></i> " +
// "button to create a new job template.",
index: false, index: false,
hover: true, hover: true,
well: true, well: true,
searchSize: 'col-lg-8 col-md-8 col-sm-12 col-xs-12',
fields: { fields: {
name: { name: {

View File

@@ -16,6 +16,7 @@ export default
hover: true, hover: true,
well: true, well: true,
listTitle: 'Jobs', listTitle: 'Jobs',
searchSize: 'col-lg-8 col-md-8 col-sm-12 col-xs-12',
fields: { fields: {
status: { status: {
@@ -27,23 +28,9 @@ export default
searchable: true, searchable: true,
nosort: true, nosort: true,
searchType: 'select', searchType: 'select',
searchOptions: [ searchOptions: [],
{ name: "Success", value: "successful" }, searchLabel: 'Status'
{ name: "Error", value: "error" },
{ name: "Failed", value: "failed" },
{ name: "Canceled", value: "canceled" }
]
}, },
/*
id: {
label: 'ID',
key: true,
noLink: true, //undocumented: 'key' above will automatically made the fields a link, but 'noLink' will override this setting
desc: true,
searchType: 'int',
columnClass: 'col-xs-2 List-staticColumnAdjacent',
},
*/
name: { name: {
key: true, key: true,
label: 'Name', label: 'Name',

View File

@@ -52,7 +52,7 @@ export default
filter: "longDate", filter: "longDate",
searchable: false, searchable: false,
columnClass: "List-staticColumn--schedulerTime hidden-xs" columnClass: "List-staticColumn--schedulerTime hidden-xs"
} },
}, },
actions: { actions: {

View File

@@ -51,9 +51,6 @@ export default
if (scope.searchCleanup) { if (scope.searchCleanup) {
scope.searchCleanup(); scope.searchCleanup();
} }
// if (!Empty(parent_scope) && parent_scope.restoreSearch) {
// parent_scope.restoreSearch();
// }
else { else {
Wait('stop'); Wait('stop');
} }
@@ -69,6 +66,7 @@ export default
height: 470, height: 470,
minWidth: 200, minWidth: 200,
callback: 'PromptForDaysFacts', callback: 'PromptForDaysFacts',
resizable: false,
onOpen: function(){ onOpen: function(){
scope.$watch('prompt_for_days_facts_form.$invalid', function(invalid) { scope.$watch('prompt_for_days_facts_form.$invalid', function(invalid) {
if (invalid === true) { if (invalid === true) {
@@ -113,16 +111,8 @@ export default
fieldScope.keep_amount = 30; fieldScope.keep_amount = 30;
fieldScope.granularity_keep_amount = 1; fieldScope.granularity_keep_amount = 1;
}, },
buttons: [{ buttons: [
"label": "Cancel", {
"onClick": function() {
$(this).dialog('close');
},
"icon": "fa-times",
"class": "btn btn-default",
"id": "prompt-for-days-facts-cancel"
},{
"label": "Launch", "label": "Launch",
"onClick": function() { "onClick": function() {
var extra_vars = { var extra_vars = {
@@ -145,9 +135,16 @@ export default
msg: 'Failed updating job ' + scope.job_template_id + ' with variables. POST returned: ' + status }); msg: 'Failed updating job ' + scope.job_template_id + ' with variables. POST returned: ' + status });
}); });
}, },
"icon": "fa-rocket",
"class": "btn btn-primary", "class": "btn btn-primary",
"id": "prompt-for-days-facts-launch" "id": "prompt-for-days-facts-launch",
},
{
"label": "Cancel",
"onClick": function() {
$(this).dialog('close');
},
"class": "btn btn-default",
"id": "prompt-for-days-facts-cancel"
}] }]
}); });
@@ -162,12 +159,8 @@ export default
}); });
}; };
$scope.submitJob = function (id, name) { $scope.submitJob = function (id, name, card) {
Wait('start'); Wait('start');
if(this.configure_job.job_type === "cleanup_facts"){
scope.submitCleanupJob(id, name);
}
else {
defaultUrl = GetBasePath('system_job_templates')+id+'/launch/'; defaultUrl = GetBasePath('system_job_templates')+id+'/launch/';
CreateDialog({ CreateDialog({
id: 'prompt-for-days' , id: 'prompt-for-days' ,
@@ -177,6 +170,7 @@ export default
height: 300, height: 300,
minWidth: 200, minWidth: 200,
callback: 'PromptForDays', callback: 'PromptForDays',
resizable: false,
onOpen: function(){ onOpen: function(){
scope.$watch('prompt_for_days_form.$invalid', function(invalid) { scope.$watch('prompt_for_days_form.$invalid', function(invalid) {
if (invalid === true) { if (invalid === true) {
@@ -191,16 +185,8 @@ export default
scope.prompt_for_days_form.$setPristine(); scope.prompt_for_days_form.$setPristine();
scope.prompt_for_days_form.$invalid = false; scope.prompt_for_days_form.$invalid = false;
}, },
buttons: [{ buttons: [
"label": "Cancel", {
"onClick": function() {
$(this).dialog('close');
},
"icon": "fa-times",
"class": "btn btn-default",
"id": "prompt-for-days-cancel"
},{
"label": "Launch", "label": "Launch",
"onClick": function() { "onClick": function() {
var extra_vars = {"days": scope.days_to_keep }, var extra_vars = {"days": scope.days_to_keep },
@@ -220,9 +206,17 @@ export default
msg: 'Failed updating job ' + scope.job_template_id + ' with variables. POST returned: ' + status }); msg: 'Failed updating job ' + scope.job_template_id + ' with variables. POST returned: ' + status });
}); });
}, },
"icon": "fa-rocket",
"class": "btn btn-primary", "class": "btn btn-primary",
"id": "prompt-for-days-launch" "id": "prompt-for-days-launch"
},
{
"label": "Cancel",
"onClick": function() {
$(this).dialog('close');
},
"class": "btn btn-default",
"id": "prompt-for-days-cancel"
}] }]
}); });
@@ -235,6 +229,14 @@ export default
$('#prompt-for-days').dialog('open'); $('#prompt-for-days').dialog('open');
Wait('stop'); Wait('stop');
}); });
};
$scope.chooseRunJob = function(id, name) {
if(id === 4) {
// Run only for 'Cleanup Fact Details'
$scope.submitCleanupJob(id, name);
} else {
$scope.submitJob(id, name);
} }
}; };

View File

@@ -7,7 +7,7 @@
<h3 class="MgmtCards-label"> {{ card.name }}</h3> <h3 class="MgmtCards-label"> {{ card.name }}</h3>
<div class="MgmtCards-actionItems"> <div class="MgmtCards-actionItems">
<button class="MgmtCards-actionItem List-actionButton" <button class="MgmtCards-actionItem List-actionButton"
ng-click='submitCleanupJob(card.id, card.name)'> ng-click='chooseRunJob(card.id, card.name)'>
<i class="MgmtCards-actionItemIcon fa fa-rocket"></i> <i class="MgmtCards-actionItemIcon fa fa-rocket"></i>
</button> </button>
<button class="MgmtCards-actionItem List-actionButton" <button class="MgmtCards-actionItem List-actionButton"

View File

@@ -114,3 +114,12 @@
margin-right: 0px; margin-right: 0px;
} }
} }
#prompt-for-days-facts, #prompt-for-days {
overflow-x: hidden;
font-family: "Open Sans";
.label-text {
text-transform: uppercase;
font-weight: normal;
}
}

View File

@@ -155,13 +155,13 @@ export default
Rest.post({}) Rest.post({})
.then(function () { .then(function () {
ngToast.success({ ngToast.success({
content: `<i class="fa fa-check-circle Toast-successIcon"></i> Test Notification Success: <b>${name}</b> `, content: `<i class="fa fa-check-circle Toast-successIcon"></i> <b>${name}:</b> Notification Succeeded.`,
}); });
}) })
.catch(function () { .catch(function () {
ngToast.danger({ ngToast.danger({
content: 'Test Notification Failure' content: `<i class="fa fa-check-circle Toast-successIcon"></i> <b>${name}:</b> Notification Failed.`,
}); });
}); });
}; };

View File

@@ -21,7 +21,6 @@ export function PortalModeJobsController($scope, $state, $rootScope, GetBasePath
id: 'portal-jobs', id: 'portal-jobs',
mode: 'edit', mode: 'edit',
scope: $scope, scope: $scope,
searchSize: 'col-md-10 col-xs-12'
}); });
SearchInit({ SearchInit({

View File

@@ -34,6 +34,14 @@ export default
resolve: { resolve: {
features: ['FeaturesService', function(FeaturesService) { features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get(); return FeaturesService.get();
}],
JobTemplateExtraVars: ['Rest', 'GetBasePath', 'ToJSON', '$stateParams', function(Rest, GetBasePath, ToJSON, $stateParams) {
var defaultUrl = GetBasePath('job_templates') + $stateParams.id + '/';
Rest.setUrl(defaultUrl);
return Rest.get().then(function(res){
// handle unescaped newlines
return JSON.parse(JSON.stringify(res.data.extra_vars))
});
}] }]
} }
}); });

View File

@@ -1,4 +1,4 @@
export default ['$compile', '$state', '$stateParams', 'AddSchedule', 'Wait', '$scope', '$rootScope', 'CreateSelect2', function($compile, $state, $stateParams, AddSchedule, Wait, $scope, $rootScope, CreateSelect2) { export default ['$compile', '$state', '$stateParams', 'AddSchedule', 'Wait', '$scope', '$rootScope', 'CreateSelect2', 'ParseTypeChange', 'JobTemplateExtraVars', function($compile, $state, $stateParams, AddSchedule, Wait, $scope, $rootScope, CreateSelect2, ParseTypeChange, JobTemplateExtraVars) {
$scope.$on("ScheduleFormCreated", function(e, scope) { $scope.$on("ScheduleFormCreated", function(e, scope) {
$scope.hideForm = false; $scope.hideForm = false;
$scope = angular.extend($scope, scope); $scope = angular.extend($scope, scope);
@@ -41,10 +41,35 @@ export default ['$compile', '$state', '$stateParams', 'AddSchedule', 'Wait', '$s
$scope.hideForm = true; $scope.hideForm = true;
$scope.formCancel = function() { $scope.formCancel = function() {
$state.go("^"); $state.go("^");
}; };
$scope.parseType = 'yaml';
$scope.extraVars = JobTemplateExtraVars === '' ? '---' : JobTemplateExtraVars;
ParseTypeChange({
scope: $scope,
variable: 'extraVars',
parse_variable: 'parseType',
field_id: 'SchedulerForm-extraVars'
});
$scope.$watch('extraVars', function(){
if ($scope.parseType === 'yaml'){
try{
$scope.serializedExtraVars = jsyaml.safeLoad($scope.extraVars);
}
catch(err){ return; }
}
else if ($scope.parseType === 'json'){
try{
$scope.serializedExtraVars = JSON.parse($scope.extraVars);
}
catch(err){ return; }
}
});
AddSchedule({ AddSchedule({
scope: $scope, scope: $scope,
callback: 'SchedulesRefresh', callback: 'SchedulesRefresh',

View File

@@ -1,4 +1,4 @@
export default ['$compile', '$state', '$stateParams', 'EditSchedule', 'Wait', '$scope', '$rootScope', 'CreateSelect2', function($compile, $state, $stateParams, EditSchedule, Wait, $scope, $rootScope, CreateSelect2) { export default ['$compile', '$state', '$stateParams', 'EditSchedule', 'Wait', '$scope', '$rootScope', 'CreateSelect2', 'ParseTypeChange', function($compile, $state, $stateParams, EditSchedule, Wait, $scope, $rootScope, CreateSelect2, ParseTypeChange) {
$scope.$on("ScheduleFormCreated", function(e, scope) { $scope.$on("ScheduleFormCreated", function(e, scope) {
$scope.hideForm = false; $scope.hideForm = false;
$scope = angular.extend($scope, scope); $scope = angular.extend($scope, scope);
@@ -41,13 +41,49 @@ export default ['$compile', '$state', '$stateParams', 'EditSchedule', 'Wait', '$
}); });
$scope.isEdit = true; $scope.isEdit = true;
$scope.hideForm = true; $scope.hideForm = true;
$scope.parseType = 'yaml';
$scope.formCancel = function() { $scope.formCancel = function() {
$state.go("^"); $state.go("^");
} }
$scope.$on('ScheduleFound', function(){
if ($scope.parseType === 'yaml'){
try{
$scope.extraVars = '---\n' + jsyaml.safeDump($scope.serializedExtraVars);
}
catch(err){ return; }
}
else if ($scope.parseType === 'json'){
try{
$scope.extraVars = JSON.stringify($scope.serializedExtraVars, null, ' ');
}
catch(err){ return; }
}
ParseTypeChange({
scope: $scope,
variable: 'extraVars',
parse_variable: 'parseType',
field_id: 'SchedulerForm-extraVars'
});
});
$scope.$watch('extraVars', function(){
if ($scope.parseType === 'yaml'){
try{
$scope.serializedExtraVars = jsyaml.safeLoad($scope.extraVars);
}
catch(err){ return; }
}
else if ($scope.parseType === 'json'){
try{
$scope.serializedExtraVars = JSON.parse($scope.extraVars);
}
catch(err){ return; }
}
});
EditSchedule({ EditSchedule({
scope: $scope, scope: $scope,
id: parseInt($stateParams.schedule_id), id: parseInt($stateParams.schedule_id),

View File

@@ -9,7 +9,6 @@
</div> </div>
</div> </div>
<div id="SchedulerFormTarget"> <div id="SchedulerFormTarget">
<form class="form Form" <form class="form Form"
role="form" role="form"
name="scheduler_form_new" name="scheduler_form_new"
@@ -526,47 +525,6 @@
<div class="RepeatFrequencyOptions-subFormBorderFixer" <div class="RepeatFrequencyOptions-subFormBorderFixer"
ng-show="schedulerFrequency.value && schedulerFrequency.value !== 'none'"> ng-show="schedulerFrequency.value && schedulerFrequency.value !== 'none'">
</div> </div>
<!-- <div class="factDetailsNote" ng-if="isFactCleanup"><span class="factDetailsHeader">Note:</span> For facts collected older than the time period specified, save one fact scan (snapshot) per time window (frequency). For example, facts older than 30 days are purged, while one weekly fact scan is kept.
Caution: Setting both numerical variables to "0" will delete all facts.</div> -->
<!-- <div class="form-group" ng-if="cleanupJob && !isFactCleanup">
<label class="Form-inputLabel"><span class="red-text">*</span> Days of data to keep</label>
<input type="number" class="form-control input-sm" name="schedulerPurgeDays" id="schedulerPurgeDays" min="1" ng-model="schedulerPurgeDays" required placeholder="Days of data to keep">
<div class="error" ng-show="scheduler_form.schedulerPurgeDays.$dirty && scheduler_form.schedulerPurgeDays.$error.required">A value is required.</div>
<div class="error" ng-show="scheduler_form.schedulerPurgeDays.$error.number">This is not a valid number.</div>
</div>
<div class="form-group cleanupStretcher factDaysToKeepCompacter" ng-if="isFactCleanup">
<div class="col-md-12">
<label class="Form-inputLabel"><span class="red-text">*</span> Select a time period after which to remove old facts</label>
</div>
<div class="col-md-6 inputSpacer inputCompactMobile">
<input type="number" id="keep_amount" name="keep_amount" ng-model="keep_amount" ng-required="true" class="form-control input-sm" aw-min=0 aw-max=9999 integer></input>
<div class="error" ng-show="scheduler_form.keep_amount.$dirty && scheduler_form.keep_amount.$error.required">Please enter the number of days you would like to keep this data.</div>
<div class="error survey_error" ng-show="scheduler_form.keep_amount.$error.number || scheduler_form.keep_amount.$error.integer" >Please enter a valid number.</div>
<div class="error survey_error" ng-show="scheduler_form.keep_amount.$error.awMin">Please enter a non-negative number.</div>
<div class="error survey_error" ng-show="scheduler_form.keep_amount.$error.awMax">Please enter a number smaller than 9999.</div>
</div>
<div class="col-md-6 inputSpacer">
<select id="keep_unit" name="keep_unit" ng-model="keep_unit" ng-options="type.label for type in keep_unit_choices track by type.value" ng-required="true" class="form-control input-sm"></select>
</div>
</div> -->
<!-- <div class="form-group cleanupStretcher" ng-if="isFactCleanup">
<div class="col-md-12">
<label class="Form-inputLabel"><span class="red-text">*</span> Select a frequency for snapshot retention</label>
</div>
<div class="col-md-6 inputSpacer inputCompactMobile">
<input type="number" class="form-control input-sm" id="granularity_keep_amount" name="granularity_keep_amount" ng-model="granularity_keep_amount" ng-required="true" aw-min=0 aw-max=9999 >
<div class="error" ng-show="scheduler_form.granularity_keep_amount.$dirty && scheduler_form.granularity_keep_amount.$error.required">Please enter the number of days you would like to keep this data.</div>
<div class="error survey_error" ng-show="scheduler_form.granularity_keep_amount.$error.number || scheduler_form.granularity_keep_amount.$error.integer" >Please enter a valid number.</div>
<div class="error survey_error" ng-show="scheduler_form.granularity_keep_amount.$error.awMin">Please enter a non-negative number.</div>
<div class="error survey_error" ng-show="scheduler_form.granularity_keep_amount.$error.awMax">Please enter a number smaller than 9999.</div>
</div>
<div class="col-md-6 inputSpacer">
<select id="granularity_keep_unit" name="granularity_keep_unit" ng-model="granularity_keep_unit" ng-options="type.label for type in granularity_keep_unit_choices track by type.value" ng-required="true" class="form-control input-sm"></select>
</div>
</div> -->
</form> </form>
<div class="SchedulerFormDetail-container <div class="SchedulerFormDetail-container
SchedulerFormDetail-container--error" SchedulerFormDetail-container--error"
@@ -632,8 +590,33 @@
{{ occurrence.local }} {{ occurrence.local }}
</li> </li>
</ul> </ul>
</div>
<div class="form-group Form-formGroup Form-textAreaLabel">
<label for="Scheduler-extraVars">
<span class="Form-inputLabel">
Extra Variables
</span>
<!-- tooltip -->
<a aw-pop-over="<p>Pass extra command line variables to the playbook. This is the -e or --extra-vars command line parameter for ansible-playbook. Provide key/value pairs using either YAML or JSON.</p>JSON:<br />
<blockquote>{<br />&quot;somevar&quot;: &quot;somevalue&quot;,<br />&quot;password&quot;: &quot;magic&quot;<br /> }</blockquote>
YAML:<br />
<blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>"
data-placement="right" data-container="body" over-title="Extra Variables" class="help-link" data-original-title="" title="" tabindex="-1">
<i class="fa fa-question-circle"></i>
</a>
<div class="parse-selection">
<input type="radio" ng-model="parseType" ng-change="parseTypeChange()" value="yaml"><span class="parse-label">YAML</span>
<input type="radio" ng-model="parseType" ng-change="parseTypeChange()" value="json"> <span class="parse-label">JSON</span>
</div> </div>
</label>
<div>
<textarea rows="6" ng-model="extraVars" name="Scheduler-extraVars" class="form-control" id="SchedulerForm-extraVars"></textarea>
</div>
</div>
</div>
<div class="buttons Form-buttons"> <div class="buttons Form-buttons">
<button type="button" <button type="button"
class="btn btn-sm Form-saveButton" class="btn btn-sm Form-saveButton"

View File

@@ -2,7 +2,8 @@ export default ['$scope', 'Refresh', 'tagSearchService',
function($scope, Refresh, tagSearchService) { function($scope, Refresh, tagSearchService) {
// JSONify passed field elements that can be searched // JSONify passed field elements that can be searched
$scope.list = JSON.parse($scope.list); $scope.list = JSON.parse($scope.list);
// Access config lines from list spec
$scope.listConfig = $scope.$parent.list;
// Grab options for the left-dropdown of the searchbar // Grab options for the left-dropdown of the searchbar
tagSearchService.getSearchTypes($scope.list, $scope.endpoint) tagSearchService.getSearchTypes($scope.list, $scope.endpoint)
.then(function(searchTypes) { .then(function(searchTypes) {

View File

@@ -1,5 +1,5 @@
<div class="TagSearch row"> <div class="TagSearch row">
<div class="col-lg-4 col-md-8 col-sm-12 col-xs-12"> <div ng-class="listConfig.searchSize || 'col-lg-4 col-md-8 col-sm-12 col-xs-12'">
<div class="TagSearch-bar"> <div class="TagSearch-bar">
<div class="TagSearch-typeDropdown" <div class="TagSearch-typeDropdown"
ng-click="toggleTypeDropdown()" ng-click="toggleTypeDropdown()"

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) {

View File

@@ -171,8 +171,9 @@
<div style="padding-bottom:15px;">For facts collected older than the time period specified, <div style="padding-bottom:15px;">For facts collected older than the time period specified,
save one fact scan (snapshot) per time window (frequency). save one fact scan (snapshot) per time window (frequency).
For example, facts older than 30 days are purged, while one For example, facts older than 30 days are purged, while one
weekly fact scan is kept.<br> weekly fact scan is kept.<br> <br>
Caution: Setting both numerical variables to "0" will delete all facts.<br>
CAUTION: Setting both numerical variables to "0" will delete all facts.<br><br>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="description"> <label for="description">