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) {