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 @@
+
+
+
+
+
+
+
Are you sure you want to disassociate this host: {{host.name}} from this group: {{disassociateGroup.name}}?
+
+
+
+
+
+
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)
])
};
});