From b5bc387d335c45af6358723122b20601753b97ad Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 16 Jun 2017 14:49:45 -0400 Subject: [PATCH 1/3] implement multi credential selection in ui for job templates --- awx/ui/client/src/shared/form-generator.js | 16 +- .../job-template-add.controller.js | 32 ++ .../job-template-edit.controller.js | 100 +++++-- .../factories/callback-help-init.factory.js | 119 +++++++- .../{ => job_templates}/job-template.form.js | 74 +---- .../src/templates/job_templates/main.js | 13 + .../job_templates/multi-credential/main.js | 7 + .../multi-credential-modal.directive.js | 281 ++++++++++++++++++ .../multi-credential-modal.partial.html | 75 +++++ .../multi-credential.block.less | 112 +++++++ .../multi-credential.directive.js | 79 +++++ .../multi-credential.partial.html | 46 +++ awx/ui/client/src/templates/main.js | 12 +- 13 files changed, 869 insertions(+), 97 deletions(-) rename awx/ui/client/src/templates/{ => job_templates}/job-template.form.js (88%) create mode 100644 awx/ui/client/src/templates/job_templates/main.js create mode 100644 awx/ui/client/src/templates/job_templates/multi-credential/main.js create mode 100644 awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.directive.js create mode 100644 awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.partial.html create mode 100644 awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.block.less create mode 100644 awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.directive.js create mode 100644 awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.partial.html diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index 01610052cc..d932814550 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -1423,9 +1423,21 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += "\n"; } - //custom fields if (field.type === 'custom') { - html += label(); + let labelOptions = {}; + + if (field.subCheckbox) { + labelOptions.checkbox = { + id: `${this.form.name}_${fld}_ask_chbox`, + ngShow: field.subCheckbox.ngShow, + ngChange: field.subCheckbox.ngChange, + ngModel: field.subCheckbox.variable, + ngDisabled: field.ngDisabled, + text: field.subCheckbox.text || '' + }; + } + + html += label(labelOptions); html += "
cred.id) + .forEach(function(cred_id) { + + Rest.setUrl(data.related.extra_credentials); + Rest.post({'id': cred_id}) + .success(function () { + }) + .error(function (data, + status) { + ProcessErrors( + $scope, + data, + status, + form, + { + hdr: 'Error!', + msg: 'Failed to add extra credential. Post returned ' + + 'status: ' + + status + }); + }); + }); + var orgDefer = $q.defer(); var associationDefer = $q.defer(); @@ -399,6 +423,14 @@ data.ask_inventory_on_launch = $scope.ask_inventory_on_launch ? $scope.ask_inventory_on_launch : false; data.ask_variables_on_launch = $scope.ask_variables_on_launch ? $scope.ask_variables_on_launch : false; data.ask_credential_on_launch = $scope.ask_credential_on_launch ? $scope.ask_credential_on_launch : false; + if ($scope.selectedCredentials && $scope.selectedCredentials + .machine && $scope.selectedCredentials + .machine) { + data.credential = $scope.selectedCredentials + .machine.id; + } else { + data.credential = null; + } data.extra_vars = ToJSON($scope.parseType, $scope.variables, true); diff --git a/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js b/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js index 11f5a5e451..487d1f9dab 100644 --- a/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js +++ b/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js @@ -55,6 +55,7 @@ export default $scope.parseType = 'yaml'; $scope.showJobType = false; $scope.instance_groups = InstanceGroupsData; + $scope.credentialNotPresent = false; SurveyControllerInit({ scope: $scope, @@ -200,8 +201,6 @@ export default // watch for changes to 'verbosity', ensure we keep our select2 in sync when it changes. $scope.$watch('verbosity', sync_verbosity_select2); - - // Turn off 'Wait' after both cloud credential and playbook list come back if ($scope.removeJobTemplateLoadFinished) { $scope.removeJobTemplateLoadFinished(); } @@ -218,19 +217,11 @@ export default }); - if ($scope.cloudCredentialReadyRemove) { - $scope.cloudCredentialReadyRemove(); - } - $scope.cloudCredentialReadyRemove = $scope.$on('cloudCredentialReady', function () { - $scope.$emit('jobTemplateLoadFinished'); - }); - - // Retrieve each related set and populate the playbook list if ($scope.jobTemplateLoadedRemove) { $scope.jobTemplateLoadedRemove(); } - $scope.jobTemplateLoadedRemove = $scope.$on('jobTemplateLoaded', function (e, related_cloud_credential, masterObject) { + $scope.jobTemplateLoadedRemove = $scope.$on('jobTemplateLoaded', function (e, masterObject) { var dft; master = masterObject; @@ -247,13 +238,7 @@ export default ParseTypeChange({ scope: $scope, field_id: 'job_template_variables', onChange: callback }); - if($scope.job_template_obj.summary_fields.cloud_credential && related_cloud_credential) { - $scope.$emit('cloudCredentialReady', $scope.job_template_obj.summary_fields.cloud_credential.name); - } else { - // No existing cloud credential - $scope.$emit('cloudCredentialReady', null); - } - + $scope.$emit('jobTemplateLoadFinished'); }); Wait('start'); @@ -411,6 +396,73 @@ export default null, true); } + let extraCredUrl = data.related.extra_credentials; + + Rest.setUrl(extraCredUrl); + Rest.get() + .then(({data}) => { + let existingCreds = data.results + .map(cred => cred.id); + + let newCreds = $scope.selectedCredentials.extra + .map(cred => cred.id); + + let toAdd, toRemove; + + [toAdd, toRemove] = _.partition(_.xor(existingCreds, newCreds), cred => (newCreds.indexOf(cred) > -1)); + + let destroyResolve = []; + + toRemove.forEach((cred_id) => { + Rest.setUrl(extraCredUrl); + destroyResolve.push( + Rest.post({'id': cred_id, 'disassociate': true}) + .catch(({data, status}) => { + ProcessErrors( + $scope, + data, + status, + form, + { + hdr: 'Error!', + msg: 'Failed to remove extra credential. Post returned ' + + 'status: ' + + status + }); + })); + }); + + $q.all(destroyResolve) + .then(() => { + toAdd.forEach((cred_id) => { + Rest.setUrl(extraCredUrl); + Rest.post({'id': cred_id}) + .catch(({data, status}) => { + ProcessErrors( + $scope, + data, + status, + form, + { + hdr: 'Error!', + msg: 'Failed to add extra credential. Post returned ' + + 'status: ' + + status + }); + }); + }); + }); + + + }) + .catch(({data, status}) => { + ProcessErrors($scope, data, status, form, { + hdr: 'Error!', + msg: 'Failed to get existing extra credentials. GET returned ' + + 'status: ' + status + }); + }); + InstanceGroupsService.editInstanceGroups(instance_group_url, $scope.instance_groups) .catch(({data, status}) => { ProcessErrors($scope, data, status, form, { @@ -546,6 +598,14 @@ export default data.ask_inventory_on_launch = $scope.ask_inventory_on_launch ? $scope.ask_inventory_on_launch : false; data.ask_variables_on_launch = $scope.ask_variables_on_launch ? $scope.ask_variables_on_launch : false; data.ask_credential_on_launch = $scope.ask_credential_on_launch ? $scope.ask_credential_on_launch : false; + if ($scope.selectedCredentials && $scope.selectedCredentials + .machine && $scope.selectedCredentials + .machine) { + data.credential = $scope.selectedCredentials + .machine.id; + } else { + data.credential = null; + } data.extra_vars = ToJSON($scope.parseType, $scope.variables, true); @@ -585,8 +645,8 @@ export default }); } catch (err) { Wait('stop'); - Alert("Error", "Error parsing extra variables. " + - "Parser returned: " + err); + Alert("Error", "Error saving job template. " + + "Error: " + err); } }; diff --git a/awx/ui/client/src/templates/job_templates/factories/callback-help-init.factory.js b/awx/ui/client/src/templates/job_templates/factories/callback-help-init.factory.js index 845216c459..2aa105ff83 100644 --- a/awx/ui/client/src/templates/job_templates/factories/callback-help-init.factory.js +++ b/awx/ui/client/src/templates/job_templates/factories/callback-help-init.factory.js @@ -1,6 +1,6 @@ export default - function CallbackHelpInit($location, GetBasePath, Rest, JobTemplateForm, GenerateForm, $stateParams, ProcessErrors, - ParseVariableString, Empty, CredentialList, Wait) { + function CallbackHelpInit($q, $location, GetBasePath, Rest, JobTemplateForm, GenerateForm, $stateParams, ProcessErrors, + ParseVariableString, Empty, Wait) { return function(params) { var scope = params.scope, defaultUrl = GetBasePath('job_templates'), @@ -13,8 +13,6 @@ export default // checkSCMStatus, getPlaybooks, callback, // choicesCount = 0; - CredentialList = _.cloneDeep(CredentialList); - // The form uses awPopOverWatch directive to 'watch' scope.callback_help for changes. Each time the // popover is activated, a function checks the value of scope.callback_help before constructing the content. scope.setCallbackHelp = function() { @@ -136,7 +134,114 @@ export default scope.can_edit = data.summary_fields.user_capabilities.edit; - scope.$emit('jobTemplateLoaded', data.related.cloud_credential, master); + scope.selectedCredentials = { + machine: null, + extra: [] + }; + + var credDefers = []; + + if (data.related.credential) { + Rest.setUrl(data.related.credential); + credDefers.push(Rest.get() + .then(({data}) => { + scope.selectedCredentials.machine = data; + }) + .catch(({data, status}) => { + ProcessErrors( + scope, + data, + status, + null, + { + hdr: 'Error!', + msg: 'Failed to get machine credential. ' + + 'Get returned status: ' + + status + }); + })); + } + + if (data.related.extra_credentials) { + Rest.setUrl(data.related.extra_credentials); + credDefers.push(Rest.get() + .then(({data}) => { + scope.selectedCredentials.extra = data.results; + }) + .catch(({data, status}) => { + ProcessErrors( + scope, + data, + status, + null, + { + hdr: 'Error!', + msg: 'Failed to get extra credentials. ' + + 'Get returned status: ' + + status + }); + })); + } + + Rest.setUrl(GetBasePath('credential_types')); + credDefers.push(Rest.get() + .then(({data}) => { + scope.credentialTypeOptions = []; + data.results.forEach((credentialType => { + if(credentialType.kind.match(/^(machine|cloud|network|ssh)$/)) { + scope.credentialTypeOptions.push({ + name: credentialType.name, + value: credentialType.id + }); + } + })); + }) + .catch(({data, status}) => { + ProcessErrors( + scope, + data, + status, + null, + { + hdr: 'Error!', + msg: 'Failed to get credential types. Get returned ' + + 'status: ' + + status + }); + })); + + $q.all(credDefers) + .then(() => { + let machineCred = []; + let extraCreds = []; + + if (scope.selectedCredentials && scope.selectedCredentials.machine) { + let mach = scope.selectedCredentials.machine; + mach.postType = "machine"; + machineCred = [scope.selectedCredentials.machine]; + } + + if (scope.selectedCredentials && scope.selectedCredentials.extra) { + extraCreds = scope.selectedCredentials.extra; + } + + extraCreds = extraCreds.map(function(cred) { + cred.postType = "extra"; + + return cred; + }); + + let credTags = machineCred.concat(extraCreds); + + scope.credentialsToPost = credTags.map(cred => ({ + name: cred.name, + id: cred.id, + postType: cred.postType, + kind: scope.credentialTypeOptions + .filter(type => parseInt(cred.credential_type) === type.value)[0].name + ":" + })); + scope.$emit('jobTemplateLoaded', master); + }); }) .error(function (data, status) { ProcessErrors(scope, data, status, form, { @@ -149,7 +254,7 @@ export default } CallbackHelpInit.$inject = - [ '$location', 'GetBasePath', 'Rest', 'JobTemplateForm', 'GenerateForm', + [ '$q', '$location', 'GetBasePath', 'Rest', 'JobTemplateForm', 'GenerateForm', '$stateParams', 'ProcessErrors', 'ParseVariableString', - 'Empty', 'CredentialList', 'Wait' + 'Empty', 'Wait' ]; diff --git a/awx/ui/client/src/templates/job-template.form.js b/awx/ui/client/src/templates/job_templates/job-template.form.js similarity index 88% rename from awx/ui/client/src/templates/job-template.form.js rename to awx/ui/client/src/templates/job_templates/job-template.form.js index abf1f94ef6..3866ec15dc 100644 --- a/awx/ui/client/src/templates/job-template.form.js +++ b/awx/ui/client/src/templates/job_templates/job-template.form.js @@ -127,68 +127,26 @@ function(NotificationsList, CompletedJobsList, i18n) { includePlaybookNotFoundError: true }, credential: { - label: i18n._('Machine Credential'), - type: 'lookup', - list: 'CredentialList', - basePath: 'credentials', - autopopulateLookup: false, - search: { - kind: 'ssh' - }, - sourceModel: 'credential', - sourceField: 'name', - awRequiredWhen: { - reqExpression: '!ask_credential_on_launch', - alwaysShowAsterisk: true - }, - requiredErrorMsg: i18n._("Please select a Machine Credential or check the Prompt on launch option."), - column: 1, - awPopOver: "

" + i18n._("Select the credential you want the job to use when accessing the remote hosts. Choose the credential containing " + - " the username and SSH key or password that Ansible will need to log into the remote hosts.") + "

", - dataTitle: i18n._('Credential'), + label: i18n._('Credentials'), + type: 'custom', + control: ` + + `, + required: true, + awPopOver: "

" + i18n._("Select credentials so that tower can access the nodes this job will be ran against.

You can only select one credential of each type, and you must either select a machine (SSH) credential or check \"Prompt on launch\". In that case, a machine credential will need to be selected at run time.

You can select credentials and still check the \"Prompt on launch\" box. In this case, the credentials selected will act as defaults that can be updated at run time.") + "

", + dataTitle: i18n._('Credentials'), dataPlacement: 'right', dataContainer: "body", subCheckbox: { variable: 'ask_credential_on_launch', text: i18n._('Prompt on launch'), - ngChange: 'job_template_form.credential_name.$validate()', - }, - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)' - }, - cloud_credential: { - label: i18n._('Cloud Credential'), - type: 'lookup', - list: 'CredentialList', - basePath: 'credentials', - search: { - cloud: 'true' - }, - sourceModel: 'cloud_credential', - sourceField: 'name', - column: 1, - awPopOver:"

" + i18n._("Selecting an optional cloud credential in the job template will pass along the access credentials to the " + - "running playbook, allowing provisioning into the cloud without manually passing parameters to the included modules.") + "

", - dataTitle: i18n._('Cloud Credential'), - dataPlacement: 'right', - dataContainer: "body", - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)' - }, - network_credential: { - label: i18n._('Network Credential'), - type: 'lookup', - list: 'CredentialList', - basePath: 'credentials', - search: { - kind: 'net' - }, - sourceModel: 'network_credential', - sourceField: 'name', - column: 1, - awPopOver: "

" + i18n._("Network credentials are used by Ansible networking modules to connect to and manage networking devices.") + "

", - dataTitle: i18n._('Network Credential'), - dataPlacement: 'right', - dataContainer: "body", - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)' + } }, forks: { label: i18n._('Forks'), @@ -417,7 +375,7 @@ function(NotificationsList, CompletedJobsList, i18n) { }, save: { ngClick: 'formSave()', //$scope.function to call on click, optional - ngDisabled: "job_template_form.$invalid",//true //Disable when $pristine or $invalid, optional and when can_edit = false, for permission reasons + ngDisabled: "job_template_form.$invalid || credentialNotPresent",//true //Disable when $pristine or $invalid, optional and when can_edit = false, for permission reasons ngShow: '(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)' } }, diff --git a/awx/ui/client/src/templates/job_templates/main.js b/awx/ui/client/src/templates/job_templates/main.js new file mode 100644 index 0000000000..82d254cfb4 --- /dev/null +++ b/awx/ui/client/src/templates/job_templates/main.js @@ -0,0 +1,13 @@ +import jobTemplateAdd from './add-job-template/main'; +import jobTemplateEdit from './edit-job-template/main'; +import multiCredential from './multi-credential/main'; +import md5Setup from './factories/md-5-setup.factory'; +import CallbackHelpInit from './factories/callback-help-init.factory'; +import JobTemplateForm from './job-template.form'; + +export default + angular.module('jobTemplates', [jobTemplateAdd.name, jobTemplateEdit.name, + multiCredential.name]) + .factory('md5Setup', md5Setup) + .factory('CallbackHelpInit', CallbackHelpInit) + .factory('JobTemplateForm', JobTemplateForm); diff --git a/awx/ui/client/src/templates/job_templates/multi-credential/main.js b/awx/ui/client/src/templates/job_templates/multi-credential/main.js new file mode 100644 index 0000000000..3ab35c1e19 --- /dev/null +++ b/awx/ui/client/src/templates/job_templates/multi-credential/main.js @@ -0,0 +1,7 @@ +import multiCredential from './multi-credential.directive'; +import multiCredentialModal from './multi-credential-modal.directive'; + +export default + angular.module('multiCredential', []) + .directive('multiCredential', multiCredential) + .directive('multiCredentialModal', multiCredentialModal); diff --git a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.directive.js b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.directive.js new file mode 100644 index 0000000000..01e265b456 --- /dev/null +++ b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.directive.js @@ -0,0 +1,281 @@ +export default ['templateUrl', 'Rest', 'GetBasePath', 'generateList', '$compile', + function(templateUrl, Rest, GetBasePath, GenerateList, $compile) { + return { + restrict: 'E', + scope: { + credentialsToPost: '=', + credentials: '=', + selectedCredentials: '=' + }, + templateUrl: templateUrl('templates/job_templates/multi-credential/multi-credential-modal'), + + link: function(scope, element) { + scope.credentialKind = "1"; + + $('#multi-credential-modal').on('hidden.bs.modal', function () { + $('#multi-credential-modal').off('hidden.bs.modal'); + $(element).remove(); + }); + + scope.showModal = function() { + $('#multi-credential-modal').modal('show'); + }; + + scope.destroyModal = function() { + scope.credentialKind = 1; + $('#multi-credential-modal').modal('hide'); + }; + + scope.generateCredentialList = function() { + let html = GenerateList.build({ + list: scope.list, + input_type: 'radio', + mode: 'lookup' + }); + $('#multi-credential-modal-body') + .append($compile(html)(scope)); + }; + + // Go out and get the credential types + Rest.setUrl(GetBasePath('credential_types')); + Rest.get() + .success(function (credentialTypeData) { + let credential_types = {}; + scope.credentialTypeOptions = []; + credentialTypeData.results.forEach((credentialType => { + credential_types[credentialType.id] = credentialType; + if(credentialType.kind + .match(/^(machine|cloud|network|ssh)$/)) { + scope.credentialTypeOptions.push({ + name: credentialType.name, + value: credentialType.id + }); + } + })); + scope.credential_types = credential_types; + scope.$emit('multiCredentialModalLinked'); + }); + }, + + controller: ['$scope', 'CredentialList', 'i18n', 'QuerySet', + 'GetBasePath', function($scope, CredentialList, i18n, qs, + GetBasePath) { + + let updateCredentialTags = function() { + let machineCred = []; + let extraCreds = []; + + if ($scope.selectedCredentials && + $scope.selectedCredentials.machine) { + let mach = $scope.selectedCredentials.machine; + mach.postType = "machine"; + machineCred = [$scope.selectedCredentials.machine]; + } + + if ($scope.selectedCredentials && + $scope.selectedCredentials.extra) { + extraCreds = $scope.selectedCredentials.extra; + } + + extraCreds = extraCreds.map(function(cred) { + cred.postType = "extra"; + + return cred; + }); + + let credTags = machineCred.concat(extraCreds); + + $scope.credTags = credTags.map(cred => ({ + name: cred.name, + id: cred.id, + postType: cred.postType, + kind: $scope.credentialTypeOptions + .filter(type => { + return parseInt(cred.credential_type) === type.value; + })[0].name + ":" + })); + }; + + let updateExtraCredentialsList = function() { + let extraCredIds = $scope.selectedCredentials.extra + .map(cred => cred.id); + $scope.credentials.forEach(cred => { + if (cred.credential_type !== 1) { + cred.checked = (extraCredIds + .indexOf(cred.id) > - 1) ? 1 : 0; + } + }); + updateCredentialTags(); + }; + + let updateMachineCredentialList = function() { + $scope.credentials.forEach(cred => { + if (cred.credential_type === 1) { + cred.checked = ($scope.selectedCredentials + .machine !== null && + cred.id === $scope.selectedCredentials + .machine.id) ? 1 : 0; + } + }); + updateCredentialTags(); + }; + + let uncheckAllCredentials = function() { + $scope.credentials.forEach(cred => { + cred.checked = 0; + }); + updateCredentialTags(); + }; + + let init = function() { + $scope.originalSelectedCredentials = _.cloneDeep($scope + .selectedCredentials); + $scope.credential_dataset = []; + $scope.credentials = $scope.credentials || []; + $scope.listRendered = false; + + let credList = _.cloneDeep(CredentialList); + credList.emptyListText = i18n._('No Credentials Matching This Type Have Been Created'); + $scope.list = credList; + + $scope.credential_default_params = { + order_by: 'name', + page_size: 5 + }; + + $scope.credential_queryset = { + order_by: 'name', + page_size: 5 + }; + + $scope.$watch('credentialKind', function(){ + $scope.credential_default_params.credential_type = $scope + .credential_queryset.credential_type = parseInt($scope + .credentialKind); + + qs.search(GetBasePath('credentials'), $scope + .credential_default_params) + .then(res => { + $scope.credential_dataset = res.data; + $scope.credentials = $scope.credential_dataset + .results; + + if(!$scope.listRendered) { + $scope.generateCredentialList(); + $scope.listRendered = true; + $scope.showModal(); + } + }); + }); + + $scope.$watchCollection('selectedCredentials.extra', () => { + if($scope.credentials && $scope.credentials.length > 0) { + if($scope.selectedCredentials.extra && + $scope.selectedCredentials.extra.length > 0 && + parseInt($scope.credentialKind) !== 1) { + updateExtraCredentialsList(); + } else { + uncheckAllCredentials(); + } + } + }); + + $scope.$watch('selectedCredentials.machine', () => { + if($scope.selectedCredentials && + $scope.selectedCredentials.machine && + parseInt($scope.credentialKind) === 1) { + updateMachineCredentialList(); + } else { + uncheckAllCredentials(); + } + }); + + $scope.$watchGroup(['credentials', + 'selectedCredentials.machine'], () => { + if($scope.credentials && + $scope.credentials.length > 0) { + if($scope.selectedCredentials && + $scope.selectedCredentials.machine && + parseInt($scope.credentialKind) === 1) { + updateMachineCredentialList(); + } else if($scope.selectedCredentials && + $scope.selectedCredentials.extra && + $scope.selectedCredentials.extra.length > 0 && + parseInt($scope.credentialKind) !== 1) { + updateExtraCredentialsList(); + } else { + uncheckAllCredentials(); + } + } + }); + }; + + $scope.$on('multiCredentialModalLinked', function() { + init(); + }); + + $scope.toggle_row = function(selectedRow) { + if(parseInt($scope.credentialKind) === 1) { + if($scope.selectedCredentials && + $scope.selectedCredentials.machine && + $scope.selectedCredentials.machine.id === selectedRow.id) { + $scope.selectedCredentials.machine = null; + } else { + $scope.selectedCredentials.machine = _.cloneDeep(selectedRow); + } + } else { + let rowDeselected = false; + for (let i = $scope.selectedCredentials.extra.length - 1; i >= 0; i--) { + if($scope.selectedCredentials.extra[i].id === selectedRow + .id) { + $scope.selectedCredentials.extra.splice(i, 1); + rowDeselected = true; + } else if(selectedRow.credential_type === $scope + .selectedCredentials.extra[i].credential_type) { + $scope.selectedCredentials.extra.splice(i, 1); + } + } + if(!rowDeselected) { + $scope.selectedCredentials.extra + .push(_.cloneDeep(selectedRow)); + } + } + }; + + $scope.removeCredential = function(credToRemove) { + $scope.credTags + .forEach(function(cred) { + if (credToRemove === cred.id) { + if (cred.postType === 'machine') { + $scope.selectedCredentials[cred.postType] = null; + } else { + $scope.selectedCredentials[cred.postType] = $scope + .selectedCredentials[cred.postType] + .filter(cred => cred + .id !== credToRemove); + } + } + }); + + $scope.credTags = $scope.credTags + .filter(cred => cred.id !== credToRemove); + + if ($scope.credentials + .filter(cred => cred.id === credToRemove).length) { + uncheckAllCredentials(); + } + }; + + $scope.cancelForm = function() { + $scope.selectedCredentials = $scope.originalSelectedCredentials; + $scope.credTags = $scope.credentialsToPost; + $scope.destroyModal(); + }; + + $scope.saveForm = function() { + $scope.credentialsToPost = $scope.credTags; + $scope.destroyModal(); + }; + }] + }; +}]; diff --git a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.partial.html b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.partial.html new file mode 100644 index 0000000000..72069cff6d --- /dev/null +++ b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.partial.html @@ -0,0 +1,75 @@ + diff --git a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.block.less b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.block.less new file mode 100644 index 0000000000..599aacea8d --- /dev/null +++ b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.block.less @@ -0,0 +1,112 @@ +@import "../../../shared/branding/colors.default.less"; + +.MultiCredential-selectedBar { + display: flex; + align-items: center; + padding: 5px 10px; + background: @default-no-items-bord; + margin-bottom: 20px; + border: 1px solid @default-icon-hov; + border-radius: 5px; +} + +.MultiCredential-selectedBarLabel { + margin-right: 20px; + font-size: 12px; + color: @default-icon; +} + +.MultiCredential-tags { + padding-left: 0px; +} + +.MultiCredential-bar i { + font-size: 16px; + color: @default-icon; +} + +.MultiCredential-flexContainer { + display: flex; + width: 100%; + flex-wrap: wrap; +} + +.MultiCredential-tagContainer { + display: flex; + max-width: 100%; +} + +.MultiCredential-tag { + border-radius: 5px; + padding: 2px 10px; + margin: 4px 0px; + font-size: 12px; + color: @default-interface-txt; + background-color: @default-bg; + margin-right: 10px; + max-width: 100%; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.MultiCredential-tag--deletable { + margin-right: 0px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + border-right: 0; + max-width: ~"calc(100% - 23px)"; + background-color: @default-link; + color: @default-bg; + margin-right: 10px; +} + +.MultiCredential-deleteContainer { + background-color: @default-link!important; + color: white; + background-color: @default-bg; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + padding: 0 5px; + margin: 4px 0px; + align-items: center; + display: flex; + cursor: pointer; +} + +.MultiCredential-tagDelete { + font-size: 13px; +} + +.MultiCredential-name { + flex: initial; + font-size: 12px; + max-width: 100%; +} + +.MultiCredential-name--label { + color: @default-list-header-bg; + font-size: 10px; + margin-left: -7px; + margin-right: 5px; + text-transform: uppercase; +} + +.MultiCredential-tag--deletable > .MultiCredential-name { + max-width: ~"calc(100% - 23px)"; +} + +.MultiCredential-deleteContainer:hover { + border-color: @default-err; + background-color: @default-err!important; +} + +.MultiCredential-deleteContainer:hover > .MultiCredential-tagDelete { + color: @default-bg; +} + +.MultiCredential-credentialSubSection { + display: flex; + justify-content: flex-end; + margin-bottom: 15px; +} diff --git a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.directive.js b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.directive.js new file mode 100644 index 0000000000..bb6a917e49 --- /dev/null +++ b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.directive.js @@ -0,0 +1,79 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['templateUrl', '$compile', + function(templateUrl, $compile) { + return { + scope: { + credentials: '=', + selectedCredentials: '=', + prompt: '=', + credentialNotPresent: '=', + credentialsToPost: '=' + }, + restrict: 'E', + templateUrl: templateUrl('templates/job_templates/multi-credential/multi-credential'), + controller: ['$scope', + function($scope) { + if (!$scope.selectedCredentials) { + $scope.selectedCredentials = { + machine: null, + extra: [] + }; + } + + if (!$scope.credentialsToPost) { + $scope.credentialsToPost = []; + } + + $scope.fieldDirty = false; + + $scope.$watchGroup(['prompt', 'credentialsToPost'], + function() { + if ($scope.prompt || + $scope.credentialsToPost.length) { + $scope.fieldDirty = true; + } + + $scope.credentialNotPresent = !$scope.prompt && + $scope.selectedCredentials.machine === null; + }); + + $scope.removeCredential = function(credToRemove) { + $scope.credentialsToPost = $scope.credentialsToPost + .filter(function(cred) { + if (cred.id === credToRemove) { + if (cred.postType === 'machine') { + $scope.selectedCredentials.machine = null; + } else { + $scope.selectedCredentials + .extra = $scope.selectedCredentials + .extra + .filter(selectedCred => { + return selectedCred + .id !== credToRemove; + }); + } + } + return cred.id !== credToRemove; + }); + }; + } + ], + link: function(scope) { + scope.openMultiCredentialModal = function() { + $('#content-container') + .append($compile(` + + `)(scope)); + }; + } + }; + } +]; diff --git a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.partial.html b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.partial.html new file mode 100644 index 0000000000..eb35adb9ec --- /dev/null +++ b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.partial.html @@ -0,0 +1,46 @@ +
+ + + + +
+
+
+
+
+ + +
+
+ + {{ tag.kind }} + + + {{ tag.name }} + +
+
+
+
+
+
+
+
+ Please select a machine (SSH) credential or check the "Prompt on launch" option. +
diff --git a/awx/ui/client/src/templates/main.js b/awx/ui/client/src/templates/main.js index c2af4a85a0..0480584109 100644 --- a/awx/ui/client/src/templates/main.js +++ b/awx/ui/client/src/templates/main.js @@ -7,8 +7,7 @@ import templatesService from './templates.service'; import surveyMaker from './survey-maker/main'; import templatesList from './list/main'; -import jobTemplatesAdd from './job_templates/add-job-template/main'; -import jobTemplatesEdit from './job_templates/edit-job-template/main'; +import jobTemplates from './job_templates/main'; import workflowAdd from './workflows/add-workflow/main'; import workflowEdit from './workflows/edit-workflow/main'; import labels from './labels/main'; @@ -18,28 +17,21 @@ import workflowControls from './workflows/workflow-controls/main'; import templatesListRoute from './list/templates-list.route'; import workflowService from './workflows/workflow.service'; import templateCopyService from './copy-template/template-copy.service'; -import CallbackHelpInit from './job_templates/factories/callback-help-init.factory'; -import md5Setup from './job_templates/factories/md-5-setup.factory'; import WorkflowForm from './workflows.form'; import CompletedJobsList from './completed-jobs.list'; import InventorySourcesList from './inventory-sources.list'; import TemplateList from './templates.list'; -import JobTemplateForm from './job-template.form'; export default -angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplatesAdd.name, - jobTemplatesEdit.name, labels.name, workflowAdd.name, workflowEdit.name, +angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplates.name, labels.name, workflowAdd.name, workflowEdit.name, workflowChart.name, workflowMaker.name, workflowControls.name ]) .service('TemplatesService', templatesService) .service('WorkflowService', workflowService) .service('TemplateCopyService', templateCopyService) - .factory('CallbackHelpInit', CallbackHelpInit) - .factory('md5Setup', md5Setup) .factory('WorkflowForm', WorkflowForm) .factory('CompletedJobsList', CompletedJobsList) .factory('TemplateList', TemplateList) - .factory('JobTemplateForm', JobTemplateForm) .value('InventorySourcesList', InventorySourcesList) .config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider', function($stateProvider, stateDefinitionsProvider, $stateExtenderProvider) { From e1fadd0c8a0207cede3dd9b5f86376c92ab479b2 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 16 Jun 2017 14:51:43 -0400 Subject: [PATCH 2/3] update help text wording for credential field on jt page based on tvo's feedback --- awx/ui/client/src/templates/job_templates/job-template.form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/templates/job_templates/job-template.form.js b/awx/ui/client/src/templates/job_templates/job-template.form.js index 3866ec15dc..3f857c73ee 100644 --- a/awx/ui/client/src/templates/job_templates/job-template.form.js +++ b/awx/ui/client/src/templates/job_templates/job-template.form.js @@ -138,7 +138,7 @@ function(NotificationsList, CompletedJobsList, i18n) { credentials-to-post="credentialsToPost"> `, required: true, - awPopOver: "

" + i18n._("Select credentials so that tower can access the nodes this job will be ran against.

You can only select one credential of each type, and you must either select a machine (SSH) credential or check \"Prompt on launch\". In that case, a machine credential will need to be selected at run time.

You can select credentials and still check the \"Prompt on launch\" box. In this case, the credentials selected will act as defaults that can be updated at run time.") + "

", + awPopOver: "

" + i18n._("Select credentials that allow Tower to access the nodes this job will be ran against. You can only select one credential of each type.

You must select either a machine (SSH) credential or \"Prompt on launch\". \"Prompt on launch\" requires you to select a machine credential at run time.

If you select credentials AND check the \"Prompt on launch\" box, you make the selected credentials the defaults that can be updated at run time.") + "

", dataTitle: i18n._('Credentials'), dataPlacement: 'right', dataContainer: "body", From adfe4ef92f3c98c3348a02c3ed53313a7e47197b Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 16 Jun 2017 14:57:40 -0400 Subject: [PATCH 3/3] update multi cred and prompt on launch modals to show net credential kinds --- awx/ui/client/src/job-submission/job-submission.controller.js | 2 +- .../multi-credential/multi-credential-modal.directive.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/job-submission/job-submission.controller.js b/awx/ui/client/src/job-submission/job-submission.controller.js index 64abe6e4c0..5aa3dde0c4 100644 --- a/awx/ui/client/src/job-submission/job-submission.controller.js +++ b/awx/ui/client/src/job-submission/job-submission.controller.js @@ -264,7 +264,7 @@ export default $scope.credentialTypeOptions = []; credentialTypeData.results.forEach((credentialType => { credential_types[credentialType.id] = credentialType; - if(credentialType.kind.match(/^(machine|cloud|network|ssh)$/)) { + if(credentialType.kind.match(/^(machine|cloud|net|ssh)$/)) { $scope.credentialTypeOptions.push({ name: credentialType.name, value: credentialType.id diff --git a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.directive.js b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.directive.js index 01e265b456..d2c5510ee8 100644 --- a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.directive.js +++ b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.directive.js @@ -45,7 +45,7 @@ export default ['templateUrl', 'Rest', 'GetBasePath', 'generateList', '$compile' credentialTypeData.results.forEach((credentialType => { credential_types[credentialType.id] = credentialType; if(credentialType.kind - .match(/^(machine|cloud|network|ssh)$/)) { + .match(/^(machine|cloud|net|ssh)$/)) { scope.credentialTypeOptions.push({ name: credentialType.name, value: credentialType.id