From 1e0af7b82efde98d7375d41c65f021168fff5d56 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Mon, 31 Mar 2014 03:25:41 -0400 Subject: [PATCH] Latest jobs page changes. Integrated log viewer. Added support to log viewer for variables (extra vars and source vars). Most things on jobs pages are working, execept for scheduled jobs bit. --- awx/ui/static/js/app.js | 8 +- awx/ui/static/js/controllers/Inventories.js | 2 +- awx/ui/static/js/controllers/Jobs.js | 85 ++----------- awx/ui/static/js/helpers/Groups.js | 21 ++-- awx/ui/static/js/helpers/JobSubmission.js | 37 +++--- awx/ui/static/js/helpers/Jobs.js | 119 +++++++++++++++++- awx/ui/static/js/helpers/LogViewer.js | 62 ++++++--- awx/ui/static/js/helpers/Variables.js | 3 +- awx/ui/static/js/lists/CompletedJobs.js | 25 ++-- awx/ui/static/js/lists/JobEvents.js | 12 +- awx/ui/static/js/lists/JobHosts.js | 12 +- awx/ui/static/js/lists/QueuedJobs.js | 27 ++-- awx/ui/static/js/lists/RunningJobs.js | 37 +++--- awx/ui/static/js/lists/ScheduledJobs.js | 2 +- awx/ui/static/less/ansible-ui.less | 40 +++--- awx/ui/static/less/jquery-ui-overrides.less | 26 ++++ awx/ui/static/less/log-viewer.less | 8 +- awx/ui/static/lib/ansible/InventoryTree.js | 12 +- awx/ui/static/lib/ansible/Utilities.js | 3 + .../static/lib/ansible/generator-helpers.js | 30 ++--- awx/ui/static/partials/job_events.html | 3 + .../static/partials/job_host_summaries.html | 3 + awx/ui/static/partials/jobs.html | 3 +- awx/ui/static/partials/logviewer.html | 8 ++ awx/ui/templates/ui/index.html | 4 +- 25 files changed, 362 insertions(+), 230 deletions(-) create mode 100644 awx/ui/static/partials/job_events.html create mode 100644 awx/ui/static/partials/job_host_summaries.html diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index 3fd08a0b21..628cef6f76 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -113,18 +113,18 @@ angular.module('ansible', [ controller: 'JobsListController' }). - when('/jobs/:id', { + /* when('/jobs/:id', { templateUrl: urlPrefix + 'partials/jobs.html', controller: 'JobsEdit' - }). + }). */ when('/jobs/:id/job_events', { - templateUrl: urlPrefix + 'partials/jobs.html', + templateUrl: urlPrefix + 'partials/job_events.html', controller: 'JobEventsList' }). when('/jobs/:id/job_host_summaries', { - templateUrl: urlPrefix + 'partials/jobs.html', + templateUrl: urlPrefix + 'partials/job_host_summaries.html', controller: 'JobHostSummaryList' }). diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index 5145bc42e1..6429cff4bf 100644 --- a/awx/ui/static/js/controllers/Inventories.js +++ b/awx/ui/static/js/controllers/Inventories.js @@ -189,7 +189,7 @@ function InventoriesList($scope, $rootScope, $location, $log, $routeParams, Rest Prompt({ hdr: 'Delete', - body: 'Are you sure you want to delete ' + name + '?', + body: '
Are you sure you want to delete ' + name + '?
', action: action }); }; diff --git a/awx/ui/static/js/controllers/Jobs.js b/awx/ui/static/js/controllers/Jobs.js index 77902099c6..54b5282388 100644 --- a/awx/ui/static/js/controllers/Jobs.js +++ b/awx/ui/static/js/controllers/Jobs.js @@ -11,14 +11,14 @@ 'use strict'; function JobsListController ($scope, $compile, ClearScope, Breadcrumbs, LoadBreadCrumbs, LoadScope, RunningJobsList, CompletedJobsList, QueuedJobsList, - ScheduledJobsList, GetChoices, GetBasePath, Wait, DeleteJob, Find, DeleteSchedule, ToggleSchedule, RelaunchInventory, RelaunchPlaybook, RelaunchSCM, + ScheduledJobsList, GetChoices, GetBasePath, Wait, Find, JobsControllerInit, DeleteSchedule, ToggleSchedule, LoadDialogPartial, ScheduledJobEdit) { ClearScope(); var e, completed_scope, running_scope, queued_scope, scheduled_scope, - choicesCount = 0, listsCount = 0, relaunch, getTypeId; + choicesCount = 0; LoadBreadCrumbs(); @@ -27,47 +27,7 @@ function JobsListController ($scope, $compile, ClearScope, Breadcrumbs, LoadBrea e.html(Breadcrumbs({ list: { editTitle: 'Jobs' } , mode: 'edit' })); $compile(e)($scope); - relaunch = function(params) { - var scope = params.scope, - id = params.id, - type = params.type, - name = params.name; - if (type === 'inventory_sync') { - RelaunchInventory({ scope: scope, id: id}); - } - else if (type === 'playbook_run') { - RelaunchPlaybook({ scope: scope, id: id, name: name }); - } - else if (type === 'scm_sync') { - RelaunchSCM({ }); - } - }; - getTypeId = function(job) { - var type_id; - if (job.type === 'inventory_sync') { - type_id = job.inventory_source; - } - else if (job.type === 'scm_sync') { - type_id = job.poject; - } - else if (job.type === 'playbook_run') { - type_id = job.id; - } - return type_id; - }; - - // After all the lists are loaded - if ($scope.removeListLoaded) { - $scope.removeListLoaded(); - } - $scope.removeListLoaded = $scope.$on('listLoaded', function() { - listsCount++; - if (listsCount === 3) { - Wait('stop'); - } - }); - // After all choices are ready, load up the lists and populate the page if ($scope.removeBuildJobsList) { $scope.removeBuildJobsList(); @@ -113,18 +73,6 @@ function JobsListController ($scope, $compile, ClearScope, Breadcrumbs, LoadBrea scheduled_scope.search(ScheduledJobsList.iterator); }); - completed_scope.deleteJob = function(id) { - DeleteJob({ scope: completed_scope, id: id }); - }; - - queued_scope.deleteJob = function(id) { - DeleteJob({ scope: queued_scope, id: id }); - }; - - running_scope.deleteJob = function(id) { - DeleteJob({ scope: running_scope, id: id }); - }; - scheduled_scope.toggleSchedule = function(id) { ToggleSchedule({ scope: scheduled_scope, @@ -144,25 +92,6 @@ function JobsListController ($scope, $compile, ClearScope, Breadcrumbs, LoadBrea scheduled_scope.editSchedule = function(id) { ScheduledJobEdit({ scope: scheduled_scope, id: id }); }; - - completed_scope.relaunch = function(id) { - var job = Find({ list: completed_scope.completed_jobs, key: 'id', val: id }), - type_id = getTypeId(job); - relaunch({ scope: completed_scope, id: type_id, type: job.type, name: job.name }); - }; - - running_scope.relaunch = function(id) { - var job = Find({ list: running_scope.running_jobs, key: 'id', val: id }), - type_id = getTypeId(job); - relaunch({ scope: running_scope, id: type_id, type: job.type, name: job.name }); - }; - - queued_scope.relaunch = function(id) { - var job = Find({ list: queued_scope.queued_jobs, key: 'id', val: id }), - type_id = getTypeId(job); - relaunch({ scope: queued_scope, id: type_id, type: job.type, name: job.name }); - }; - }); if ($scope.removeChoicesReady) { @@ -198,11 +127,17 @@ function JobsListController ($scope, $compile, ClearScope, Breadcrumbs, LoadBrea element_id: 'schedule-dialog-target', callback: 'choicesReady' }); + + $scope.refreshJobs = function() { + queued_scope.search('queued_job'); + running_scope.search('running_job'); + completed_scope.search('completed_job'); + }; } JobsListController.$inject = ['$scope', '$compile', 'ClearScope', 'Breadcrumbs', 'LoadBreadCrumbs', 'LoadScope', 'RunningJobsList', 'CompletedJobsList', - 'QueuedJobsList', 'ScheduledJobsList', 'GetChoices', 'GetBasePath', 'Wait', 'DeleteJob', 'Find', 'DeleteSchedule', 'ToggleSchedule', 'RelaunchInventory', - 'RelaunchPlaybook', 'RelaunchSCM', 'LoadDialogPartial', 'ScheduledJobEdit']; + 'QueuedJobsList', 'ScheduledJobsList', 'GetChoices', 'GetBasePath', 'Wait', 'Find', 'JobsControllerInit', + 'DeleteSchedule', 'ToggleSchedule', 'LoadDialogPartial', 'ScheduledJobEdit']; function JobsEdit($scope, $rootScope, $compile, $location, $log, $routeParams, JobForm, JobTemplateForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, RelatedSearchInit, RelatedPaginateInit, ReturnToCaller, ClearScope, InventoryList, diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index a57ad173b4..7f5c91ae6a 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -37,8 +37,7 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G scope.$emit('sourceTypeOptionsReady'); }) .error(function (data, status) { - ProcessErrors(scope, data, status, null, { - hdr: 'Error!', + ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve options for inventory_sources.source. OPTIONS status: ' + status }); }); @@ -651,7 +650,7 @@ function(SchedulerInit, Rest, Wait, SetSchedulesInnerDialogSize) { if (ww > 1199) { // desktop x = 675; - y = (780 > wh) ? wh - 15 : 780; + y = (800 > wh) ? wh - 15 : 800; maxrows = 18; } else if (ww <= 1199 && ww >= 768) { x = 550; @@ -740,8 +739,8 @@ function(SchedulerInit, Rest, Wait, SetSchedulesInnerDialogSize) { if (sources_scope.codeMirror) { sources_scope.codeMirror.destroy(); } - $('#group-modal-dialog').dialog('destroy'); $('#group-modal-dialog').hide(); + $('#group-modal-dialog').dialog('destroy'); $('#properties-tab').empty(); $('#sources-tab').empty(); $('#schedules-list').empty(); @@ -983,9 +982,12 @@ function(SchedulerInit, Rest, Wait, SetSchedulesInnerDialogSize) { if (modal_scope.searchCleanUp) { modal_scope.searchCleanup(); } - - $('#group-modal-dialog').dialog('close'); - + try { + $('#group-modal-dialog').dialog('close'); + } + catch(e) { + // ignore + } // Change the selected group if (groups_reload && parent_scope.selected_tree_id !== tree_id) { parent_scope.showHosts(tree_id, group_id, false); @@ -1058,7 +1060,7 @@ function(SchedulerInit, Rest, Wait, SetSchedulesInnerDialogSize) { overwrite: sources_scope.overwrite, overwrite_vars: sources_scope.overwrite_vars, update_on_launch: sources_scope.update_on_launch, - update_cache_timeout: sources_scope.update_cache_timeout + update_cache_timeout: (sources_scope.update_cache_timeout || 0) }; // Create a string out of selected list of regions @@ -1165,7 +1167,6 @@ function(SchedulerInit, Rest, Wait, SetSchedulesInnerDialogSize) { } }) .error(function (data, status) { - Wait('stop'); ProcessErrors(properties_scope, data, status, GroupForm, { hdr: 'Error!', msg: 'Failed to create group: ' + group_id + '. POST status: ' + status }); @@ -1243,7 +1244,7 @@ function(SchedulerInit, Rest, Wait, SetSchedulesInnerDialogSize) { Prompt({ hdr: 'Delete Group', - body: '

Are you sure you want to delete group ' + node.name + '?

', + body: '
Are you sure you want to delete group ' + node.name + '?
', action: action_to_take, 'class': 'btn-danger' }); diff --git a/awx/ui/static/js/helpers/JobSubmission.js b/awx/ui/static/js/helpers/JobSubmission.js index fd5367f44d..3080faf7b3 100644 --- a/awx/ui/static/js/helpers/JobSubmission.js +++ b/awx/ui/static/js/helpers/JobSubmission.js @@ -8,7 +8,7 @@ 'use strict'; angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'CredentialFormDefinition', 'CredentialsListDefinition', - 'LookUpHelper', 'JobSubmissionHelper' ]) + 'LookUpHelper', 'JobSubmissionHelper', 'JobTemplateFormDefinition' ]) .factory('LaunchJob', ['Rest', 'Wait', 'ProcessErrors', function(Rest, Wait, ProcessErrors) { return function(params) { @@ -48,7 +48,7 @@ function(Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialList) { LookUpInit({ url: GetBasePath('credentials') + '?kind=ssh', scope: scope, - form: JobTemplateForm, + form: JobTemplateForm(), current_item: null, list: CredentialList, field: 'credential', @@ -68,7 +68,7 @@ function(Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialList) { callback = params.callback || 'PasswordsAccepted', password, form = CredentialForm, - html, + html = "", acceptedPasswords = {}, scope = parent_scope.$new(); @@ -80,14 +80,12 @@ function(Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialList) { password = passwords.pop(); // Prompt for password - html += "
\n"; + html += "\n"; field = form.fields[password]; fld = password; scope[fld] = ''; html += "
\n"; - html += "\n"; - html += "
\n"; + html += "\n"; html += "A value is required!\n"; html += "\n"; html += "
\n"; - html += "
\n"; // Add the related confirm field if (field.associated) { @@ -108,9 +105,7 @@ function(Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialList) { field = form.fields[field.associated]; scope[fld] = ''; html += "
\n"; - html += "\n"; - html += "
\n"; + html += "\n"; html += "Must match Password value\n" : ""; html += "\n"; html += "
\n"; - html += "
\n"; } html += "
\n"; $('#password-body').empty().html(html); @@ -139,8 +133,9 @@ function(Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialList) { } scope.passwordAccept = function() { + $('#password-modal').modal('hide'); acceptedPasswords[password] = scope[password]; - if (password.length > 0) { + if (passwords.length > 0) { promptPassword(); } else { @@ -149,9 +144,12 @@ function(Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialList) { }; scope.passwordCancel = function() { - Alert('Missing Password', 'Required password(s) not provided. The request will not be submitted.', 'alert-info'); + $('#password-modal').modal('hide'); + Alert('Missing Password', 'Required password(s) not provided. Your request will not be submitted.', 'alert-info'); parent_scope.$emit('PasswordsCanceled'); }; + + promptPassword(); }; }]) @@ -178,7 +176,7 @@ function(Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialList) { new_job_id = data.id; launch_url = data.related.start; if (data.passwords_needed_to_start.length > 0) { - scope.$emit('PromptForPasswords'); + scope.$emit('PromptForPasswords', data.passwords_needed_to_start); } else { scope.$emit('StartPlaybookRun', {}); } @@ -293,8 +291,13 @@ function(Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialList) { // Refresh the project list after update request submitted Wait('stop'); Alert('Update Started', 'The request to start the SCM update process was submitted. ' + - 'To monitor the update status, refresh the page by clicking the Refresh button.', 'alert-info'); - scope.refresh(); + 'To monitor the update status, refresh the page by clicking the button.', 'alert-info'); + if (scope.refreshJobs) { + scope.refreshJobs(); + } + else if (scope.refresh) { + scope.refresh(); + } }); if (scope.removePromptForPasswords) { diff --git a/awx/ui/static/js/helpers/Jobs.js b/awx/ui/static/js/helpers/Jobs.js index 9daafff92e..237e4bd564 100644 --- a/awx/ui/static/js/helpers/Jobs.js +++ b/awx/ui/static/js/helpers/Jobs.js @@ -10,7 +10,98 @@ 'use strict'; angular.module('JobsHelper', ['Utilities', 'RestServices', 'FormGenerator', 'JobSummaryDefinition', 'InventoryHelper', 'GeneratorHelpers', - 'JobSubmissionHelper', 'SchedulesHelper']) + 'JobSubmissionHelper', 'SchedulesHelper', 'LogViewerHelper']) + +/** + * JobsControllerInit({ scope: $scope }); + * + * Initialize calling scope with all the bits required to support a jobs list + * + */ +.factory('JobsControllerInit', ['$location', 'Find', 'DeleteJob', 'RelaunchJob', 'LogViewer', + function($location, Find, DeleteJob, RelaunchJob, LogViewer) { + return function(params) { + var scope = params.scope, + parent_scope = params.parent_scope; + + scope.deleteJob = function(id) { + DeleteJob({ scope: scope, id: id }); + }; + + scope.relaunchJob = function(id) { + var list, job, typeId; + if (scope.completed_jobs) { + list = scope.completed_jobs; + } + else if (scope.running_jobs) { + list = scope.running_jobs; + } + else if (scope.queued_jobs) { + list = scope.queued_jobs; + } + job = Find({ list: list, key: 'id', val: id }); + if (job.type === 'inventory_update') { + typeId = job.inventory_source; + } + else if (job.type === 'project_update') { + typeId = job.project; + } + else if (job.type === 'job') { + typeId = job.id; + } + RelaunchJob({ scope: scope, id: typeId, type: job.type, name: job.name }); + }; + + scope.refreshJobs = function() { + parent_scope.refreshJobs(); + }; + + scope.viewJobLog = function(id, url) { + var list, job; + if (url) { + $location.path(url); + } + else { + if (scope.completed_jobs) { + list = scope.completed_jobs; + } + else if (scope.running_jobs) { + list = scope.running_jobs; + } + else if (scope.queued_jobs) { + list = scope.queued_jobs; + } + job = Find({ list: list, key: 'id', val: id }); + LogViewer({ + scope: scope, + url: job.url, + status_icon: 'icon-job-' + job.status + }); + } + }; + }; + } +]) + +.factory('RelaunchJob', ['RelaunchInventory', 'RelaunchPlaybook', 'RelaunchSCM', + function(RelaunchInventory, RelaunchPlaybook, RelaunchSCM) { + return function(params) { + var scope = params.scope, + id = params.id, + type = params.type, + name = params.name; + if (type === 'inventory_update') { + RelaunchInventory({ scope: scope, id: id}); + } + else if (type === 'job') { + RelaunchPlaybook({ scope: scope, id: id, name: name }); + } + else if (type === 'project_update') { + RelaunchSCM({ scope: scope, id: id }); + } + }; + } +]) .factory('JobStatusToolTip', [ function () { @@ -170,8 +261,8 @@ angular.module('JobsHelper', ['Utilities', 'RestServices', 'FormGenerator', 'Job * Called from JobsList controller to load each section or list on the page * */ -.factory('LoadScope', ['SearchInit', 'PaginateInit', 'GenerateList', 'PageRangeSetup', 'ProcessErrors', 'Rest', - function(SearchInit, PaginateInit, GenerateList) { +.factory('LoadScope', ['SearchInit', 'PaginateInit', 'GenerateList', 'JobsControllerInit', 'Rest', + function(SearchInit, PaginateInit, GenerateList, JobsControllerInit, Rest) { return function(params) { var parent_scope = params.parent_scope, scope = params.scope, @@ -199,7 +290,7 @@ angular.module('JobsHelper', ['Utilities', 'RestServices', 'FormGenerator', 'Job scope: scope, list: list, url: url, - pageSize: 10 + pageSize: 5 }); scope.iterator = list.iterator; @@ -208,6 +299,8 @@ angular.module('JobsHelper', ['Utilities', 'RestServices', 'FormGenerator', 'Job scope.removePostRefresh(); } scope.$on('PostRefresh', function(){ + + JobsControllerInit({ scope: scope, parent_scope: parent_scope }); scope[list.name].forEach(function(item, item_idx) { var fld, field, @@ -231,6 +324,20 @@ angular.module('JobsHelper', ['Utilities', 'RestServices', 'FormGenerator', 'Job } return true; }); + //Set the name link + if (item.type === "inventory_update") { + Rest.setUrl(item.related.inventory_source); + Rest.get() + .success(function(data) { + itm.nameHref = "/inventories/" + data.inventory; + }); + } + else if (item.type === "project_update") { + itm.nameHref = "/projects/" + item.project; + } + else if (item.type === "job") { + itm.nameHref = ""; + } if (list.name === 'completed_jobs' || list.name === 'running_jobs') { itm.status_tip = itm.status_label + '. Click for details.'; @@ -288,7 +395,7 @@ function(Find, GetBasePath, Rest, Wait, ProcessErrors, Prompt){ action_label = 'cancel'; hdr = 'Cancel Job'; } else { - url = GetBasePath('jobs') + id + '/'; + url = job.related.cancel; //GetBasePath('unified_jobs') + id + '/'; action_label = 'delete'; hdr = 'Delete Job'; } @@ -368,7 +475,7 @@ function(Find, Wait, Rest, InventoryUpdate, ProcessErrors, GetBasePath) { return function(params) { var scope = params.scope, id = params.id; - ProjectUpdate({ scope: scope, id: id }); + ProjectUpdate({ scope: scope, project_id: id }); }; }]) diff --git a/awx/ui/static/js/helpers/LogViewer.js b/awx/ui/static/js/helpers/LogViewer.js index 44e5d8d7a8..56b8404eef 100644 --- a/awx/ui/static/js/helpers/LogViewer.js +++ b/awx/ui/static/js/helpers/LogViewer.js @@ -7,12 +7,12 @@ 'use strict'; -angular.module('LogViewerHelper', ['ModalDialog', 'Utilities', 'FormGenerator']) +angular.module('LogViewerHelper', ['ModalDialog', 'Utilities', 'FormGenerator', 'VariablesHelper']) .factory('LogViewer', ['$compile', 'CreateDialog', 'GetJob', 'Wait', 'GenerateForm', 'LogViewerStatusForm', 'AddTable', 'AddTextarea', - 'LogViewerOptionsForm', 'EnvTable', 'GetBasePath', 'LookUpName', 'Empty', + 'LogViewerOptionsForm', 'EnvTable', 'GetBasePath', 'LookUpName', 'Empty', 'AddPreFormattedText', 'ParseVariableString', function($compile, CreateDialog, GetJob, Wait, GenerateForm, LogViewerStatusForm, AddTable, AddTextarea, LogViewerOptionsForm, EnvTable, - GetBasePath, LookUpName, Empty) { + GetBasePath, LookUpName, Empty, AddPreFormattedText, ParseVariableString) { return function(params) { var parent_scope = params.scope, url = params.url, @@ -41,21 +41,19 @@ angular.module('LogViewerHelper', ['ModalDialog', 'Utilities', 'FormGenerator']) AddTable({ scope: scope, form: LogViewerOptionsForm, id: 'options-form-container', status_icon: status_icon }); if (data.result_stdout) { - AddTextarea({ - container_id: 'stdout-form-container', - val: data.result_stdout, - fld_id: 'stdout-textarea' + AddPreFormattedText({ + id: 'stdout-form-container', + val: data.result_stdout }); } else { - $('#logview-tabs li:eq(2)').hide(); + $('#logview-tabs li:eq(1)').hide(); } if (data.result_traceback) { - AddTextarea({ - container_id: 'traceback-form-container', - val: data.result_traceback, - fld_id: 'traceback-textarea' + AddPreFormattedText({ + id: 'traceback-form-container', + val: data.result_traceback }); } else { @@ -68,6 +66,28 @@ angular.module('LogViewerHelper', ['ModalDialog', 'Utilities', 'FormGenerator']) vars: data.job_env }); }*/ + + if (data.extra_vars) { + AddTextarea({ + container_id: 'variables-container', + fld_id: 'variables', + val: ParseVariableString(data.extra_vars) + }); + } + else { + $('#logview-tabs li:eq(4)').hide(); + } + + if (data.source_vars) { + AddTextarea({ + container_id: 'source-container', + fld_id: 'source-variables', + val: ParseVariableString(data.source_vars) + }); + } + else { + $('#logview-tabs li:eq(5)').hide(); + } if (!Empty(scope.credential)) { LookUpName({ @@ -81,7 +101,7 @@ angular.module('LogViewerHelper', ['ModalDialog', 'Utilities', 'FormGenerator']) LookUpName({ scope: scope, scope_var: 'inventory', - url: GetBasePath('inventories') + scope.inventory + '/' + url: GetBasePath('inventory') + scope.inventory + '/' }); } @@ -115,8 +135,8 @@ angular.module('LogViewerHelper', ['ModalDialog', 'Utilities', 'FormGenerator']) rows = Math.floor((h - u) / 20); rows -= 3; rows = (rows < 6) ? 6 : rows; - $('#stdout-textarea').attr({ rows: rows }); - $('#traceback-textarea').attr({ rows: rows }); + $('#logviewer-modal-dialog #variables').attr({ rows: rows }); + $('#logviewer-modal-dialog #source-variables').attr({ rows: rows }); }; elem = angular.element(document.getElementById('logviewer-modal-dialog')); @@ -237,12 +257,22 @@ angular.module('LogViewerHelper', ['ModalDialog', 'Utilities', 'FormGenerator']) 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); + }; + }]) + .factory('EnvTable', [ function() { return function(params) { var id = params.id, diff --git a/awx/ui/static/js/helpers/Variables.js b/awx/ui/static/js/helpers/Variables.js index da6f3a6f04..c01d768fce 100644 --- a/awx/ui/static/js/helpers/Variables.js +++ b/awx/ui/static/js/helpers/Variables.js @@ -3,8 +3,6 @@ * * VariablesHelper * - * Show the CodeMirror variable editor and allow - * toggle between JSON and YAML * */ @@ -35,6 +33,7 @@ angular.module('VariablesHelper', ['Utilities']) $log.info('Attempt to parse extra_vars as JSON failed. Attempting to parse as YAML'); try { json_obj = jsyaml.safeLoad(variables); + json_obj = SortVariables(json_obj); result = jsyaml.safeDump(json_obj); } catch(e2) { diff --git a/awx/ui/static/js/lists/CompletedJobs.js b/awx/ui/static/js/lists/CompletedJobs.js index e39e6efddb..294f8f419b 100644 --- a/awx/ui/static/js/lists/CompletedJobs.js +++ b/awx/ui/static/js/lists/CompletedJobs.js @@ -22,7 +22,7 @@ angular.module('CompletedJobsDefinition', []) fields: { id: { label: 'Job ID', - linkTo: '/#/jobs/{{ completed_job.id }}', + ngClick:"viewJobLog(completed_job.id)", key: true, desc: true, searchType: 'int', @@ -30,13 +30,15 @@ angular.module('CompletedJobsDefinition', []) }, status: { label: 'Status', + columnClass: 'col-md-2 col-sm-2 col-xs-2', awToolTip: "{{ completed_job.status_tip }}", awTipPlacement: "top", dataTitle: "{{ completed_job.status_popover_title }}", icon: 'icon-job-{{ completed_job.status }}', iconOnly: true, - awPopOver: "{{ completed_job.status_popover }}", - dataPlacement: 'right', + ngClick:"viewJobLog(completed_job.id)", + /*awPopOver: "{{ completed_job.status_popover }}", + dataPlacement: 'right',*/ searchType: 'select', searchOptions: [ { name: "Success", value: "successful" }, @@ -50,8 +52,8 @@ angular.module('CompletedJobsDefinition', []) searchType: 'int', searchOnly: true }, - modified: { - label: 'Completed On', + finished: { + label: 'Finished On', link: false, searchable: false, filter: "date:'MM/dd/yy HH:mm:ss'", @@ -66,9 +68,7 @@ angular.module('CompletedJobsDefinition', []) name: { label: 'Name', columnClass: 'col-md-3 col-xs-5', - ngHref: 'nameHref', - sourceModel: 'template', - sourceField: 'name' + ngClick: "viewJobLog(completed_job.id, completed_job.nameHref)" }, failed: { label: 'Job failed?', @@ -85,7 +85,7 @@ angular.module('CompletedJobsDefinition', []) refresh: { mode: 'all', awToolTip: "Refresh the page", - ngClick: "refresh()" + ngClick: "refreshJobs()" } }, @@ -93,8 +93,8 @@ angular.module('CompletedJobsDefinition', []) submit: { icon: 'icon-rocket', mode: 'all', - ngClick: 'relaunch(completed_job.id)', - awToolTip: 'Relaunch the job', + ngClick: 'relaunchJob(completed_job.id)', + awToolTip: 'Relaunch using the same parameters', dataPlacement: 'top' }, "delete": { @@ -105,11 +105,12 @@ angular.module('CompletedJobsDefinition', []) }, dropdown: { type: 'DropDown', + ngShow: "completed_job.type === 'job'", label: 'View', icon: 'fa-search-plus', 'class': 'btn-default btn-xs', options: [ - { ngHref: '/#/jobs/{{ completed_job.id }}', label: 'Status' }, + //{ ngHref: '/#/jobs/{{ completed_job.id }}', label: 'Status' }, { ngHref: '/#/jobs/{{ completed_job.id }}/job_events', label: 'Events', ngHide: "completed_job.status == 'new'" }, { ngHref: '/#/jobs/{{ completed_job.id }}/job_host_summaries', label: 'Host Summary' } ] diff --git a/awx/ui/static/js/lists/JobEvents.js b/awx/ui/static/js/lists/JobEvents.js index ec52f1f173..8050e50d4c 100644 --- a/awx/ui/static/js/lists/JobEvents.js +++ b/awx/ui/static/js/lists/JobEvents.js @@ -21,12 +21,12 @@ angular.module('JobEventsListDefinition', []) filterBy: '{ show: true }', navigationLinks: { - details: { - href: '/#/jobs/{{ job_id }}', - label: 'Status', - icon: 'icon-zoom-in', - ngShow: 'job_id !== null' - }, + //details: { + // href: '/#/jobs/{{ job_id }}', + // label: 'Status', + // icon: 'icon-zoom-in', + // ngShow: 'job_id !== null' + //}, events: { href: '/#/jobs/{{ job_id }}/job_events', label: 'Events', diff --git a/awx/ui/static/js/lists/JobHosts.js b/awx/ui/static/js/lists/JobHosts.js index 025063456b..a78cbf0383 100644 --- a/awx/ui/static/js/lists/JobHosts.js +++ b/awx/ui/static/js/lists/JobHosts.js @@ -20,12 +20,12 @@ angular.module('JobHostDefinition', []) navigationLinks: { ngHide: 'host_id !== null', - details: { - href: "/#/jobs/{{ job_id }}", - label: 'Status', - icon: 'icon-zoom-in', - ngShow: "job_id !== null" - }, + //details: { + // href: "/#/jobs/{{ job_id }}", + // label: 'Status', + // icon: 'icon-zoom-in', + // ngShow: "job_id !== null" + //}, events: { href: "/#/jobs/{{ job_id }}/job_events", label: 'Events', diff --git a/awx/ui/static/js/lists/QueuedJobs.js b/awx/ui/static/js/lists/QueuedJobs.js index 7a4c5cd6db..3ed336d514 100644 --- a/awx/ui/static/js/lists/QueuedJobs.js +++ b/awx/ui/static/js/lists/QueuedJobs.js @@ -22,11 +22,22 @@ angular.module('QueuedJobsDefinition', []) fields: { id: { label: 'Job ID', + ngClick:"viewJobLog(queued_job.id)", key: true, desc: true, searchType: 'int', columnClass: 'col-md-1 col-sm-2 col-xs-2' }, + status: { + label: 'Status', + columnClass: 'col-md-2 col-sm-2 col-xs-2', + awToolTip: "{{ queued_job.status_tip }}", + awTipPlacement: "top", + dataTitle: "{{ queued_job.status_popover_title }}", + icon: 'icon-job-{{ queued_job.status }}', + iconOnly: true, + ngClick:"viewJobLog(queued_job.id)" + }, inventory: { label: 'Inventory ID', searchType: 'int', @@ -48,9 +59,7 @@ angular.module('QueuedJobsDefinition', []) name: { label: 'Name', columnClass: 'col-sm-3 col-xs-5', - ngHref: 'nameHref', - sourceModel: 'template', - sourceField: 'name' + ngClick: "viewJobLog(queued_job.id, queued_job.nameHref)" } }, @@ -59,22 +68,16 @@ angular.module('QueuedJobsDefinition', []) refresh: { mode: 'all', awToolTip: "Refresh the page", - ngClick: "refresh()" + ngClick: "refreshJobs()" } }, fieldActions: { - status: { - mode: 'all', - iconClass: 'fa icon-job-{{ queued_job.status }}', - awToolTip: "{{ queued_job.statusToolTip }}", - dataPlacement: 'top' - }, submit: { icon: 'icon-rocket', mode: 'all', - ngClick: 'relaunch(queued_job.id)', - awToolTip: 'Launch another instance of the job', + ngClick: 'relaunchJob(queued_job.id)', + awToolTip: 'Relaunch using the same parameters', dataPlacement: 'top' }, cancel: { diff --git a/awx/ui/static/js/lists/RunningJobs.js b/awx/ui/static/js/lists/RunningJobs.js index 811afaefd9..83b7605573 100644 --- a/awx/ui/static/js/lists/RunningJobs.js +++ b/awx/ui/static/js/lists/RunningJobs.js @@ -22,18 +22,29 @@ angular.module('RunningJobsDefinition', []) fields: { id: { label: 'Job ID', + ngClick:"viewJobLog(running_job.id)", key: true, desc: true, searchType: 'int', columnClass: 'col-md-1 col-sm-2 col-xs-2' }, + status: { + label: 'Status', + columnClass: 'col-md-2 col-sm-2 col-xs-2', + awToolTip: "{{ running_job.status_tip }}", + awTipPlacement: "top", + dataTitle: "{{ running_job.status_popover_title }}", + icon: 'icon-job-{{ running_job.status }}', + iconOnly: true, + ngClick:"viewJobLog(running_job.id)" + }, inventory: { label: 'Inventory ID', searchType: 'int', searchOnly: true }, - modified: { - label: 'Last Updated', + started: { + label: 'Started On', link: false, searchable: false, filter: "date:'MM/dd/yy HH:mm:ss'", @@ -48,9 +59,7 @@ angular.module('RunningJobsDefinition', []) name: { label: 'Name', columnClass: 'col-md-3 col-xs-5', - ngHref: 'nameHref', - sourceModel: 'template', - sourceField: 'name' + ngClick: "viewJobLog(running_job.id, running_job.nameHref)" } }, @@ -59,25 +68,16 @@ angular.module('RunningJobsDefinition', []) refresh: { mode: 'all', awToolTip: "Refresh the page", - ngClick: "refresh()" + ngClick: "refreshJobs()" } }, fieldActions: { - status: { - mode: 'all', - awToolTip: "{{ running_job.status_tip }}", - awTipPlacement: "top", - dataTitle: "{{ running_job.status_popover_title }}", - iconClass: 'fa icon-job-{{ running_job.status }}', - awPopOver: "{{ running_job.status_popover }}", - dataPlacement: 'left' - }, submit: { icon: 'icon-rocket', mode: 'all', - ngClick: 'relaunch(running_job.id)', - awToolTip: 'Launch another instance of the job', + ngClick: 'relaunchJob(running_job.id)', + awToolTip: 'Relaunch using the same parameters', dataPlacement: 'top' }, cancel: { @@ -88,11 +88,12 @@ angular.module('RunningJobsDefinition', []) }, dropdown: { type: 'DropDown', + ngShow: "running_job.type === 'job'", label: 'View', icon: 'fa-search-plus', 'class': 'btn-default btn-xs', options: [ - { ngHref: '/#/jobs/{{ running_job.id }}', label: 'Status' }, + //{ ngHref: '/#/jobs/{{ running_job.id }}', label: 'Status' }, { ngHref: '/#/jobs/{{ running_job.id }}/job_events', label: 'Events' }, { ngHref: '/#/jobs/{{ running_job.id }}/job_host_summaries', label: 'Host Summary' } ] diff --git a/awx/ui/static/js/lists/ScheduledJobs.js b/awx/ui/static/js/lists/ScheduledJobs.js index 6b0d77b0f2..e9bee7c964 100644 --- a/awx/ui/static/js/lists/ScheduledJobs.js +++ b/awx/ui/static/js/lists/ScheduledJobs.js @@ -53,7 +53,7 @@ angular.module('ScheduledJobsDefinition', []) refresh: { mode: 'all', awToolTip: "Refresh the page", - ngClick: "refresh()" + ngClick: "refreshJobs()" } }, diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 721a1813e9..6423eb3a31 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -64,9 +64,14 @@ body.modal-open { .nowrap { white-space: nowrap; } .capitalize { text-transform: capitalize; } .grey-txt { color: @grey; } -.red-txt { color: @red; } a.red-txt:hover { color: @red; } //make red links (for things like cancel) .text-center { text-align: center !important; } +.red-txt, +a.red-txt:visited, +a.red-txt:hover, +a.red-txt:active { + color: @red; +} /* Used on inventory groups/hosts lists for long names */ .ellipsis { @@ -973,12 +978,9 @@ input[type="checkbox"].checkbox-no-label { color: @green; } - .icon-job-pending:before, .icon-job-running:before, .icon-job-success:before, .icon-job-successful:before, - .icon-job-waiting:before, - .icon-job-new:before, .icon-job-changed:before { content: "\f111"; } @@ -989,12 +991,16 @@ input[type="checkbox"].checkbox-no-label { content: "\f06a"; } - .icon-job-pending, + .icon-job-pending:before, + .icon-job-waiting:before, + .icon-job-new:before, + .icon-job-none:before { + content: "\f10c"; + } + .icon-job-running, .icon-job-success, - .icon-job-successful, - .icon-job-waiting, - .icon-job-new { + .icon-job-successful { color: @green; } @@ -1013,15 +1019,14 @@ input[type="checkbox"].checkbox-no-label { color: @red; } - .icon-job-none { + .icon-job-none, + .icon-job-pending, + .icon-job-waiting, + .icon-job-new { color: @grey; opacity: 0.45; } - .icon-job-none:before { - content: "\f10c"; - } - .icon-schedule-enabled-true:before { content: "\f04c"; } @@ -1048,14 +1053,11 @@ input[type="checkbox"].checkbox-no-label { .pagination li a { font-size: 12px; } - .list-table-container { + /*.list-table-container { min-height: 338px; - } + }*/ } - .job-list-target { - min-height: 445px; - } - + /* Inventory job status badge */ .failures-true { background-color: @red; diff --git a/awx/ui/static/less/jquery-ui-overrides.less b/awx/ui/static/less/jquery-ui-overrides.less index bcb1d8de1e..086a0eb1fa 100644 --- a/awx/ui/static/less/jquery-ui-overrides.less +++ b/awx/ui/static/less/jquery-ui-overrides.less @@ -88,3 +88,29 @@ table.ui-datepicker-calendar { .ui-dialog-content.ui-widget-content { padding-top: 20px; } + +.ui-widget-content { + a, + a:visited, + a:active { + color: @blue; + text-decoration: none; + } + + a:hover, + a:focus { + color: @blue-dark; + text-decoration: none; + } + + .red-txt, + a.red-txt:visited, + a.red-txt:hover, + a.red-txt:active { + color: @red; + } + + .dropdown-menu>li>a { + color: @black; + } +} diff --git a/awx/ui/static/less/log-viewer.less b/awx/ui/static/less/log-viewer.less index ab9824baf4..2f93c77d4e 100644 --- a/awx/ui/static/less/log-viewer.less +++ b/awx/ui/static/less/log-viewer.less @@ -10,7 +10,13 @@ #logviewer-modal-dialog { textarea { - overflow: auto; + overflow: scroll; + } + pre { + overflow: scroll; + word-wrap: normal; + word-break: normal; + white-space: pre-wrap; } } diff --git a/awx/ui/static/lib/ansible/InventoryTree.js b/awx/ui/static/lib/ansible/InventoryTree.js index 60c5a2cb56..52f60dbbe3 100644 --- a/awx/ui/static/lib/ansible/InventoryTree.js +++ b/awx/ui/static/lib/ansible/InventoryTree.js @@ -162,9 +162,7 @@ angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'P } }) .error(function (data, status) { - Wait('stop'); - ProcessErrors(scope, data, status, null, { - hdr: 'Error!', + ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get inventory tree for: ' + inventory_id + '. GET returned: ' + status }); }); @@ -309,10 +307,10 @@ angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'P html += "
\n"; if (target.id === 1) { - html += "

Are you sure you want to move group " + inbound.name + " to the top level?

"; + html += "
Are you sure you want to move group " + inbound.name + " to the top level?
"; } else if (inbound.parent === 0) { - html += "

Are you sure you want to move group " + inbound.name + " from the top level and make it a child of " + - target.name + "?

"; + html += "
Are you sure you want to move group " + inbound.name + " from the top level and make it a child of " + + target.name + "?
"; } else { html += "
\n"; html += "

Would you like to copy or move group " + inbound.name + " to group " + target.name + "?

\n"; @@ -502,7 +500,7 @@ angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'P html += "

Copy Host

\n"; html += "
\n"; html += "
\n"; - html += "

Are you sure you want to copy host " + host.name + ' to group ' + target.name + '?

'; + html += "
Are you sure you want to copy host " + host.name + ' to group ' + target.name + '?
'; html += "
\n"; html += "
\n"; html += "No\n"; diff --git a/awx/ui/static/lib/ansible/Utilities.js b/awx/ui/static/lib/ansible/Utilities.js index 84fd39dec3..4a65dac2c8 100644 --- a/awx/ui/static/lib/ansible/Utilities.js +++ b/awx/ui/static/lib/ansible/Utilities.js @@ -79,6 +79,7 @@ angular.module('Utilities', ['RestServices', 'Utilities']) return function (hdr, msg, cls, action, secondAlert, disableButtons) { var scope = $rootScope.$new(), e; if (secondAlert) { + $('#alert2-modal-msg').attr({ "class": "alert" }); scope.alertHeader2 = hdr; scope.alertBody2 = msg; scope.alertClass2 = (cls) ? cls : 'alert-danger'; //default alert class is alert-danger @@ -102,9 +103,11 @@ angular.module('Utilities', ['RestServices', 'Utilities']) } }); } else { + $('#alert-modal-msg').attr({ "class": "alert" }); scope.alertHeader = hdr; scope.alertBody = msg; scope.alertClass = (cls) ? cls : 'alert-danger'; //default alert class is alert-danger + //console.log('msg: ' + msg + ' cls: ' + cls); e = angular.element(document.getElementById('alert-modal')); $compile(e)(scope); $('#alert-modal').modal({ diff --git a/awx/ui/static/lib/ansible/generator-helpers.js b/awx/ui/static/lib/ansible/generator-helpers.js index 8f9b6e6e51..17f7905d16 100644 --- a/awx/ui/static/lib/ansible/generator-helpers.js +++ b/awx/ui/static/lib/ansible/generator-helpers.js @@ -264,6 +264,7 @@ angular.module('GeneratorHelpers', []) html += "" : ""; + if (field.awPopOver) { + html += "aw-pop-over=\"" + field.awPopOver + "\" "; + html += (field.dataPlacement) ? "data-placement=\"" + field.dataPlacement + "\" " : ""; + } + html += ">"; } // Add icon: @@ -563,7 +565,7 @@ angular.module('GeneratorHelpers', []) //} // close the link - if ((field.key || field.link || field.linkTo || field.ngClick || field.ngHref) && + if ((field.key || field.link || field.linkTo || field.ngClick || field.ngHref || field.awToolTip || field.awPopOver) && options.mode !== 'lookup' && options.mode !== 'select' && !field.noLink && !field.ngBindHtml) { html += ""; } diff --git a/awx/ui/static/partials/job_events.html b/awx/ui/static/partials/job_events.html new file mode 100644 index 0000000000..b9a842f6ae --- /dev/null +++ b/awx/ui/static/partials/job_events.html @@ -0,0 +1,3 @@ +
+
+
diff --git a/awx/ui/static/partials/job_host_summaries.html b/awx/ui/static/partials/job_host_summaries.html new file mode 100644 index 0000000000..b9ca5b8eea --- /dev/null +++ b/awx/ui/static/partials/job_host_summaries.html @@ -0,0 +1,3 @@ +
+
+
diff --git a/awx/ui/static/partials/jobs.html b/awx/ui/static/partials/jobs.html index 4ecc3e92e7..a3e6f3bb01 100644 --- a/awx/ui/static/partials/jobs.html +++ b/awx/ui/static/partials/jobs.html @@ -33,6 +33,7 @@
-
+
+
\ No newline at end of file diff --git a/awx/ui/static/partials/logviewer.html b/awx/ui/static/partials/logviewer.html index 6469c6d752..2fafebf3f7 100644 --- a/awx/ui/static/partials/logviewer.html +++ b/awx/ui/static/partials/logviewer.html @@ -5,6 +5,8 @@
  • Standard Out
  • Traceback
  • Options
  • +
  • Extra Vars
  • +
  • Source Vars
  • @@ -19,5 +21,11 @@
    +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index 6db7986a3a..06227ff9e0 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -331,7 +331,7 @@