From 88e5abd4d9f286cd2fa0c5728d8143a5cd693109 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 11 May 2017 15:57:35 -0400 Subject: [PATCH] Fixed host list related groups tab. Can now associate and disassociate a host from a group --- .../groups/list/groups-list.controller.js | 5 +- .../nested-groups-list.controller.js | 5 +- .../host-groups-associate.block.less | 14 +++ .../host-groups-associate.controller.js | 111 ++++++++++++++++++ .../host-groups-associate.directive.js | 58 +++++++++ .../host-groups-associate.partial.html | 21 ++++ .../host-groups-associate.route.js | 46 ++++++++ .../host-groups/host-groups.controller.js | 84 +++++++++++++ .../host-groups/host-groups.list.js | 69 +++++++++++ .../host-groups/host-groups.partial.html | 31 +++++ .../host-groups/host-groups.route.js | 62 ++++++++++ .../src/inventories/host-groups/main.js | 15 +++ .../client/src/inventories/hosts/host.form.js | 11 +- awx/ui/client/src/inventories/main.js | 17 ++- 14 files changed, 532 insertions(+), 17 deletions(-) create mode 100644 awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.block.less create mode 100644 awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.controller.js create mode 100644 awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.directive.js create mode 100644 awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.partial.html create mode 100644 awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.route.js create mode 100644 awx/ui/client/src/inventories/host-groups/host-groups.controller.js create mode 100644 awx/ui/client/src/inventories/host-groups/host-groups.list.js create mode 100644 awx/ui/client/src/inventories/host-groups/host-groups.partial.html create mode 100644 awx/ui/client/src/inventories/host-groups/host-groups.route.js create mode 100644 awx/ui/client/src/inventories/host-groups/main.js diff --git a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js index 01995f48bb..35a6549289 100644 --- a/awx/ui/client/src/inventories/groups/list/groups-list.controller.js +++ b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js @@ -36,7 +36,10 @@ } $scope.inventory_id = $stateParams.inventory_id; - _.forEach($scope[list.name], buildStatusIndicators); + + $scope.$watchCollection(list.name, function(){ + _.forEach($scope[list.name], buildStatusIndicators); + }); $scope.$on('selectedOrDeselected', function(e, value) { let item = value.value; diff --git a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js index a8d5492e5b..d25499826f 100644 --- a/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js +++ b/awx/ui/client/src/inventories/groups/nested-groups/nested-groups-list.controller.js @@ -36,7 +36,10 @@ } $scope.inventory_id = $stateParams.inventory_id; - _.forEach($scope[list.name], buildStatusIndicators); + + $scope.$watchCollection(list.name, function(){ + _.forEach($scope[list.name], buildStatusIndicators); + }); } diff --git a/awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.block.less b/awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.block.less new file mode 100644 index 0000000000..b450855d67 --- /dev/null +++ b/awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.block.less @@ -0,0 +1,14 @@ +@import "../../../shared/branding/colors.default.less"; + +.HostGroupsAssociate-modalBody { + padding-top: 0px; +} +.HostGroupsAssociate-backDrop { + width: 100vw; + height: 100vh; + position: fixed; + top: 0; + left: 0; + opacity: 0; + transition: 0.5s opacity; +} diff --git a/awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.controller.js b/awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.controller.js new file mode 100644 index 0000000000..3f662b5145 --- /dev/null +++ b/awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.controller.js @@ -0,0 +1,111 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + export default ['$scope', '$rootScope', 'ProcessErrors', 'GetBasePath', 'generateList', + '$state', 'Rest', '$q', 'Wait', '$window', 'QuerySet', 'GroupList', 'HostManageService', + function($scope, $rootScope, ProcessErrors, GetBasePath, generateList, + $state, Rest, $q, Wait, $window, qs, GroupList, HostManageService) { + $scope.$on("linkLists", function() { + + init(); + + function init(){ + $scope.associate_group_default_params = { + order_by: 'name', + page_size: 5 + }; + + $scope.associate_group_queryset = { + order_by: 'name', + page_size: 5 + }; + + let list = _.cloneDeep(GroupList); + list.basePath = GetBasePath('inventory') + $state.params.inventory_id + '/groups'; + list.iterator = 'associate_group'; + list.name = 'associate_groups'; + list.multiSelect = true; + list.fields.name.ngClick = 'linkoutHostGroup(associate_group.id)'; + list.trackBy = 'associate_group.id'; + delete list.actions; + delete list.fieldActions; + delete list.fields.failed_hosts; + list.well = false; + $scope.list = list; + + // Fire off the initial search + qs.search(list.basePath, $scope.associate_group_default_params) + .then(function(res) { + $scope.associate_group_dataset = res.data; + $scope.associate_groups = $scope.associate_group_dataset.results; + + let html = generateList.build({ + list: list, + mode: 'edit', + title: false + }); + + $scope.compileList(html); + + $scope.$watchCollection('associate_groups', function () { + if($scope.selectedItems) { + $scope.associate_groups.forEach(function(row, i) { + if (_.includes($scope.selectedItems, row.id)) { + $scope.associate_groups[i].isSelected = true; + } + }); + } + }); + + }); + + $scope.selectedItems = []; + $scope.$on('selectedOrDeselected', function(e, value) { + let item = value.value; + + if (value.isSelected) { + $scope.selectedItems.push(item.id); + } + else { + // _.remove() Returns the new array of removed elements. + // This will pull all the values out of the array that don't + // match the deselected item effectively removing it + $scope.selectedItems = _.remove($scope.selectedItems, function(selectedItem) { + return selectedItem !== item.id; + }); + } + }); + } + + $scope.linkHostGroup = function() { + // HostManageService.associateGroup(host, $scope.disassociateGroup.id).then(() => { + // $state.go($state.current, null, {reload: true}); + // $('#host-disassociate-modal').modal('hide'); + // $('body').removeClass('modal-open'); + // $('.modal-backdrop').remove(); + // }); + + $q.all( _.map($scope.selectedItems, (id) => HostManageService.associateGroup({id: $state.params.host_id}, id)) ) + .then( () =>{ + Wait('stop'); + $scope.closeModal(); + }, (error) => { + $scope.closeModal(); + ProcessErrors(null, error.data, error.status, null, { + hdr: 'Error!', + msg: 'Failed to associate host to group(s): POST returned status' + + error.status + }); + }); + }; + + $scope.linkoutHostGroup = function(userId) { + // Open the edit user form in a new tab so as not to navigate the user + // away from the modal + $window.open('/#/users/' + userId,'_blank'); + }; + }); + }]; diff --git a/awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.directive.js b/awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.directive.js new file mode 100644 index 0000000000..749dcd5343 --- /dev/null +++ b/awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.directive.js @@ -0,0 +1,58 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ +import controller from './host-groups-associate.controller'; + +/* jshint unused: vars */ +export default ['templateUrl', 'Wait', '$compile', '$state', + function(templateUrl, Wait, $compile, $state) { + return { + restrict: 'E', + transclude: true, + scope: false, + controller: controller, + templateUrl: templateUrl('inventories/host-groups/host-groups-associate/host-groups-associate'), + link: function(scope, element, attrs, controller, transcludefn) { + + $("body").addClass("is-modalOpen"); + + //$("body").append(element); + + Wait('start'); + + scope.$broadcast("linkLists"); + + setTimeout(function() { + $('#host-groups-associate-modal').modal("show"); + }, 200); + + $('.modal[aria-hidden=false]').each(function () { + if ($(this).attr('id') !== 'host-groups-associate-modal') { + $(this).modal('hide'); + } + }); + + scope.closeModal = function() { + $("body").removeClass("is-modalOpen"); + $('#host-groups-associate-modal').on('hidden.bs.modal', + function () { + $('.AddUsers').remove(); + }); + $('#host-groups-associate-modal').modal('hide'); + + $state.go('^', null, {reload: true}); + }; + + scope.compileList = function(html) { + $('#host-groups-associate-list').append($compile(html)(scope)); + }; + + Wait('stop'); + + window.scrollTo(0,0); + } + }; + } +]; diff --git a/awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.partial.html b/awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.partial.html new file mode 100644 index 0000000000..8267b4fd21 --- /dev/null +++ b/awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.partial.html @@ -0,0 +1,21 @@ + diff --git a/awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.route.js b/awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.route.js new file mode 100644 index 0000000000..0ff657bb83 --- /dev/null +++ b/awx/ui/client/src/inventories/host-groups/host-groups-associate/host-groups-associate.route.js @@ -0,0 +1,46 @@ +export default { + name: 'hosts.edit.groups.associate', + squashSearchUrl: true, + url: '/associate?inventory_id', + params: { + associate_group_search: { + value: {order_by: 'name', page_size: '5', role_level: 'admin_role'}, + dynamic: true + } + }, + ncyBreadcrumb:{ + skip:true + }, + views: { + [`modal@hosts.edit`]: { + templateProvider: function(ListDefinition, generateList) { + + let list_html = generateList.build({ + mode: 'lookup', + list: ListDefinition, + input_type: 'radio' + }); + + return `${list_html}`; + } + } + }, + resolve: { + ListDefinition: ['GroupList', (GroupList) => { + return GroupList; + }], + groupsDataset: ['QuerySet', '$stateParams', 'GetBasePath', + function(qs, $stateParams, GetBasePath) { + let path = GetBasePath('groups'); + return qs.search(path, $stateParams.associate_group_search); + } + ] + }, + onExit: function($state) { + if ($state.transition) { + $('#add-permissions-modal').modal('hide'); + $('.modal-backdrop').remove(); + $('body').removeClass('modal-open'); + } + }, +}; diff --git a/awx/ui/client/src/inventories/host-groups/host-groups.controller.js b/awx/ui/client/src/inventories/host-groups/host-groups.controller.js new file mode 100644 index 0000000000..31fe1d236d --- /dev/null +++ b/awx/ui/client/src/inventories/host-groups/host-groups.controller.js @@ -0,0 +1,84 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + export default + ['$scope', '$rootScope', '$state', '$stateParams', 'HostGroupsList', 'InventoryUpdate', + 'HostManageService', 'CancelSourceUpdate', 'rbacUiControlService', 'GetBasePath', + 'GetHostsStatusMsg', 'Dataset', 'Find', 'QuerySet', 'inventoryData', 'host', + function($scope, $rootScope, $state, $stateParams, HostGroupsList, InventoryUpdate, + HostManageService, CancelSourceUpdate, rbacUiControlService, GetBasePath, + GetHostsStatusMsg, Dataset, Find, qs, inventoryData, host){ + + let list = HostGroupsList; + + init(); + + function init(){ + $scope.inventory_id = inventoryData.id; + $scope.canAdd = false; + + rbacUiControlService.canAdd(GetBasePath('inventory') + $scope.inventory_id + "/groups") + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + + // Search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + + $scope.$watchCollection(list.name, function(){ + _.forEach($scope[list.name], buildStatusIndicators); + }); + } + + function buildStatusIndicators(group){ + if (group === undefined || group === null) { + group = {}; + } + + let hosts_status; + + hosts_status = GetHostsStatusMsg({ + active_failures: group.hosts_with_active_failures, + total_hosts: group.total_hosts, + inventory_id: $scope.inventory_id, + group_id: group.id + }); + _.assign(group, + {hosts_status_tip: hosts_status.tooltip}, + {hosts_status_class: hosts_status.class}); + } + + $scope.editGroup = function(id){ + $state.go('inventories.edit.groups.edit', {inventory_id: $scope.inventory_id, group_id: id}); + }; + + $scope.associateGroup = function() { + $state.go('.associate', {inventory_id: $scope.inventory_id}); + }; + + $scope.disassociateHost = function(group){ + $scope.disassociateGroup = {}; + angular.extend($scope.disassociateGroup, group); + $('#host-disassociate-modal').modal('show'); + }; + + $scope.confirmDisassociate = function(){ + + $('#host-disassociate-modal').off('hidden.bs.modal').on('hidden.bs.modal', function () { + $('#host-disassociate-modal').off('hidden.bs.modal'); + $state.go('.', null, {reload: true}); + }); + + HostManageService.disassociateGroup(host, $scope.disassociateGroup.id).then(() => { + $state.go($state.current, null, {reload: true}); + $('#host-disassociate-modal').modal('hide'); + $('body').removeClass('modal-open'); + $('.modal-backdrop').remove(); + }); + + }; + }]; diff --git a/awx/ui/client/src/inventories/host-groups/host-groups.list.js b/awx/ui/client/src/inventories/host-groups/host-groups.list.js new file mode 100644 index 0000000000..5bb9c85ee3 --- /dev/null +++ b/awx/ui/client/src/inventories/host-groups/host-groups.list.js @@ -0,0 +1,69 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default { + name: 'groups', + iterator: 'group', + editTitle: '{{ host.name }}', + well: true, + wellOverride: true, + index: false, + hover: true, + trackBy: 'group.id', + basePath: 'api/v2/hosts/{{$stateParams.host_id}}/groups/', + + fields: { + failed_hosts: { + label: '', + nosort: true, + mode: 'all', + iconOnly: true, + awToolTip: "{{ group.hosts_status_tip }}", + dataPlacement: "top", + icon: "{{ 'fa icon-job-' + group.hosts_status_class }}", + columnClass: 'status-column List-staticColumn--smallStatus' + }, + name: { + label: 'Groups', + key: true, + ngClick: "editGroup(group.id)", + columnClass: 'col-lg-6 col-md-6 col-sm-6 col-xs-6', + class: 'InventoryManage-breakWord', + } + }, + + actions: { + refresh: { + mode: 'all', + awToolTip: "Refresh the page", + ngClick: "refreshGroups()", + ngShow: "socketStatus == 'error'", + actionClass: 'btn List-buttonDefault', + buttonContent: 'REFRESH' + }, + associate: { + mode: 'all', + ngClick: "associateGroup()", + awToolTip: "Associate this host with a new group", + actionClass: 'btn List-buttonSubmit', + buttonContent: '+ ADD GROUP', + ngShow: 'canAdd', + dataPlacement: "top", + } + }, + + fieldActions: { + columnClass: 'col-lg-6 col-md-6 col-sm-6 col-xs-6 text-right', + "delete": { + //label: 'Delete', + mode: 'all', + ngClick: "disassociateHost(group)", + awToolTip: 'Disassociate host', + dataPlacement: "top", + ngShow: "group.summary_fields.user_capabilities.delete" + } + } +}; diff --git a/awx/ui/client/src/inventories/host-groups/host-groups.partial.html b/awx/ui/client/src/inventories/host-groups/host-groups.partial.html new file mode 100644 index 0000000000..81d3a90e85 --- /dev/null +++ b/awx/ui/client/src/inventories/host-groups/host-groups.partial.html @@ -0,0 +1,31 @@ + diff --git a/awx/ui/client/src/inventories/host-groups/host-groups.route.js b/awx/ui/client/src/inventories/host-groups/host-groups.route.js new file mode 100644 index 0000000000..b367ee7d37 --- /dev/null +++ b/awx/ui/client/src/inventories/host-groups/host-groups.route.js @@ -0,0 +1,62 @@ +import { N_ } from '../../i18n'; +import {templateUrl} from '../../shared/template-url/template-url.factory'; + +export default { + name: "hosts.edit.groups", + url: "/groups?{group_search:queryset}", + resolve: { + Dataset: ['HostGroupsList', 'QuerySet', '$stateParams', 'GetBasePath', '$interpolate', '$rootScope', + (list, qs, $stateParams, GetBasePath, $interpolate, $rootScope) => { + // allow related list definitions to use interpolated $rootScope / $stateParams in basePath field + let path, interpolator; + if (GetBasePath(list.basePath)) { + path = GetBasePath(list.basePath); + } else { + interpolator = $interpolate(list.basePath); + path = interpolator({ $rootScope: $rootScope, $stateParams: $stateParams }); + } + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ], + host: ['$stateParams', 'HostManageService', function($stateParams, HostManageService) { + if($stateParams.host_id){ + return HostManageService.get({ id: $stateParams.host_id }).then(function(res) { + return res.data.results[0]; + }); + } + }], + inventoryData: ['InventoryManageService', '$stateParams', 'host', function(InventoryManageService, $stateParams, host) { + var id = ($stateParams.inventory_id) ? $stateParams.inventory_id : host.summary_fields.inventory.id; + return InventoryManageService.getInventory(id).then(res => res.data); + }] + }, + params: { + group_search: { + value: { + page_size: "20", + order_by: "name" + }, + dynamic: true, + squash: "" + } + }, + ncyBreadcrumb: { + parent: "hosts.edit", + label: N_("GROUPS") + }, + views: { + 'related': { + templateProvider: function(HostGroupsList, generateList, $templateRequest) { + let html = generateList.build({ + list: HostGroupsList, + mode: 'edit' + }); + + return $templateRequest(templateUrl('inventories/host-groups/host-groups')).then((template) => { + return html.concat(template); + }); + }, + controller: 'HostGroupsController' + } + } +}; diff --git a/awx/ui/client/src/inventories/host-groups/main.js b/awx/ui/client/src/inventories/host-groups/main.js new file mode 100644 index 0000000000..1e8939d02e --- /dev/null +++ b/awx/ui/client/src/inventories/host-groups/main.js @@ -0,0 +1,15 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import controller from './host-groups.controller'; +import hostGroupsDefinition from './host-groups.list'; +import hostGroupsAssociate from './host-groups-associate/host-groups-associate.directive'; + +export default + angular.module('hostGroups', []) + .value('HostGroupsList', hostGroupsDefinition) + .directive('hostGroupsAssociate', hostGroupsAssociate) + .controller('HostGroupsController', controller); diff --git a/awx/ui/client/src/inventories/hosts/host.form.js b/awx/ui/client/src/inventories/hosts/host.form.js index 474121457a..6f45b7b338 100644 --- a/awx/ui/client/src/inventories/hosts/host.form.js +++ b/awx/ui/client/src/inventories/hosts/host.form.js @@ -111,15 +111,14 @@ function(i18n) { title: i18n._('Facts'), skipGenerator: true }, - nested_groups: { - name: 'nested_groups', + groups: { + name: 'groups', awToolTip: i18n._('Please save before defining groups'), dataPlacement: 'top', - ngClick: "$state.go('hosts.edit.nested_groups')", - include: "NestedGroupListDefinition", - includeForm: "NestedGroupFormDefinition", + ngClick: "$state.go('hosts.edit.groups')", title: i18n._('Groups'), - iterator: 'nested_group' + iterator: 'group', + skipGenerator: true }, insights: { name: 'insights', diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js index e3362436c7..7b49c710b1 100644 --- a/awx/ui/client/src/inventories/main.js +++ b/awx/ui/client/src/inventories/main.js @@ -13,6 +13,7 @@ import inventoryCompletedJobs from './completed_jobs/main'; import inventoryAdd from './add/main'; import inventoryEdit from './edit/main'; import inventoryList from './list/main'; +import hostGroups from './host-groups/main'; import { templateUrl } from '../shared/template-url/template-url.factory'; import { N_ } from '../i18n'; import InventoryList from './inventory.list'; @@ -45,6 +46,8 @@ import nestedHostsAdd from './groups/nested-hosts/nested-hosts-add.route'; import nestedHostsEdit from './groups/nested-hosts/nested-hosts-edit.route'; import ansibleFactsRoute from './ansible_facts/ansible_facts.route'; import insightsRoute from './insights/insights.route'; +import hostGroupsRoute from './host-groups/host-groups.route'; +import hostGroupsAssociateRoute from './host-groups/host-groups-associate/host-groups-associate.route'; export default angular.module('inventory', [ @@ -59,7 +62,8 @@ angular.module('inventory', [ inventoryList.name, ansibleFacts.name, insights.name, - copyMove.name + copyMove.name, + hostGroups.name ]) .factory('InventoryForm', InventoryForm) .factory('InventoryList', InventoryList) @@ -245,13 +249,6 @@ angular.module('inventory', [ let hostInsights = _.cloneDeep(insightsRoute); hostInsights.name = 'hosts.edit.insights'; - let hostsEditGroups = _.cloneDeep(nestedGroups); - hostsEditGroups.name = "hosts.edit.nested_groups"; - hostsEditGroups.ncyBreadcrumb = { - parent: "hosts.edit", - label: "ASSOCIATED GROUPS" - }; - return Promise.all([ hostTree ]).then((generated) => { @@ -260,7 +257,9 @@ angular.module('inventory', [ return result.concat(definition.states); }, [ stateExtender.buildDefinition(hostAnsibleFacts), - stateExtender.buildDefinition(hostInsights) + stateExtender.buildDefinition(hostInsights), + stateExtender.buildDefinition(hostGroupsRoute), + stateExtender.buildDefinition(hostGroupsAssociateRoute) ]) }; });