Job detail page

Built new event viewer. Based on LogViewer.js that provides a common log viewing dialog.  Event viewer dialog has the same look and feel.
This commit is contained in:
Chris Houseknecht 2014-07-01 13:43:24 -04:00
parent 52a463305f
commit 254c552734
9 changed files with 362 additions and 126 deletions

View File

@ -98,6 +98,7 @@ angular.module('Tower', [
'LogViewerStatusDefinition',
'LogViewerHelper',
'LogViewerOptionsDefinition',
'EventViewerHelper',
'JobDetailHelper',
'SocketIO'
])

View File

@ -9,7 +9,7 @@
function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log, ClearScope, Breadcrumbs, LoadBreadCrumbs, GetBasePath, Wait, Rest,
ProcessErrors, SelectPlay, SelectTask, Socket, GetElapsed, FilterAllByHostName, DrawGraph, LoadHostSummary, ReloadHostSummaryList,
JobIsFinished, SetTaskStyles, DigestEvent, UpdateDOM, ViewHostResults) {
JobIsFinished, SetTaskStyles, DigestEvent, UpdateDOM, EventViewer) {
ClearScope();
@ -1155,9 +1155,10 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log,
};
scope.viewHostResults = function(id) {
ViewHostResults({
EventViewer({
scope: scope,
id: id
url: scope.job.related.job_events + '?id=' + id,
title: 'Host Result'
});
};
@ -1165,5 +1166,5 @@ function JobDetailController ($rootScope, $scope, $compile, $routeParams, $log,
JobDetailController.$inject = [ '$rootScope', '$scope', '$compile', '$routeParams', '$log', 'ClearScope', 'Breadcrumbs', 'LoadBreadCrumbs', 'GetBasePath',
'Wait', 'Rest', 'ProcessErrors', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed', 'FilterAllByHostName', 'DrawGraph',
'LoadHostSummary', 'ReloadHostSummaryList', 'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'ViewHostResults'
'LoadHostSummary', 'ReloadHostSummaryList', 'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'EventViewer'
];

View File

@ -0,0 +1,298 @@
/*********************************************
* Copyright (c) 2014 AnsibleWorks, Inc.
*
* LogViewer.js
*
*/
'use strict';
angular.module('EventViewerHelper', ['ModalDialog', 'Utilities'])
.factory('EventViewer', ['$compile', 'CreateDialog', 'GetEvent', 'Wait', 'AddTable', 'GetBasePath', 'LookUpName', 'Empty', 'AddPreFormattedText',
function($compile, CreateDialog, GetEvent, Wait, AddTable, GetBasePath, LookUpName, Empty, AddPreFormattedText) {
return function(params) {
var parent_scope = params.scope,
url = params.url,
title = params.title, //optional
scope = parent_scope.$new(true);
if (scope.removeModalReady) {
scope.removeModalReady();
}
scope.removeModalReady = scope.$on('ModalReady', function() {
Wait('stop');
$('#eventviewer-modal-dialog').dialog('open');
});
if (scope.removeJobReady) {
scope.removeJobReady();
}
scope.removeEventReady = scope.$on('EventReady', function(e, data) {
var elem;
$('#status-form-container').empty();
$('#stdout-form-container').empty();
$('#stderr-form-container').empty();
$('#traceback-form-container').empty();
$('#eventview-tabs li:eq(1)').hide();
$('#eventview-tabs li:eq(2)').hide();
$('#eventview-tabs li:eq(3)').hide();
AddTable({ scope: scope, id: 'status-form-container', event: data });
if (data.stdout) {
$('#eventview-tabs li:eq(1)').show();
AddPreFormattedText({
id: 'stdout-form-container',
val: data.stdout
});
}
if (data.stderr) {
$('#eventview-tabs li:eq(2)').show();
AddPreFormattedText({
id: 'stderr-form-container',
val: data.stderr
});
}
if (data.traceback) {
$('#eventview-tabs li:eq(3)').show();
AddPreFormattedText({
id: 'traceback-form-container',
val: data.traceback
});
}
elem = angular.element(document.getElementById('eventviewer-modal-dialog'));
$compile(elem)(scope);
CreateDialog({
scope: scope,
width: 675,
height: 600,
minWidth: 450,
callback: 'ModalReady',
id: 'eventviewer-modal-dialog',
// onResizeStop: resizeText,
title: ( (title) ? title : 'Event Details' ),
onOpen: function() {
$('#eventview-tabs a:first').tab('show');
$('#dialog-ok-button').focus();
}
});
});
GetEvent({
url: url,
scope: scope
});
scope.modalOK = function() {
$('#eventviewer-modal-dialog').dialog('close');
scope.$destroy();
};
};
}])
.factory('GetEvent', ['Wait', 'Rest', 'ProcessErrors', function(Wait, Rest, ProcessErrors) {
return function(params) {
var url = params.url,
scope = params.scope;
function getStatus(data) {
return (data.results[0].event === "runner_on_unreachable") ? "unreachable" : (data.results[0].event === "runner_on_skipped") ? 'skipped' : (data.results[0].failed) ? 'failed' :
(data.results[0].changed) ? 'changed' : 'successful';
}
Wait('start');
Rest.setUrl(url);
Rest.get()
.success( function(data) {
var key, event_data = {};
if (data.results.length > 0 && data.results[0].event_data.res) {
for (key in data.results[0].event_data) {
if (key !== "res") {
data.results[0].event_data.res[key] = data.results[0].event_data[key];
}
}
if (data.results[0].event_data.res.ansible_facts) {
// don't show fact gathering results
delete data.results[0].event_data.res.ansible_facts;
}
data.results[0].event_data.res.status = getStatus(data);
event_data = data.results[0].event_data.res;
}
else {
data.results[0].event_data.status = getStatus(data);
event_data = data.results[0].event_data;
}
// convert results to stdout
if (event_data.results && typeof event_data.results === "object" && Array.isArray(event_data.results)) {
event_data.stdout = "";
event_data.results.forEach(function(row) {
event_data.stdout += row + "\n";
});
delete event_data.results;
}
if (event_data.invocation) {
for (key in event_data.invocation) {
event_data[key] = event_data.invocation[key];
}
delete event_data.invocation;
}
event_data.parent = data.results[0].parent;
event_data.play = data.results[0].play;
event_data.task = data.results[0].task;
event_data.created = data.results[0].created;
event_data.role = data.results[0].role;
event_data.host_id = data.results[0].host;
event_data.host_name = data.results[0].host_name;
event_data.id = data.results[0].id;
event_data.parent = data.results[0].parent;
scope.$emit('EventReady', event_data);
})
.error(function(data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to get event ' + url + '. GET returned: ' + status });
});
};
}])
.factory('LookUpName', ['Rest', 'ProcessErrors', 'Empty', function(Rest, ProcessErrors, Empty) {
return function(params) {
var url = params.url,
scope_var = params.scope_var,
scope = params.scope;
Rest.setUrl(url);
Rest.get()
.success(function(data) {
if (scope_var === 'inventory_source') {
scope[scope_var + '_name'] = data.summary_fields.group.name;
}
else if (!Empty(data.name)) {
scope[scope_var + '_name'] = data.name;
}
if (!Empty(data.group)) {
// Used for inventory_source
scope.group = data.group;
}
})
.error(function(data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to retrieve ' + url + '. GET returned: ' + status });
});
};
}])
.factory('AddTable', ['$compile', '$filter', 'Empty', function($compile, $filter, Empty) {
return function(params) {
var scope = params.scope,
id = params.id,
event = params.event,
html = '', e;
function keyToLabel(key) {
var label = '';
switch(key) {
case "id":
label = "Event ID";
break;
case "parent":
label = "Parent Event ID";
break;
case "rc":
label = "Return Code";
break;
default:
label = key.charAt(0).toUpperCase() + key.slice(1);
label = label.replace(/(\_.)/g, function(match) {
var res;
res = match.replace(/\_/,'');
res = ' ' + res.toUpperCase();
return res;
});
}
return label;
}
function parseJSON(obj) {
var html = '', keys;
if (typeof obj === "object") {
html += "<table class=\"table eventviewer-status\">\n";
html += "<tbody>\n";
keys = Object.keys(obj).sort();
keys.forEach(function(key) {
var label;
if (key !== "stdout" && key !== "stderr" && key !== "traceback" && key !== "host_id" && key !== "host") {
label = keyToLabel(key);
if (Empty(obj[key])) {
// exclude empty items
}
else if (typeof obj[key] === "boolean" || typeof obj[key] === "number" || typeof obj[key] === "string") {
html += "<tr><td class=\"key\">" + label + ":</td><td class=\"value\">";
if (key === "status") {
html += "<i class=\"fa icon-job-" + obj[key] + "\"></i> " + obj[key];
}
else if (key === "start" || key === "end" || key === "created") {
html += $filter('date')(obj[key], 'MM/dd/yy HH:mm:ss');
}
else if (key === "host_name") {
html += "<a href=\"#/home/hosts/?id=" + obj.host_id + "\" target=\"_blank\" " +
"aw-tool-tip=\"Click to view host.<br />Opens in new tab or window.\" data-placement=\"right\" " +
"ng-click=\"modalOK()\">" + obj[key] + "</a>";
}
else {
html += obj[key];
}
html += "</td></tr>\n";
}
else if (typeof obj[key] === "object" && Array.isArray(obj[key])) {
html += "<tr><td class=\"key\">" + label + ":</td><td class=\"value\">";
obj[key].forEach(function(row) {
html += "[" + row + "],";
});
html = html.replace(/,$/,'');
html += "</td></tr>\n";
}
else if (typeof obj[key] === "object") {
html += "<tr><td class=\"key\">" + label + ":</td><td class=\"nested-table\">\n" + parseJSON(obj[key]) + "</td></tr>\n";
}
}
});
html += "</tbody>\n";
html += "</table>\n";
}
return html;
}
html = parseJSON(event);
e = angular.element(document.getElementById(id));
e.empty().html(html);
$compile(e)(scope);
};
}])
.factory('AddTextarea', [ function() {
return function(params) {
var container_id = params.container_id,
val = params.val,
fld_id = params.fld_id,
html;
html = "<div class=\"form-group\">\n" +
"<textarea id=\"" + fld_id + "\" class=\"form-control mono-space\" rows=\"12\" readonly>" + val + "</textarea>" +
"</div>\n";
$('#' + container_id).empty().html(html);
};
}])
.factory('AddPreFormattedText', [function() {
return function(params) {
var id = params.id,
val = params.val,
html;
html = "<pre>" + val + "</pre>\n";
$('#' + id).empty().html(html);
};
}]);

View File

@ -1168,124 +1168,4 @@ function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, Ge
msg: 'Call to ' + url + '. GET returned: ' + status });
});
};
}])
.factory('ViewHostResults', ['$log', 'CreateDialog', 'Rest', 'ProcessErrors', 'Wait', function($log, CreateDialog, Rest, ProcessErrors, Wait) {
return function(params) {
var scope = params.scope,
my_scope = params.scope.$new(),
id = params.id,
url;
function parseJSON(obj) {
var html="", keys;
if (typeof obj === "object") {
html += "<table class=\"object-list\">\n";
html += "<tbody>\n";
keys = Object.keys(obj).sort();
keys.forEach(function(key) {
if (typeof obj[key] === "boolean" || typeof obj[key] === "number" || typeof obj[key] === "string") {
html += "<tr><td class=\"key\">" + key + ":</td><td class=\"value";
html += (key === "results" || key === "stdout" || key === "stderr") ? " mono-space" : "";
html += "\">";
html += (key === "status") ? "<i class=\"fa icon-job-" + obj[key] + "\"></i> " + obj[key] : obj[key];
html += "</td></tr>\n";
}
else if (obj[key] === null || obj[key] === undefined) {
// html += "<tr><td class=\"key\">" + key + ":</td><td class=\"value\">null</td></tr>\n";
}
else if (typeof obj[key] === "object" && Array.isArray(obj[key])) {
html += "<tr><td class=\"key\">" + key + ":</td><td class=\"value";
html += (key === "results" || key === "stdout" || key === "stderr") ? " mono-space" : "";
html += "\">";
obj[key].forEach(function(row) {
html += "<p>" + row + "</p>";
});
html += "</td></tr>\n";
}
else if (typeof obj[key] === "object") {
html += "<tr><td class=\"key\">" + key + ":</td><td class=\"nested-table\">\n" + parseJSON(obj[key]) + "</td></tr>\n";
}
});
html += "</tbody>\n";
html += "</table>\n";
}
return html;
}
if (my_scope.removeDataReady) {
my_scope.removeDataReady();
}
my_scope.removeDataReady = my_scope.$on('DataReady', function(e, event_data, host) {
//var html = "<div class=\"title-section\">\n";
//html += "<h4>" + host.name + "</h4>\n";
//html += (host.description && host.description !== "imported") ? "<h5>" + host.description + "</h5>" : "";
//html += "<p>Event " + id + " details:</p>\n";
//html += "</div>\n";
var html = "<div class=\"results\">\n";
event_data.host = host.name;
html += parseJSON(event_data);
html += "<div class=\"spacer\"></div>\n";
html += "</div>\n";
$('#event-viewer-dialog').empty().html(html);
CreateDialog({
scope: my_scope,
width: 600,
height: 550,
minWidth: 450,
callback: 'ModalReady',
id: 'event-viewer-dialog',
title: 'Host Results',
onOpen: function() {
$('#dialog-ok-button').focus();
}
});
});
if (my_scope.removeModalReady) {
my_scope.removeModalReady();
}
my_scope.removeModalReady = my_scope.$on('ModalReady', function() {
Wait('stop');
$('#event-viewer-dialog').dialog('open');
});
url = scope.job.related.job_events + "?id=" + id;
Wait('start');
Rest.setUrl(url);
Rest.get()
.success( function(data) {
var key;
Wait('stop');
if (data.results.length > 0 && data.results[0].event_data.res) {
for (key in data.results[0].event_data) {
if (key !== "res") {
data.results[0].event_data.res[key] = data.results[0].event_data[key];
}
}
if (data.results[0].event_data.res.ansible_facts) {
delete data.results[0].event_data.res.ansible_facts;
}
data.results[0].event_data.res.status = (data.results[0].event === "runner_on_skipped") ? 'skipped' : (data.results[0].failed) ? 'failed' :
(data.results[0].changed) ? 'changed' : 'successful';
my_scope.$emit('DataReady', data.results[0].event_data.res, data.results[0].summary_fields.host, data.results[0].id);
}
else {
data.results[0].event_data.status = (data.results[0].event === "runner_on_skipped") ? 'skipped' : (data.results[0].failed) ? 'failed' :
(data.results[0].changed) ? 'changed' : 'successful';
my_scope.$emit('DataReady', data.results[0].event_data, data.results[0].summary_fields.host, data.results[0].id);
}
})
.error(function(data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
scope.modalOK = function() {
$('#event-viewer-dialog').dialog('close');
my_scope.$destroy();
};
};
}]);

View File

@ -51,6 +51,7 @@
@import "codemirror.less";
@import "angular-scheduler.less";
@import "log-viewer.less";
@import "event-viewer.less";
@import "job-details.less";
@import "jobs.less";
@import "inventory-edit.less";
@ -60,7 +61,6 @@
@import "new-dashboard.less";
/* Bootstrap fix that's causing a right margin to appear
whenver a modal is opened */
body.modal-open {

View File

@ -0,0 +1,32 @@
/*********************************************
* Copyright (c) 2014 AnsibleWorks, Inc.
*
* event-viewer.less
*
* custom styles for EventViewer.js helper
*
*/
#eventviewer-modal-dialog {
textarea {
overflow: scroll;
}
pre {
overflow: scroll;
word-wrap: normal;
word-break: normal;
white-space: pre-wrap;
}
}
table.eventviewer-status {
margin-top: 20px;
.key {
font-weight: bold;
}
.value i {
font-size: 12px;
}
}

View File

@ -0,0 +1,22 @@
<div id="eventviewer-modal-dialog" title="Log View" style="display: none;">
<ul id="eventview-tabs" class="nav nav-tabs">
<li class="active"><a href="#status" id="status-link" data-toggle="tab" ng-click="toggleTab($event, 'status-link', 'eventview-tabs')">Status</a></li>
<li><a href="#stdout" id="stdout-link" data-toggle="tab" ng-click="toggleTab($event, 'stdout-link', 'eventview-tabs')">Standard Out</a></li>
<li><a href="#stderr" id="stderr-link" data-toggle="tab" ng-click="toggleTab($event, 'stderr-link', 'eventview-tabs')">Standard Error</a></li>
<li><a href="#traceback" id="traceback-link" data-toggle="tab" ng-click="toggleTab($event, 'traceback-link', 'eventview-tabs')">Traceback</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="status">
<div id="status-form-container"></div>
</div>
<div class="tab-pane" id="stdout">
<div id="stdout-form-container"></div>
</div>
<div class="tab-pane" id="stderr">
<div id="stderr-form-container"></div>
</div>
<div class="tab-pane" id="traceback">
<div id="traceback-form-container"></div>
</div>
</div>
</div>

View File

@ -257,5 +257,6 @@
</div>
</div>
<div id="event-viewer-dialog" style="display: none;"></div>
<div ng-include="'/static/partials/eventviewer.html'"></div>
</div>

View File

@ -158,6 +158,7 @@
<script src="{{ STATIC_URL }}js/helpers/Variables.js"></script>
<script src="{{ STATIC_URL }}js/helpers/Schedules.js"></script>
<script src="{{ STATIC_URL }}js/helpers/LogViewer.js"></script>
<script src="{{ STATIC_URL }}js/helpers/EventViewer.js"></script>
<script src="{{ STATIC_URL }}js/helpers/JobDetail.js"></script>
<script src="{{ STATIC_URL }}js/helpers/JobTemplates.js"></script>
<script src="{{ STATIC_URL }}js/widgets/JobStatus.js"></script>