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 += "
\n";
diff --git a/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js b/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js
index 5625bcb0d9..5b886f55de 100644
--- a/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js
+++ b/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js
@@ -46,6 +46,7 @@
$scope.playbook_options = [];
$scope.mode = "add";
$scope.parseType = 'yaml';
+ $scope.credentialNotPresent = false;
md5Setup({
scope: $scope,
@@ -261,6 +262,29 @@
null, true);
}
+ $scope.selectedCredentials.extra.map(cred => 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 @@
+
+
+
+
+
+
+
+
+ {{option.name}}
+
+
+
+
+
+
+
+
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 @@
+
+
+ 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) {