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/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..3f857c73ee 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 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", 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..d2c5510ee8 --- /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|net|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) {