diff --git a/awx/ui/static/js/controllers/Jobs.js b/awx/ui/static/js/controllers/Jobs.js index 965f0f1d12..d439e47db3 100644 --- a/awx/ui/static/js/controllers/Jobs.js +++ b/awx/ui/static/js/controllers/Jobs.js @@ -85,34 +85,289 @@ function JobsList($scope, $compile, ClearScope, Breadcrumbs, LoadScope, RunningJ callback: 'choicesReady' }); - /* Use for types later GetChoices({ scope: $scope, - url: GetBasePath('jobs'), + url: '/static/sample/data/types/data.json', //GetBasePath('jobs') field: 'type', - variable: 'types', - callback: '' + variable: 'type_choices', + callback: 'choicesReady' }); - */ - $scope.type_choices = [{ - label: 'Inventory sync', - value: 'inventory_sync', - name: 'inventory_sync' - },{ - label: 'Project sync', - value: 'scm_sync', - name: 'scm_sync' - },{ - label: 'Playbook run', - value: 'playbook_run', - name: 'playbook_run' - }]; - $scope.$emit('choicesReady'); } JobsList.$inject = ['$scope', '$compile', 'ClearScope', 'Breadcrumbs', 'LoadScope', 'RunningJobsList', 'CompletedJobsList', 'QueuedJobsList', 'GetChoices', 'GetBasePath', 'Wait']; +function JobsEdit($scope, $rootScope, $compile, $location, $log, $routeParams, JobForm, JobTemplateForm, GenerateForm, Rest, + Alert, ProcessErrors, LoadBreadCrumbs, RelatedSearchInit, RelatedPaginateInit, ReturnToCaller, ClearScope, InventoryList, + CredentialList, ProjectList, LookUpInit, PromptPasswords, GetBasePath, md5Setup, FormatDate, JobStatusToolTip, Wait, Empty, + ParseVariableString, GetChoices) { + + ClearScope(); + + var defaultUrl = GetBasePath('jobs'), + generator = GenerateForm, + id = $routeParams.id, + loadingFinishedCount = 0, + templateForm = {}, + choicesCount = 0; + + generator.inject(JobForm, { mode: 'edit', related: true, scope: $scope }); + + $scope.job_id = id; + $scope.parseType = 'yaml'; + $scope.statusSearchSpin = false; + $scope.disableParseSelection = true; + + function getPlaybooks(project, playbook) { + if (!Empty(project)) { + var url = GetBasePath('projects') + project + '/playbooks/'; + Rest.setUrl(url); + Rest.get() + .success(function (data) { + var i; + $scope.playbook_options = []; + for (i = 0; i < data.length; i++) { + $scope.playbook_options.push(data[i]); + } + for (i = 0; i < $scope.playbook_options.length; i++) { + if ($scope.playbook_options[i] === playbook) { + $scope.playbook = $scope.playbook_options[i]; + } + } + $scope.$emit('jobTemplateLoadFinished'); + }) + .error(function () { + $scope.$emit('jobTemplateLoadFinished'); + }); + } else { + $scope.$emit('jobTemplateLoadFinished'); + } + } + // Retrieve each related set and populate the playbook list + if ($scope.jobLoadedRemove) { + $scope.jobLoadedRemove(); + } + $scope.jobLoadedRemove = $scope.$on('jobLoaded', function (e, related_cloud_credential, project, playbook) { + + getPlaybooks(project, playbook); + if (related_cloud_credential) { + //Get the name of the cloud credential + Rest.setUrl(related_cloud_credential); + Rest.get() + .success(function (data) { + $scope.cloud_credential_name = data.name; + $scope.$emit('jobTemplateLoadFinished'); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to related cloud credential. GET returned status: ' + status }); + }); + } else { + $scope.$emit('jobTemplateLoadFinished'); + } + + }); + + // Turn off 'Wait' after both cloud credential and playbook list come back + if ($scope.removeJobTemplateLoadFinished) { + $scope.removeJobTemplateLoadFinished(); + } + $scope.removeJobTemplateLoadFinished = $scope.$on('jobTemplateLoadFinished', function () { + loadingFinishedCount++; + if (loadingFinishedCount >= 2) { + // The initial template load finished. Now load related jobs, which + // will turn off the 'working' spinner. + Wait('stop'); + } + }); + + $scope.verbosity_options = [{ + value: 0, + label: 'Default' + }, { + value: 1, + label: 'Verbose' + }, { + value: 3, + label: 'Debug' + }]; + + $scope.playbook_options = null; + $scope.playbook = null; + + function calcRows(content) { + var n = content.match(/\n/g), + rows = (n) ? n.length : 1; + return (rows > 15) ? 15 : rows; + } + + if ($scope.removeLoadJobTemplate) { + $scope.removeLoadJobTemplate(); + } + $scope.removeLoadJobTemplate = $scope.$on('loadJobTemplate', function() { + // Retrieve the job detail record and prepopulate the form + Rest.setUrl(defaultUrl + ':id/'); + Rest.get({ params: { id: id } }) + .success(function (data) { + + var i, fld; + + LoadBreadCrumbs(); + + $scope.status = data.status; + $scope.created = FormatDate(data.created); + $scope.modified = FormatDate(data.modified); + $scope.result_stdout = data.result_stdout; + $scope.result_traceback = data.result_traceback; + $scope.stdout_rows = calcRows($scope.result_stdout); + $scope.traceback_rows = calcRows($scope.result_traceback); + $scope.job_explanation = data.job_explanation || 'Things may have ended badly or gone swimingly well'; + + // Now load the job template form + templateForm.addTitle = 'Create Job Templates'; + templateForm.editTitle = '{{ name }}'; + templateForm.name = 'job_templates'; + templateForm.twoColumns = true; + templateForm.fields = angular.copy(JobTemplateForm.fields); + for (fld in templateForm.fields) { + templateForm.fields[fld].readonly = true; + } + + if (data.type === "playbook_run") { + $('#ui-accordion-jobs-collapse-0-panel-1').empty(); + generator.inject(templateForm, { + mode: 'edit', + id: 'ui-accordion-jobs-collapse-0-panel-1', + related: false, + scope: $scope, + breadCrumbs: false + }); + } + else { + $('#ui-accordion-jobs-collapse-0-header-1').hide(); + $('#ui-accordion-jobs-collapse-0-panel-1').empty().hide(); + $('#jobs-collapse-0').accordion( "option", "collapsible", false ); + } + + for (fld in templateForm.fields) { + if (fld !== 'variables' && data[fld] !== null && data[fld] !== undefined) { + if (JobTemplateForm.fields[fld].type === 'select') { + if ($scope[fld + '_options'] && $scope[fld + '_options'].length > 0) { + for (i = 0; i < $scope[fld + '_options'].length; i++) { + if (data[fld] === $scope[fld + '_options'][i].value) { + $scope[fld] = $scope[fld + '_options'][i]; + } + } + } else { + $scope[fld] = data[fld]; + } + } else { + $scope[fld] = data[fld]; + } + } + if (fld === 'variables') { + $scope.variables = ParseVariableString(data.extra_vars); + } + if (JobTemplateForm.fields[fld].type === 'lookup' && data.summary_fields[JobTemplateForm.fields[fld].sourceModel]) { + $scope[JobTemplateForm.fields[fld].sourceModel + '_' + JobTemplateForm.fields[fld].sourceField] = + data.summary_fields[JobTemplateForm.fields[fld].sourceModel][JobTemplateForm.fields[fld].sourceField]; + } + } + + $scope.id = data.id; + $scope.name = (data.summary_fields && data.summary_fields.job_template) ? data.summary_fields.job_template.name : ''; + $scope.statusToolTip = JobStatusToolTip(data.status); + $scope.url = data.url; + $scope.project = data.project; + $scope.launch_type = data.launch_type; + + // set the type + data.type = 'playbook_run'; //temporary + $scope.type_choices.every( function(choice) { + if (choice.value === data.type) { + $scope.type = choice.label; + return false; + } + return true; + }); + + $scope.$emit('jobLoaded', data.related.cloud_credential, data.project, data.playbook); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve job: ' + $routeParams.id + '. GET status: ' + status }); + }); + }); + + Wait('start'); + + if ($scope.removeChoicesReady) { + $scope.removeChoicesReady(); + } + $scope.removeChoicesReady = $scope.$on('choicesReady', function() { + choicesCount++; + if (choicesCount === 2) { + $scope.$emit('loadJobTemplate'); + } + }); + + GetChoices({ + scope: $scope, + url: GetBasePath('jobs'), + field: 'job_type', + variable: 'job_type_options', + callback: 'choicesReady' + }); + + /*GetChoices({ + scope: $scope, + url: GetBasePath('jobs'), + field: 'status', + variable: 'status_choices', + callback: 'choicesReady' + });*/ + + GetChoices({ + scope: $scope, + url: '/static/sample/data/types/data.json', //GetBasePath('jobs') + field: 'type', + variable: 'type_choices', + callback: 'choicesReady' + }); + + $scope.refresh = function () { + Wait('start'); + Rest.setUrl(defaultUrl + id + '/'); + Rest.get() + .success(function (data) { + $scope.status = data.status; + $scope.result_stdout = data.result_stdout; + $scope.result_traceback = data.result_traceback; + $scope.stdout_rows = calcRows($scope.result_stdout); + $scope.traceback_rows = calcRows($scope.result_traceback); + Wait('stop'); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Attempt to load job failed. GET returned status: ' + status }); + }); + }; + + $scope.jobSummary = function () { + $location.path('/jobs/' + id + '/job_host_summaries'); + }; + + $scope.jobEvents = function () { + $location.path('/jobs/' + id + '/job_events'); + }; +} + +JobsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'JobForm', 'JobTemplateForm', + 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'RelatedSearchInit', 'RelatedPaginateInit', + 'ReturnToCaller', 'ClearScope', 'InventoryList', 'CredentialList', 'ProjectList', 'LookUpInit', 'PromptPasswords', + 'GetBasePath', 'md5Setup', 'FormatDate', 'JobStatusToolTip', 'Wait', 'Empty', 'ParseVariableString', 'GetChoices' +]; + diff --git a/awx/ui/static/js/forms/Jobs.js b/awx/ui/static/js/forms/Jobs.js index 846a98d437..4e08672cfe 100644 --- a/awx/ui/static/js/forms/Jobs.js +++ b/awx/ui/static/js/forms/Jobs.js @@ -42,13 +42,7 @@ angular.module('JobFormDefinition', []) fields: { status: { type: 'custom', - control: "
Status " + - " {{ status }}
", - readonly: true - }, - created: { - label: 'Created On', - type: 'text', + control: "  {{ job_explanation }}", readonly: true }, result_stdout: { @@ -57,7 +51,7 @@ angular.module('JobFormDefinition', []) readonly: true, xtraWide: true, rows: "{{ stdout_rows }}", - "class": 'nowrap mono-space', + "class": 'nowrap mono-space allowresize', ngShow: "result_stdout != ''" }, result_traceback: { @@ -66,8 +60,28 @@ angular.module('JobFormDefinition', []) xtraWide: true, readonly: true, rows: "{{ traceback_rows }}", - "class": 'nowrap mono-space', + "class": 'nowrap mono-space allowresize', ngShow: "result_traceback != ''" + }, + type: { + label: 'Job Type', + type: 'text', + readonly: true + }, + launch_type: { + label: 'Launch Type', + type: 'text', + readonly: true + }, + created: { + label: 'Created On', + type: 'text', + readonly: true + }, + modified: { + label: 'Last Updated', + type: 'text', + readonly: true } }, @@ -95,4 +109,5 @@ angular.module('JobFormDefinition', []) fields: { } } } + }); \ No newline at end of file diff --git a/awx/ui/static/js/helpers/Jobs.js b/awx/ui/static/js/helpers/Jobs.js index d0c75b3183..44fea28a78 100644 --- a/awx/ui/static/js/helpers/Jobs.js +++ b/awx/ui/static/js/helpers/Jobs.js @@ -220,8 +220,9 @@ angular.module('JobsHelper', ['Utilities', 'FormGenerator', 'JobSummaryDefinitio scope[list.name] = data.results; window.scrollTo(0, 0); - if (list.fields.type) { - scope[list.name].forEach(function(item, item_idx) { + scope[list.name].forEach(function(item, item_idx) { + // Set the item type label + if (list.fields.type) { parent_scope.type_choices.every(function(choice) { if (choice.value === item.type) { scope[list.name][item_idx].type = choice.label; @@ -229,8 +230,26 @@ angular.module('JobsHelper', ['Utilities', 'FormGenerator', 'JobSummaryDefinitio } return true; }); + } + // Set the job status label + parent_scope.status_choices.every(function(status) { + if (status.value === item.status) { + scope[list.name][item_idx].status_label = status.label; + return false; + } + return true; }); - } + if (list.name === 'completed_jobs' || list.name === 'running_jobs') { + scope[list.name][item_idx].status_tip = scope[list.name][item_idx].status_label + '. Click for details.'; + } + else { + scope[list.name][item_idx].status_tip = 'Pending'; + } + scope[list.name][item_idx].status_popover_title = scope[list.name][item_idx].status_label; + scope[list.name][item_idx].status_popover = "

" + scope[list.name][item_idx].job_explanation + "

\n"; + scope[list.name][item_idx].status_popover += "

More...

\n"; + }); + parent_scope.$emit('listLoaded'); }); diff --git a/awx/ui/static/js/lists/CompletedJobs.js b/awx/ui/static/js/lists/CompletedJobs.js index c1655c23fb..2a5ee79d01 100644 --- a/awx/ui/static/js/lists/CompletedJobs.js +++ b/awx/ui/static/js/lists/CompletedJobs.js @@ -65,29 +65,12 @@ angular.module('CompletedJobsDefinition', []) fieldActions: { status: { mode: 'all', - //"class": 'job-{{ completed_job.status }}', - //searchType: 'select', - //linkTo: "{{ completed_job.statusLinkTo }}", - //searchOptions: [ - // { name: "new", value: "new" }, - // { name: "waiting", value: "waiting" }, - // { name: "pending", value: "pending" }, - // { name: "running", value: "running" }, - // { name: "successful", value: "successful" }, - // { name: "error", value: "error" }, - // { name: "failed", value: "failed" }, - // { name: "canceled", value: "canceled" } - //], + awToolTip: "{{ completed_job.status_tip }}", + awTipPlacement: "top", + dataTitle: "{{ completed_job.status_popover_title }}", iconClass: 'fa icon-job-{{ completed_job.status }}', - awToolTip: "{{ completed_job.statusToolTip }}", - dataPlacement: 'top' - //badgeIcon: 'fa icon-job-{{ completed_job.status }}', - //badgePlacement: 'left', - //badgeToolTip: "{{ completed_job.statusBadgeToolTip }}", - //badgeTipPlacement: 'top', - //badgeNgHref: "{{ completed_job.statusLinkTo }}", - //awToolTip: "{{ completed_job.statusBadgeToolTip }}", - //dataPlacement: 'top' + awPopOver: "{{ completed_job.status_popover }}", + dataPlacement: 'left' }, submit: { icon: 'icon-rocket', diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 050b44b438..d8fb3a1045 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -158,6 +158,10 @@ textarea { resize: none; } +textarea.allowresize { + resize: both; +} + /* Working... spinner */ .spinny { display: none; @@ -210,7 +214,7 @@ textarea { .popover-content { width: 100%; } - h3.popover-title, .popover-content, .popover-content blockquote { + h3.popover-title, .popover-content, .popover-content blockquote, .popover-content a { font-size: 12px; } .flyout thead> tr> th, .flyout tbody> tr> td, .flyout tbody> tr> td> a { @@ -1164,7 +1168,6 @@ input[type="checkbox"].checkbox-no-label { display: inline-block; margin-top: 5px; font-size: 15px; - font-weight: bold; } /*.form-items .search-widget { diff --git a/awx/ui/static/lib/ansible/directives.js b/awx/ui/static/lib/ansible/directives.js index b29ab26b55..ba521a97ed 100644 --- a/awx/ui/static/lib/ansible/directives.js +++ b/awx/ui/static/lib/ansible/directives.js @@ -286,9 +286,10 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job return function(scope, element, attrs) { var placement = (attrs.placement !== undefined && attrs.placement !== null) ? attrs.placement : 'left', title = (attrs.title !== undefined && attrs.title !== null) ? attrs.title : 'Help', - container = (attrs.container !== undefined) ? attrs.container : false; + container = (attrs.container !== undefined) ? attrs.container : false, + trigger = (attrs.trigger !== undefined) ? attrs.trigger : 'manua'; $(element).popover({ placement: placement, delay: 0, title: title, - content: attrs.awPopOver, trigger: 'manual', html: true, container: container }); + content: attrs.awPopOver, trigger: trigger, html: true, container: container }); $(element).click(function() { var self = $(this).attr('id'); $('.help-link, .help-link-white').each( function() { diff --git a/awx/ui/static/lib/ansible/form-generator.js b/awx/ui/static/lib/ansible/form-generator.js index 04bb1f7b60..4850257b61 100644 --- a/awx/ui/static/lib/ansible/form-generator.js +++ b/awx/ui/static/lib/ansible/form-generator.js @@ -1308,16 +1308,13 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies', 'Utilities']) if (this.form.collapse && this.form.collapseMode === options.mode) { html += "\n"; - //html += "\n"; } - if ((!this.modal) && options.related && this.form.related) { html += this.buildCollections(options); } return html; - }, buildCollections: function (options) { @@ -1339,7 +1336,7 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies', 'Utilities']) for (itm in form.related) { if (form.related[itm].type === 'collection') { - html += "

" + form.related[itm].title + "

\n"; + html += "

" + form.related[itm].title + "

\n"; html += "
\n"; if (form.related[itm].instructions) { diff --git a/awx/ui/static/lib/ansible/generator-helpers.js b/awx/ui/static/lib/ansible/generator-helpers.js index 51b51b9693..1ceba4fed3 100644 --- a/awx/ui/static/lib/ansible/generator-helpers.js +++ b/awx/ui/static/lib/ansible/generator-helpers.js @@ -42,6 +42,7 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers']) result += (obj.dataPlacement) ? "data-placement=\"" + obj.dataPlacement + "\" " : ""; result += (obj.dataContainer) ? "data-container=\"" + obj.dataContainer + "\" " : ""; result += (obj.dataTitle) ? "data-title=\"" + obj.dataTitle + "\" " : ""; + result += (obj.dataTrigger) ? "data-trigger=\"" + obj.dataTrigger + "\" " : ""; result += "class=\"help-link\" "; result += "> "; break; @@ -271,7 +272,8 @@ angular.module('GeneratorHelpers', ['GeneratorHelpers']) html += "