1
0
mirror of https://github.com/ansible/awx.git synced 2024-11-02 01:21:21 +03:00

Merge pull request #6609 from jlmitch5/newCredOnJT

multi-credential ui selector on job template page
This commit is contained in:
jlmitch5 2017-06-19 10:31:52 -04:00 committed by GitHub
commit 8061667ace
14 changed files with 870 additions and 98 deletions

View File

@ -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

View File

@ -1423,9 +1423,21 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += "</div>\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 += "<div ";
html += (horizontal) ? "class=\"" + getFieldWidth() + "\"" : "";
html += ">\n";

View File

@ -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);

View File

@ -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);
}
};

View File

@ -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'
];

View File

@ -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: "<p>" + 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.") + "</p>",
dataTitle: i18n._('Credential'),
label: i18n._('Credentials'),
type: 'custom',
control: `
<multi-credential
credentials="credentials"
prompt="ask_credential_on_launch"
selected-credentials="selectedCredentials"
credential-not-present="credentialNotPresent"
credentials-to-post="credentialsToPost">
</multi-credential>`,
required: true,
awPopOver: "<p>" + 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.<br /><br />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.<br /><br />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.") + "</p>",
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:"<p>" + 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.") + "</p>",
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: "<p>" + i18n._("Network credentials are used by Ansible networking modules to connect to and manage networking devices.") + "</p>",
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)'
}
},

View File

@ -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);

View File

@ -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);

View File

@ -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();
};
}]
};
}];

View File

@ -0,0 +1,75 @@
<div id="multi-credential-modal" class="Lookup modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header Form-header">
<div class="Form-title Form-title--uppercase"
translate>
CREDENTIALS
</div>
<div class="Form-header--fields"></div>
<div class="Form-exitHolder">
<button type="button" class="Form-exit"
ng-click="cancelForm()">
<i class="fa fa-times-circle"></i>
</button>
</div>
</div>
<div class="modal-body">
<div class="MultiCredential-selectedBar"
ng-show="credTags && credTags.length">
<span class="MultiCredential-selectedBarLabel" translate>
SELECTED:
</span>
<div class="MultiCredential-tags">
<div class="MultiCredential-tagSection">
<div class="MultiCredential-flexContainer">
<div
class="MultiCredential-tagContainer ng-scope"
ng-repeat="tag in credTags track by $index">
<div class="MultiCredential-deleteContainer"
ng-click="removeCredential(tag.id)">
<i class="fa fa-times
MultiCredential-tagDelete">
</i>
</div>
<div class="MultiCredential-tag
MultiCredential-tag--deletable">
<span
class="MultiCredential-name--label
ng-binding">
{{ tag.kind }}
</span>
<span class="MultiCredential-name
ng-binding">
{{ tag.name }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="MultiCredential-credentialSubSection">
<select ng-model="credentialKind">
<option ng-repeat="option in credentialTypeOptions"
value="{{option.value}}">{{option.name}}</option>
</select>
</div>
<div id="multi-credential-modal-body"></div>
</div>
<div class="modal-footer">
<button type="button" ng-click="cancelForm()"
class="Lookup-cancel btn btn-sm Form-cancelButton"
translate>
CANCEL
</button>
<button type="button"
ng-click="saveForm()"
ng-disabled="!credentials || credentials.length === 0"
class="Lookup-cancel btn btn-sm Form-saveButton" translate>
SELECT
</button>
</div>
</div>
</div>
</div>

View File

@ -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;
}

View File

@ -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(`
<multi-credential-modal
credentials="credentials"
credentials-to-post="credentialsToPost"
selected-credentials="selectedCredentials">
</multi-credential-modal>`)(scope));
};
}
};
}
];

View File

@ -0,0 +1,46 @@
<div class="input-group Form-mixedInputGroup">
<span class="input-group-btn Form-variableHeightButtonGroup">
<button type="button"
class="Form-lookupButton
Form-lookupButton--variableHeight btn btn-default"
ng-click="openMultiCredentialModal()">
<i class="fa fa-search"></i>
</button>
</span>
<span class="form-control Form-textInput Form-textInput--variableHeight
input-medium lookup"
ng-class="{
'ng-invalid': credentialNotPresent,
'ng-dirty': fieldDirty
}"
style="padding: 4px 6px;">
<div class="MultiCredential-tags">
<div class="MultiCredential-tagSection">
<div class="MultiCredential-flexContainer">
<div class="MultiCredential-tagContainer ng-scope"
ng-repeat="tag in credentialsToPost track by $index">
<div class="MultiCredential-deleteContainer"
ng-click="removeCredential(tag.id)">
<i class="fa fa-times MultiCredential-tagDelete">
</i>
</div>
<div class="MultiCredential-tag
MultiCredential-tag--deletable">
<span class="MultiCredential-name--label
ng-binding">
{{ tag.kind }}
</span>
<span class="MultiCredential-name ng-binding">
{{ tag.name }}
</span>
</div>
</div>
</div>
</div>
</div>
</span>
</div>
<div class="error" ng-cloak ng-show="credentialNotPresent && fieldDirty"
translate>
Please select a machine (SSH) credential or check the "Prompt on launch" option.
</div>

View File

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