From 254c552734220772d2818280d375c78e9c7d4db1 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 1 Jul 2014 13:43:24 -0400 Subject: [PATCH] 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. --- awx/ui/static/js/app.js | 1 + awx/ui/static/js/controllers/JobDetail.js | 9 +- awx/ui/static/js/helpers/EventViewer.js | 298 ++++++++++++++++++++++ awx/ui/static/js/helpers/JobDetail.js | 120 --------- awx/ui/static/less/ansible-ui.less | 2 +- awx/ui/static/less/event-viewer.less | 32 +++ awx/ui/static/partials/eventviewer.html | 22 ++ awx/ui/static/partials/job_detail.html | 3 +- awx/ui/templates/ui/index.html | 1 + 9 files changed, 362 insertions(+), 126 deletions(-) create mode 100644 awx/ui/static/js/helpers/EventViewer.js create mode 100644 awx/ui/static/less/event-viewer.less create mode 100644 awx/ui/static/partials/eventviewer.html diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index cd0c710470..0a8801d4b7 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -98,6 +98,7 @@ angular.module('Tower', [ 'LogViewerStatusDefinition', 'LogViewerHelper', 'LogViewerOptionsDefinition', + 'EventViewerHelper', 'JobDetailHelper', 'SocketIO' ]) diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js index ceed30f3b3..1bf027c587 100644 --- a/awx/ui/static/js/controllers/JobDetail.js +++ b/awx/ui/static/js/controllers/JobDetail.js @@ -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' ]; diff --git a/awx/ui/static/js/helpers/EventViewer.js b/awx/ui/static/js/helpers/EventViewer.js new file mode 100644 index 0000000000..06a356c84c --- /dev/null +++ b/awx/ui/static/js/helpers/EventViewer.js @@ -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 += "\n"; + html += "\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 += "\n"; + } + else if (typeof obj[key] === "object" && Array.isArray(obj[key])) { + html += "\n"; + } + else if (typeof obj[key] === "object") { + html += "\n"; + } + } + }); + html += "\n"; + html += "
" + label + ":"; + if (key === "status") { + html += " " + 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 += "Opens in new tab or window.\" data-placement=\"right\" " + + "ng-click=\"modalOK()\">" + obj[key] + ""; + } + else { + html += obj[key]; + } + + html += "
" + label + ":"; + obj[key].forEach(function(row) { + html += "[" + row + "],"; + }); + html = html.replace(/,$/,''); + html += "
" + label + ":\n" + parseJSON(obj[key]) + "
\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 = "
\n" + + "" + + "
\n"; + $('#' + container_id).empty().html(html); + }; + }]) + + .factory('AddPreFormattedText', [function() { + return function(params) { + var id = params.id, + val = params.val, + html; + html = "
" + val + "
\n"; + $('#' + id).empty().html(html); + }; + }]); \ No newline at end of file diff --git a/awx/ui/static/js/helpers/JobDetail.js b/awx/ui/static/js/helpers/JobDetail.js index 74374b4f7d..6f37d4db53 100644 --- a/awx/ui/static/js/helpers/JobDetail.js +++ b/awx/ui/static/js/helpers/JobDetail.js @@ -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 += "\n"; - html += "\n"; - keys = Object.keys(obj).sort(); - keys.forEach(function(key) { - if (typeof obj[key] === "boolean" || typeof obj[key] === "number" || typeof obj[key] === "string") { - html += "\n"; - } - else if (obj[key] === null || obj[key] === undefined) { - // html += "\n"; - } - else if (typeof obj[key] === "object" && Array.isArray(obj[key])) { - html += "\n"; - } - else if (typeof obj[key] === "object") { - html += "\n"; - } - }); - html += "\n"; - html += "
" + key + ":"; - html += (key === "status") ? " " + obj[key] : obj[key]; - html += "
" + key + ":null
" + key + ":"; - obj[key].forEach(function(row) { - html += "

" + row + "

"; - }); - html += "
" + key + ":\n" + parseJSON(obj[key]) + "
\n"; - } - return html; - } - - if (my_scope.removeDataReady) { - my_scope.removeDataReady(); - } - my_scope.removeDataReady = my_scope.$on('DataReady', function(e, event_data, host) { - //var html = "
\n"; - //html += "

" + host.name + "

\n"; - //html += (host.description && host.description !== "imported") ? "
" + host.description + "
" : ""; - //html += "

Event " + id + " details:

\n"; - //html += "
\n"; - var html = "
\n"; - event_data.host = host.name; - html += parseJSON(event_data); - html += "
\n"; - html += "
\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(); - }; - }; }]); diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 4e1eb65628..1ea206fd47 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -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 { diff --git a/awx/ui/static/less/event-viewer.less b/awx/ui/static/less/event-viewer.less new file mode 100644 index 0000000000..566510f87c --- /dev/null +++ b/awx/ui/static/less/event-viewer.less @@ -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; + } +} \ No newline at end of file diff --git a/awx/ui/static/partials/eventviewer.html b/awx/ui/static/partials/eventviewer.html new file mode 100644 index 0000000000..04ff6bbb1d --- /dev/null +++ b/awx/ui/static/partials/eventviewer.html @@ -0,0 +1,22 @@ + \ No newline at end of file diff --git a/awx/ui/static/partials/job_detail.html b/awx/ui/static/partials/job_detail.html index a65c5d2097..db2d8b5ce3 100644 --- a/awx/ui/static/partials/job_detail.html +++ b/awx/ui/static/partials/job_detail.html @@ -257,5 +257,6 @@ - +
+ diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index 84213f1054..fa6b7d5b3a 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -158,6 +158,7 @@ +