diff --git a/awx/ui/client/src/organizations/add/organizations-add.controller.js b/awx/ui/client/src/organizations/add/organizations-add.controller.js index cd6aea6cb9..69fdfce1db 100644 --- a/awx/ui/client/src/organizations/add/organizations-add.controller.js +++ b/awx/ui/client/src/organizations/add/organizations-add.controller.js @@ -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] : ""; diff --git a/awx/ui/client/src/organizations/edit/organizations-edit.controller.js b/awx/ui/client/src/organizations/edit/organizations-edit.controller.js index 74397b02cc..9ffe8e859b 100644 --- a/awx/ui/client/src/organizations/edit/organizations-edit.controller.js +++ b/awx/ui/client/src/organizations/edit/organizations-edit.controller.js @@ -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; }) diff --git a/awx/ui/client/src/organizations/galaxy-credentials-multiselect/galaxy-credentials-modal/galaxy-credentials-modal.directive.js b/awx/ui/client/src/organizations/galaxy-credentials-multiselect/galaxy-credentials-modal/galaxy-credentials-modal.directive.js new file mode 100644 index 0000000000..145a835d88 --- /dev/null +++ b/awx/ui/client/src/organizations/galaxy-credentials-multiselect/galaxy-credentials-modal/galaxy-credentials-modal.directive.js @@ -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(); + }; + }] + }; +}]; diff --git a/awx/ui/client/src/organizations/galaxy-credentials-multiselect/galaxy-credentials-modal/galaxy-credentials-modal.partial.html b/awx/ui/client/src/organizations/galaxy-credentials-multiselect/galaxy-credentials-modal/galaxy-credentials-modal.partial.html new file mode 100644 index 0000000000..dbf481005e --- /dev/null +++ b/awx/ui/client/src/organizations/galaxy-credentials-multiselect/galaxy-credentials-modal/galaxy-credentials-modal.partial.html @@ -0,0 +1,22 @@ + diff --git a/awx/ui/client/src/organizations/galaxy-credentials-multiselect/galaxy-credentials-multiselect.controller.js b/awx/ui/client/src/organizations/galaxy-credentials-multiselect/galaxy-credentials-multiselect.controller.js new file mode 100644 index 0000000000..548f528798 --- /dev/null +++ b/awx/ui/client/src/organizations/galaxy-credentials-multiselect/galaxy-credentials-multiselect.controller.js @@ -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}); + }; + } +]; \ No newline at end of file diff --git a/awx/ui/client/src/organizations/galaxy-credentials-multiselect/galaxy-credentials.block.less b/awx/ui/client/src/organizations/galaxy-credentials-multiselect/galaxy-credentials.block.less new file mode 100644 index 0000000000..bbfef9de99 --- /dev/null +++ b/awx/ui/client/src/organizations/galaxy-credentials-multiselect/galaxy-credentials.block.less @@ -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; + } +} diff --git a/awx/ui/client/src/organizations/galaxy-credentials-multiselect/galaxy-credentials.directive.js b/awx/ui/client/src/organizations/galaxy-credentials-multiselect/galaxy-credentials.directive.js new file mode 100644 index 0000000000..d966c5e519 --- /dev/null +++ b/awx/ui/client/src/organizations/galaxy-credentials-multiselect/galaxy-credentials.directive.js @@ -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('')(scope)); + }; + } + }; + } +]; diff --git a/awx/ui/client/src/organizations/galaxy-credentials-multiselect/galaxy-credentials.partial.html b/awx/ui/client/src/organizations/galaxy-credentials-multiselect/galaxy-credentials.partial.html new file mode 100644 index 0000000000..09202327b4 --- /dev/null +++ b/awx/ui/client/src/organizations/galaxy-credentials-multiselect/galaxy-credentials.partial.html @@ -0,0 +1,18 @@ +
+ + + + +
+ +
+
+ {{tag.name | sanitize}} +
+
+
diff --git a/awx/ui/client/src/organizations/main.js b/awx/ui/client/src/organizations/main.js index 8540261ede..a831c13095 100644 --- a/awx/ui/client/src/organizations/main.js +++ b/awx/ui/client/src/organizations/main.js @@ -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/`; diff --git a/awx/ui/client/src/organizations/organizations.form.js b/awx/ui/client/src/organizations/organizations.form.js index 19f6e87419..712e6be9ba 100644 --- a/awx/ui/client/src/organizations/organizations.form.js +++ b/awx/ui/client/src/organizations/organizations.form.js @@ -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: "

" + i18n._("Select Galaxy credentials. The selection order sets precedence for the sync and lookup of the content") + "

", + dataTitle: i18n._('Galaxy Credentials'), + dataContainer: 'body', + dataPlacement: 'right', + control: '', + }, max_hosts: { label: i18n._('Max Hosts'), type: 'number', @@ -69,7 +78,7 @@ export default ['NotificationsList', 'i18n', awPopOver: "

" + 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.") + "

", ngDisabled: '!current_user.is_superuser', ngShow: 'BRAND_NAME === "Tower"' - } + }, }, buttons: { //for now always generates