From daf730e212ba6e2efdf9f9a56f61cc239e73acfa Mon Sep 17 00:00:00 2001 From: chouseknecht Date: Mon, 22 Jul 2013 07:17:23 -0400 Subject: [PATCH] AC-232 Event Detail changes. Moved back to modal dialog. Added accordion. Now removing fields (i.e. not displaying fields) that are empty. --- awx/ui/static/css/ansible-ui.css | 8 + awx/ui/static/js/app.js | 1 - awx/ui/static/js/controllers/JobEvents.js | 32 +- awx/ui/static/js/forms/JobEvents.js | 150 --------- awx/ui/static/js/helpers/Events.js | 340 ++++++++++++++++++-- awx/ui/static/lib/ansible/form-generator.js | 136 ++++---- awx/ui/templates/ui/index.html | 1 - 7 files changed, 414 insertions(+), 254 deletions(-) delete mode 100644 awx/ui/static/js/forms/JobEvents.js diff --git a/awx/ui/static/css/ansible-ui.css b/awx/ui/static/css/ansible-ui.css index 032ae04370..18162b2e43 100644 --- a/awx/ui/static/css/ansible-ui.css +++ b/awx/ui/static/css/ansible-ui.css @@ -609,6 +609,10 @@ width: 350px; } + .modal-input-xxlarge { + width: 460px; + } + .form-section-title { font-weight: bold; width: 100%; @@ -666,6 +670,10 @@ padding-bottom: 30px; } +.skinny-modal .modal-body { + padding: 5px 10px 0px 10px; +} + /* form navigation */ .navigation-buttons { height: 40px; diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index b535f5a0f8..8619f1405d 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -50,7 +50,6 @@ angular.module('ansible', [ 'JobsListDefinition', 'JobFormDefinition', 'JobEventsListDefinition', - 'JobEventFormDefinition', 'JobModalEventDefinition', 'JobHostDefinition', 'GroupsHelper', diff --git a/awx/ui/static/js/controllers/JobEvents.js b/awx/ui/static/js/controllers/JobEvents.js index e8ce9f5a12..d9096173e0 100644 --- a/awx/ui/static/js/controllers/JobEvents.js +++ b/awx/ui/static/js/controllers/JobEvents.js @@ -13,7 +13,7 @@ function JobEventsList ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, JobEventList, GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, LookUpInit, ToggleChildren, - FormatDate) + FormatDate, EventView) { ClearScope('htmlTemplate'); var list = JobEventList; @@ -68,7 +68,6 @@ function JobEventsList ($scope, $rootScope, $location, $log, $routeParams, Rest, found = true; } if ( fld == "results" && Array.isArray(eventData.res[fld]) && eventData.res[fld].length > 0 ) { - html += "\n"; //html += "\n"; - found = true; + if (txt !== '') { + html += "\n"; + html += "\n"; + found = true; + } } - if (fld == "rc" && eventData.res[fld] != 0) { + if (fld == "rc" && eventData.res[fld] != '') { html += "\n"; html += "\n"; found = true; @@ -158,11 +160,12 @@ function JobEventsList ($scope, $rootScope, $location, $log, $routeParams, Rest, LoadBreadCrumbs(); scope.viewJobEvent = function(id) { - var url = '/jobs/' + $routeParams.id + '/job_events/' + id; - if (scope['jobeventPage']) { - url += '?&page=' + (scope['jobeventPage'] + 1); - } - $location.url(url); + //var url = '/jobs/' + $routeParams.id + '/job_events/' + id; + //if (scope['jobeventPage']) { + // url += '?&page=' + (scope['jobeventPage'] + 1); + //} + //$location.url(url); + EventView({ event_id: id }); } scope.refresh = function() { @@ -182,7 +185,7 @@ function JobEventsList ($scope, $rootScope, $location, $log, $routeParams, Rest, JobEventsList.$inject = [ '$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'JobEventList', 'GenerateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', - 'ProcessErrors','GetBasePath', 'LookUpInit', 'ToggleChildren', 'FormatDate' + 'ProcessErrors','GetBasePath', 'LookUpInit', 'ToggleChildren', 'FormatDate', 'EventView' ]; function JobEventsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, JobEventForm, GenerateForm, @@ -249,17 +252,12 @@ function JobEventsEdit ($scope, $rootScope, $compile, $location, $log, $routePar scope[fld] = data['event_data']['res'][fld]; if (form.fields[fld].type == 'textarea') { var n = data['event_data']['res'][fld].match(/\n/g); - rows = (n) ? n.length : 1; + var rows = (n) ? n.length : 1; rows = (rows > 15) ? 5 : rows; $('textarea[name="' + fld + '"]').attr('rows',rows); } } break; - case 'conditional': - if (data['event_data']['res']) { - scope[fld] = data['event_data']['res']['is_conditional']; - } - break; case 'module_name': case 'module_args': if (data['event_data']['res'] && data['event_data']['res']['invocation']) { diff --git a/awx/ui/static/js/forms/JobEvents.js b/awx/ui/static/js/forms/JobEvents.js deleted file mode 100644 index 8c2c4701ce..0000000000 --- a/awx/ui/static/js/forms/JobEvents.js +++ /dev/null @@ -1,150 +0,0 @@ -/********************************************* - * Copyright (c) 2013 AnsibleWorks, Inc. - * - * JobEvents.js - * Form definition for Job Events model - * - * - */ -angular.module('JobEventFormDefinition', []) - .value( - 'JobEventForm', { - - editTitle: '{{ id }} - {{ event_display }}', //Legend in edit mode - name: 'job_events', - well: false, - - fields: { - status: { - labelClass: 'job-\{\{ status \}\}', - icon: 'icon-circle', - type: 'custom', - control: '
\{\{ status \}\}
', - section: 'Event' - }, - id: { - label: 'ID', - type: 'text', - readonly: true, - section: 'Event', - 'class': 'span1' - }, - created: { - label: 'Created', - type: 'text', - readonly: true, - section: 'Event' - }, - host: { - label: 'Host', - type: 'text', - readonly: true, - section: 'Event' - }, - play: { - label: 'Play', - type: 'text', - readonly: true, - section: 'Event' - }, - task: { - label: 'Task', - type: 'text', - readonly: true, - section: 'Event' - }, - conditional: { - label: 'Conditional?', - type: 'checkbox', - readonly: true, - section: 'Event' - }, - rc: { - label: 'Return Code', - type: 'text', - readonly: true, - section: 'Results', - 'class': 'span1' - }, - msg: { - label: 'Message', - type: 'textarea', - readonly: true, - section: 'Results', - 'class': 'span12', - rows: 1 - }, - stdout: { - label: 'Std Out', - type: 'textarea', - readonly: true, - section: 'Results', - 'class': 'span12', - rows: 1 - }, - stderr: { - label: 'Std Error', - type: 'textarea', - readonly: true, - section: 'Results', - 'class': 'span12', - rows: 1 - }, - start: { - label: 'Start', - type: 'text', - readonly: true, - section: 'Timing' - }, - end: { - label: 'End', - type: 'text', - readonly: true, - section: 'Timing' - }, - delta: { - label: 'Elapsed', - type: 'text', - readonly: true, - section: 'Timing' - }, - module_name: { - label: 'Name', - type: 'text', - readonly: true, - section: 'Module' - }, - module_args: { - label: 'Arguments', - type: 'text', - readonly: true, - section: 'Module' - } - }, - - navigation: { - back_top: { - label: 'Back', - position: ['top-left','top-right', 'bottom-left', 'bottom-right'], - 'class': 'btn-small', - icon: 'icon-arrow-left', - ngClick: 'navigateBack()' - }, - raw_view: { - label: 'View raw JSON results', - icon: 'icon-zoom-in', - position: ['bottom-left'], - 'class': 'btn-small', - ngClick: 'rawView()' - } - }, - - buttons: { - }, - - related: { //related colletions (and maybe items?) - - } - - }); //Form - diff --git a/awx/ui/static/js/helpers/Events.js b/awx/ui/static/js/helpers/Events.js index 57be20f66e..891a958910 100644 --- a/awx/ui/static/js/helpers/Events.js +++ b/awx/ui/static/js/helpers/Events.js @@ -6,45 +6,341 @@ * EventView - show the job_events form in a modal dialog * */ -angular.module('EventsHelper', ['RestServices', 'Utilities', 'JobModalEventDefinition']) -.factory('EventView', ['$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'JobModalEventForm', 'GenerateForm', +angular.module('EventsHelper', ['RestServices', 'Utilities']) +.factory('EventView', ['$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'GenerateForm', 'Prompt', 'ProcessErrors', 'GetBasePath', 'FormatDate', - function($rootScope, $location, $log, $routeParams, Rest, Alert, JobEventForm, GenerateForm, Prompt, ProcessErrors, GetBasePath, + function($rootScope, $location, $log, $routeParams, Rest, Alert, GenerateForm, Prompt, ProcessErrors, GetBasePath, FormatDate) { return function(params) { + + // We're going to manipulate the form object each time user clicks on View button. We can't rely on what's + // left of the form in memory each time. Instead we have to define the form from scratch, so for now we're + // keeping it here inline rather than in a separate file. + var form = { + name: 'job_events', + well: false, + forceListeners: true, + 'class': 'horizontal-narrow', + fields: { + status: { + labelClass: 'job-\{\{ status \}\}', + icon: 'icon-circle', + type: 'custom', + section: 'Event', + control: '
\{\{ status \}\}
' + }, + id: { + label: 'ID', + type: 'text', + readonly: true, + section: 'Event', + 'class': 'span1' + }, + created: { + label: 'Created', + type: 'text', + section: 'Event', + readonly: true + }, + host: { + label: 'Host', + type: 'text', + readonly: true, + section: 'Event', + ngShow: "host !== ''" + }, + play: { + label: 'Play', + type: 'text', + readonly: true, + section: 'Event', + ngShow: "play !== ''" + }, + task: { + label: 'Task', + type: 'text', + readonly: true, + section: 'Event', + ngShow: "task !== ''" + }, + rc: { + label: 'Return Code', + type: 'text', + readonly: true, + section: 'Event', + 'class': 'span1', + ngShow: "rc !== ''" + }, + msg: { + label: false, + type: 'textarea', + readonly: true, + section: 'Output', + 'class': 'modal-input-xxlarge', + ngShow: "msg !== ''", + rows: 10 + }, + stdout: { + label: false, + type: 'textarea', + readonly: true, + section: 'Output', + 'class': 'modal-input-xxlarge', + ngShow: "stdout !== ''", + rows: 10 + }, + stderr: { + label: false, + type: 'textarea', + readonly: true, + section: 'Output', + 'class': 'modal-input-xxlarge', + ngShow: "stderr !== ''", + rows: 10 + }, + results: { + label: false, + type: 'textarea', + readonly: true, + 'class': 'modal-input-xxlarge', + ngShow: "results !== ''", + rows: 10 + }, + start: { + label: 'Start', + type: 'text', + readonly: true, + section: 'Timing', + ngShow: "start !== ''" + }, + traceback: { + label: false, + type: 'textarea', + readonly: true, + section: 'Traceback', + 'class': 'modal-input-xxlarge', + ngShow: "traceback !== ''", + rows: 10 + }, + end: { + label: 'End', + type: 'text', + readonly: true, + section: 'Timing', + ngShow: "end !== ''" + }, + delta: { + label: 'Elapsed', + type: 'text', + readonly: true, + section: 'Timing', + ngShow: "delta !== ''" + }, + module_name: { + label: 'Name', + type: 'text', + readonly: true, + section: 'Module', + ngShow: "module_name !== ''" + }, + module_args: { + label: 'Arguments', + type: 'text', + readonly: true, + section: 'Module', + ngShow: "module_args !== ''" + } + } + }; + var event_id = params.event_id; var generator = GenerateForm; - var form = JobEventForm; + var scope; var defaultUrl = GetBasePath('base') + 'job_events/' + event_id + '/'; - var scope = generator.inject(form, { mode: 'edit', modal: true, related: false}); - generator.reset(); - var master = {}; - - scope.formModalAction = function() { - $('#form-modal').modal("hide"); - } - - scope.formModalActionLabel = 'OK'; - scope.formModalCancelShow = false; - - $('#form-modal .btn-success').removeClass('btn-success').addClass('btn-none'); // Retrieve detail record and prepopulate the form Rest.setUrl(defaultUrl); Rest.get() .success( function(data, status, headers, config) { - scope.formModalHeader = data['event_display']; - scope.event_data = JSON.stringify(data['event_data'], null, '\t'); + + // If event_data is not available or not very useful + if ($.isEmptyObject(data['event_data']) || !data['event_data']['res'] || typeof data['event_data']['res'] == 'string') { + for (var fld in form.fields) { + switch(fld) { + case 'start': + case 'end': + case 'delta': + case 'msg': + case 'stdout': + case 'stderr': + case 'msg': + case 'results': + case 'module_name': + case 'module_args': + case 'traceback': + delete form.fields[fld]; + break; + } + } + } + else if (typeof data['event_data']['res'] != 'string') { + delete form.fields['traceback']; + } + + // Remove remaining form fields that do not have a corresponding data value + for (var fld in form.fields) { + switch (fld) { + case 'start': + case 'end': + case 'delta': + case 'msg': + case 'stdout': + case 'stderr': + case 'msg': + if (data['event_data'] && data['event_data']['res'] && data['event_data']['res'][fld] == undefined) { + delete form.fields[fld]; + } + else { + if (form.fields[fld].type == 'textarea') { + var n = data['event_data']['res'][fld].match(/\n/g); + var rows = (n) ? n.length : 1; + rows = (rows > 10) ? 10 : rows; + rows = (rows < 3) ? 3 : rows; + form.fields[fld].rows = rows; + } + } + break; + case 'results': + if ( data['event_data'] && data['event_data']['res'] && data['event_data']['res'][fld] == undefined) { + // not defined + delete form.fields[fld]; + } + else if (!Array.isArray(data['event_data']['res'][fld]) || data['event_data']['res'][fld].length == 0) { + // defined, but empty + delete form.fields[fld]; + } + else { + // defined and not empty, so attempt to size the textarea field + var txt = ''; + for (var i=0; i < data['event_data']['res'][fld].length; i++) { + txt += data['event_data']['res'][fld][i]; + } + if (txt == '') { + // there's an array, but the actual text is empty + delete form.fields[fld]; + } + else { + var n = txt.match(/\n/g); + var rows = (n) ? n.length : 1; + rows = (rows > 10) ? 10 : rows; + rows = (rows < 3) ? 3 : rows; + form.fields[fld].rows = rows; + } + } + break; + case 'module_name': + case 'module_args': + if (data['event_data'] && data['event_data']['res']) { + if (data['event_data']['res']['invocation'] === undefined || + data['event_data']['res']['invocation'][fld] === undefined) { + delete form.fields[fld]; + } + } + break; + } + } + + // load up the form + scope = generator.inject(form, { mode: 'edit', modal: true, related: false}); + generator.reset(); + scope.formModalAction = function() { + $('#form-modal').modal("hide"); + } + scope.formModalActionLabel = 'OK'; + scope.formModalCancelShow = false; + $('#form-modal .btn-success').removeClass('btn-success').addClass('btn-none'); + $('#form-modal').addClass('skinny-modal'); + + scope.formModalHeader = data['event_display'].replace(/^\u00a0*/g,''); + + if (typeof data['event_data']['res'] == 'string') { + scope['traceback'] = data['event_data']['res']; + } + + //scope.event_data = JSON.stringify(data['event_data'], null, '\t'); + for (var fld in form.fields) { + switch(fld) { + case 'status': + if (data['failed']) { + scope['status'] = 'error'; + } + else if (data['changed']) { + scope['status'] = 'changed'; + } + else { + scope['status'] = 'success'; + } + break; + case 'created': + var cDate = new Date(data['created']); + scope['created'] = FormatDate(cDate); + break; + case 'host': + if (data['summary_fields'] && data['summary_fields']['host']) { + scope['host'] = data['summary_fields']['host']['name']; + } + break; + case 'id': + case 'task': + case 'play': + scope[fld] = data[fld]; + break; + case 'start': + case 'end': + if (data['event_data'] && data['event_data']['res'] && data['event_data']['res'][fld] !== undefined) { + var cDate = new Date(data['event_data']['res'][fld]); + scope[fld] = FormatDate(cDate); + } + break; + case 'results': + if (Array.isArray(data['event_data']['res'][fld]) && data['event_data']['res'][fld].length > 0 ) { + var txt = ''; + for (var i=0; i < data['event_data']['res'][fld].length; i++) { + txt += data['event_data']['res'][fld][i]; + } + if (txt !== '') { + scope[fld] = txt; + } + } + break; + case 'msg': + case 'stdout': + case 'stderr': + case 'delta': + case 'rc': + if (data['event_data'] && data['event_data']['res'] && data['event_data']['res'][fld] !== undefined) { + scope[fld] = data['event_data']['res'][fld]; + } + break; + case 'module_name': + case 'module_args': + if (data['event_data']['res'] && data['event_data']['res']['invocation']) { + scope[fld] = data['event_data']['res']['invocation'][fld]; + } + break; + } + } + + if (!scope.$$phase) { + scope.$digest(); + } + }) .error( function(data, status, headers, config) { $('#form-modal').modal("hide"); ProcessErrors(scope, data, status, form, { hdr: 'Error!', msg: 'Failed to retrieve event: ' + event_id + '. GET status: ' + status }); }); - - if (!scope.$$phase) { - scope.$digest(); - } } }]); \ No newline at end of file diff --git a/awx/ui/static/lib/ansible/form-generator.js b/awx/ui/static/lib/ansible/form-generator.js index e5550e7237..7d154adaf4 100644 --- a/awx/ui/static/lib/ansible/form-generator.js +++ b/awx/ui/static/lib/ansible/form-generator.js @@ -47,7 +47,7 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) // From here use 'scope' to manipulate the form, as the form is not in '$scope' $compile(element)(this.scope); - if ((!options.modal) && options.related) { + if ( ((!options.modal) && options.related) || this.form.forceListeners ) { this.addListeners(); } @@ -58,6 +58,7 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) if (options.modal) { this.scope.formHeader = (options.mode == 'add') ? form.addTitle : form.editTitle; $('.popover').remove(); //remove any lingering pop-overs + $('#form-modal').removeClass('skinny-modal'); //Used in job_events to remove white space $('#form-modal').modal({ backdrop: 'static', keyboard: false }); } return this.scope; @@ -107,61 +108,64 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) }, addListeners: function() { - - $('.jqui-accordion').each( function(index) { - - var active = false; - var list = $cookieStore.get('accordions'); - var found = false; - if (list) { - var id = $(this).attr('id'); - var base = ($location.path().replace(/^\//,'').split('/')[0]); - for (var i=0; i < list.length && found == false; i++) { - if (list[i].base == base && list[i].id == id) { - found = true; - active = list[i].active; - } - } - } - if (found == false && $(this).attr('data-open') == 'true') { - active = 0; - } - - $(this).accordion({ - collapsible: true, + if (this.modal) { + $('.jqui-accordion-modal').accordion({ + collapsible: false, heightStyle: 'content', - active: active, - activate: function( event, ui ) { - $('.jqui-accordion').each( function(index) { - var active = $(this).accordion('option', 'active'); - var id = $(this).attr('id'); - var base = ($location.path().replace(/^\//,'').split('/')[0]); - var list = $cookieStore.get('accordions'); - if (list == null || list == undefined) { - list = []; - } - var found = false; - for (var i=0; i < list.length && found == false; i++) { - if ( list[i].base == base && list[i].id == id) { - found = true; - list[i].active = active; - } - } - if (found == false) { - list.push({ base: base, id: id, active: active }); - } - $cookieStore.put('accordions',list); - }); - } + active: 0 }); - - - - }); - - + } + else { + $('.jqui-accordion').each( function(index) { + + var active = false; + var list = $cookieStore.get('accordions'); + var found = false; + if (list) { + var id = $(this).attr('id'); + var base = ($location.path().replace(/^\//,'').split('/')[0]); + for (var i=0; i < list.length && found == false; i++) { + if (list[i].base == base && list[i].id == id) { + found = true; + active = list[i].active; + } + } + } + if (found == false && $(this).attr('data-open') == 'true') { + active = 0; + } + + $(this).accordion({ + collapsible: true, + heightStyle: 'content', + active: active, + activate: function( event, ui ) { + $('.jqui-accordion').each( function(index) { + var active = $(this).accordion('option', 'active'); + var id = $(this).attr('id'); + var base = ($location.path().replace(/^\//,'').split('/')[0]); + var list = $cookieStore.get('accordions'); + if (list == null || list == undefined) { + list = []; + } + var found = false; + for (var i=0; i < list.length && found == false; i++) { + if ( list[i].base == base && list[i].id == id) { + found = true; + list[i].active = active; + } + } + if (found == false) { + list.push({ base: base, id: id, active: active }); + } + $cookieStore.put('accordions',list); + }); + } + }); + }); + } }, genID: function() { @@ -288,11 +292,14 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) html += "
'; - html += (field.awPopOver) ? this.attr(field, 'awPopOver') : ""; - html += field.label + '' + "\n"; - html += "
\n"; + if (field.label !== false) { + html += "' + "\n"; + html += "
\n"; + } + // Variable editing if (fld == "variables" || fld == "extra_vars" || fld == 'inventory_variables') { html += "
Parse as: " + @@ -322,7 +329,9 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) this.form.name + '_form.' + fld + ".$error.required\">A value is required!\n"; } html += "\n"; - html += "
\n"; + if (field.label !== false) { + html += "
\n"; + } html += "
\n"; } } @@ -660,20 +669,21 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) var field = this.form.fields[fld]; if (field.section && field.section != section) { if (section !== '') { - html += "
\n\n"; + html += "\n"; } else { - html += "
\n"; + html += "
"; + html += "
\n"; } - html += "

" + field.section + "

\n"; - html += "
\n"; - html += "
\n"; + var sectionShow = (this.form[field.section + 'Show']) ? " ng-show=\"" + this.form[field.section + 'Show'] + "\"" : ""; + html += "" + field.section + "\n"; + html += "\n"; section = field.section; } html += this.buildField(fld, field, options); } if (section !== '') { - html += "
\n
\n
\n"; + html += "\n\n"; } } diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index b1dedf88ec..1f571dc8c9 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -54,7 +54,6 @@ -