diff --git a/lib/ui/static/css/ansible-ui.css b/lib/ui/static/css/ansible-ui.css index b2d1f6d1df..017c22b754 100644 --- a/lib/ui/static/css/ansible-ui.css +++ b/lib/ui/static/css/ansible-ui.css @@ -244,7 +244,7 @@ margin-left: 15px; margin-top: 3px; } - /* Display list actions */ + /* End Display list actions */ .well { padding-bottom: 0; @@ -256,7 +256,7 @@ background-color: #fff; } - .job-error, .job-failure { + .job-error, .job-failed { color: #da4f49; } @@ -267,4 +267,5 @@ .job-pending, .job-running { color: #5bb75b; } + /* End Jobs Page */ diff --git a/lib/ui/static/js/app.js b/lib/ui/static/js/app.js index 272b58a50b..53a0a4c275 100644 --- a/lib/ui/static/js/app.js +++ b/lib/ui/static/js/app.js @@ -45,12 +45,16 @@ angular.module('ansible', [ 'JobTemplateFormDefinition', 'JobTemplateHelper', 'ProjectsListDefinition', - 'JobsListDefinition' + 'JobsListDefinition', + 'JobFormDefinition' ]) .config(['$routeProvider', function($routeProvider) { $routeProvider. when('/jobs', { templateUrl: urlPrefix + 'partials/jobs.html', controller: JobsList }). + + when('/jobs/:id', + { templateUrl: urlPrefix + 'partials/jobs.html', controller: JobsEdit }). when('/job_templates', { templateUrl: urlPrefix + 'partials/job_templates.html', controller: JobTemplatesList }). diff --git a/lib/ui/static/js/controllers/Groups.js b/lib/ui/static/js/controllers/Groups.js index 81ab3f86d3..2025d762fa 100644 --- a/lib/ui/static/js/controllers/Groups.js +++ b/lib/ui/static/js/controllers/Groups.js @@ -155,7 +155,7 @@ function GroupsAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, // Inject dynamic view var defaultUrl = ($routeParams.group_id) ? GetBasePath('groups') + $routeParams.group_id + '/children/' : - GetBasePath('inventory') + $routeParams.inventory_id + '/'; + GetBasePath('inventory') + $routeParams.inventory_id + '/groups/'; var form = GroupForm; var generator = GenerateForm; var scope = generator.inject(form, {mode: 'add', related: false}); diff --git a/lib/ui/static/js/controllers/JobTemplates.js b/lib/ui/static/js/controllers/JobTemplates.js index 5ab83c120a..b8fd18281a 100644 --- a/lib/ui/static/js/controllers/JobTemplates.js +++ b/lib/ui/static/js/controllers/JobTemplates.js @@ -136,8 +136,7 @@ function JobTemplatesList ($scope, $rootScope, $location, $log, $routeParams, Re } function postJob(data) { - // Once we have a credential and all required passwords, use this - // to create and start a job + // Create the job record (scope.credentialWatchRemove) ? scope.credentialWatchRemove() : null; var dt = new Date().toISOString(); Rest.setUrl(data.related.jobs); @@ -155,17 +154,31 @@ function JobTemplatesList ($scope, $rootScope, $location, $log, $routeParams, Re extra_vars: data.extra_vars }) .success( function(data, status, headers, config) { - // Prompt for passwords and start the job - PromptPasswords({ - scope: scope, - passwords: data.passwords_needed_to_start, - start_url: data.related.start - }); - }) + if (data.passwords_needed_to_start.length > 0) { + // Passwords needed. Prompt for passwords, then start job. + PromptPasswords({ + scope: scope, + passwords: data.passwords_needed_to_start, + start_url: data.related.start + }); + } + else { + // No passwords needed, start the job! + Rest.setUrl(data.related.start); + Rest.post() + .success( function(data, status, headers, config) { + $location.path(GetBasePath('jobs')); + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, null, + { hdr: 'Error!', msg: 'Failed to start job. POST returned status: ' + status }); + }); + } + }) .error( function(data, status, headers, config) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to create job. POST returned status: ' + status }); - }); + }); }; scope.submitJob = function(id) { diff --git a/lib/ui/static/js/controllers/Jobs.js b/lib/ui/static/js/controllers/Jobs.js index 952bc5897d..1dc0b94f1f 100644 --- a/lib/ui/static/js/controllers/Jobs.js +++ b/lib/ui/static/js/controllers/Jobs.js @@ -27,10 +27,242 @@ function JobsList ($scope, $rootScope, $location, $log, $routeParams, Rest, Aler scope.search(list.iterator); LoadBreadCrumbs(); + + scope.editJob = function(id) { + $location.path($location.path() + '/' + id); + } } JobsList.$inject = [ '$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'JobList', 'GenerateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors','GetBasePath', 'LookUpInit' - ]; \ No newline at end of file + ]; + + +function JobsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, JobForm, + GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, RelatedSearchInit, + RelatedPaginateInit, ReturnToCaller, ClearScope, InventoryList, CredentialList, + ProjectList, LookUpInit, PromptPasswords, GetBasePath) +{ + ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior + //scope. + + var defaultUrl= GetBasePath('jobs'); + var generator = GenerateForm; + var form = JobForm; + + var scope = generator.inject(form, {mode: 'edit', related: true}); + generator.reset(); + var base = $location.path().replace(/^\//,'').split('/')[0]; + var master = {}; + var id = $routeParams.id; + var relatedSets = {}; + + + function getPlaybooks(project) { + if (project !== null && project !== '' && project !== undefined) { + var url = GetBasePath('projects') + project + '/playbooks/'; + Rest.setUrl(url); + Rest.get() + .success( function(data, status, headers, config) { + scope.playbook_options = []; + for (var i=0; i < data.length; i++) { + scope.playbook_options.push(data[i]); + } + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, form, + { hdr: 'Error!', msg: 'Failed to get playbook list for ' + url +'. GET returned status: ' + status }); + }); + } + } + + // Register a watcher on project_name. Refresh the playbook list on change. + if (scope.selectPlaybookUnregister) { + scope.selectPlaybookUnregister(); + } + scope.selectPlaybookUnregister = scope.$watch('project_name', function(oldValue, newValue) { + if (oldValue !== newValue && newValue !== '' && newValue !== null && newValue !== undefined) { + scope.playbook = null; + getPlaybooks(scope.project); + } + }); + + // Retrieve each related set and populate the playbook list + if (scope.jobLoadedRemove) { + scope.jobLoadedRemove(); + } + scope.jobLoadedRemove = scope.$on('jobLoaded', function() { + + scope[form.name + 'ReadOnly'] = (scope.status == 'new') ? false : true; + + // Load related sets + for (var set in relatedSets) { + scope.search(relatedSets[set].iterator); + } + // Set the playbook lookup + getPlaybooks(scope.project); + }); + + // Our job type options + scope.job_type_options = [{ value: 'run', label: 'Run' }, { value: 'check', label: 'Check' }]; + scope.playbook_options = null; + scope.playbook = null; + + // Retrieve detail record and prepopulate the form + Rest.setUrl(defaultUrl + ':id/'); + Rest.get({ params: {id: id} }) + .success( function(data, status, headers, config) { + LoadBreadCrumbs({ path: '/job_templates/' + id, title: data.name }); + for (var fld in form.fields) { + if (data[fld] !== null && data[fld] !== undefined) { + if (form.fields[fld].type == 'select') { + if (scope[fld + '_options'] && scope[fld + '_options'].length > 0) { + for (var 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]; + } + master[fld] = scope[fld]; + } + if (form.fields[fld].type == 'lookup' && data.summary_fields[form.fields[fld].sourceModel]) { + scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; + master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField]; + } + } + + for (var fld in form.statusFields) { + if (data[fld] !== null && data[fld] !== undefined) { + scope[fld] = data[fld]; + } + } + + scope.url = data.url; + var related = data.related; + for (var set in form.related) { + if (related[set]) { + relatedSets[set] = { url: related[set], iterator: form.related[set].iterator }; + } + } + + LookUpInit({ + scope: scope, + form: form, + current_item: data.inventory, + list: InventoryList, + field: 'inventory' + }); + + LookUpInit({ + scope: scope, + form: form, + current_item: data.credential, + list: CredentialList, + field: 'credential' + }); + + LookUpInit({ + scope: scope, + form: form, + current_item: data.project, + list: ProjectList, + field: 'project' + }); + + // Initialize related search functions. Doing it here to make sure relatedSets object is populated. + RelatedSearchInit({ scope: scope, form: form, relatedSets: relatedSets }); + RelatedPaginateInit({ scope: scope, relatedSets: relatedSets }); + scope.$emit('jobLoaded'); + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, form, + { hdr: 'Error!', msg: 'Failed to retrieve job template: ' + $routeParams.id + '. GET status: ' + status }); + }); + + // Save changes to the parent + scope.formSave = function() { + Rest.setUrl(defaultUrl + $routeParams.id); + var data = {} + for (var fld in form.fields) { + if (form.fields[fld].type == 'select' && fld != 'playbook') { + data[fld] = scope[fld].value; + } + else { + data[fld] = scope[fld]; + } + } + Rest.put(data) + .success( function(data, status, headers, config) { + var base = $location.path().replace(/^\//,'').split('/')[0]; + (base == 'job_templates') ? ReturnToCaller() : ReturnToCaller(1); + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, form, + { hdr: 'Error!', msg: 'Failed to update team: ' + $routeParams.id + '. PUT status: ' + status }); + }); + }; + + // Cancel + scope.formReset = function() { + generator.reset(); + for (var fld in master) { + scope[fld] = master[fld]; + } + }; + + // Related set: Add button + scope.add = function(set) { + $rootScope.flashMessage = null; + $location.path('/' + base + '/' + $routeParams.id + '/' + set); + }; + + // Related set: Edit button + scope.edit = function(set, id, name) { + $rootScope.flashMessage = null; + $location.path('/' + base + '/' + $routeParams.id + '/' + set + '/' + id); + }; + + // Related set: Delete button + scope.delete = function(set, itm_id, name, title) { + $rootScope.flashMessage = null; + + var action = function() { + var url = defaultUrl + id + '/' + set + '/'; + Rest.setUrl(url); + Rest.post({ id: itm_id, disassociate: 1 }) + .success( function(data, status, headers, config) { + $('#prompt-modal').modal('hide'); + scope.search(form.related[set].iterator); + }) + .error( function(data, status, headers, config) { + $('#prompt-modal').modal('hide'); + ProcessErrors(scope, data, status, null, + { hdr: 'Error!', msg: 'Call to ' + url + ' failed. POST returned status: ' + status }); + }); + }; + + Prompt({ hdr: 'Delete', + body: 'Are you sure you want to remove ' + name + ' from ' + scope.name + ' ' + title + '?', + action: action + }); + + } + +} + +JobsEdit.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'JobForm', + 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'RelatedSearchInit', + 'RelatedPaginateInit', 'ReturnToCaller', 'ClearScope', 'InventoryList', 'CredentialList', + 'ProjectList', 'LookUpInit', 'PromptPasswords', 'GetBasePath' + ]; diff --git a/lib/ui/static/js/forms/JobTemplates.js b/lib/ui/static/js/forms/JobTemplates.js index 27227daf2f..be89205d96 100644 --- a/lib/ui/static/js/forms/JobTemplates.js +++ b/lib/ui/static/js/forms/JobTemplates.js @@ -2,7 +2,7 @@ * Copyright (c) 2013 AnsibleWorks, Inc. * * JobTemplates.js - * Form definition for Credential model + * Form definition for Job Template model * * */ diff --git a/lib/ui/static/js/forms/Jobs.js b/lib/ui/static/js/forms/Jobs.js new file mode 100644 index 0000000000..6fb03ab131 --- /dev/null +++ b/lib/ui/static/js/forms/Jobs.js @@ -0,0 +1,168 @@ +/********************************************* + * Copyright (c) 2013 AnsibleWorks, Inc. + * + * Jobs.js + * Form definition for Jobs model + * + * + */ +angular.module('JobFormDefinition', []) + .value( + 'JobForm', { + + addTitle: 'Create Job', //Legend in add mode + editTitle: '{{ name }}', //Legend in edit mode + name: 'jobs', + well: true, + twoColumns: true, + allowReadonly: true, + + fields: { + name: { + label: 'Name', + type: 'text', + addRequired: true, + editRequired: true, + column: 1 + }, + description: { + label: 'Description', + type: 'text', + addRequired: false, + editRequired: false, + column: 1 + }, + job_type: { + label: 'Job Type', + type: 'select', + ngOptions: 'type.label for type in job_type_options', + default: 'run', + addRequired: true, + editRequired: true, + column: 1 + }, + inventory: { + label: 'Inventory', + type: 'lookup', + sourceModel: 'inventory', + sourceField: 'name', + addRequired: true, + editRequired: true, + ngClick: 'lookUpInventory()', + column: 1 + }, + project: { + label: 'Project', + type: 'lookup', + sourceModel: 'project', + sourceField: 'name', + addRequired: true, + editRequired: true, + ngClick: 'lookUpProject()', + column: 1 + }, + playbook: { + label: 'Playbook', + type:'select', + ngOptions: 'book for book in playbook_options', + id: 'playbook-select', + addRequired: true, + editRequired: true, + column: 1 + }, + credential: { + label: 'Credential', + type: 'lookup', + sourceModel: 'credential', + sourceField: 'name', + ngClick: 'lookUpCredential()', + addRequired: false, + editRequired: false, + column: 2 + }, + forks: { + label: 'Forks', + type: 'number', + integer: true, + min: 0, + max: 100, + default: 0, + addRequired: false, + editRequired: false, + column: 2 + }, + limit: { + label: 'Limit', + type: 'text', + addRequired: false, + editRequired: false, + column: 2 + }, + verbosity: { + label: 'Verbosity', + type: 'number', + integer: true, + default: 0, + min: 0, + max: 3, + addRequired: false, + editRequired: false, + column: 2 + }, + extra_vars: { + label: 'Extra Variables', + type: 'textarea', + rows: 6, + class: 'span5', + addRequired: false, + editRequired: false, + column: 2 + } + }, + + buttons: { //for now always generates \n"; - } - if (this.has('buttons')) { - html += "\n"; - html += "\n"; - } + } + if (this.has('buttons')) { + html += "\n"; + html += "\n"; + } } html += "\n"; @@ -448,10 +485,20 @@ angular.module('FormGenerator', ['GeneratorHelpers']) html += "\n"; } + if ((!this.modal && this.form.statusFields)) { + // Add status fields section (used in Jobs form) + html += "
\n"; + for (var fld in this.form.statusFields) { + field = this.form.statusFields[fld]; + html += this.buildField(fld, field, options); + } + html += "
\n"; + } + if ((!this.modal) && options.related && this.form.related) { html += this.buildCollections(); } - + return html; }, diff --git a/lib/ui/static/lib/ansible/list-generator.js b/lib/ui/static/lib/ansible/list-generator.js index 04d804df1b..6bc9670a44 100644 --- a/lib/ui/static/lib/ansible/list-generator.js +++ b/lib/ui/static/lib/ansible/list-generator.js @@ -193,22 +193,22 @@ angular.module('ListGenerator', ['GeneratorHelpers',]) html += ""; - if ((list.fields[fld].key || list.fields[fld].link) && options.mode != 'lookup') { + if ((list.fields[fld].key || list.fields[fld].link) && options.mode != 'lookup' && options.mode != 'select') { html += ""; } html += (list.fields[fld].icon) ? this.icon(list.fields[fld].icon) : ""; html += "{{" + list.iterator + "." + fld + "}}"; - html += ((list.fields[fld].key || list.fields[fld].link) && options.mode != 'lookup') ? "" : ""; + html += ((list.fields[fld].key || list.fields[fld].link) && options.mode != 'lookup' && options.mode != 'select') ? "" : ""; html += "\n"; } else { html += ""; - if ((list.fields[fld].key || list.fields[fld].link) && options.mode != 'lookup') { + if ((list.fields[fld].key || list.fields[fld].link) && options.mode != 'lookup' && options.mode != 'select') { html += ""; } html += (list.fields[fld].icon) ? this.icon(list.fields[fld].icon) : ""; html += "{{ " + list.fields[fld].ngBind + " }}"; - html += ((list.fields[fld].key || list.fields[fld].link) && options.mode != 'lookup') ? "" : ""; + html += ((list.fields[fld].key || list.fields[fld].link) && options.mode != 'lookup' && options.mode != 'select') ? "" : ""; html += "\n"; } } diff --git a/lib/ui/templates/ui/index.html b/lib/ui/templates/ui/index.html index 8d0e45835c..4d72f2232e 100644 --- a/lib/ui/templates/ui/index.html +++ b/lib/ui/templates/ui/index.html @@ -44,6 +44,7 @@ +