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{
margin-top: 15px;
margin: 0 auto;
}
path.slice{

View File

@@ -40,9 +40,9 @@ export default
angular.module('JobDetailHelper', ['Utilities', 'RestServices', 'ModalDialog'])
.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,
UpdateTaskStatus, DrawGraph, LoadHostSummary, JobIsFinished, AddNewTask, AddNewPlay) {
UpdateTaskStatus, JobIsFinished, AddNewTask, AddNewPlay, longDateFilter) {
return function(params) {
var scope = params.scope,
@@ -185,7 +185,7 @@ export default
};
}])
.factory('GetElapsed', [ function() {
.factory('GetElapsed', [function() {
return function(params) {
var start = params.start,
end = params.end,
@@ -299,7 +299,7 @@ export default
};
}])
.factory('AddNewTask', ['DrawGraph', 'UpdatePlayStatus', 'SetActivePlay', 'SetActiveTask', function(DrawGraph, UpdatePlayStatus, SetActivePlay, SetActiveTask) {
.factory('AddNewTask', ['UpdatePlayStatus', 'SetActivePlay', 'SetActiveTask', function(UpdatePlayStatus, SetActivePlay, SetActiveTask) {
return function(params) {
var scope = params.scope,
event = params.event,
@@ -363,15 +363,15 @@ export default
scope.job_status.status = 'failed';
}
if (JobIsFinished(scope) && !Empty(modified)) {
scope.job_status.finished = modified;
scope.job_status.finished = longDateFilter(modified)
}
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)) {
scope.job_status.elapsed = GetElapsed({
start: scope.job_status.started,
end: scope.job_status.finished
start: started,
end: finished
});
}
};
@@ -488,15 +488,6 @@ export default
counter = params.counter,
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) {
scope.jobData.hostSummaries[host_id].ok += (status === 'successful') ? 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'
};
}
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({
scope: scope,
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_status === 'failed') ? '&failed=true' : '';
url += '&page_size=' + scope.tasksMaxRows + '&order=id';
scope.plays.every(function(p, idx) {
if (p.id === scope.selectedPlay) {
play = scope.plays[idx];
@@ -931,7 +898,7 @@ export default
}])
// Call when the selected task needs to change
.factory('SelectTask', ['LoadHosts', function(LoadHosts) {
.factory('SelectTask', ['JobDetailService', function(JobDetailService) {
return function(params) {
var scope = params.scope,
id = params.id,
@@ -946,239 +913,52 @@ export default
scope.tasks[idx].taskActiveClass = '';
}
});
LoadHosts({
scope: scope,
callback: callback,
clear: true
});
var params = {
parent: scope.selectedTask,
event__startswith: 'runner',
page_size: scope.hostResultsMaxRows,
order: 'host_name,counter',
};
}])
// 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
});
}
});
JobDetailService.getRelatedJobEvents(scope.job.id, params).success(function(res){
scope.hostResults = JobDetailService.processHostEvents(res.results)
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) {
return function(params) {
var scope = params.scope,
var count = params.count,
graph_data = [];
// Ready the data
if (scope.host_summary.ok) {
if (count.ok.length > 0) {
graph_data.push({
label: 'OK',
value: scope.host_summary.ok,
value: count.ok.length,
color: '#60D66F'
});
}
if (scope.host_summary.changed) {
if (count.changed.length > 0) {
graph_data.push({
label: 'CHANGED',
value: scope.host_summary.changed,
value: count.changed.length,
color: '#FF9900'
});
}
if (scope.host_summary.unreachable) {
if (count.unreachable.length > 0) {
graph_data.push({
label: 'UNREACHABLE',
value: scope.host_summary.unreachable,
value: count.unreachable.length,
color: '#FF0000'
});
}
if (scope.host_summary.failed) {
if (count.failures.length > 0) {
graph_data.push({
label: 'FAILED',
value: scope.host_summary.failed,
value: count.failures.length,
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({
data: graph_data
});
@@ -1220,44 +1000,42 @@ export default
"font-family": 'Open Sans',
"font-style": "normal",
"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])
.attr("class", "DashboardGraphs-hostStatusLabel--successful")
.attr("class", "HostSummary-graph--successful")
.style({
"font-family": 'Open Sans',
"text-anchor": "start",
"font-size": "16px",
"text-transform" : "uppercase",
"fill" : colors[0],
"src": "url(/static/assets/OpenSans-Regular.ttf)"
});
d3.select(element.find(".nv-label text")[1])
.attr("class", "DashboardGraphs-hostStatusLabel--failed")
.attr("class", "HostSummary-graph--changed")
.style({
"font-family": 'Open Sans',
"text-anchor" : "end !imporant",
"font-size": "16px",
"text-transform" : "uppercase",
"fill" : colors[1],
"src": "url(/static/assets/OpenSans-Regular.ttf)"
});
d3.select(element.find(".nv-label text")[2])
.attr("class", "DashboardGraphs-hostStatusLabel--successful")
.attr("class", "HostSummary-graph--failed")
.style({
"font-family": 'Open Sans',
"text-anchor" : "end !imporant",
"font-size": "16px",
"text-transform" : "uppercase",
"fill" : colors[2],
"src": "url(/static/assets/OpenSans-Regular.ttf)"
});
d3.select(element.find(".nv-label text")[3])
.attr("class", "DashboardGraphs-hostStatusLabel--failed")
.attr("class", "HostSummary-graph--unreachable")
.style({
"font-family": 'Open Sans',
"text-anchor" : "end !imporant",
"font-size": "16px",
"text-transform" : "uppercase",
"fill" : colors[3],
@@ -1274,7 +1052,8 @@ export default
idx = 0,
result = [],
newKeys = [],
plays = JSON.parse(JSON.stringify(scope.jobData.plays)),
//plays = JSON.parse(JSON.stringify(scope.jobData.plays)),
plays = scope.jobData.plays,
filteredListX = [],
filteredListA = [],
filteredListB = [],
@@ -1363,7 +1142,8 @@ export default
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
for (key in tasks) {
@@ -1437,7 +1217,8 @@ export default
if (scope.activePlay && scope.activeTask && scope.jobData.plays[scope.activePlay] &&
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) {
for (key in hostResults) {
@@ -1498,88 +1279,20 @@ export default
};
}])
.factory('DrawHostSummaries', [ function() {
return function(params) {
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) {
.factory('UpdateDOM', ['DrawPlays', 'DrawTasks', 'DrawHostResults',
function(DrawPlays, DrawTasks, DrawHostResults) {
return function(params) {
var scope = params.scope;
if (!scope.pauseLiveEvents) {
DrawPlays({ scope: scope });
DrawTasks({ scope: scope });
DrawHostResults({ scope: scope });
}
DrawHostSummaries({ scope: scope });
setTimeout(function() {
scope.playsLoading = false;
scope.tasksLoading = false;
scope.hostResultsLoading = false;
scope.LoadHostSummaries = false;
},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
.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',
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) {
return function (params) {
var //parent_scope = params.scope,
@@ -803,7 +803,8 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm,
var job = data.job || data.system_job;
if((scope.portalMode===false || scope.$parent.portalMode===false ) && Empty(data.system_job) ||
(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) {
var goToJobDetails = function(state) {
$state.go(state, {id: job.id});
$state.go(state, {id: job.id}, {reload:true});
}
switch(job.type) {

View File

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

View File

@@ -12,9 +12,9 @@
<span class="HostEvent-field--label">STATUS</span>
<span class="HostEvent-field--content">
<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>
{{event.status || "No result found"}}
{{processEventStatus(event).status || "No result found"}}
</span>
</div>
<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-content">
<!-- modal body -->
@@ -14,8 +14,7 @@
<!-- 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.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="event.timing" ui-sref="jobDetail.host-event.timing" type="button" class="btn btn-sm btn-default ">Timing</button>
<button ng-show="stdout" ui-sref="jobDetail.host-event.stdout" type="button" class="btn btn-sm btn-default ">Standard Out</button>
</div>
<div class="HostEvent-body">

View File

@@ -1,13 +1,2 @@
<div class="EventHost-stdoutPanel Panel">
<div class="StandardOut-panelHeader">
<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>
<textarea id="HostEvent-stdout" class="HostEvent-stdout">
</textarea>

View File

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

View File

@@ -4,63 +4,71 @@
* All Rights Reserved
*************************************************/
export default
['$stateParams', '$scope', '$state', 'Wait', 'JobDetailService', 'moment', 'event',
function($stateParams, $scope, $state, Wait, JobDetailService, moment, event){
['$stateParams', '$scope', '$state', 'Wait', 'JobDetailService', 'event',
function($stateParams, $scope, $state, Wait, JobDetailService, event){
$scope.processEventStatus = JobDetailService.processEventStatus;
$scope.hostResults = [];
// Avoid rendering objects in the details fieldset
// ng-if="processResults(value)" via host-event-details.partial.html
$scope.processResults = function(value){
if (typeof value == 'object'){return false}
else {return true}
if (typeof value == 'object'){return false;}
else {return true;}
};
var codeMirror = function(){
var el = $('#HostEvent-json')[0];
var editor = CodeMirror.fromTextArea(el, {
var codeMirror = function(el, json){
var container = $(el)[0];
var editor = CodeMirror.fromTextArea(container, {
lineNumbers: 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(){
var result = $scope.hostResults.filter(function( obj ) {
return obj.id == $scope.event.id;
});
return $scope.hostResults.indexOf(result[0])
return $scope.hostResults.indexOf(result[0]);
};
$scope.showPrev = function(){
return $scope.getActiveHostIndex() != 0
return $scope.getActiveHostIndex() != 0;
};
$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(){
var index = $scope.getActiveHostIndex() + 1;
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(){
var index = $scope.getActiveHostIndex() - 1;
var id = $scope.hostResults[index].id;
$state.go('jobDetail.host-event.details', {eventId: id})
$state.go('jobDetail.host-event.details', {eventId: id});
};
var init = function(){
$scope.event = event.data.results[0];
$scope.event.created = moment($scope.event.created).format();
$scope.processEventStatus = JobDetailService.processEventStatus($scope.event);
$scope.hostResults = $stateParams.hostResults;
$scope.event = event;
JobDetailService.getJobEventChildren($stateParams.taskId).success(function(res){
$scope.hostResults = res.results;
});
$scope.json = JobDetailService.processJson($scope.event);
if ($state.current.name == 'jobDetail.host-event.json'){
codeMirror();
codeMirror('#HostEvent-json', $scope.json);
}
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){
$scope.sdout = null;

View File

@@ -8,23 +8,20 @@
var hostEventModal = {
name: 'jobDetail.host-event',
url: '/host-event/:eventId',
url: '/task/:taskId/host-event/:eventId',
controller: 'HostEventController',
params:{
hostResults: {
value: null,
squash: false,
}
},
templateUrl: templateUrl('job-detail/host-event/host-event-modal'),
resolve: {
features: ['FeaturesService', function(FeaturesService){
return FeaturesService.get();
}],
event: ['JobDetailService','$stateParams', function(JobDetailService, $stateParams) {
event: ['JobDetailService','$stateParams', 'moment', function(JobDetailService, $stateParams, moment) {
return JobDetailService.getRelatedJobEvents($stateParams.id, {
id: $stateParams.eventId
}).success(function(res){ return res.results[0]})
id: $stateParams.eventId,
}).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){
@@ -35,52 +32,27 @@ var hostEventModal = {
$('.modal-backdrop').remove();
$('body').removeClass('modal-open');
}
}
};
var hostEventDetails = {
name: 'jobDetail.host-event.details',
url: '/details',
controller: 'HostEventController',
templateUrl: templateUrl('job-detail/host-event/host-event-details'),
resolve: {
features: ['FeaturesService', function(FeaturesService){
return FeaturesService.get();
}]
}
}
};
var hostEventJson = {
name: 'jobDetail.host-event.json',
url: '/json',
controller: 'HostEventController',
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();
}]
}
templateUrl: templateUrl('job-detail/host-event/host-event-json')
};
var hostEventStdout = {
name: 'jobDetail.host-event.stdout',
url: '/stdout',
controller: 'HostEventController',
templateUrl: templateUrl('job-detail/host-event/host-event-stdout'),
resolve: {
features: ['FeaturesService', function(FeaturesService){
return FeaturesService.get();
}]
}
templateUrl: templateUrl('job-detail/host-event/host-event-stdout')
};
export {hostEventDetails, hostEventJson, hostEventTiming, hostEventStdout, hostEventModal}
export {hostEventDetails, hostEventJson, hostEventStdout, hostEventModal}

View File

@@ -4,7 +4,7 @@
* All Rights Reserved
*************************************************/
import {hostEventModal, hostEventDetails, hostEventTiming,
import {hostEventModal, hostEventDetails,
hostEventJson, hostEventStdout} from './host-event.route';
import controller from './host-event.controller';
@@ -15,7 +15,6 @@
.run(['$stateExtender', function($stateExtender){
$stateExtender.addState(hostEventModal);
$stateExtender.addState(hostEventDetails);
$stateExtender.addState(hostEventTiming);
$stateExtender.addState(hostEventJson);
$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.default.less";
.HostEvents .CodeMirror{
border: none;
}
.HostEvents .modal-footer{
border: 0;
margin-top: 0px;

View File

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

View File

@@ -36,9 +36,9 @@
<td class=HostEvents-table--cell>
<!-- status circles -->
<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>
{{event.status}}
{{processEventStatus(event).status}}
</td>
<td class=HostEvents-table--cell>{{event.play}}</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.default.less';
@import '../shared/layouts/one-plus-one.less';
@breakpoint-md: 1200px;
@breakpoint-sm: 420px;
.JobDetail{
display: flex;
flex-direction: row;
.OnePlusOne-container(100%, @breakpoint-md);
}
.JobDetail-leftSide{
flex: 1 0 auto;
width: 50%;
padding-right: 20px;
.OnePlusOne-panel--left(100%, @breakpoint-md);
}
.JobDetail-rightSide{
flex: 1 0 auto;
width: 50%;
.OnePlusOne-panel--right(100%, @breakpoint-md);
}
.JobDetail-panelHeader{
display: flex;
height: 30px;
}
.JobDetail-expandContainer{
flex: 1;
margin: 0px;
@@ -62,11 +60,17 @@
flex-wrap: wrap;
flex-direction: row;
padding-top: 25px;
@media screen and(max-width: @breakpoint-sm){
flex-direction: column;
}
}
.JobDetail-resultRow{
width: 50%;
display: flex;
@media screen and(max-width: @breakpoint-sm){
width: 100%;
}
}
.JobDetail-resultRowLabel{
@@ -78,6 +82,9 @@
font-size: 14px;
font-weight: normal!important;
flex: 1 0 auto;
@media screen and(max-width: @breakpoint-md){
flex: 2.5 0 auto;
}
}
.JobDetail-resultRow--variables{
@@ -109,10 +116,16 @@
flex-direction: row;
height: 50px;
margin-top: 25px;
@media screen and(max-width: @breakpoint-sm){
height: auto;
}
}
.JobDetail-searchContainer{
flex: 1 0 auto;
flex: 2 0 auto;
@media screen and(max-width: @breakpoint-sm){
margin-bottom: 0px;
}
}
.JobDetail-tableToggleContainer{

View File

@@ -14,18 +14,17 @@ export default
[ '$location', '$rootScope', '$filter', '$scope', '$compile',
'$stateParams', '$log', 'ClearScope', 'GetBasePath', 'Wait',
'ProcessErrors', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed',
'DrawGraph', 'LoadHostSummary', 'ReloadHostSummaryList',
'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'DeleteJob', 'PlaybookRun',
'LoadPlays', 'LoadTasks', 'LoadHosts', 'HostsEdit',
'LoadPlays', 'LoadTasks', 'HostsEdit',
'ParseVariableString', 'GetChoices', 'fieldChoices', 'fieldLabels',
'EditSchedule', 'ParseTypeChange', 'JobDetailService',
function(
$location, $rootScope, $filter, $scope, $compile, $stateParams,
$log, ClearScope, GetBasePath, Wait, ProcessErrors,
SelectPlay, SelectTask, Socket, GetElapsed, DrawGraph,
LoadHostSummary, ReloadHostSummaryList, JobIsFinished,
SelectPlay, SelectTask, Socket, GetElapsed,
JobIsFinished,
SetTaskStyles, DigestEvent, UpdateDOM, DeleteJob,
PlaybookRun, LoadPlays, LoadTasks, LoadHosts,
PlaybookRun, LoadPlays, LoadTasks,
HostsEdit, ParseVariableString, GetChoices, fieldChoices,
fieldLabels, EditSchedule, ParseTypeChange, JobDetailService
) {
@@ -82,38 +81,6 @@ export default
}
});
scope.hosts = [];
scope.$watch('hosts', function(hosts) {
for (var host in hosts) {
if (hosts[host].ok) {
hosts[host].okTip = hosts[host].ok;
hosts[host].okTip += (hosts[host].ok === 1) ? " host event was" : " host events were";
hosts[host].okTip += " ok.";
} else {
hosts[host].okTip = "No host events were ok.";
}
if (hosts[host].changed) {
hosts[host].changedTip = hosts[host].changed;
hosts[host].changedTip += (hosts[host].changed === 1) ? " host event" : " host events";
hosts[host].changedTip += " changed.";
} else {
hosts[host].changedTip = "No host events changed.";
}
if (hosts[host].failed) {
hosts[host].failedTip = hosts[host].failed;
hosts[host].failedTip += (hosts[host].failed === 1) ? " host event" : " host events";
hosts[host].failedTip += " failed.";
} else {
hosts[host].failedTip = "No host events failed.";
}
if (hosts[host].unreachable) {
hosts[host].unreachableTip = hosts[host].unreachable;
hosts[host].unreachableTip += (hosts[host].unreachable === 1) ? " host event was" : " hosts events were";
hosts[host].unreachableTip += " unreachable";
} else {
hosts[host].unreachableTip = "No host events were unreachable.";
}
}
});
scope.tasks = [];
scope.$watch('tasks', function(tasks) {
for (var task in tasks) {
@@ -169,7 +136,6 @@ export default
scope.hostResults = [];
scope.hostResultsMaxRows = 200;
scope.hostSummariesMaxRows = 200;
scope.tasksMaxRows = 200;
scope.playsMaxRows = 200;
@@ -177,7 +143,6 @@ export default
scope.playsLoading = true;
scope.tasksLoading = true;
scope.hostResultsLoading = true;
scope.hostSummariesLoading = true;
// Turn on the 'Waiting...' message until events begin arriving
scope.waiting = true;
@@ -192,11 +157,9 @@ export default
scope.searchPlaysEnabled = true;
scope.searchTasksEnabled = true;
scope.searchHostsEnabled = true;
scope.searchHostSummaryEnabled = true;
scope.search_play_status = 'all';
scope.search_task_status = 'all';
scope.search_host_status = 'all';
scope.search_host_summary_status = 'all';
scope.haltEventQueue = false;
scope.processing = false;
@@ -204,13 +167,6 @@ export default
scope.lessDetail = false;
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.hostSummaries = {};
@@ -230,7 +186,6 @@ export default
url: GetBasePath('unified_jobs'),
field: 'status',
variable: 'status_choices',
// callback: 'choicesReady'
});
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;
DigestEvent({ scope: scope, event: data });
}
UpdateDOM({ scope: scope });
});
}
openSocket();
@@ -258,9 +214,6 @@ export default
if (data.status === 'failed' || data.status === 'canceled' ||
data.status === 'error' || data.status === 'successful' || data.status === 'running') {
$scope.liveEventProcessing = false;
if ($rootScope.jobDetailInterval) {
window.clearInterval($rootScope.jobDetailInterval);
}
if (!scope.pauseLiveEvents) {
$scope.$emit('LoadJob'); //this is what is used for the refresh
}
@@ -274,10 +227,9 @@ export default
$rootScope.removeJobSummaryComplete = $rootScope.$on('JobSummaryComplete', function() {
// the job host summary should now be available from the API
$log.debug('Trigging reload of job_host_summaries');
scope.$emit('LoadHostSummaries');
scope.$emit('InitialLoadComplete');
});
if (scope.removeInitialLoadComplete) {
scope.removeInitialLoadComplete();
}
@@ -292,73 +244,19 @@ export default
};
JobDetailService.getRelatedJobEvents(scope.job.id, params)
.success(function(data) {
if (data.results.length > 0) {
LoadHostSummary({
scope: scope,
data: data.results[0].event_data
});
}
UpdateDOM({ scope: scope });
})
.error(function(data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
if ($rootScope.jobDetailInterval) {
window.clearInterval($rootScope.jobDetailInterval);
}
$log.debug('Job completed!');
$log.debug(scope.jobData);
}
else {
api_complete = true; //trigger events to start processing
if ($rootScope.jobDetailInterval) {
window.clearInterval($rootScope.jobDetailInterval);
UpdateDOM({ scope: scope})
}
$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) {
@@ -376,81 +274,22 @@ export default
var params = {
parent: task.id,
event__startswith: 'runner',
page_size: scope.hostResultsMaxRows
};
JobDetailService.getRelatedJobEvents(scope.job.id, params)
.success(function(data) {
var idx, event, status, status_text, item, msg;
var event, status, status_text, item, msg;
if (data.results.length > 0) {
lastEventId = data.results[0].id;
}
scope.next_host_results = data.next;
for (idx=data.results.length - 1; idx >= 0; idx--) {
event = data.results[idx];
if (event.event === "runner_on_skipped") {
status = 'skipped';
}
else if (event.event === "runner_on_unreachable") {
status = 'unreachable';
}
else {
status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful';
}
switch(status) {
case "successful":
status_text = 'OK';
break;
case "changed":
status_text = "Changed";
break;
case "failed":
status_text = "Failed";
break;
case "unreachable":
status_text = "Unreachable";
break;
case "skipped":
status_text = "Skipped";
}
if (event.event_data && event.event_data.res) {
item = event.event_data.res.item;
if (typeof item === "object") {
item = JSON.stringify(item);
}
}
msg = '';
if (event.event_data && event.event_data.res) {
if (typeof event.event_data.res === 'object') {
msg = event.event_data.res.msg;
} else {
msg = event.event_data.res;
}
}
if (event.event !== "runner_on_no_hosts") {
task.hostResults[event.id] = {
id: event.id,
status: status,
status_text: status_text,
host_id: event.host,
task_id: event.parent,
name: event.event_data.host,
created: event.created,
msg: msg,
counter: event.counter,
item: item
};
}
}
scope.$emit('LoadHostSummaries');
task.hostResults = JobDetailService.processHostEvents(data.results);
scope.$emit('InitialLoadComplete');
});
} else {
scope.$emit('LoadHostSummaries');
scope.$emit('InitialLoadComplete');
}
} else {
scope.$emit('LoadHostSummaries');
scope.$emit('InitialLoadComplete');
}
});
@@ -559,10 +398,10 @@ export default
msg: 'Call to ' + url + '. GET returned: ' + status });
});
} else {
scope.$emit('LoadHostSummaries');
scope.$emit('InitialLoadComplete');
}
} else {
scope.$emit('LoadHostSummaries');
scope.$emit('InitialLoadComplete');
}
});
@@ -570,12 +409,6 @@ export default
scope.removeLoadPlays();
}
scope.removeLoadPlays = scope.$on('LoadPlays', function(e, events_url) {
scope.host_summary.ok = 0;
scope.host_summary.changed = 0;
scope.host_summary.unreachable = 0;
scope.host_summary.failed = 0;
scope.host_summary.total = 0;
scope.jobData.plays = {};
var params = {
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_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]) {
scope.jobData.plays[scope.activePlay].playActiveClass = 'JobDetail-tableRow--selected';
@@ -685,7 +511,6 @@ export default
scope.playsLoading = true;
scope.tasksLoading = true;
scope.hostResultsLoading = true;
scope.LoadHostSummaries = true;
// Load the job record
JobDetailService.getJob(job_id)
@@ -769,7 +594,6 @@ export default
scope.playsLoading = false;
scope.tasksLoading = false;
scope.hostResultsLoading = false;
scope.hostSummariesLoading = false;
}
else {
scope.job_status.finished = null;
@@ -811,10 +635,6 @@ export default
// First time. User just loaded page.
scope.$emit('LoadJob');
}
else {
// Check if the graph needs to redraw
setTimeout(function() { DrawGraph({ scope: scope, resize: true }); }, 500);
}
});
scope.adjustSize = function() {
@@ -859,11 +679,6 @@ export default
$('#tasks-table-detail').height(120 + (height * 0.20));
$('#hosts-table-detail').height(150 + (height * 0.70));
}
// Summary table height adjusting.
height = ($('#job-detail-container').height() / 2) - $('#hosts-summary-section .JobDetail-searchHeaderRow').outerHeight() -
$('#hosts-summary-section .table-header').outerHeight() - 20;
// $('#hosts-summary-table').height(height);
//$('#hosts-summary-table').mCustomScrollbar("update");
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) {
if (angular.isObject(obj)) {
return (Object.keys(obj).length > 0) ? false : true;
@@ -965,6 +734,17 @@ export default
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() {
if (!scope.lessStatus) {
$('#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.search_play_status = (scope.search_play_status === 'all') ? 'failed' : 'all';
if (!scope.liveEventProcessing || scope.pauseLiveEvents) {
@@ -1037,6 +805,9 @@ export default
scope.searchTasksEnabled = true;
}
if (!scope.liveEventProcessing || scope.pauseLiveEvents) {
if (scope.search_task_status === 'failed'){
params.failed = true;
}
LoadTasks({
scope: scope
});
@@ -1058,66 +829,24 @@ export default
scope.searchHostsEnabled = true;
}
if (!scope.liveEventProcessing || scope.pauseLiveEvents) {
LoadHosts({
scope: scope
scope.hostResultsLoading = true;
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) {
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.$emit('LoadJob');
};

View File

@@ -62,7 +62,7 @@
<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>
<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 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">
<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 class="form-group JobDetail-resultRow toggle-show" ng-show="created_by">
@@ -344,7 +344,7 @@
<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">
<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 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>
@@ -388,76 +388,11 @@
</div>
</div>
<div id="events-summary" style="display:none">
<div id="hosts-summary-section" class="section">
<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>
<!-- Host Summary view -->
<div id="events-summary" ng-hide="lessEvents">
<div ui-view="host-summary@jobDetail"></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-->

View File

@@ -1,16 +1,15 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {templateUrl} from '../shared/template-url/template-url.factory';
import HostSummaryController from './host-summary/host-summary.controller';
export default {
name: 'jobDetail',
url: '/jobs/:id',
templateUrl: templateUrl('job-detail/job-detail'),
controller: 'JobDetailController',
ncyBreadcrumb: {
parent: 'jobs',
label: "{{ job.id }} - {{ job.name }}"
@@ -26,10 +25,32 @@ export default {
endpoint: "job_events"
});
$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;
} else {
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 {
/*
For ES6
it might be useful to set some default params here, e.g.
getJobHostSummaries: function(id, page_size=200, order='host_name'){}
without ES6, we'd have to supply defaults like this:
this.page_size = params.page_size ? params.page_size : 200;
* For ES6
* it might be useful to set some default params here, e.g.
* getJobHostSummaries: function(id, page_size=200, order='host_name'){}
* without ES6, we'd have to supply defaults like this:
* this.page_size = params.page_size ? params.page_size : 200;
*/
// the the API passes through Ansible's event_data response
// we need to massage away the verbose and redundant properties
processJson: function(data){
// a deep copy
var result = $.extend(true, {}, data);
@@ -28,7 +27,7 @@ export default
// remove ignored properties
Object.keys(result).forEach(function(key, index){
if (ignored.indexOf(key) > -1) {
delete result[key]
delete result[key];
}
});
@@ -37,51 +36,93 @@ export default
result.event_data = {};
Object.keys(data.event_data.res).forEach(function(key, index){
if (ignored.indexOf(key) > -1) {
return
return;
}
else{
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
// the stack for which status to display is
// unreachable > failed > changed > ok
// uses the API's runner events and convenience properties .failed .changed to determine status.
// see: job_event_callback.py
if (event.event == 'runner_on_unreachable'){
event.status = 'Unreachable';
return 'HostEvents-status--unreachable'
// see: job_event_callback.py for more filters to support
processEventStatus: function(event){
if (event.event === 'runner_on_unreachable'){
return {
class: 'HostEvents-status--unreachable',
status: 'unreachable'
};
}
// equiv to 'runner_on_error' && 'runner on failed'
if (event.failed){
event.status = 'Failed';
return 'HostEvents-status--failed'
return {
class: 'HostEvents-status--failed',
status: 'failed'
}
}
// catch the changed case before ok, because both can be true
if (event.changed){
event.status = 'Changed';
return 'HostEvents-status--changed'
return {
class: 'HostEvents-status--changed',
status: 'changed'
};
}
if (event.event == 'runner_on_ok'){
event.status = 'OK';
return 'HostEvents-status--ok'
if (event.event === 'runner_on_ok' || event.event === 'runner_on_async_ok'){
return {
class: 'HostEvents-status--ok',
status: 'ok'
};
}
if (event.event == 'runner_on_skipped'){
event.status = 'Skipped';
return 'HostEvents-status--skipped'
}
else{
// study a case where none of these apply
if (event.event === 'runner_on_skipped'){
return {
class: 'HostEvents-status--skipped',
status: 'skipped'
};
}
},
// Consumes a response from this.getRelatedJobEvents(id, params)
// returns an array for view logic to iterate over to build host result rows
processHostEvents: function(data){
var self = this;
var results = [];
data.forEach(function(event){
if (event.event !== 'runner_on_no_hosts'){
var status = self.processEventStatus(event);
var msg = self.processEventMsg(event);
var item = self.processEventItem(event);
results.push({
id: event.id,
status: status.status,
status_text: _.head(status.status).toUpperCase() + _.tail(status.status),
host_id: event.host,
task_id: event.parent,
name: event.event_data.host,
created: event.created,
msg: typeof msg === 'undefined' ? undefined : msg,
item: typeof item === 'undefined' ? undefined : item
});
}
});
return results;
},
// GET events related to a job run
// e.g.
// ?event=playbook_on_stats
@@ -95,6 +136,19 @@ export default
url = url + '&' + key + '=' + params[key];
});
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data;
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
},
getJobEventChildren: function(id){
var url = GetBasePath('job_events');
url = url + id + '/children/';
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data
@@ -108,7 +162,7 @@ export default
// e.g. ?page_size=200&order=host_name
getJobHostSummaries: function(id, params){
var url = GetBasePath('jobs');
url = url + id + '/job_host_summaries/?'
url = url + id + '/job_host_summaries/?';
Object.keys(params).forEach(function(key, index) {
// the API is tolerant of extra ampersands
url = url + '&' + key + '=' + params[key];
@@ -116,7 +170,7 @@ export default
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data
return data;
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
@@ -135,7 +189,7 @@ export default
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data
return data;
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
@@ -152,7 +206,7 @@ export default
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data
return data;
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
@@ -165,7 +219,7 @@ export default
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data
return data;
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
@@ -176,15 +230,15 @@ export default
// expects 'next' param returned by the API e.g.
// "/api/v1/jobs/51/job_plays/?order_by=id&page=2&page_size=1"
getNextPage: function(url){
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data
return data;
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
}
}
}
];
};
}];

View File

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

View File

@@ -11,14 +11,12 @@ export default
name: 'job_templates',
iterator: 'job_template',
// selectTitle: 'Add Job Template',
editTitle: '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,
hover: true,
well: true,
searchSize: 'col-lg-8 col-md-8 col-sm-12 col-xs-12',
fields: {
name: {

View File

@@ -16,6 +16,7 @@ export default
hover: true,
well: true,
listTitle: 'Jobs',
searchSize: 'col-lg-8 col-md-8 col-sm-12 col-xs-12',
fields: {
status: {
@@ -27,23 +28,9 @@ export default
searchable: true,
nosort: true,
searchType: 'select',
searchOptions: [
{ name: "Success", value: "successful" },
{ name: "Error", value: "error" },
{ name: "Failed", value: "failed" },
{ name: "Canceled", value: "canceled" }
]
searchOptions: [],
searchLabel: 'Status'
},
/*
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: {
key: true,
label: 'Name',

View File

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

View File

@@ -51,9 +51,6 @@ export default
if (scope.searchCleanup) {
scope.searchCleanup();
}
// if (!Empty(parent_scope) && parent_scope.restoreSearch) {
// parent_scope.restoreSearch();
// }
else {
Wait('stop');
}
@@ -69,6 +66,7 @@ export default
height: 470,
minWidth: 200,
callback: 'PromptForDaysFacts',
resizable: false,
onOpen: function(){
scope.$watch('prompt_for_days_facts_form.$invalid', function(invalid) {
if (invalid === true) {
@@ -113,16 +111,8 @@ export default
fieldScope.keep_amount = 30;
fieldScope.granularity_keep_amount = 1;
},
buttons: [{
"label": "Cancel",
"onClick": function() {
$(this).dialog('close');
},
"icon": "fa-times",
"class": "btn btn-default",
"id": "prompt-for-days-facts-cancel"
},{
buttons: [
{
"label": "Launch",
"onClick": function() {
var extra_vars = {
@@ -145,9 +135,16 @@ export default
msg: 'Failed updating job ' + scope.job_template_id + ' with variables. POST returned: ' + status });
});
},
"icon": "fa-rocket",
"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');
if(this.configure_job.job_type === "cleanup_facts"){
scope.submitCleanupJob(id, name);
}
else {
defaultUrl = GetBasePath('system_job_templates')+id+'/launch/';
CreateDialog({
id: 'prompt-for-days' ,
@@ -177,6 +170,7 @@ export default
height: 300,
minWidth: 200,
callback: 'PromptForDays',
resizable: false,
onOpen: function(){
scope.$watch('prompt_for_days_form.$invalid', function(invalid) {
if (invalid === true) {
@@ -191,16 +185,8 @@ export default
scope.prompt_for_days_form.$setPristine();
scope.prompt_for_days_form.$invalid = false;
},
buttons: [{
"label": "Cancel",
"onClick": function() {
$(this).dialog('close');
},
"icon": "fa-times",
"class": "btn btn-default",
"id": "prompt-for-days-cancel"
},{
buttons: [
{
"label": "Launch",
"onClick": function() {
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 });
});
},
"icon": "fa-rocket",
"class": "btn btn-primary",
"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');
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>
<div class="MgmtCards-actionItems">
<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>
</button>
<button class="MgmtCards-actionItem List-actionButton"

View File

@@ -114,3 +114,12 @@
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({})
.then(function () {
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 () {
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',
mode: 'edit',
scope: $scope,
searchSize: 'col-md-10 col-xs-12'
});
SearchInit({

View File

@@ -34,6 +34,14 @@ export default
resolve: {
features: ['FeaturesService', function(FeaturesService) {
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.hideForm = false;
$scope = angular.extend($scope, scope);
@@ -41,10 +41,35 @@ export default ['$compile', '$state', '$stateParams', 'AddSchedule', 'Wait', '$s
$scope.hideForm = true;
$scope.formCancel = function() {
$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({
scope: $scope,
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.hideForm = false;
$scope = angular.extend($scope, scope);
@@ -41,13 +41,49 @@ export default ['$compile', '$state', '$stateParams', 'EditSchedule', 'Wait', '$
});
$scope.isEdit = true;
$scope.hideForm = true;
$scope.parseType = 'yaml';
$scope.formCancel = function() {
$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({
scope: $scope,
id: parseInt($stateParams.schedule_id),

View File

@@ -9,7 +9,6 @@
</div>
</div>
<div id="SchedulerFormTarget">
<form class="form Form"
role="form"
name="scheduler_form_new"
@@ -526,47 +525,6 @@
<div class="RepeatFrequencyOptions-subFormBorderFixer"
ng-show="schedulerFrequency.value && schedulerFrequency.value !== 'none'">
</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>
<div class="SchedulerFormDetail-container
SchedulerFormDetail-container--error"
@@ -632,8 +590,33 @@
{{ occurrence.local }}
</li>
</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>
</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">
<button type="button"
class="btn btn-sm Form-saveButton"

View File

@@ -2,7 +2,8 @@ export default ['$scope', 'Refresh', 'tagSearchService',
function($scope, Refresh, tagSearchService) {
// JSONify passed field elements that can be searched
$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
tagSearchService.getSearchTypes($scope.list, $scope.endpoint)
.then(function(searchTypes) {

View File

@@ -1,5 +1,5 @@
<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-typeDropdown"
ng-click="toggleTypeDropdown()"

View File

@@ -35,6 +35,10 @@ angular.module('ApiLoader', ['Utilities'])
.success(function (data) {
data.base = base;
$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);
})
.error(function (data, status) {

View File

@@ -171,10 +171,11 @@
<div style="padding-bottom:15px;">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.<br>
Caution: Setting both numerical variables to "0" will delete all facts.<br>
weekly fact scan is kept.<br> <br>
CAUTION: Setting both numerical variables to "0" will delete all facts.<br><br>
</div>
<div class="form-group ">
<div class="form-group">
<label for="description">
<span class="label-text">
Select a time period after which to remove old facts