Add a Galaxy Credential multi-select field to the Organizations form

This commit is contained in:
mabashian 2020-08-05 11:36:25 -04:00 committed by Ryan Petrello
parent 011822b1f0
commit 458807c0c7
No known key found for this signature in database
GPG Key ID: F2AA5F2122351777
11 changed files with 336 additions and 17 deletions

View File

@ -4,11 +4,12 @@
* All Rights Reserved
*************************************************/
export default ['$scope', '$rootScope', '$location', '$stateParams',
'OrganizationForm', 'GenerateForm', 'Rest', 'Alert',
'ProcessErrors', 'GetBasePath', 'Wait', 'CreateSelect2', '$state','InstanceGroupsService', 'ConfigData',
function($scope, $rootScope, $location, $stateParams, OrganizationForm,
GenerateForm, Rest, Alert, ProcessErrors, GetBasePath, Wait, CreateSelect2, $state, InstanceGroupsService, ConfigData) {
export default ['$scope', '$rootScope', '$location', '$stateParams', 'OrganizationForm',
'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'GetBasePath', 'Wait', 'CreateSelect2',
'$state','InstanceGroupsService', 'ConfigData', 'MultiCredentialService',
function($scope, $rootScope, $location, $stateParams, OrganizationForm,
GenerateForm, Rest, Alert, ProcessErrors, GetBasePath, Wait, CreateSelect2,
$state, InstanceGroupsService, ConfigData, MultiCredentialService) {
Rest.setUrl(GetBasePath('organizations'));
Rest.options()
@ -57,18 +58,32 @@ export default ['$scope', '$rootScope', '$location', '$stateParams',
const organization_id = data.id,
instance_group_url = data.related.instance_groups;
InstanceGroupsService.addInstanceGroups(instance_group_url, $scope.instance_groups)
MultiCredentialService
.saveRelatedSequentially({
related: {
credentials: data.related.galaxy_credentials
}
}, $scope.credentials)
.then(() => {
Wait('stop');
$rootScope.$broadcast("EditIndicatorChange", "organizations", organization_id);
$state.go('organizations.edit', {organization_id: organization_id}, {reload: true});
})
.catch(({data, status}) => {
InstanceGroupsService.addInstanceGroups(instance_group_url, $scope.instance_groups)
.then(() => {
Wait('stop');
$rootScope.$broadcast("EditIndicatorChange", "organizations", organization_id);
$state.go('organizations.edit', {organization_id: organization_id}, {reload: true});
})
.catch(({data, status}) => {
ProcessErrors($scope, data, status, form, {
hdr: 'Error!',
msg: 'Failed to save instance groups. POST returned status: ' + status
});
});
}).catch(({data, status}) => {
ProcessErrors($scope, data, status, form, {
hdr: 'Error!',
msg: 'Failed to save instance groups. POST returned status: ' + status
msg: 'Failed to save Galaxy credentials. POST returned status: ' + status
});
});
})
.catch(({data, status}) => {
let explanation = _.has(data, "name") ? data.name[0] : "";

View File

@ -6,10 +6,12 @@
export default ['$scope', '$location', '$stateParams', 'isOrgAdmin', 'isNotificationAdmin',
'OrganizationForm', 'Rest', 'ProcessErrors', 'Prompt', 'i18n', 'isOrgAuditor',
'GetBasePath', 'Wait', '$state', 'ToggleNotification', 'CreateSelect2', 'InstanceGroupsService', 'InstanceGroupsData', 'ConfigData',
'GetBasePath', 'Wait', '$state', 'ToggleNotification', 'CreateSelect2', 'InstanceGroupsService',
'InstanceGroupsData', 'ConfigData', 'GalaxyCredentialsData', 'MultiCredentialService',
function($scope, $location, $stateParams, isOrgAdmin, isNotificationAdmin,
OrganizationForm, Rest, ProcessErrors, Prompt, i18n, isOrgAuditor,
GetBasePath, Wait, $state, ToggleNotification, CreateSelect2, InstanceGroupsService, InstanceGroupsData, ConfigData) {
GetBasePath, Wait, $state, ToggleNotification, CreateSelect2, InstanceGroupsService,
InstanceGroupsData, ConfigData, GalaxyCredentialsData, MultiCredentialService) {
let form = OrganizationForm(),
defaultUrl = GetBasePath('organizations'),
@ -29,6 +31,7 @@ export default ['$scope', '$location', '$stateParams', 'isOrgAdmin', 'isNotifica
});
$scope.instance_groups = InstanceGroupsData;
$scope.credentials = GalaxyCredentialsData;
const virtualEnvs = ConfigData.custom_virtualenvs || [];
$scope.custom_virtualenvs_visible = virtualEnvs.length > 1;
$scope.custom_virtualenvs_options = virtualEnvs.filter(
@ -100,7 +103,14 @@ export default ['$scope', '$location', '$stateParams', 'isOrgAdmin', 'isNotifica
Rest.setUrl(defaultUrl + id + '/');
Rest.put(params)
.then(() => {
InstanceGroupsService.editInstanceGroups(instance_group_url, $scope.instance_groups)
MultiCredentialService
.saveRelatedSequentially({
related: {
credentials: $scope.organization_obj.related.galaxy_credentials
}
}, $scope.credentials)
.then(() => {
InstanceGroupsService.editInstanceGroups(instance_group_url, $scope.instance_groups)
.then(() => {
Wait('stop');
$state.go($state.current, {}, { reload: true });
@ -111,6 +121,12 @@ export default ['$scope', '$location', '$stateParams', 'isOrgAdmin', 'isNotifica
msg: 'Failed to update instance groups. POST returned status: ' + status
});
});
}).catch(({data, status}) => {
ProcessErrors($scope, data, status, form, {
hdr: 'Error!',
msg: 'Failed to save Galaxy credentials. POST returned status: ' + status
});
});
$scope.organization_name = $scope.name;
main = params;
})

View File

@ -0,0 +1,123 @@
export default ['templateUrl', '$window', function(templateUrl, $window) {
return {
restrict: 'E',
scope: {
galaxyCredentials: '='
},
templateUrl: templateUrl('organizations/galaxy-credentials-multiselect/galaxy-credentials-modal/galaxy-credentials-modal'),
link: function(scope, element) {
$('#galaxy-credentials-modal').on('hidden.bs.modal', function () {
$('#galaxy-credentials-modal').off('hidden.bs.modal');
$(element).remove();
});
scope.showModal = function() {
$('#galaxy-credentials-modal').modal('show');
};
scope.destroyModal = function() {
$('#galaxy-credentials-modal').modal('hide');
};
},
controller: ['$scope', '$compile', 'QuerySet', 'GetBasePath','generateList', 'CredentialList', function($scope, $compile, qs, GetBasePath, GenerateList, CredentialList) {
function init() {
$scope.credential_queryset = {
order_by: 'name',
page_size: 5,
credential_type__kind: 'galaxy'
};
$scope.credential_default_params = {
order_by: 'name',
page_size: 5,
credential_type__kind: 'galaxy'
};
qs.search(GetBasePath('credentials'), $scope.credential_queryset)
.then(res => {
$scope.credential_dataset = res.data;
$scope.credentials = $scope.credential_dataset.results;
let credentialList = _.cloneDeep(CredentialList);
credentialList.listTitle = false;
credentialList.well = false;
credentialList.multiSelect = true;
credentialList.multiSelectPreview = {
selectedRows: 'igTags',
availableRows: 'credentials'
};
credentialList.fields.name.ngClick = "linkoutCredential(credential)";
credentialList.fields.name.columnClass = 'col-md-11 col-sm-11 col-xs-11';
delete credentialList.fields.consumed_capacity;
delete credentialList.fields.jobs_running;
let html = `${GenerateList.build({
list: credentialList,
input_type: 'galaxy-credentials-modal-body',
hideViewPerPage: true,
mode: 'lookup'
})}`;
$scope.list = credentialList;
$('#galaxy-credentials-modal-body').append($compile(html)($scope));
if ($scope.galaxyCredentials) {
$scope.galaxyCredentials = $scope.galaxyCredentials.map( (item) => {
item.isSelected = true;
if (!$scope.igTags) {
$scope.igTags = [];
}
$scope.igTags.push(item);
return item;
});
}
$scope.showModal();
});
$scope.$watch('credentials', function(){
angular.forEach($scope.credentials, function(credentialRow) {
angular.forEach($scope.igTags, function(selectedCredential){
if(selectedCredential.id === credentialRow.id) {
credentialRow.isSelected = true;
}
});
});
});
}
init();
$scope.$on("selectedOrDeselected", function(e, value) {
let item = value.value;
if (value.isSelected) {
if(!$scope.igTags) {
$scope.igTags = [];
}
$scope.igTags.push(item);
} else {
_.remove($scope.igTags, { id: item.id });
}
});
$scope.linkoutCredential = function(credential) {
$window.open('/#/credentials/' + credential.id,'_blank');
};
$scope.cancelForm = function() {
$scope.destroyModal();
};
$scope.saveForm = function() {
$scope.galaxyCredentials = $scope.igTags;
$scope.destroyModal();
};
}]
};
}];

View File

@ -0,0 +1,22 @@
<div id="galaxy-credentials-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>Select Galaxy Credentials</div>
<div class="Form-header--fields"></div>
<div class="Form-exitHolder">
<button aria-label="{{'Close'|translate}}" type="button" class="Form-exit" ng-click="cancelForm()">
<i class="fa fa-times-circle"></i>
</button>
</div>
</div>
<div class="modal-body">
<div id="galaxy-credentials-modal-body"> {{ credential }} </div>
</div>
<div class="modal-footer">
<button type="button" ng-click="cancelForm()" class="btn btn-default" translate>CANCEL</button>
<button type="button" ng-click="saveForm()" ng-disabled="!credentials || credentials.length === 0" class="Lookup-save btn btn-primary" translate>SAVE</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,14 @@
export default ['$scope',
function($scope) {
$scope.galaxyCredentialsTags = [];
$scope.$watch('galaxyCredentials', function() {
$scope.galaxyCredentialsTags = $scope.galaxyCredentials;
}, true);
$scope.deleteTag = function(tag){
_.remove($scope.galaxyCredentials, {id: tag.id});
};
}
];

View File

@ -0,0 +1,15 @@
#instance-groups-panel {
table {
overflow: hidden;
}
.List-header {
margin-bottom: 20px;
}
.isActive {
border-left: 10px solid @list-row-select-bord;
}
.instances-list,
.instance-jobs-list {
margin-top: 20px;
}
}

View File

@ -0,0 +1,19 @@
import galaxyCredentialsMultiselectController from './galaxy-credentials-multiselect.controller';
export default ['templateUrl', '$compile',
function(templateUrl, $compile) {
return {
scope: {
galaxyCredentials: '=',
fieldIsDisabled: '='
},
restrict: 'E',
templateUrl: templateUrl('organizations/galaxy-credentials-multiselect/galaxy-credentials'),
controller: galaxyCredentialsMultiselectController,
link: function(scope) {
scope.openInstanceGroupsModal = function() {
$('#content-container').append($compile('<galaxy-credentials-modal galaxy-credentials="galaxyCredentials"></galaxy-credentials-modal>')(scope));
};
}
};
}
];

View File

@ -0,0 +1,18 @@
<div class="input-group Form-mixedInputGroup">
<span class="input-group-btn input-group-prepend Form-variableHeightButtonGroup">
<button aria-label="{{'Open Galaxy credentials'|translate}}" type="button" class="Form-lookupButton Form-lookupButton--variableHeight btn btn-default" ng-click="openInstanceGroupsModal()"
ng-disabled="fieldIsDisabled">
<i class="fa fa-search"></i>
</button>
</span>
<span id="InstanceGroups" class="form-control Form-textInput Form-textInput--variableHeight input-medium lookup LabelList-lookupTags"
ng-disabled="fieldIsDisabled"
ng-class="{'LabelList-lookupTags--disabled' : fieldIsDisabled}">
<div ng-if="!fieldIsDisabled" class="LabelList-tagContainer" ng-repeat="tag in galaxyCredentialsTags">
<at-tag tag="tag.name" remove-tag="deleteTag(tag)"></at-tag>
</div>
<div ng-if="fieldIsDisabled" class="LabelList-tag" ng-repeat="tag in galaxyCredentialsTags">
<span class="LabelList-name">{{tag.name | sanitize}}</span>
</div>
</span>
</div>

View File

@ -12,8 +12,10 @@ import organizationsLinkout from './linkout/main';
import OrganizationsLinkoutStates from './linkout/organizations-linkout.route';
import OrganizationForm from './organizations.form';
import OrganizationList from './organizations.list';
import { N_ } from '../i18n';
import galaxyCredentialsMultiselect from './galaxy-credentials-multiselect/galaxy-credentials.directive';
import galaxyCredentialsModal from './galaxy-credentials-multiselect/galaxy-credentials-modal/galaxy-credentials-modal.directive';
import { N_ } from '../i18n';
export default
angular.module('Organizations', [
@ -24,6 +26,8 @@ angular.module('Organizations', [
.controller('OrganizationsEdit', OrganizationsEdit)
.factory('OrganizationForm', OrganizationForm)
.factory('OrganizationList', OrganizationList)
.directive('galaxyCredentialsMultiselect', galaxyCredentialsMultiselect)
.directive('galaxyCredentialsModal', galaxyCredentialsModal)
.config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider',
function($stateProvider, stateDefinitionsProvider, $stateExtenderProvider) {
let stateExtender = $stateExtenderProvider.$get(),
@ -81,6 +85,24 @@ angular.module('Organizations', [
});
});
}],
GalaxyCredentialsData: ['$stateParams', 'Rest', 'GetBasePath', 'ProcessErrors',
function($stateParams, Rest, GetBasePath, ProcessErrors){
let path = `${GetBasePath('organizations')}${$stateParams.organization_id}/galaxy_credentials/`;
Rest.setUrl(path);
return Rest.get()
.then(({data}) => {
if (data.results.length > 0) {
return data.results;
}
})
.catch(({data, status}) => {
ProcessErrors(null, data, status, null, {
hdr: 'Error!',
msg: 'Failed to get credentials. GET returned ' +
'status: ' + status
});
});
}],
InstanceGroupsData: ['$stateParams', 'Rest', 'GetBasePath', 'ProcessErrors',
function($stateParams, Rest, GetBasePath, ProcessErrors){
let path = `${GetBasePath('organizations')}${$stateParams.organization_id}/instance_groups/`;

View File

@ -55,6 +55,15 @@ export default ['NotificationsList', 'i18n',
ngDisabled: '!(organization_obj.summary_fields.user_capabilities.edit || canAdd)',
ngShow: 'custom_virtualenvs_visible'
},
credential: {
label: i18n._('Galaxy Credentials'),
type: 'custom',
awPopOver: "<p>" + i18n._("Select Galaxy credentials. The selection order sets precedence for the sync and lookup of the content") + "</p>",
dataTitle: i18n._('Galaxy Credentials'),
dataContainer: 'body',
dataPlacement: 'right',
control: '<galaxy-credentials-multiselect galaxy-credentials="credentials" field-is-disabled="!(organization_obj.summary_fields.user_capabilities.edit || canAdd) || (!current_user.is_superuser && isOrgAdmin)"></galaxy-credentials-multiselect>',
},
max_hosts: {
label: i18n._('Max Hosts'),
type: 'number',
@ -69,7 +78,7 @@ export default ['NotificationsList', 'i18n',
awPopOver: "<p>" + i18n._("The maximum number of hosts allowed to be managed by this organization. Value defaults to 0 which means no limit. Refer to the Ansible documentation for more details.") + "</p>",
ngDisabled: '!current_user.is_superuser',
ngShow: 'BRAND_NAME === "Tower"'
}
},
},
buttons: { //for now always generates <button> tags

View File

@ -46,6 +46,52 @@ function MultiCredentialService (Rest, ProcessErrors, $q, GetBasePath) {
});
};
this.saveRelatedSequentially = ({ related }, credentials) => {
Rest.setUrl(related.credentials);
return Rest
.get()
.then(res => {
const { data: { results = [] } } = res;
const updatedCredentialIds = (credentials || []).map(({ id }) => id);
const currentCredentialIds = results.map(({ id }) => id);
const credentialIdsToAssociate = [];
const credentialIdsToDisassociate = [];
let disassociateRemainingIds = false;
currentCredentialIds.forEach((currentId, position) => {
if (!disassociateRemainingIds && updatedCredentialIds[position] !== currentId) {
disassociateRemainingIds = true;
}
if (disassociateRemainingIds) {
credentialIdsToDisassociate.push(currentId);
}
});
updatedCredentialIds.forEach(updatedId => {
if (credentialIdsToDisassociate.includes(updatedId)) {
credentialIdsToAssociate.push(updatedId);
} else if (!currentCredentialIds.includes(updatedId)) {
credentialIdsToAssociate.push(updatedId);
}
});
let disassociationPromise = Promise.resolve();
credentialIdsToDisassociate.forEach(id => {
disassociationPromise = disassociationPromise.then(() => disassociate({ related }, id));
});
return disassociationPromise
.then(() => {
let associationPromise = Promise.resolve();
credentialIdsToAssociate.forEach(id => {
associationPromise = associationPromise.then(() => associate({ related }, id));
});
return associationPromise;
});
});
};
this.getRelated = ({ related }, params = { permitted: [] }) => {
Rest.setUrl(related.credentials);
return Rest