From 6a7d8b52aa4811aa86280768915d3bd6e91da097 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Thu, 31 Oct 2013 19:17:45 +0000 Subject: [PATCH] Changes to Credentials for supporting various Kinds. --- awx/ui/static/js/app.js | 6 +- awx/ui/static/js/controllers/Credentials.js | 315 +++++++++++++++----- awx/ui/static/js/controllers/Inventories.js | 6 +- awx/ui/static/js/controllers/JobEvents.js | 6 +- awx/ui/static/js/controllers/JobHosts.js | 6 +- awx/ui/static/js/controllers/Jobs.js | 6 +- awx/ui/static/js/controllers/Projects.js | 6 +- awx/ui/static/js/controllers/Teams.js | 6 +- awx/ui/static/js/forms/Credentials.js | 85 +++++- awx/ui/static/js/helpers/Credentials.js | 63 ++++ awx/ui/static/js/helpers/Lookup.js | 40 ++- awx/ui/static/js/lists/Credentials.js | 2 - awx/ui/static/less/ansible-ui.less | 4 + awx/ui/static/lib/ansible/Utilities.js | 33 +- awx/ui/static/lib/ansible/directives.js | 3 +- awx/ui/static/lib/ansible/form-generator.js | 60 +++- awx/ui/static/lib/ansible/list-generator.js | 1 + awx/ui/templates/ui/index.html | 1 + 18 files changed, 506 insertions(+), 143 deletions(-) create mode 100644 awx/ui/static/js/helpers/Credentials.js diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index 65d62bf5a4..cc80427b2b 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -78,7 +78,8 @@ angular.module('ansible', [ 'InventoryStatusDefinition', 'InventorySummaryHelpDefinition', 'InventoryHostsHelpDefinition', - 'TreeSelector' + 'TreeSelector', + 'CredentialsHelper' ]) .config(['$routeProvider', function($routeProvider) { $routeProvider. @@ -205,6 +206,9 @@ angular.module('ansible', [ when('/credentials', { templateUrl: urlPrefix + 'partials/credentials.html', controller: CredentialsList }). + when('/credentials/add', { templateUrl: urlPrefix + 'partials/credentials.html', + controller: CredentialsAdd }). + when('/credentials/:credential_id', { templateUrl: urlPrefix + 'partials/credentials.html', controller: CredentialsEdit }). diff --git a/awx/ui/static/js/controllers/Credentials.js b/awx/ui/static/js/controllers/Credentials.js index 233e9b77dd..bb5beacb89 100644 --- a/awx/ui/static/js/controllers/Credentials.js +++ b/awx/ui/static/js/controllers/Credentials.js @@ -29,10 +29,10 @@ function CredentialsList ($scope, $rootScope, $location, $log, $routeParams, Res SelectionInit({ scope: scope, list: list, url: url, returnToCaller: 1 }); - if (scope.PostRefreshRemove) { - scope.PostRefreshRemove(); + if (scope.removePostRefresh) { + scope.removePostRefresh(); } - scope.PostRefershRemove = scope.$on('PostRefresh', function() { + scope.removePostRefresh = scope.$on('PostRefresh', function() { // After a refresh, populate the organization name on each row for(var i=0; i < scope.credentials.length; i++) { if (scope.credentials[i].summary_fields.user) { @@ -89,7 +89,8 @@ CredentialsList.$inject = [ '$scope', '$rootScope', '$location', '$log', '$route function CredentialsAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, CredentialForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, ClearScope, - GenerateList, SearchInit, PaginateInit, LookUpInit, UserList, TeamList, GetBasePath) + GenerateList, SearchInit, PaginateInit, LookUpInit, UserList, TeamList, GetBasePath, + GetChoices, Empty, KindChange) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -100,40 +101,114 @@ function CredentialsAdd ($scope, $rootScope, $compile, $location, $log, $routePa var generator = GenerateForm; var scope = generator.inject(form, {mode: 'add', related: false}); var base = $location.path().replace(/^\//,'').split('/')[0]; - var defaultUrl = GetBasePath(base); - defaultUrl += (base == 'teams') ? $routeParams.team_id + '/credentials/' : $routeParams.user_id + '/credentials/'; + var defaultUrl = GetBasePath('credentials'); generator.reset(); LoadBreadCrumbs(); + + // Load the list of options for Kind + GetChoices({ + scope: scope, + url: defaultUrl, + field: 'kind', + variable: 'credential_kind_options' + }); + + LookUpInit({ + scope: scope, + form: form, + current_item: ($routeParams.user_id) ? $routeParams.user_id : null, + list: UserList, + field: 'user' + }); + + LookUpInit({ + scope: scope, + form: form, + current_item: ($routeParams.team_id) ? $routeParams.team_id : null, + list: TeamList, + field: 'team' + }); + + if (!Empty($routeParams.user_id)) { + // Get the username based on incoming route + var url = GetBasePath('users') + $routeParams.user_id + '/'; + scope['user'] = $routeParams.user_id; + Rest.setUrl(url); + Rest.get() + .success( function(data, status, headers, config) { + scope['user_username'] = data.username; + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, null, + { hdr: 'Error!', msg: 'Failed to retrieve user. GET status: ' + status }); + }); + } + if (!Empty($routeParams.team_id)) { + // Get the username based on incoming route + var url = GetBasePath('teams') + $routeParams.team_id + '/'; + scope['team'] = $routeParams.team_id; + Rest.setUrl(url); + Rest.get() + .success( function(data, status, headers, config) { + scope['team_name'] = data.name; + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, null, + { hdr: 'Error!', msg: 'Failed to retrieve team. GET status: ' + status }); + }); + } + + // Handle Kind change + scope.kindChange = function () { + KindChange({ scope: scope, form: form, reset: true }); + } + // Save scope.formSave = function() { generator.clearApiErrors(); - Rest.setUrl(defaultUrl); + var data = {} for (var fld in form.fields) { - data[fld] = scope[fld]; - } + if (scope[fld] === null) { + data[fld] = ""; + } + else { + data[fld] = scope[fld]; + } + } - if (base == 'teams') { - data['team'] = $routeParams.team_id; + if (!Empty(scope.team)) { + data.team = scope.team; } else { - data['user'] = $routeParams.user_id; + data.user = scope.user; } - Rest.post(data) - .success( function(data, status, headers, config) { - ReturnToCaller(1); - }) - .error( function(data, status, headers, config) { - ProcessErrors(scope, data, status, form, - { hdr: 'Error!', msg: 'Failed to add new Credential. Post returned status: ' + status }); - }); + data['kind'] = scope['kind'].value; + + if (!Empty(data.team) && empty(data.user)) { + Alert('Missing User or Team', 'You must provide either a User or a Team. If this credential will only be accessed by a specific ' + + 'user, select a User. To allow a team of users to access this credential, select a Team.', 'alert-danger'); + } + else { + var url = (!Empty(data.team)) ? GetBasePath('teams') + data.team + '/credentials/' : + GetBasePath('users') + data.user + '/credentials/'; + Rest.setUrl(url); + Rest.post(data) + .success( function(data, status, headers, config) { + var base = $location.path().replace(/^\//,'').split('/')[0]; + (base == 'credentials') ? ReturnToCaller() : ReturnToCaller(1); + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, form, + { hdr: 'Error!', msg: 'Failed to create new Credential. POST status: ' + status }); + }); + } }; - // Reset + // Reset defaults scope.formReset = function() { - // Defaults generator.reset(); }; @@ -170,12 +245,15 @@ function CredentialsAdd ($scope, $rootScope, $compile, $location, $log, $routePa CredentialsAdd.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'CredentialForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller', 'ClearScope', 'GenerateList', - 'SearchInit', 'PaginateInit', 'LookUpInit', 'UserList', 'TeamList', 'GetBasePath' ]; + 'SearchInit', 'PaginateInit', 'LookUpInit', 'UserList', 'TeamList', 'GetBasePath', 'GetChoices', 'Empty', + 'KindChange' ]; function CredentialsEdit ($scope, $rootScope, $compile, $location, $log, $routeParams, CredentialForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, RelatedSearchInit, - RelatedPaginateInit, ReturnToCaller, ClearScope, Prompt, GetBasePath) + RelatedPaginateInit, ReturnToCaller, ClearScope, Prompt, GetBasePath, GetChoices, + KindChange, UserList, TeamList, LookUpInit, Empty + ) { ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior //scope. @@ -191,11 +269,10 @@ function CredentialsEdit ($scope, $rootScope, $compile, $location, $log, $routeP var master = {}; var id = $routeParams.credential_id; var relatedSets = {}; - function setAskCheckboxes() { for (var fld in form.fields) { - if (form.fields[fld].type == 'password' && form.fields[fld].ask && scope[fld] == 'ASK') { + if (form.fields[fld].type == 'password' && scope[fld] == 'ASK') { // turn on 'ask' checkbox for password fields with value of 'ASK' $("#" + fld + "-clear-btn").attr("disabled","disabled"); scope[fld + '_ask'] = true; @@ -206,73 +283,150 @@ function CredentialsEdit ($scope, $rootScope, $compile, $location, $log, $routeP } master[fld + '_ask'] = scope[fld + '_ask']; } + + // Set kind field to the correct option + for (var i=0; i < scope['credential_kind_options'].length; i++) { + if (scope['kind'] == scope['credential_kind_options'][i].value) { + scope['kind'] = scope['credential_kind_options'][i]; + break; + } + } } - // After Credential is loaded, retrieve each related set and any lookups - if (scope.credentialLoadedRemove) { - scope.credentialLoadedRemove(); + if (scope.removeCredentialLoaded) { + scope.removeCredentialLoaded(); } - scope.credentialLoadedRemove = scope.$on('credentialLoaded', function() { - for (var set in relatedSets) { - scope.search(relatedSets[set].iterator); - } - }); - - // Retrieve detail record and prepopulate the form - Rest.setUrl(defaultUrl + ':id/'); - Rest.get({ params: {id: id} }) - .success( function(data, status, headers, config) { - LoadBreadCrumbs({ path: '/credentials/' + id, title: data.name }); - for (var fld in form.fields) { - if (data[fld]) { - scope[fld] = data[fld]; - master[fld] = scope[fld]; - } - } - scope.team = data.team; - scope.user = data.user; - setAskCheckboxes(); - - var related = data.related; - for (var set in form.related) { - if (related[set]) { - relatedSets[set] = { url: related[set], iterator: form.related[set].iterator }; - } - } - - // 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('credentialLoaded'); - }) - .error( function(data, status, headers, config) { - ProcessErrors(scope, data, status, form, - { hdr: 'Error!', msg: 'Failed to retrieve Credential: ' + $routeParams.id + '. GET status: ' + status }); + scope.removeCredentialLoaded = scope.$on('credentialLoaded', function() { + LookUpInit({ + scope: scope, + form: form, + current_item: ($scope['user_id']) ? scope['user_id'] : null, + list: UserList, + field: 'user' }); + LookUpInit({ + scope: scope, + form: form, + current_item: ($scope['team_id']) ? scope['team_id'] : null, + list: TeamList, + field: 'team' + }); + setAskCheckboxes(); + KindChange({ scope: scope, form: form, reset: false }); + }); + + if (scope.removeChoicesReady) { + scope.removeChoicesReady(); + } + scope.removeChoicesReady = scope.$on('choicesReady', function() { + // Retrieve detail record and prepopulate the form + Rest.setUrl(defaultUrl + ':id/'); + Rest.get({ params: {id: id} }) + .success( function(data, status, headers, config) { + LoadBreadCrumbs({ path: '/credentials/' + id, title: data.name }); + for (var fld in form.fields) { + if (data[fld] !== null && data[fld] !== undefined) { + 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]; + } + } + scope.$emit('credentialLoaded'); + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, form, + { hdr: 'Error!', msg: 'Failed to retrieve Credential: ' + $routeParams.id + '. GET status: ' + status }); + }); + }); + + GetChoices({ + scope: scope, + url: defaultUrl, + field: 'kind', + variable: 'credential_kind_options', + callback: 'choicesReady' + }); + // Save changes to the parent scope.formSave = function() { generator.clearApiErrors(); - Rest.setUrl(defaultUrl + id + '/'); + var data = {} for (var fld in form.fields) { - data[fld] = scope[fld]; + if (scope[fld] === null) { + data[fld] = ""; + } + else { + data[fld] = scope[fld]; + } } - data.team = scope.team; - data.user = scope.user; + if (!Empty(scope.team)) { + data.team = scope.team; + } + else { + data.user = scope.user; + } - Rest.put(data) - .success( function(data, status, headers, config) { - var base = $location.path().replace(/^\//,'').split('/')[0]; - (base == 'credentials') ? ReturnToCaller() : ReturnToCaller(1); - }) - .error( function(data, status, headers, config) { - ProcessErrors(scope, data, status, form, - { hdr: 'Error!', msg: 'Failed to update Credential: ' + $routeParams.id + '. PUT status: ' + status }); - }); - }; + data['kind'] = scope['kind'].value; + + if (!Empty(data.team) && empty(data.user)) { + Alert('Missing User or Team', 'You must provide either a User or a Team. If this credential will only be accessed by a specific ' + + 'user, select a User. To allow a team of users to access this credential, select a Team.', 'alert-danger'); + } + else { + // Save changes to the credential record + Rest.setUrl(defaultUrl + id + '/'); + Rest.put(data) + .success( function(data, status, headers, config) { + scope.$emit('moveUser', data); + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, form, + { hdr: 'Error!', msg: 'Failed to update Credential. PUT status: ' + status }); + }); + } + } + + // When we're finally done updating the API, navigate out of here + function finished() { + var base = $location.path().replace(/^\//,'').split('/')[0]; + (base == 'credentials') ? ReturnToCaller() : ReturnToCaller(1); + } + + // Did we change users? + if (scope.removeMoveUser) { + scope.removeMoveUser(); + } + scope.removeMoveUser = scope.$on('moveUser', function(e, data) { + if (master.user !== scope.user && !Empty(scope.user)) { + var url = GetBasePath('users') + scope.user + '/'; + Rest.setUrl(url); + Rest.post(data) + .success( function(data, status, headers, config) { + finished(); + }) + .error( function(data, status, headers, config) { + ProcessErrors(scope, data, status, null, + { hdr: 'Error!', msg: 'Call to ' + url + ' failed. POST status: ' + status }); + }); + } + else { + finished(); + } + }); + + // Handle Kind change + scope.kindChange = function () { + KindChange({ scope: scope, form: form, reset: true }); + } // Cancel scope.formReset = function() { @@ -355,5 +509,6 @@ function CredentialsEdit ($scope, $rootScope, $compile, $location, $log, $routeP CredentialsEdit.$inject = [ '$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'CredentialForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'RelatedSearchInit', - 'RelatedPaginateInit', 'ReturnToCaller', 'ClearScope', 'Prompt', 'GetBasePath' ]; + 'RelatedPaginateInit', 'ReturnToCaller', 'ClearScope', 'Prompt', 'GetBasePath', 'GetChoices', + 'KindChange', 'UserList', 'TeamList', 'LookUpInit', 'Empty']; diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index 457e710341..8ece087afb 100644 --- a/awx/ui/static/js/controllers/Inventories.js +++ b/awx/ui/static/js/controllers/Inventories.js @@ -31,10 +31,10 @@ function InventoriesList ($scope, $rootScope, $location, $log, $routeParams, Res LoadBreadCrumbs(); - if (scope.projectsPostRefresh) { - scope.projectsPostRefresh(); + if (scope.removePostRefresh) { + scope.removePostRefresh(); } - scope.projectsPostRefresh = scope.$on('PostRefresh', function() { + scope.removePostRefresh = scope.$on('PostRefresh', function() { for (var i=0; i < scope.inventories.length; i++) { // Set values for Failed Hosts column diff --git a/awx/ui/static/js/controllers/JobEvents.js b/awx/ui/static/js/controllers/JobEvents.js index 82afcc2f2d..64034afbc7 100644 --- a/awx/ui/static/js/controllers/JobEvents.js +++ b/awx/ui/static/js/controllers/JobEvents.js @@ -121,10 +121,10 @@ function JobEventsList ($scope, $rootScope, $location, $log, $routeParams, Rest, return html; } - if (scope.PostRefreshRemove) { - scope.PostRefreshRemove(); + if (scope.removePostRefresh) { + scope.removePostRefresh(); } - scope.PostRefreshRemove = scope.$on('PostRefresh', function() { + scope.removePostRefresh = scope.$on('PostRefresh', function() { // Initialize the parent levels var set = scope[list.name]; var cDate; diff --git a/awx/ui/static/js/controllers/JobHosts.js b/awx/ui/static/js/controllers/JobHosts.js index 41cca7ffc6..e782870210 100644 --- a/awx/ui/static/js/controllers/JobHosts.js +++ b/awx/ui/static/js/controllers/JobHosts.js @@ -54,10 +54,10 @@ function JobHostSummaryList ($scope, $rootScope, $location, $log, $routeParams, }); // After a refresh, populate any needed summary field values on each row - if (scope.PostRefreshRemove) { - scope.PostRefreshRemove(); + if (scope.removePostRefresh) { + scope.removePostRefresh(); } - scope.PostRefreshRemove = scope.$on('PostRefresh', function() { + scope.removePostRefresh = scope.$on('PostRefresh', function() { // Set status, tooltips, badget icons, etc. for( var i=0; i < scope.jobhosts.length; i++) { diff --git a/awx/ui/static/js/controllers/Jobs.js b/awx/ui/static/js/controllers/Jobs.js index f1bda6313a..c01963e7a1 100644 --- a/awx/ui/static/js/controllers/Jobs.js +++ b/awx/ui/static/js/controllers/Jobs.js @@ -24,10 +24,10 @@ function JobsListCtrl ($scope, $rootScope, $location, $log, $routeParams, Rest, $rootScope.flashMessage = null; scope.selected = []; - if (scope.PostRefreshRemove) { - scope.PostRefreshRemove(); + if (scope.removePostRefresh) { + scope.removePostRefresh(); } - scope.PostRefreshRemove = scope.$on('PostRefresh', function() { + scope.removePostRefresh = scope.$on('PostRefresh', function() { $("tr.success").each(function(index) { // Make sure no rows have a green background var ngc = $(this).attr('ng-class'); diff --git a/awx/ui/static/js/controllers/Projects.js b/awx/ui/static/js/controllers/Projects.js index aa7445ead2..8c942c616e 100644 --- a/awx/ui/static/js/controllers/Projects.js +++ b/awx/ui/static/js/controllers/Projects.js @@ -32,10 +32,10 @@ function ProjectsList ($scope, $rootScope, $location, $log, $routeParams, Rest, SelectionInit({ scope: scope, list: list, url: url, returnToCaller: 1 }); } - if (scope.projectsPostRefresh) { - scope.projectsPostRefresh(); + if (scope.removePostRefresh) { + scope.removePostRefresh(); } - scope.projectsPostRefresh = scope.$on('PostRefresh', function() { + scope.removePostRefresh = scope.$on('PostRefresh', function() { if (scope.projects) { for (var i=0; i < scope.projects.length; i++) { if (scope.projects[i].status == 'ok') { diff --git a/awx/ui/static/js/controllers/Teams.js b/awx/ui/static/js/controllers/Teams.js index 9138146d5e..b5e66cfe7f 100644 --- a/awx/ui/static/js/controllers/Teams.js +++ b/awx/ui/static/js/controllers/Teams.js @@ -27,10 +27,10 @@ function TeamsList ($scope, $rootScope, $location, $log, $routeParams, Rest, Ale var url = GetBasePath('base') + $location.path() + '/'; SelectionInit({ scope: scope, list: list, url: url, returnToCaller: 1 }); - if (scope.PostRefreshRemove) { - scope.PostRefreshRemove(); + if (scope.removePostRefresh) { + scope.removePostRefresh(); } - scope.PostRefershRemove = scope.$on('PostRefresh', function() { + scope.removePostRefresh = scope.$on('PostRefresh', function() { // After a refresh, populate the organization name on each row if (scope.teams) { for ( var i=0; i < scope.teams.length; i++) { diff --git a/awx/ui/static/js/forms/Credentials.js b/awx/ui/static/js/forms/Credentials.js index 654dc50a4b..bd4c34bca4 100644 --- a/awx/ui/static/js/forms/Credentials.js +++ b/awx/ui/static/js/forms/Credentials.js @@ -28,33 +28,81 @@ angular.module('CredentialFormDefinition', []) addRequired: false, editRequired: false }, + user: { + label: 'User', + type: 'lookup', + sourceModel: 'user', + sourceField: 'username', + ngClick: 'lookUpUser()', + ngShow: "team == '' || team == null", + awPopOver: "

A credential must be associated with either a user or a team. Choosing a user allows only the selected user access " + + "to the credential.

", + dataTitle: 'User', + dataPlacement: 'right', + dataContainer: "body" + }, + team: { + label: 'Team', + type: 'lookup', + sourceModel: 'team', + sourceField: 'name', + ngClick: 'lookUpTeam()', + ngShow: "user == '' || user == null", + awPopOver: "

A credential must be associated with either a user or a team. Choose a team to share a credential with " + + "all users in the team.

", + dataTitle: 'Team', + dataPlacement: 'right', + dataContainer: "body" + }, kind: { - label: 'Kind', - type: 'radio', // FIXME: Make select, pull from OPTIONS request - options: [{ label: 'Machine', value: 'ssh' }, { label: 'SCM', value: 'scm'}, { label: 'AWS', value: 'aws'}, { label: 'Rackspace', value: 'rax'}], - //ngChange: 'selectCategory()' + label: 'Type', + excludeModal: true, + type: 'select', + ngOptions: 'kind.label for kind in credential_kind_options', + ngChange: 'kindChange()', + addRequired: true, + editRequired: true + }, + access_key: { + label: 'Access Key', + type: 'text', + ngShow: "kind.value == 'aws'", + awRequiredWhen: {variable: "aws_required", init: "false" }, + autocomplete: false, + apiField: 'username' + }, + secret_key: { + label: 'Secrent Key', + type: 'password', + ngShow: "kind.value == 'aws'", + awRequiredWhen: {variable: "aws_required", init: "false" }, + autocomplete: false, + ask: false, + clear: false, + apiField: 'passwowrd' }, "username": { - label: 'Username', + labelBind: 'usernameLabel', type: 'text', - addRequired: false, - editRequired: false, + ngShow: "kind.value && kind.value !== 'aws'", + awRequiredWhen: {variable: 'rackspace_required', init: false }, autocomplete: false }, "password": { - label: 'Password', + labelBind: 'passwordLabel', type: 'password', - addRequired: false, - editRequired: false, + ngShow: "kind.value && kind.value !== 'aws'", + awRequiredWhen: {variable: 'rackspace_required', init: false }, ngChange: "clearPWConfirm('password_confirm')", - ask: true, - clear: true, + ask: false, + clear: false, associated: 'password_confirm', autocomplete: false }, "password_confirm": { - label: 'Confirm Password', + labelBind: 'passwordConfirmLabel', type: 'password', + ngShow: "kind.value && kind.value !== 'aws'", addRequired: false, editRequired: false, awPassMatch: true, @@ -62,17 +110,18 @@ angular.module('CredentialFormDefinition', []) autocomplete: false }, "ssh_key_data": { - label: 'SSH Private Key', + labelBind: 'sshKeyDataLabel', type: 'textarea', + ngShow: "kind.value == 'ssh' || kind.value == 'scm'", addRequired: false, editRequired: false, 'class': 'ssh-key-field', - rows: 10, - xtraWide: true + rows: 10 }, "ssh_key_unlock": { label: 'Key Password', type: 'password', + ngShow: "kind.value == 'ssh'", addRequired: false, editRequired: false, ngChange: "clearPWConfirm('ssh_key_unlock_confirm')", @@ -83,6 +132,7 @@ angular.module('CredentialFormDefinition', []) "ssh_key_unlock_confirm": { label: 'Confirm Key Password', type: 'password', + ngShow: "kind.value == 'ssh'", addRequired: false, editRequired: false, awPassMatch: true, @@ -91,6 +141,7 @@ angular.module('CredentialFormDefinition', []) "sudo_username": { label: 'Sudo Username', type: 'text', + ngShow: "kind.value == 'ssh'", addRequired: false, editRequired: false, autocomplete: false @@ -98,6 +149,7 @@ angular.module('CredentialFormDefinition', []) "sudo_password": { label: 'Sudo Password', type: 'password', + ngShow: "kind.value == 'ssh'", addRequired: false, editRequired: false, ngChange: "clearPWConfirm('sudo_password_confirm')", @@ -109,6 +161,7 @@ angular.module('CredentialFormDefinition', []) "sudo_password_confirm": { label: 'Confirm Sudo Password', type: 'password', + ngShow: "kind.value == 'ssh'", addRequired: false, editRequired: false, awPassMatch: true, diff --git a/awx/ui/static/js/helpers/Credentials.js b/awx/ui/static/js/helpers/Credentials.js new file mode 100644 index 0000000000..171ea4fb7b --- /dev/null +++ b/awx/ui/static/js/helpers/Credentials.js @@ -0,0 +1,63 @@ +/********************************************* + * Copyright (c) 2013 AnsibleWorks, Inc. + * + * Credentials.js + * + * Functions shared amongst Credential related controllers + * + */ + +angular.module('CredentialsHelper', ['Utilities']) + + .factory('KindChange', [ function() { + return function(params) { + + var scope = params.scope; + var form = params.form; + var reset = params.reset; + + // Set field lables + if (scope.kind.value !== 'ssh') { + scope['usernameLabel'] = 'Username'; + scope['passwordLabel'] = 'Password'; + scope['passwordConfirmLabel'] = 'Confirm Password'; + scope['sshKeyDataLabel'] = 'SCM Private Key'; + } + else { + scope['usernameLabel'] = 'SSH Username'; + scope['passwordLabel'] = 'SSH Password'; + scope['passwordConfirmLabel'] = 'Confirm SSH Password'; + scope['sshKeyDataLabel'] = 'SSH Private Key'; + } + + scope['aws_required'] = (scope.kind.value == 'aws') ? true : false; + + if (scope.kind.value == 'rax') { + scope['rackspace_required'] = true; + form.fields['password'].clear = true; + form.fields['password'].ask = true; + } + else { + scope['rackspace_required'] = false; + form.fields['password'].clear = false; + form.fields['password'].ask = false; + } + + // Reset all the fields related to Kind. + if (reset) { + scope['access_key'] = null; + scope['secret_key'] = null; + scope['username'] = null; + scope['password'] = null; + scope['password_confirm'] = null; + scope['ssh_key_data'] = null; + scope['ssh_key_unlock'] = null; + scope['ssh_key_unlock_confirm'] = null; + scope['sudo_username'] = null; + scope['sudo_password'] = null; + scope['sudo_password_confirm'] = null; + } + + } + }]); + \ No newline at end of file diff --git a/awx/ui/static/js/helpers/Lookup.js b/awx/ui/static/js/helpers/Lookup.js index 2e8019bce3..c1e141bb07 100644 --- a/awx/ui/static/js/helpers/Lookup.js +++ b/awx/ui/static/js/helpers/Lookup.js @@ -15,8 +15,8 @@ */ angular.module('LookUpHelper', [ 'RestServices', 'Utilities', 'SearchHelper', 'PaginateHelper', 'ListGenerator', 'ApiLoader' ]) - .factory('LookUpInit', ['Alert', 'Rest', 'GenerateList', 'SearchInit', 'PaginateInit', 'GetBasePath', 'FormatDate', - function(Alert, Rest, GenerateList, SearchInit, PaginateInit, GetBasePath, FormatDate) { + .factory('LookUpInit', ['Alert', 'Rest', 'GenerateList', 'SearchInit', 'PaginateInit', 'GetBasePath', 'FormatDate', 'Empty', + function(Alert, Rest, GenerateList, SearchInit, PaginateInit, GetBasePath, FormatDate, Empty) { return function(params) { var scope = params.scope; // form scope @@ -34,11 +34,22 @@ angular.module('LookUpHelper', [ 'RestServices', 'Utilities', 'SearchHelper', 'P $('input[name="' + form.fields[field].sourceModel + '_' + form.fields[field].sourceField + '"]').attr('data-url',defaultUrl + '?' + form.fields[field].sourceField + '__' + 'iexact=:value'); $('input[name="' + form.fields[field].sourceModel + '_' + form.fields[field].sourceField + '"]').attr('data-source',field); - + scope['lookUp' + name] = function() { var listGenerator = GenerateList; var listScope = listGenerator.inject(list, { mode: 'lookup', hdr: hdr }); - listScope = scope; + + $('#lookup-modal').on('hidden.bs.modal', function() { + // If user clicks cancel without making a selection, make sure that field values are + // in synch. + if (scope[field] == '' || scope[field] == null) { + scope[form.fields[field].sourceModel + '_' + form.fields[field].sourceField] = ''; + if (!scope.$$phase) { + scope.$digest(); + } + } + }); + listScope.selectAction = function() { var found = false; var name; @@ -100,9 +111,26 @@ angular.module('LookUpHelper', [ 'RestServices', 'Utilities', 'SearchHelper', 'P } } } - listScope['toggle_' + list.iterator](scope[field]); - }); + // List generator creates the form, resetting it and losing the previously selected value. + // Put it back based on the value of sourceModel_sourceName + if (scope[form.fields[field].sourceModel + '_' + form.fields[field].sourceField] !== '' && + scope[form.fields[field].sourceModel + '_' + form.fields[field].sourceField] !== null) { + for (var i=0; i < listScope[list.name].length; i++) { + if (listScope[list.name][i][form.fields[field].sourceField] == + scope[form.fields[field].sourceModel + '_' + form.fields[field].sourceField]) { + scope[field] = listScope[list.name][i].id; + break; + } + } + + } + + if (!Empty(current_item)) { + listScope['toggle_' + list.iterator](current_item); + } + }); + listScope.search(list.iterator); } diff --git a/awx/ui/static/js/lists/Credentials.js b/awx/ui/static/js/lists/Credentials.js index 6c936cfa63..6866b23981 100644 --- a/awx/ui/static/js/lists/Credentials.js +++ b/awx/ui/static/js/lists/Credentials.js @@ -16,7 +16,6 @@ angular.module('CredentialsListDefinition', []) editTitle: 'Credentials', selectInstructions: '

Select existing credentials by clicking each credential or checking the related checkbox. When finished, click the blue ' + 'Select button, located bottom right.

Create a brand new credential by clicking the green Create New button.

', - editInstructions: 'Create a new credential from either the Teams tab or the Users tab. Teams and Users each have an associated set of Credentials.', index: true, hover: true, @@ -52,7 +51,6 @@ angular.module('CredentialsListDefinition', []) label: 'Create New', mode: 'all', // One of: edit, select, all ngClick: 'addCredential()', - basePaths: ['teams','users'], // base path must be in list, or action not available "class": 'btn-success btn-xs', awToolTip: 'Create a new credential' } diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 692a23b0de..2931745912 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -614,6 +614,10 @@ select.field-mini-height { margin: 0; } +input[type="checkbox"].checkbox-no-label { + margin-top: 10px; +} + .checkbox-options { font-weight: normal; padding-right: 20px; diff --git a/awx/ui/static/lib/ansible/Utilities.js b/awx/ui/static/lib/ansible/Utilities.js index 667ed517c7..b709fa0fa6 100644 --- a/awx/ui/static/lib/ansible/Utilities.js +++ b/awx/ui/static/lib/ansible/Utilities.js @@ -434,12 +434,26 @@ angular.module('Utilities',['RestServices', 'Utilities']) var scope = params.scope; var url = params.url; var field = params.field; - var emit_callback = params.emit; - + var variable = params.variable; + var callback = params.callback; // Optional. Provide if you want scop.$emit on completion. + + if (scope[variable]) { + scope[variable].length = 0; + } + else { + scope[variable] = []; + } + Rest.setUrl(url); Rest.options() .success( function(data, status, headers, config) { - scope.$emit(emit_callback, data.actions['GET'][field].choices); + var choices = data.actions.GET[field].choices + for (var i=0; i < choices.length; i++) { + scope[variable].push({ label: choices[i][1], value: choices[i][0] }); + } + if (callback) { + scope.$emit(callback); + } }) .error( function(data, status, headers, config) { ProcessErrors(scope, data, status, null, @@ -466,7 +480,18 @@ angular.module('Utilities',['RestServices', 'Utilities']) } }); } - }]); + }]) + + /* Empty() + * + * Test if a value is 'empty'. Returns true if val is null/''/undefined + * + */ + .factory('Empty', [ function() { + return function(val) { + return (val === null || val === undefined || val === '') ? true : false; + } + }]); diff --git a/awx/ui/static/lib/ansible/directives.js b/awx/ui/static/lib/ansible/directives.js index c747530df3..ba92facb45 100644 --- a/awx/ui/static/lib/ansible/directives.js +++ b/awx/ui/static/lib/ansible/directives.js @@ -159,7 +159,7 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Hos require: 'ngModel', link: function(scope, elm, attrs, ctrl) { ctrl.$parsers.unshift( function(viewValue) { - if (viewValue !== '') { + if (viewValue !== '' && viewValue !== null) { var url = elm.attr('data-url'); url = url.replace(/\:value/,escape(viewValue)); scope[elm.attr('data-source')] = null; @@ -179,7 +179,6 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Hos return undefined; } }); - } else { ctrl.$setValidity('awlookup', true); diff --git a/awx/ui/static/lib/ansible/form-generator.js b/awx/ui/static/lib/ansible/form-generator.js index 1fc37b1d25..517451030c 100644 --- a/awx/ui/static/lib/ansible/form-generator.js +++ b/awx/ui/static/lib/ansible/form-generator.js @@ -131,12 +131,14 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) $(options.modal_selector).on('shown.bs.modal', function() { $(options.modal_select + ' input:first').focus(); }); + $(options.modal_selector).unbind('hidden.bs.modal'); } else { $('#form-modal').modal({ show: true, backdrop: 'static', keyboard: true }); $('#form-modal').on('shown.bs.modal', function() { $('#form-modal input:first').focus(); }); + $('#form-modal').on('hidden.bs.modal'); } $(document).bind('keydown', function(e) { if (e.keyCode === 27) { @@ -376,27 +378,41 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies']) return html; } - function buildCheckbox(form, field, fld, idx) { - var html=''; - html += "