diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index 8498859bda..6b5b7fabf5 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -65,7 +65,8 @@ angular.module('ansible', [ 'SelectionHelper', 'LicenseFormDefinition', 'License', - 'HostGroupsFormDefinition' + 'HostGroupsFormDefinition', + 'SCMUpdateHelper' ]) .config(['$routeProvider', function($routeProvider) { $routeProvider. diff --git a/awx/ui/static/js/controllers/Projects.js b/awx/ui/static/js/controllers/Projects.js index 88371cb38c..e1d69c87a7 100644 --- a/awx/ui/static/js/controllers/Projects.js +++ b/awx/ui/static/js/controllers/Projects.js @@ -12,7 +12,7 @@ function ProjectsList ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, ProjectList, GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, - ClearScope, ProcessErrors, GetBasePath, SelectionInit) + ClearScope, ProcessErrors, GetBasePath, SelectionInit, SCMUpdate) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -63,11 +63,25 @@ function ProjectsList ($scope, $rootScope, $location, $log, $routeParams, Rest, action: action }); } + + scope.scmUpdate = function(project_id) { + for (var i=0; i < scope.projects.length; i++) { + if (scope.projects[i].id == project_id) { + if (scope.projects[i].scm_type == "") { + Alert('Missing SCM Setup', 'Before running an SCM update, edit the project and provide the SCM access information.', 'alert-info'); + } + else { + SCMUpdate({ scope: scope, project: scope.projects[i]}); + } + break; + } + } + } } ProjectsList.$inject = [ '$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'ProjectList', 'GenerateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', - 'GetBasePath', 'SelectionInit']; + 'GetBasePath', 'SelectionInit', 'SCMUpdate']; function ProjectsAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, ProjectsForm, @@ -90,6 +104,10 @@ function ProjectsAdd ($scope, $rootScope, $compile, $location, $log, $routeParam LoadBreadCrumbs(); GetProjectPath({ scope: scope, master: master }); + scope.scm_type_options = [ + { label: 'GitHub', value: 'git' }, + { label: 'SVN', value: 'svn' }]; + LookUpInit({ scope: scope, form: form, @@ -163,10 +181,29 @@ function ProjectsEdit ($scope, $rootScope, $compile, $location, $log, $routePara var master = {}; var id = $routeParams.id; var relatedSets = {}; + + scope.scm_type_options = [ + { label: 'GitHub', value: 'git' }, + { label: 'SVN', value: 'svn' }]; scope.project_local_paths = []; scope.base_dir = ''; + function setAskCheckboxes() { + for (var fld in form.fields) { + if (form.fields[fld].type == 'password' && form.fields[fld].ask && scope[fld] == 'ASK') { + // turn on 'ask' checkbox for password fields with value of 'ASK' + $("#" + fld + "-clear-btn").attr("disabled","disabled"); + scope[fld + '_ask'] = true; + } + else { + scope[fld + '_ask'] = false; + $("#" + fld + "-clear-btn").removeAttr("disabled"); + } + + } + } + // After the project is loaded, retrieve each related set if (scope.projectLoadedRemove) { scope.projectLoadedRemove(); @@ -203,6 +240,9 @@ function ProjectsEdit ($scope, $rootScope, $compile, $location, $log, $routePara relatedSets[set] = { url: related[set], iterator: form.related[set].iterator }; } } + + setAskCheckboxes(); + // 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 }); @@ -221,6 +261,9 @@ function ProjectsEdit ($scope, $rootScope, $compile, $location, $log, $routePara for (var fld in form.fields) { params[fld] = scope[fld]; } + if (scope.scm_type) { + params.scm_type = scope.scm_type.value; + } Rest.setUrl(defaultUrl); Rest.put(params) .success( function(data, status, headers, config) { @@ -276,6 +319,35 @@ function ProjectsEdit ($scope, $rootScope, $compile, $location, $log, $routePara action: action }); } + + // Password change + scope.clearPWConfirm = function(fld) { + // If password value changes, make sure password_confirm must be re-entered + scope[fld] = ''; + scope[form.name + '_form'][fld].$setValidity('awpassmatch', false); + } + + // Respond to 'Ask at runtime?' checkbox + scope.ask = function(fld, associated) { + if (scope[fld + '_ask']) { + $("#" + fld + "-clear-btn").attr("disabled","disabled"); + scope[fld] = 'ASK'; + scope[associated] = ''; + scope[form.name + '_form'][associated].$setValidity('awpassmatch', true); + } + else { + $("#" + fld + "-clear-btn").removeAttr("disabled"); + scope[fld] = ''; + scope[associated] = ''; + scope[form.name + '_form'][associated].$setValidity('awpassmatch', true); + } + } + + scope.clear = function(fld, associated) { + scope[fld] = ''; + scope[associated] = ''; + scope[form.name + '_form'][associated].$setValidity('awpassmatch', true); + } } ProjectsEdit.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'ProjectsForm', diff --git a/awx/ui/static/js/forms/Projects.js b/awx/ui/static/js/forms/Projects.js index 14017ce166..6d862d1030 100644 --- a/awx/ui/static/js/forms/Projects.js +++ b/awx/ui/static/js/forms/Projects.js @@ -11,10 +11,10 @@ angular.module('ProjectFormDefinition', []) .value( 'ProjectsForm', { - addTitle: 'Create Project', //Title in add mode - editTitle: '{{ name }}', //Title in edit mode - name: 'project', //entity or model name in singular form - well: true, //Wrap the form with TB well/ + addTitle: 'Create Project', // Title in add mode + editTitle: '{{ name }}', // Title in edit mode + name: 'project', // entity or model name in singular form + well: true, // Wrap the form with TB well fields: { name: { @@ -30,7 +30,7 @@ angular.module('ProjectFormDefinition', []) addRequired: false, editRequired: false }, - organization: { + organization: { label: 'Organization', type: 'lookup', sourceModel: 'organization', @@ -43,6 +43,7 @@ angular.module('ProjectFormDefinition', []) awPopOver: '
A project must have at least one organization. Pick one organization now to create the project, and then after ' + 'the project is created you can add additional organizations.' , dataTitle: 'Organization', + dataContainer: 'body', dataPlacement: 'right' }, base_dir: { @@ -54,6 +55,7 @@ angular.module('ProjectFormDefinition', []) 'Together the base path and selected playbook directory provide the full path used to locate playbooks.
' + 'Use PROJECTS_ROOT in your environment settings file to determine the base path value.
', dataTitle: 'Project Base Path', + dataContainer: 'body', dataPlacement: 'right' }, local_path: { @@ -67,7 +69,136 @@ angular.module('ProjectFormDefinition', []) 'Together the base path and the playbook directory provide the full path used to locate playbooks.' + 'Use PROJECTS_ROOT in your environment settings file to determine the base path value.
', dataTitle: 'Project Path', + dataContainer: 'body', dataPlacement: 'right' + }, + scm_type: { + label: 'SCM Type', + type: 'select', + ngOptions: 'type.label for type in scm_type_options', + "default": '', + addRequired: false, + editRequired: false + }, + scm_url: { + label: 'SCM URL', + type: 'text', + ngShow: "scm_type !== '' && scm_type !== null", + addRequired: false, + editRequired: false, + awRequiredWhen: {variable: "scm_type", init: "true" } + }, + scm_branch: { + label: 'SCM Branch', + type: 'text', + ngShow: "scm_type !== '' && scm_type !== null", + addRequired: false, + editRequired: false + }, + scm_username: { + label: 'SCM Username', + type: 'text', + ngShow: "scm_type !== '' && scm_type !== null", + addRequired: false, + editRequired: false + }, + "scm_password": { + label: 'SCM Password', + type: 'password', + ngShow: "scm_type !== '' && scm_type !== null", + addRequired: false, + editRequired: false, + ngChange: "clearPWConfirm('scm_password_confirm')", + ask: true, + clear: true, + associated: 'scm_password_confirm', + autocomplete: false + }, + "scm_password_confirm": { + label: 'Confirm Password', + type: 'password', + ngShow: "scm_type !== '' && scm_type !== null", + addRequired: false, + editRequired: false, + awPassMatch: true, + associated: 'scm_password', + autocomplete: false + }, + "scm_key_data": { + label: 'SCM Private Key', + type: 'textarea', + ngShow: "scm_type !== '' && scm_type !== null", + addRequired: false, + editRequired: false, + rows: 10 + }, + "scm_key_unlock": { + label: 'SCM Key Password', + type: 'password', + ngShow: "scm_type !== '' && scm_type !== null && scm_key_data", + addRequired: false, + editRequired: false, + ngChange: "clearPWConfirm('scm_key_unlock_confirm')", + associated: 'scm_key_unlock_confirm', + ask: true, + clear: true + }, + "scm_key_unlock_confirm": { + label: 'Confirm Key Password', + type: 'password', + ngShow: "scm_type !== '' && scm_type !== null && scm_key_data", + addRequired: false, + editRequired: false, + awPassMatch: true, + associated: 'scm_key_unlock' + }, + checkbox_group: { + label: 'SCM Options', + type: 'checkbox_group', + ngShow: "scm_type !== '' && scm_type !== null", + + fields: [ + { + name: 'scm_clean', + label: 'Clean', + type: 'checkbox', + ngShow: "scm_type !== '' && scm_type !== null", + addRequired: false, + editRequired: false, + awPopOver: 'Remove any local modifications prior to performing an update.
', + dataTitle: 'SCM Clean', + dataContainer: 'body', + dataPlacement: 'right', + labelClass: 'checkbox-options' + }, + { + name: 'scm_delete_on_update', + label: 'Delete on Update', + type: 'checkbox', + ngShow: "scm_type !== '' && scm_type !== null", + addRequired: false, + editRequired: false, + awPopOver: 'Delete the local repository in its entirety prior to performing an update.
Depending on the size of the ' + + 'repository this may significantly increase the amount of time required to complete an update.
', + dataTitle: 'SCM Delete', + dataContainer: 'body', + dataPlacement: 'right', + labelClass: 'checkbox-options' + }, + { + name: 'scm_update_on_launch', + label: 'Update on Launch', + type: 'checkbox', + ngShow: "scm_type !== '' && scm_type !== null", + addRequired: false, + editRequired: false, + awPopOver: 'Each time a job runs using this project, perform an update to the local repository prior to starting the job.
', + dataTitle: 'SCM Update', + dataContainer: 'body', + dataPlacement: 'right', + labelClass: 'checkbox-options' + } + ] } }, diff --git a/awx/ui/static/js/helpers/SCMUpdate.js b/awx/ui/static/js/helpers/SCMUpdate.js new file mode 100644 index 0000000000..f76ba707db --- /dev/null +++ b/awx/ui/static/js/helpers/SCMUpdate.js @@ -0,0 +1,28 @@ +/********************************************* + * Copyright (c) 2013 AnsibleWorks, Inc. + * + * SCMUpdate.js + * + * Use to kick off an update the project + * + */ + +angular.module('SCMUpdateHelper', ['RestServices', 'Utilities']) + .factory('SCMUpdate', ['Alert', 'Rest', 'GetBasePath','ProcessErrors', + function(Alert, Rest, GetBasePath, ProcessErrors) { + return function(params) { + + var scope = params.scope; + var project = params.project; + + Rest.setUrl(project.related.update); + Rest.get() + .success( function(data, status, headers, config) { + console.log(data); + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, null, + { hdr: 'Error!', msg: 'Failed to access ' + project.related.update + '. GET status: ' + status }); + }); + } + }]); \ No newline at end of file diff --git a/awx/ui/static/js/lists/Inventories.js b/awx/ui/static/js/lists/Inventories.js index a2230cfcd8..07e81dea93 100644 --- a/awx/ui/static/js/lists/Inventories.js +++ b/awx/ui/static/js/lists/Inventories.js @@ -52,7 +52,7 @@ angular.module('InventoriesListDefinition', []) dropdown: { type: 'DropDown', - label: 'View Jobs', + label: 'View', 'class': 'btn-xs', options: [ { ngClick: 'viewJobs(\{\{ inventory.id \}\})', label: 'Jobs' }, diff --git a/awx/ui/static/js/lists/Projects.js b/awx/ui/static/js/lists/Projects.js index a9cda0e137..4b7f421999 100644 --- a/awx/ui/static/js/lists/Projects.js +++ b/awx/ui/static/js/lists/Projects.js @@ -41,6 +41,7 @@ angular.module('ProjectsListDefinition', []) }, fieldActions: { + edit: { label: 'Edit', ngClick: "editProject(\{\{ project.id \}\})", @@ -48,6 +49,14 @@ angular.module('ProjectsListDefinition', []) "class": 'btn-xs btn-default', awToolTip: 'View/edit project' }, + + scm_update: { + label: 'Update', + icon: 'icon-cloud-download', + "class": 'btn-xs btn-success', + ngClick: 'scmUpdate(\{\{ project.id \}\})', + awToolTip: 'Perform an SCM update on this project' + }, "delete": { label: 'Delete', diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 106202c327..5901e57b57 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -231,6 +231,10 @@ a:hover { max-width: 100px; } +.form-horizontal .buttons { + margin-top: 25px; +} + /* Outline required fields in Red when focused */ .form-control[required]:focus { @@ -411,6 +415,11 @@ select.field-mini-height { margin: 0; } +.checkbox-options { + padding-left: 40px; +} + + /* Display list actions next to search widget */ .list-actions { diff --git a/awx/ui/static/lib/ansible/form-generator.js b/awx/ui/static/lib/ansible/form-generator.js index b2a5febc5d..43287f1ae3 100644 --- a/awx/ui/static/lib/ansible/form-generator.js +++ b/awx/ui/static/lib/ansible/form-generator.js @@ -265,41 +265,63 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) buildField: function(fld, field, options, form) { function getFieldWidth() { - var x; - if (form.formFieldSize) { - x = form.formFieldSize; - } - else if (field.xtraWide) { - x = "col-lg-10"; - } - else if (field.column) { - x = "col-lg-8"; - } - else if (!form.formFieldSize && options.modal) { - x = "col-lg-10"; - } - else { - x = "col-lg-6"; - } - return x; + var x; + if (form.formFieldSize) { + x = form.formFieldSize; + } + else if (field.xtraWide) { + x = "col-lg-10"; + } + else if (field.column) { + x = "col-lg-8"; + } + else if (!form.formFieldSize && options.modal) { + x = "col-lg-10"; + } + else { + x = "col-lg-6"; + } + return x; } function getLabelWidth() { - var x; - if (form.formLabelSize) { - x = form.formLabelSize; - } - else if (field.column) { - x = "col-lg-4"; - } - else if (!form.formLabelSize && options.modal) { - x = "col-lg-2"; - } - else { - x = "col-lg-2"; - } - return x; + var x; + if (form.formLabelSize) { + x = form.formLabelSize; + } + else if (field.column) { + x = "col-lg-4"; + } + else if (!form.formLabelSize && options.modal) { + x = "col-lg-2"; + } + else { + x = "col-lg-2"; + } + return x; } + + function buildCheckbox(field, fld) { + var html=''; + html += "\n"; + return html; + } var html = ''; @@ -516,6 +538,29 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) html += "\n"; html += "\n"; } + + //checkbox group + if (field.type == 'checkbox_group') { + html += "\n"; + for (var i=0; i < field.fields.length; i++) { + html += buildCheckbox(field.fields[i], field.fields[i].name); + } + // Add error messages + if ( (options.mode == 'add' && field.addRequired) || (options.mode == 'edit' && field.editRequired) ) { + html += "