diff --git a/awx/ui/client/src/inventories/groups/factories/get-hosts-status-msg.factory.js b/awx/ui/client/src/inventories/groups/factories/get-hosts-status-msg.factory.js
new file mode 100644
index 0000000000..19a846c414
--- /dev/null
+++ b/awx/ui/client/src/inventories/groups/factories/get-hosts-status-msg.factory.js
@@ -0,0 +1,33 @@
+export default
+ function GetHostsStatusMsg() {
+ return function(params) {
+ var active_failures = params.active_failures,
+ total_hosts = params.total_hosts,
+ tip, failures, html_class;
+
+ // Return values for use on host status indicator
+
+ if (active_failures > 0) {
+ tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. ' + active_failures + ' with failed jobs.';
+ html_class = 'error';
+ failures = true;
+ } else {
+ failures = false;
+ if (total_hosts === 0) {
+ // no hosts
+ tip = "Contains 0 hosts.";
+ html_class = 'none';
+ } else {
+ // many hosts with 0 failures
+ tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. No job failures';
+ html_class = 'success';
+ }
+ }
+
+ return {
+ tooltip: tip,
+ failures: failures,
+ 'class': html_class
+ };
+ };
+ }
diff --git a/awx/ui/client/src/inventories/groups/factories/get-source-type-options.factory.js b/awx/ui/client/src/inventories/groups/factories/get-source-type-options.factory.js
new file mode 100644
index 0000000000..befef8a499
--- /dev/null
+++ b/awx/ui/client/src/inventories/groups/factories/get-source-type-options.factory.js
@@ -0,0 +1,37 @@
+export default
+ function GetSourceTypeOptions(Rest, ProcessErrors, GetBasePath) {
+ return function(params) {
+ var scope = params.scope,
+ variable = params.variable;
+
+ if (scope[variable] === undefined) {
+ scope[variable] = [];
+ Rest.setUrl(GetBasePath('inventory_sources'));
+ Rest.options()
+ .success(function (data) {
+ var i, choices = data.actions.GET.source.choices;
+ for (i = 0; i < choices.length; i++) {
+ if (choices[i][0] !== 'file') {
+ scope[variable].push({
+ label: choices[i][1],
+ value: choices[i][0]
+ });
+ }
+ }
+ scope.cloudCredentialRequired = false;
+ scope.$emit('sourceTypeOptionsReady');
+ })
+ .error(function (data, status) {
+ ProcessErrors(scope, data, status, null, { hdr: 'Error!',
+ msg: 'Failed to retrieve options for inventory_sources.source. OPTIONS status: ' + status
+ });
+ });
+ }
+ };
+ }
+
+GetSourceTypeOptions.$inject =
+ [ 'Rest',
+ 'ProcessErrors',
+ 'GetBasePath'
+ ];
diff --git a/awx/ui/client/src/inventories/groups/factories/get-sync-status-msg.factory.js b/awx/ui/client/src/inventories/groups/factories/get-sync-status-msg.factory.js
new file mode 100644
index 0000000000..2541abcc27
--- /dev/null
+++ b/awx/ui/client/src/inventories/groups/factories/get-sync-status-msg.factory.js
@@ -0,0 +1,77 @@
+export default
+ function GetSyncStatusMsg(Empty) {
+ return function(params) {
+ var status = params.status,
+ source = params.source,
+ has_inventory_sources = params.has_inventory_sources,
+ launch_class = '',
+ launch_tip = 'Start sync process',
+ schedule_tip = 'Schedule future inventory syncs',
+ stat, stat_class, status_tip;
+
+ stat = status;
+ stat_class = stat;
+
+ switch (status) {
+ case 'never updated':
+ stat = 'never';
+ stat_class = 'na';
+ status_tip = 'Sync not performed. Click to start it now.';
+ break;
+ case 'none':
+ case 'ok':
+ case '':
+ launch_class = 'btn-disabled';
+ stat = 'n/a';
+ stat_class = 'na';
+ status_tip = 'Cloud source not configured. Click to update.';
+ launch_tip = 'Cloud source not configured.';
+ break;
+ case 'canceled':
+ status_tip = 'Sync canceled. Click to view log.';
+ break;
+ case 'failed':
+ status_tip = 'Sync failed. Click to view log.';
+ break;
+ case 'successful':
+ status_tip = 'Sync completed. Click to view log.';
+ break;
+ case 'pending':
+ status_tip = 'Sync pending.';
+ launch_class = "btn-disabled";
+ launch_tip = "Sync pending";
+ break;
+ case 'updating':
+ case 'running':
+ launch_class = "btn-disabled";
+ launch_tip = "Sync running";
+ status_tip = "Sync running. Click to view log.";
+ break;
+ }
+
+ if (has_inventory_sources && Empty(source)) {
+ // parent has a source, therefore this group should not have a source
+ launch_class = "btn-disabled";
+ status_tip = 'Managed by an external cloud source.';
+ launch_tip = 'Can only be updated by running a sync on the parent group.';
+ }
+
+ if (has_inventory_sources === false && Empty(source)) {
+ launch_class = 'btn-disabled';
+ status_tip = 'Cloud source not configured. Click to update.';
+ launch_tip = 'Cloud source not configured.';
+ }
+
+ return {
+ "class": stat_class,
+ "tooltip": status_tip,
+ "status": stat,
+ "launch_class": launch_class,
+ "launch_tip": launch_tip,
+ "schedule_tip": schedule_tip
+ };
+ };
+ }
+
+GetSyncStatusMsg.$inject =
+ [ 'Empty' ];
diff --git a/awx/ui/client/src/inventories/groups/factories/groups-cancel-update.factory.js b/awx/ui/client/src/inventories/groups/factories/groups-cancel-update.factory.js
new file mode 100644
index 0000000000..1447d0aa1c
--- /dev/null
+++ b/awx/ui/client/src/inventories/groups/factories/groups-cancel-update.factory.js
@@ -0,0 +1,81 @@
+export default
+ function GroupsCancelUpdate(Empty, Rest, ProcessErrors, Alert, Wait, Find) {
+ return function(params) {
+ var scope = params.scope,
+ id = params.id,
+ group = params.group;
+
+ if (scope.removeCancelUpdate) {
+ scope.removeCancelUpdate();
+ }
+ scope.removeCancelUpdate = scope.$on('CancelUpdate', function (e, url) {
+ // Cancel the update process
+ Rest.setUrl(url);
+ Rest.post()
+ .success(function () {
+ Wait('stop');
+ //Alert('Inventory Sync Cancelled', 'Request to cancel the sync process was submitted to the task manger. ' +
+ // 'Click the button to monitor the status.', 'alert-info');
+ })
+ .error(function (data, status) {
+ ProcessErrors(scope, data, status, null, { hdr: 'Error!',
+ msg: 'Call to ' + url + ' failed. POST status: ' + status
+ });
+ });
+ });
+
+ if (scope.removeCheckCancel) {
+ scope.removeCheckCancel();
+ }
+ scope.removeCheckCancel = scope.$on('CheckCancel', function (e, last_update, current_update) {
+ // Check that we have access to cancelling an update
+ var url = (current_update) ? current_update : last_update;
+ url += 'cancel/';
+ Rest.setUrl(url);
+ Rest.get()
+ .success(function (data) {
+ if (data.can_cancel) {
+ scope.$emit('CancelUpdate', url);
+ //} else {
+ // Wait('stop');
+ // Alert('Cancel Inventory Sync', 'The sync process completed. Click the button to view ' +
+ // 'the latest status.', 'alert-info');
+ }
+ else {
+ Wait('stop');
+ }
+ })
+ .error(function (data, status) {
+ ProcessErrors(scope, data, status, null, { hdr: 'Error!',
+ msg: 'Call to ' + url + ' failed. GET status: ' + status
+ });
+ });
+ });
+
+ // Cancel the update process
+ if (Empty(group)) {
+ group = Find({ list: scope.groups, key: 'id', val: id });
+ scope.selected_group_id = group.id;
+ }
+
+ if (group && (group.status === 'running' || group.status === 'pending')) {
+ // We found the group, and there is a running update
+ Wait('start');
+ Rest.setUrl(group.related.inventory_source);
+ Rest.get()
+ .success(function (data) {
+ scope.$emit('CheckCancel', data.related.last_update, data.related.current_update);
+ })
+ .error(function (data, status) {
+ ProcessErrors(scope, data, status, null, { hdr: 'Error!',
+ msg: 'Call to ' + group.related.inventory_source + ' failed. GET status: ' + status
+ });
+ });
+ }
+ };
+ }
+
+GroupsCancelUpdate.$inject =
+ [ 'Empty', 'Rest', 'ProcessErrors',
+ 'Alert', 'Wait', 'Find'
+ ];
diff --git a/awx/ui/client/src/inventories/groups/factories/view-update-status.factory.js b/awx/ui/client/src/inventories/groups/factories/view-update-status.factory.js
new file mode 100644
index 0000000000..1f3280b51c
--- /dev/null
+++ b/awx/ui/client/src/inventories/groups/factories/view-update-status.factory.js
@@ -0,0 +1,46 @@
+export default
+ function ViewUpdateStatus($state, Rest, ProcessErrors, Alert, Wait, Empty, Find) {
+ return function(params) {
+ var scope = params.scope,
+ group_id = params.group_id,
+ group = Find({ list: scope.groups, key: 'id', val: group_id });
+
+ if (scope.removeSourceReady) {
+ scope.removeSourceReady();
+ }
+ scope.removeSourceReady = scope.$on('SourceReady', function(e, source) {
+
+ // Get the ID from the correct summary field
+ var update_id = (source.summary_fields.current_update) ? source.summary_fields.current_update.id : source.summary_fields.last_update.id;
+
+ $state.go('inventorySyncStdout', {id: update_id});
+
+ });
+
+ if (group) {
+ if (Empty(group.source)) {
+ // do nothing
+ } else if (Empty(group.status) || group.status === "never updated") {
+ Alert('No Status Available', '
An inventory sync has not been performed for the selected group. Start the process by ' +
+ 'clicking the button.
', 'alert-info', null, null, null, null, true);
+ } else {
+ Wait('start');
+ Rest.setUrl(group.related.inventory_source);
+ Rest.get()
+ .success(function (data) {
+ scope.$emit('SourceReady', data);
+ })
+ .error(function (data, status) {
+ ProcessErrors(scope, data, status, null, { hdr: 'Error!',
+ msg: 'Failed to retrieve inventory source: ' + group.related.inventory_source +
+ ' GET returned status: ' + status });
+ });
+ }
+ }
+ };
+ }
+
+ViewUpdateStatus.$inject =
+ [ '$state', 'Rest', 'ProcessErrors',
+ 'Alert', 'Wait', 'Empty', 'Find'
+ ];
diff --git a/awx/ui/client/src/inventories/groups/groups.service.js b/awx/ui/client/src/inventories/groups/groups.service.js
new file mode 100644
index 0000000000..54ed90dfc7
--- /dev/null
+++ b/awx/ui/client/src/inventories/groups/groups.service.js
@@ -0,0 +1,113 @@
+export default
+ ['$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', function($rootScope, Rest, GetBasePath, ProcessErrors, Wait){
+ return {
+ stringifyParams: function(params){
+ return _.reduce(params, (result, value, key) => {
+ return result + key + '=' + value + '&';
+ }, '');
+ },
+ // cute abstractions via fn.bind()
+ url: function(){
+ return '';
+ },
+ error: function(data, status) {
+ ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
+ msg: 'Call to ' + this.url + '. GET returned: ' + status });
+ },
+ success: function(data){
+ return data;
+ },
+ // HTTP methods
+ get: function(params){
+ Wait('start');
+ this.url = GetBasePath('groups') + '?' + this.stringifyParams(params);
+ Rest.setUrl(this.url);
+ return Rest.get()
+ .success(this.success.bind(this))
+ .error(this.error.bind(this))
+ .finally(Wait('stop'));
+ },
+ post: function(group){
+ Wait('start');
+ this.url = GetBasePath('groups');
+ Rest.setUrl(this.url);
+ return Rest.post(group)
+ .success(this.success.bind(this))
+ .error(this.error.bind(this))
+ .finally(Wait('stop'));
+ },
+ put: function(group){
+ Wait('start');
+ this.url = GetBasePath('groups') + group.id;
+ Rest.setUrl(this.url);
+ return Rest.put(group)
+ .success(this.success.bind(this))
+ .error(this.error.bind(this))
+ .finally(Wait('stop'));
+ },
+ delete: function(id){
+ Wait('start');
+ this.url = GetBasePath('groups') + id;
+ Rest.setUrl(this.url);
+ return Rest.destroy()
+ .success(this.success.bind(this))
+ .error(this.error.bind(this))
+ .finally(Wait('stop'));
+ },
+ getCredential: function(id){
+ Wait('start');
+ this.url = GetBasePath('credentials') + id;
+ Rest.setUrl(this.url);
+ return Rest.get()
+ .success(this.success.bind(this))
+ .error(this.error.bind(this))
+ .finally(Wait('stop'));
+ },
+ getInventorySource: function(params){
+ Wait('start');
+ this.url = GetBasePath('inventory_sources') + '?' + this.stringifyParams(params);
+ Rest.setUrl(this.url);
+ return Rest.get()
+ .success(this.success.bind(this))
+ .error(this.error.bind(this))
+ .finally(Wait('stop'));
+ },
+ putInventorySource: function(params, url){
+ Wait('start');
+ this.url = url;
+ Rest.setUrl(this.url);
+ return Rest.put(params)
+ .success(this.success.bind(this))
+ .error(this.error.bind(this))
+ .finally(Wait('stop'));
+ },
+ // these relationship setters could be consolidated, but verbosity makes the operation feel more clear @ controller level
+ associateGroup: function(group, target){
+ Wait('start');
+ this.url = GetBasePath('groups') + target + '/children/';
+ Rest.setUrl(this.url);
+ return Rest.post(group)
+ .success(this.success.bind(this))
+ .error(this.error.bind(this))
+ .finally(Wait('stop'));
+ },
+ disassociateGroup: function(group, parent){
+ Wait('start');
+ this.url = GetBasePath('groups') + parent + '/children/';
+ Rest.setUrl(this.url);
+ return Rest.post({id: group, disassociate: 1})
+ .success(this.success.bind(this))
+ .error(this.error.bind(this))
+ .finally(Wait('stop'));
+ },
+ promote: function(group, inventory){
+ Wait('start');
+ this.url = GetBasePath('inventory') + inventory + '/groups/';
+ Rest.setUrl(this.url);
+ return Rest.post({id: group, disassociate: 1})
+ .success(this.success.bind(this))
+ .error(this.error.bind(this))
+ .finally(Wait('stop'));
+ }
+ };
+ }];
diff --git a/awx/ui/client/src/inventories/groups/list/build-groups-list-state.factory.js b/awx/ui/client/src/inventories/groups/list/build-groups-list-state.factory.js
new file mode 100644
index 0000000000..f1af8925fd
--- /dev/null
+++ b/awx/ui/client/src/inventories/groups/list/build-groups-list-state.factory.js
@@ -0,0 +1,99 @@
+/*************************************************
+* Copyright (c) 2017 Ansible, Inc.
+*
+* All Rights Reserved
+*************************************************/
+
+export default ['InventoryGroupsList', '$stateExtender', 'templateUrl', '$injector',
+ function(InventoryGroupsList, $stateExtender, templateUrl, $injector){
+ var val = function(field, formStateDefinition, params) {
+ let state,
+ list = field.include ? $injector.get(field.include) : field,
+ breadcrumbLabel = (field.iterator.replace('_', ' ') + 's').toUpperCase(),
+ stateConfig = {
+ searchPrefix: `${list.iterator}`,
+ name: `${formStateDefinition.name}.${list.iterator}s`,
+ url: `/${list.iterator}s`,
+ ncyBreadcrumb: {
+ parent: `${formStateDefinition.name}`,
+ label: `${breadcrumbLabel}`
+ },
+ params: {
+ [list.iterator + '_search']: {
+ value: { order_by: field.order_by ? field.order_by : 'name' }
+ },
+ },
+ views: {
+ 'related': {
+ templateProvider: function(InventoryGroupsList, generateList, $templateRequest, $stateParams, GetBasePath) {
+ let list = _.cloneDeep(InventoryGroupsList);
+ if($stateParams && $stateParams.group) {
+ list.basePath = GetBasePath('groups') + _.last($stateParams.group) + '/children';
+ }
+ else {
+ //reaches here if the user is on the root level group
+ list.basePath = GetBasePath('inventory') + $stateParams.inventory_id + '/root_groups';
+ }
+
+ let html = generateList.build({
+ list: list,
+ mode: 'edit'
+ });
+ // Include the custom group delete modal template
+ return $templateRequest(templateUrl('inventories/groups/list/groups-list')).then((template) => {
+ return html.concat(template);
+ });
+ },
+ // controller: GroupsListController
+ }
+ },
+ resolve: {
+ ListDefinition: () => {
+ return list;
+ },
+ Dataset: ['ListDefinition', '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`]);
+ }
+ ],
+ inventoryData: ['InventoryManageService', '$stateParams', function(InventoryManageService, $stateParams) {
+ return InventoryManageService.getInventory($stateParams.inventory_id).then(res => res.data);
+ }]
+ }
+ };
+
+ if(params.controllers && params.controllers.related && params.controllers.related[field.name]) {
+ stateConfig.views.related.controller = params.controllers.related[field.name];
+ }
+ else if(field.name === 'permissions') {
+ stateConfig.views.related.controller = 'PermissionsList';
+ }
+ else {
+ // Generic controller
+ stateConfig.views.related.controller = ['$scope', 'ListDefinition', 'Dataset',
+ function($scope, list, Dataset) {
+ $scope.list = list;
+ $scope[`${list.iterator}_dataset`] = Dataset.data;
+ $scope[`${list.iterator}s`] = $scope[`${list.iterator}_dataset`].results;
+ }
+ ];
+ }
+
+ state = $stateExtender.buildDefinition(stateConfig);
+ // appy any default search parameters in form definition
+ if (field.search) {
+ state.params[`${field.iterator}_search`].value = _.merge(state.params[`${field.iterator}_search`].value, field.search);
+ }
+ return state;
+ };
+ return val;
+ }
+];
diff --git a/awx/ui/client/src/inventories/groups/list/group-list.controller.js b/awx/ui/client/src/inventories/groups/list/group-list.controller.js
new file mode 100644
index 0000000000..8bc2bb8154
--- /dev/null
+++ b/awx/ui/client/src/inventories/groups/list/group-list.controller.js
@@ -0,0 +1,236 @@
+/*************************************************
+ * Copyright (c) 2016 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+ export default
+ ['$scope', '$rootScope', '$state', '$stateParams', 'InventoryGroupsList', 'InventoryUpdate',
+ 'GroupManageService', 'GroupsCancelUpdate', 'ViewUpdateStatus', 'rbacUiControlService', 'GetBasePath',
+ 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Dataset', 'Find', 'QuerySet', 'inventoryData',
+ function($scope, $rootScope, $state, $stateParams, InventoryGroupsList, InventoryUpdate,
+ GroupManageService, GroupsCancelUpdate, ViewUpdateStatus, rbacUiControlService, GetBasePath,
+ GetSyncStatusMsg, GetHostsStatusMsg, Dataset, Find, qs, inventoryData){
+
+ let list = InventoryGroupsList;
+
+ init();
+
+ function init(){
+ $scope.inventory_id = $stateParams.inventory_id;
+ $scope.canAdhoc = inventoryData.summary_fields.user_capabilities.adhoc;
+ $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;
+
+ // The ncy breadcrumb directive will look at this attribute when attempting to bind to the correct scope.
+ // In this case, we don't want to incidentally bind to this scope when editing a host or a group. See:
+ // https://github.com/ncuillery/angular-breadcrumb/issues/42 for a little more information on the
+ // problem that this solves.
+ $scope.ncyBreadcrumbIgnore = true;
+ if($state.current.name === "inventoryManage.editGroup") {
+ $scope.rowBeingEdited = $state.params.group_id;
+ $scope.listBeingEdited = "groups";
+ }
+
+ $scope.inventory_id = $stateParams.inventory_id;
+ _.forEach($scope[list.name], buildStatusIndicators);
+
+ }
+
+ function buildStatusIndicators(group){
+ if (group === undefined || group === null) {
+ group = {};
+ }
+
+ let group_status, hosts_status;
+
+ group_status = GetSyncStatusMsg({
+ status: group.summary_fields.inventory_source.status,
+ has_inventory_sources: group.has_inventory_sources,
+ source: ( (group.summary_fields.inventory_source) ? group.summary_fields.inventory_source.source : null )
+ });
+ 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,
+ {status_class: group_status.class},
+ {status_tooltip: group_status.tooltip},
+ {launch_tooltip: group_status.launch_tip},
+ {launch_class: group_status.launch_class},
+ {group_schedule_tooltip: group_status.schedule_tip},
+ {hosts_status_tip: hosts_status.tooltip},
+ {hosts_status_class: hosts_status.class},
+ {source: group.summary_fields.inventory_source ? group.summary_fields.inventory_source.source : null},
+ {status: group.summary_fields.inventory_source ? group.summary_fields.inventory_source.status : null});
+ }
+
+ $scope.groupSelect = function(id){
+ var group = $stateParams.group === undefined ? [id] : _($stateParams.group).concat(id).value();
+ $state.go('inventoryManage', {
+ inventory_id: $stateParams.inventory_id,
+ group: group,
+ group_search: {
+ page_size: '20',
+ page: '1',
+ order_by: 'name',
+ }
+ }, {reload: true});
+ };
+ $scope.createGroup = function(){
+ $state.go('inventoryManage.addGroup');
+ };
+ $scope.editGroup = function(id){
+ $state.go('inventoryManage.editGroup', {group_id: id});
+ };
+ $scope.deleteGroup = function(group){
+ $scope.toDelete = {};
+ angular.extend($scope.toDelete, group);
+ if($scope.toDelete.total_groups === 0 && $scope.toDelete.total_hosts === 0) {
+ // This group doesn't have any child groups or hosts - the user is just trying to delete
+ // the group
+ $scope.deleteOption = "delete";
+ }
+ $('#group-delete-modal').modal('show');
+ };
+ $scope.confirmDelete = function(){
+
+ // Bind an even listener for the modal closing. Trying to $state.go() before the modal closes
+ // will mean that these two things are running async and the modal may not finish closing before
+ // the state finishes transitioning.
+ $('#group-delete-modal').off('hidden.bs.modal').on('hidden.bs.modal', function () {
+ // Remove the event handler so that we don't end up with multiple bindings
+ $('#group-delete-modal').off('hidden.bs.modal');
+ // Reload the inventory manage page and show that the group has been removed
+ $state.go('inventoryManage', null, {reload: true});
+ });
+
+ switch($scope.deleteOption){
+ case 'promote':
+ GroupManageService.promote($scope.toDelete.id, $stateParams.inventory_id)
+ .then(() => {
+ if (parseInt($state.params.group_id) === $scope.toDelete.id) {
+ $state.go("inventoryManage", null, {reload: true});
+ } else {
+ $state.go($state.current, null, {reload: true});
+ }
+ $('#group-delete-modal').modal('hide');
+ $('body').removeClass('modal-open');
+ $('.modal-backdrop').remove();
+ });
+ break;
+ default:
+ GroupManageService.delete($scope.toDelete.id).then(() => {
+ if (parseInt($state.params.group_id) === $scope.toDelete.id) {
+ $state.go("inventoryManage", null, {reload: true});
+ } else {
+ $state.go($state.current, null, {reload: true});
+ }
+ $('#group-delete-modal').modal('hide');
+ $('body').removeClass('modal-open');
+ $('.modal-backdrop').remove();
+ });
+ }
+ };
+ $scope.updateGroup = function(group) {
+ GroupManageService.getInventorySource({group: group.id}).then(res =>InventoryUpdate({
+ scope: $scope,
+ group_id: group.id,
+ url: res.data.results[0].related.update,
+ group_name: group.name,
+ group_source: res.data.results[0].source
+ }));
+ };
+
+ $scope.$on(`ws-jobs`, function(e, data){
+ var group = Find({ list: $scope.groups, key: 'id', val: data.group_id });
+
+ if (group === undefined || group === null) {
+ group = {};
+ }
+
+ if(data.status === 'failed' || data.status === 'successful'){
+ let path;
+ if($stateParams && $stateParams.group && $stateParams.group.length > 0) {
+ path = GetBasePath('groups') + _.last($stateParams.group) + '/children';
+ }
+ else {
+ //reaches here if the user is on the root level group
+ path = GetBasePath('inventory') + $stateParams.inventory_id + '/root_groups';
+ }
+ qs.search(path, $state.params[`${list.iterator}_search`])
+ .then(function(searchResponse) {
+ $scope[`${list.iterator}_dataset`] = searchResponse.data;
+ $scope[list.name] = $scope[`${list.iterator}_dataset`].results;
+ _.forEach($scope[list.name], buildStatusIndicators);
+ });
+ } else {
+ var status = GetSyncStatusMsg({
+ status: data.status,
+ has_inventory_sources: group.has_inventory_sources,
+ source: group.source
+ });
+ group.status = data.status;
+ group.status_class = status.class;
+ group.status_tooltip = status.tooltip;
+ group.launch_tooltip = status.launch_tip;
+ group.launch_class = status.launch_class;
+ }
+ });
+
+ $scope.cancelUpdate = function (id) {
+ GroupsCancelUpdate({ scope: $scope, id: id });
+ };
+ $scope.viewUpdateStatus = function (id) {
+ ViewUpdateStatus({
+ scope: $scope,
+ group_id: id
+ });
+ };
+ $scope.showFailedHosts = function() {
+ $state.go('inventoryManage', {failed: true}, {reload: true});
+ };
+ $scope.scheduleGroup = function(id) {
+ // Add this group's id to the array of group id's so that it gets
+ // added to the breadcrumb trail
+ var groupsArr = $stateParams.group ? $stateParams.group : [];
+ groupsArr.push(id);
+ $state.go('inventoryManage.editGroup.schedules', {group_id: id, group: groupsArr}, {reload: true});
+ };
+ // $scope.$parent governed by InventoryManageController, for unified multiSelect options
+ $scope.$on('multiSelectList.selectionChanged', (event, selection) => {
+ $scope.$parent.groupsSelected = selection.length > 0 ? true : false;
+ $scope.$parent.groupsSelectedItems = selection.selectedItems;
+ });
+
+ $scope.copyMoveGroup = function(id){
+ $state.go('inventoryManage.copyMoveGroup', {group_id: id, groups: $stateParams.groups});
+ };
+
+ var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) {
+ if (toState.name === "inventoryManage.editGroup") {
+ $scope.rowBeingEdited = toParams.group_id;
+ $scope.listBeingEdited = "groups";
+ }
+ else {
+ delete $scope.rowBeingEdited;
+ delete $scope.listBeingEdited;
+ }
+ });
+
+ // Remove the listener when the scope is destroyed to avoid a memory leak
+ $scope.$on('$destroy', function() {
+ cleanUpStateChangeListener();
+ });
+
+ }];
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
new file mode 100644
index 0000000000..8bc2bb8154
--- /dev/null
+++ b/awx/ui/client/src/inventories/groups/list/groups-list.controller.js
@@ -0,0 +1,236 @@
+/*************************************************
+ * Copyright (c) 2016 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+ export default
+ ['$scope', '$rootScope', '$state', '$stateParams', 'InventoryGroupsList', 'InventoryUpdate',
+ 'GroupManageService', 'GroupsCancelUpdate', 'ViewUpdateStatus', 'rbacUiControlService', 'GetBasePath',
+ 'GetSyncStatusMsg', 'GetHostsStatusMsg', 'Dataset', 'Find', 'QuerySet', 'inventoryData',
+ function($scope, $rootScope, $state, $stateParams, InventoryGroupsList, InventoryUpdate,
+ GroupManageService, GroupsCancelUpdate, ViewUpdateStatus, rbacUiControlService, GetBasePath,
+ GetSyncStatusMsg, GetHostsStatusMsg, Dataset, Find, qs, inventoryData){
+
+ let list = InventoryGroupsList;
+
+ init();
+
+ function init(){
+ $scope.inventory_id = $stateParams.inventory_id;
+ $scope.canAdhoc = inventoryData.summary_fields.user_capabilities.adhoc;
+ $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;
+
+ // The ncy breadcrumb directive will look at this attribute when attempting to bind to the correct scope.
+ // In this case, we don't want to incidentally bind to this scope when editing a host or a group. See:
+ // https://github.com/ncuillery/angular-breadcrumb/issues/42 for a little more information on the
+ // problem that this solves.
+ $scope.ncyBreadcrumbIgnore = true;
+ if($state.current.name === "inventoryManage.editGroup") {
+ $scope.rowBeingEdited = $state.params.group_id;
+ $scope.listBeingEdited = "groups";
+ }
+
+ $scope.inventory_id = $stateParams.inventory_id;
+ _.forEach($scope[list.name], buildStatusIndicators);
+
+ }
+
+ function buildStatusIndicators(group){
+ if (group === undefined || group === null) {
+ group = {};
+ }
+
+ let group_status, hosts_status;
+
+ group_status = GetSyncStatusMsg({
+ status: group.summary_fields.inventory_source.status,
+ has_inventory_sources: group.has_inventory_sources,
+ source: ( (group.summary_fields.inventory_source) ? group.summary_fields.inventory_source.source : null )
+ });
+ 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,
+ {status_class: group_status.class},
+ {status_tooltip: group_status.tooltip},
+ {launch_tooltip: group_status.launch_tip},
+ {launch_class: group_status.launch_class},
+ {group_schedule_tooltip: group_status.schedule_tip},
+ {hosts_status_tip: hosts_status.tooltip},
+ {hosts_status_class: hosts_status.class},
+ {source: group.summary_fields.inventory_source ? group.summary_fields.inventory_source.source : null},
+ {status: group.summary_fields.inventory_source ? group.summary_fields.inventory_source.status : null});
+ }
+
+ $scope.groupSelect = function(id){
+ var group = $stateParams.group === undefined ? [id] : _($stateParams.group).concat(id).value();
+ $state.go('inventoryManage', {
+ inventory_id: $stateParams.inventory_id,
+ group: group,
+ group_search: {
+ page_size: '20',
+ page: '1',
+ order_by: 'name',
+ }
+ }, {reload: true});
+ };
+ $scope.createGroup = function(){
+ $state.go('inventoryManage.addGroup');
+ };
+ $scope.editGroup = function(id){
+ $state.go('inventoryManage.editGroup', {group_id: id});
+ };
+ $scope.deleteGroup = function(group){
+ $scope.toDelete = {};
+ angular.extend($scope.toDelete, group);
+ if($scope.toDelete.total_groups === 0 && $scope.toDelete.total_hosts === 0) {
+ // This group doesn't have any child groups or hosts - the user is just trying to delete
+ // the group
+ $scope.deleteOption = "delete";
+ }
+ $('#group-delete-modal').modal('show');
+ };
+ $scope.confirmDelete = function(){
+
+ // Bind an even listener for the modal closing. Trying to $state.go() before the modal closes
+ // will mean that these two things are running async and the modal may not finish closing before
+ // the state finishes transitioning.
+ $('#group-delete-modal').off('hidden.bs.modal').on('hidden.bs.modal', function () {
+ // Remove the event handler so that we don't end up with multiple bindings
+ $('#group-delete-modal').off('hidden.bs.modal');
+ // Reload the inventory manage page and show that the group has been removed
+ $state.go('inventoryManage', null, {reload: true});
+ });
+
+ switch($scope.deleteOption){
+ case 'promote':
+ GroupManageService.promote($scope.toDelete.id, $stateParams.inventory_id)
+ .then(() => {
+ if (parseInt($state.params.group_id) === $scope.toDelete.id) {
+ $state.go("inventoryManage", null, {reload: true});
+ } else {
+ $state.go($state.current, null, {reload: true});
+ }
+ $('#group-delete-modal').modal('hide');
+ $('body').removeClass('modal-open');
+ $('.modal-backdrop').remove();
+ });
+ break;
+ default:
+ GroupManageService.delete($scope.toDelete.id).then(() => {
+ if (parseInt($state.params.group_id) === $scope.toDelete.id) {
+ $state.go("inventoryManage", null, {reload: true});
+ } else {
+ $state.go($state.current, null, {reload: true});
+ }
+ $('#group-delete-modal').modal('hide');
+ $('body').removeClass('modal-open');
+ $('.modal-backdrop').remove();
+ });
+ }
+ };
+ $scope.updateGroup = function(group) {
+ GroupManageService.getInventorySource({group: group.id}).then(res =>InventoryUpdate({
+ scope: $scope,
+ group_id: group.id,
+ url: res.data.results[0].related.update,
+ group_name: group.name,
+ group_source: res.data.results[0].source
+ }));
+ };
+
+ $scope.$on(`ws-jobs`, function(e, data){
+ var group = Find({ list: $scope.groups, key: 'id', val: data.group_id });
+
+ if (group === undefined || group === null) {
+ group = {};
+ }
+
+ if(data.status === 'failed' || data.status === 'successful'){
+ let path;
+ if($stateParams && $stateParams.group && $stateParams.group.length > 0) {
+ path = GetBasePath('groups') + _.last($stateParams.group) + '/children';
+ }
+ else {
+ //reaches here if the user is on the root level group
+ path = GetBasePath('inventory') + $stateParams.inventory_id + '/root_groups';
+ }
+ qs.search(path, $state.params[`${list.iterator}_search`])
+ .then(function(searchResponse) {
+ $scope[`${list.iterator}_dataset`] = searchResponse.data;
+ $scope[list.name] = $scope[`${list.iterator}_dataset`].results;
+ _.forEach($scope[list.name], buildStatusIndicators);
+ });
+ } else {
+ var status = GetSyncStatusMsg({
+ status: data.status,
+ has_inventory_sources: group.has_inventory_sources,
+ source: group.source
+ });
+ group.status = data.status;
+ group.status_class = status.class;
+ group.status_tooltip = status.tooltip;
+ group.launch_tooltip = status.launch_tip;
+ group.launch_class = status.launch_class;
+ }
+ });
+
+ $scope.cancelUpdate = function (id) {
+ GroupsCancelUpdate({ scope: $scope, id: id });
+ };
+ $scope.viewUpdateStatus = function (id) {
+ ViewUpdateStatus({
+ scope: $scope,
+ group_id: id
+ });
+ };
+ $scope.showFailedHosts = function() {
+ $state.go('inventoryManage', {failed: true}, {reload: true});
+ };
+ $scope.scheduleGroup = function(id) {
+ // Add this group's id to the array of group id's so that it gets
+ // added to the breadcrumb trail
+ var groupsArr = $stateParams.group ? $stateParams.group : [];
+ groupsArr.push(id);
+ $state.go('inventoryManage.editGroup.schedules', {group_id: id, group: groupsArr}, {reload: true});
+ };
+ // $scope.$parent governed by InventoryManageController, for unified multiSelect options
+ $scope.$on('multiSelectList.selectionChanged', (event, selection) => {
+ $scope.$parent.groupsSelected = selection.length > 0 ? true : false;
+ $scope.$parent.groupsSelectedItems = selection.selectedItems;
+ });
+
+ $scope.copyMoveGroup = function(id){
+ $state.go('inventoryManage.copyMoveGroup', {group_id: id, groups: $stateParams.groups});
+ };
+
+ var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) {
+ if (toState.name === "inventoryManage.editGroup") {
+ $scope.rowBeingEdited = toParams.group_id;
+ $scope.listBeingEdited = "groups";
+ }
+ else {
+ delete $scope.rowBeingEdited;
+ delete $scope.listBeingEdited;
+ }
+ });
+
+ // Remove the listener when the scope is destroyed to avoid a memory leak
+ $scope.$on('$destroy', function() {
+ cleanUpStateChangeListener();
+ });
+
+ }];
diff --git a/awx/ui/client/src/inventories/groups/list/groups-list.partial.html b/awx/ui/client/src/inventories/groups/list/groups-list.partial.html
new file mode 100644
index 0000000000..1a02f3a515
--- /dev/null
+++ b/awx/ui/client/src/inventories/groups/list/groups-list.partial.html
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
Deleting group {{ toDelete.name }}.
+ This group contains {{ toDelete.total_groups }} groups and {{ toDelete.total_hosts }} hosts.
+ This group contains {{ toDelete.total_hosts }} hosts.
+ This group contains {{ toDelete.total_groups }} groups.
+ Delete or promote the group's children?
+
+
+
+
+
+
+
Are you sure you want to permanently delete the group below from the inventory?
+
{{ toDelete.name }}
+
+
+
+
+
+
diff --git a/awx/ui/client/src/inventories/groups/list/inventory-groups.list.js b/awx/ui/client/src/inventories/groups/list/inventory-groups.list.js
new file mode 100644
index 0000000000..0536161ab9
--- /dev/null
+++ b/awx/ui/client/src/inventories/groups/list/inventory-groups.list.js
@@ -0,0 +1,165 @@
+/*************************************************
+ * Copyright (c) 2017 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+export default {
+ name: 'groups',
+ iterator: 'group',
+ editTitle: '{{ inventory.name }}',
+ well: true,
+ wellOverride: true,
+ index: false,
+ hover: true,
+ multiSelect: true,
+ trackBy: 'group.id',
+ basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/root_groups/',
+
+ fields: {
+ sync_status: {
+ label: '',
+ nosort: true,
+ mode: 'all',
+ iconOnly: true,
+ ngClick: 'viewUpdateStatus(group.id)',
+ awToolTip: "{{ group.status_tooltip }}",
+ dataTipWatch: "group.status_tooltip",
+ icon: "{{ 'fa icon-cloud-' + group.status_class }}",
+ ngClass: "group.status_class",
+ dataPlacement: "top",
+ columnClass: 'status-column List-staticColumn--smallStatus'
+ },
+ failed_hosts: {
+ label: '',
+ nosort: true,
+ mode: 'all',
+ iconOnly: true,
+ awToolTip: "{{ group.hosts_status_tip }}",
+ dataPlacement: "top",
+ ngClick: "showFailedHosts(group)",
+ icon: "{{ 'fa icon-job-' + group.hosts_status_class }}",
+ columnClass: 'status-column List-staticColumn--smallStatus'
+ },
+ name: {
+ label: 'Groups',
+ key: true,
+ ngClick: "groupSelect(group.id)",
+ columnClass: 'col-lg-6 col-md-6 col-sm-6 col-xs-6',
+ class: 'InventoryManage-breakWord',
+ },
+ total_groups: {
+ nosort: true,
+ label: '',
+ type: 'badgeCount',
+ ngHide: 'group.total_groups == 0',
+ noLink: true,
+ awToolTip: "{{group.name | sanitize}} contains {{group.total_groups}} {{group.total_groups === 1 ? 'child' : 'children'}}"
+ }
+ },
+
+ actions: {
+ refresh: {
+ mode: 'all',
+ awToolTip: "Refresh the page",
+ ngClick: "refreshGroups()",
+ ngShow: "socketStatus == 'error'",
+ actionClass: 'btn List-buttonDefault',
+ buttonContent: 'REFRESH'
+ },
+ launch: {
+ mode: 'all',
+ // $scope.$parent is governed by InventoryManageController,
+ ngDisabled: '!$parent.groupsSelected && !$parent.hostsSelected',
+ ngClick: '$parent.setAdhocPattern()',
+ awToolTip: "Select an inventory source by clicking the check box beside it. The inventory source can be a single group or host, a selection of multiple hosts, or a selection of multiple groups.",
+ dataTipWatch: "adhocCommandTooltip",
+ actionClass: 'btn List-buttonDefault',
+ buttonContent: 'RUN COMMANDS',
+ showTipWhenDisabled: true,
+ tooltipInnerClass: "Tooltip-wide",
+ ngShow: 'canAdhoc'
+ // TODO: set up a tip watcher and change text based on when
+ // things are selected/not selected. This is started and
+ // commented out in the inventory controller within the watchers.
+ // awToolTip: "{{ adhocButtonTipContents }}",
+ // dataTipWatch: "adhocButtonTipContents"
+ },
+ create: {
+ mode: 'all',
+ ngClick: "createGroup()",
+ awToolTip: "Create 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',
+
+ // group_update: {
+ // //label: 'Sync',
+ // mode: 'all',
+ // ngClick: 'updateGroup(group)',
+ // awToolTip: "{{ group.launch_tooltip }}",
+ // dataTipWatch: "group.launch_tooltip",
+ // ngShow: "(group.status !== 'running' && group.status " +
+ // "!== 'pending' && group.status !== 'updating') && group.summary_fields.user_capabilities.start",
+ // ngClass: "group.launch_class",
+ // dataPlacement: "top",
+ // },
+ // cancel: {
+ // //label: 'Cancel',
+ // mode: 'all',
+ // ngClick: "cancelUpdate(group.id)",
+ // awToolTip: "Cancel sync process",
+ // 'class': 'red-txt',
+ // ngShow: "(group.status == 'running' || group.status == 'pending' " +
+ // "|| group.status == 'updating') && group.summary_fields.user_capabilities.start",
+ // dataPlacement: "top",
+ // iconClass: "fa fa-minus-circle"
+ // },
+ copy: {
+ mode: 'all',
+ ngClick: "copyMoveGroup(group.id)",
+ awToolTip: 'Copy or move group',
+ ngShow: "group.id > 0 && group.summary_fields.user_capabilities.copy",
+ dataPlacement: "top"
+ },
+ // schedule: {
+ // mode: 'all',
+ // ngClick: "scheduleGroup(group.id)",
+ // awToolTip: "{{ group.group_schedule_tooltip }}",
+ // ngClass: "group.scm_type_class",
+ // dataPlacement: 'top',
+ // ngShow: "!(group.summary_fields.inventory_source.source === '')"
+ // },
+ edit: {
+ //label: 'Edit',
+ mode: 'all',
+ ngClick: "editGroup(group.id)",
+ awToolTip: 'Edit group',
+ dataPlacement: "top",
+ ngShow: "group.summary_fields.user_capabilities.edit"
+ },
+ view: {
+ //label: 'Edit',
+ mode: 'all',
+ ngClick: "editGroup(group.id)",
+ awToolTip: 'View group',
+ dataPlacement: "top",
+ ngShow: "!group.summary_fields.user_capabilities.edit"
+ },
+ "delete": {
+ //label: 'Delete',
+ mode: 'all',
+ ngClick: "deleteGroup(group)",
+ awToolTip: 'Delete group',
+ dataPlacement: "top",
+ ngShow: "group.summary_fields.user_capabilities.delete"
+ }
+ }
+};
diff --git a/awx/ui/client/src/inventories/groups/list/main.js b/awx/ui/client/src/inventories/groups/list/main.js
new file mode 100644
index 0000000000..b36162d4be
--- /dev/null
+++ b/awx/ui/client/src/inventories/groups/list/main.js
@@ -0,0 +1,15 @@
+/*************************************************
+ * Copyright (c) 2017 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+import buildGroupListState from './build-groups-list-state.factory';
+import controller from './groups-list.controller';
+import InventoryGroupsList from './inventory-groups.list';
+
+export default
+ angular.module('groupList', [])
+ .factory('buildGroupListState', buildGroupListState)
+ .value('InventoryGroupsList', InventoryGroupsList)
+ .controller('GroupsListController', controller);
diff --git a/awx/ui/client/src/inventories/groups/main.js b/awx/ui/client/src/inventories/groups/main.js
new file mode 100644
index 0000000000..d8af95c6f2
--- /dev/null
+++ b/awx/ui/client/src/inventories/groups/main.js
@@ -0,0 +1,24 @@
+/*************************************************
+ * Copyright (c) 2017 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+import groupList from './list/main';
+import service from './groups.service';
+import GetHostsStatusMsg from './factories/get-hosts-status-msg.factory';
+import GetSourceTypeOptions from './factories/get-source-type-options.factory';
+import GetSyncStatusMsg from './factories/get-sync-status-msg.factory';
+import GroupsCancelUpdate from './factories/groups-cancel-update.factory';
+import ViewUpdateStatus from './factories/view-update-status.factory';
+
+export default
+ angular.module('group', [
+ groupList.name
+ ])
+ .factory('GetHostsStatusMsg', GetHostsStatusMsg)
+ .factory('GetSourceTypeOptions', GetSourceTypeOptions)
+ .factory('GetSyncStatusMsg', GetSyncStatusMsg)
+ .factory('GroupsCancelUpdate', GroupsCancelUpdate)
+ .factory('ViewUpdateStatus', ViewUpdateStatus)
+ .service('GroupManageService', service);
diff --git a/awx/ui/client/src/inventories/hosts/add/host-add.controller.js b/awx/ui/client/src/inventories/hosts/add/host-add.controller.js
index 151d75401d..8e5b36ee7d 100644
--- a/awx/ui/client/src/inventories/hosts/add/host-add.controller.js
+++ b/awx/ui/client/src/inventories/hosts/add/host-add.controller.js
@@ -4,11 +4,11 @@
* All Rights Reserved
*************************************************/
-function HostsAdd($scope) {
+function HostsAdd() {
console.log('inside host add');
}
-export default ['$scope', HostsAdd
+export default [ HostsAdd
];
diff --git a/awx/ui/client/src/inventories/hosts/smart-inventory/edit/smart-inventory-edit.controller.js b/awx/ui/client/src/inventories/hosts/smart-inventory/edit/smart-inventory-edit.controller.js
index 5cf8bf1612..26d10c9234 100644
--- a/awx/ui/client/src/inventories/hosts/smart-inventory/edit/smart-inventory-edit.controller.js
+++ b/awx/ui/client/src/inventories/hosts/smart-inventory/edit/smart-inventory-edit.controller.js
@@ -4,11 +4,11 @@
* All Rights Reserved
*************************************************/
-function SmartInventoryEdit($scope) {
+function SmartInventoryEdit() {
console.log('inside smart inventory add');
}
-export default ['$scope', SmartInventoryEdit
+export default [ SmartInventoryEdit
];
diff --git a/awx/ui/client/src/inventories/inventory.form.js b/awx/ui/client/src/inventories/inventory.form.js
index f0646c9a40..91c181a006 100644
--- a/awx/ui/client/src/inventories/inventory.form.js
+++ b/awx/ui/client/src/inventories/inventory.form.js
@@ -1,5 +1,5 @@
/*************************************************
- * Copyright (c) 2015 Ansible, Inc.
+ * Copyright (c) 2017 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
@@ -10,8 +10,9 @@
* @description This form is for adding/editing an inventory
*/
-export default ['i18n', function(i18n) {
- return {
+export default ['i18n', 'buildGroupListState',
+function(i18n,buildGroupListState) {
+ return {
addTitle: i18n._('NEW INVENTORY'),
editTitle: '{{ inventory_name }}',
@@ -133,35 +134,10 @@ export default ['i18n', function(i18n) {
},
groups: {
name: 'groups',
- // awToolTip: i18n._('Please save before assigning permissions'),
- // dataPlacement: 'top',
- basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/root_groups/',
- type: 'collection',
+ include: "InventoryGroupsList",
title: i18n._('Groups'),
iterator: 'group',
- index: false,
- open: false,
- // search: {
- // order_by: 'username'
- // },
- actions: {
- add: {
- label: i18n._('Add'),
- ngClick: "$state.go('.add')",
- awToolTip: i18n._('Add a permission'),
- actionClass: 'btn List-buttonSubmit',
- buttonContent: '+ ADD',
- // ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)'
-
- }
- },
- fields: {
- name: {
- label: i18n._('Name'),
- // linkBase: 'users',
- class: 'col-lg-3 col-md-3 col-sm-3 col-xs-4'
- }
- }
+ stateGeneratorFunction: buildGroupListState
},
hosts: {
name: 'hosts',
diff --git a/awx/ui/client/src/inventories/main.js b/awx/ui/client/src/inventories/main.js
index 3ae1d9d7cf..be59ca6b4e 100644
--- a/awx/ui/client/src/inventories/main.js
+++ b/awx/ui/client/src/inventories/main.js
@@ -5,6 +5,7 @@
*************************************************/
import host from './hosts/main';
+import group from './groups/main';
import inventoryAdd from './add/main';
import inventoryEdit from './edit/main';
import inventoryList from './list/main';
@@ -16,6 +17,7 @@ import InventoryManageService from './inventory-manage.service';
export default
angular.module('inventory', [
host.name,
+ group.name,
inventoryAdd.name,
inventoryEdit.name,
inventoryList.name
@@ -175,7 +177,10 @@ angular.module('inventory', [
controllers: {
list: 'InventoryListController',
add: 'InventoryAddController',
- edit: 'InventoryEditController'
+ edit: 'InventoryEditController',
+ related: {
+ groups: 'GroupsListController'
+ }
},
urls: {
list: '/inventories'
diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js
index 5f136d7a97..e986c685e4 100644
--- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js
+++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js
@@ -170,7 +170,9 @@ export default ['$compile', 'Attr', 'Icon',
}
if (options.mode !== 'lookup' && (list.well === undefined || list.well)) {
- html += ``;
+ html += `
`;
+ html += (!list.wellOverride) ? "List-well" : "";
+ html += `">`;
// List actions
html += "
";
html += "
";
diff --git a/awx/ui/client/src/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js
index b42aaebd91..6e3a6ef426 100644
--- a/awx/ui/client/src/shared/stateDefinitions.factory.js
+++ b/awx/ui/client/src/shared/stateDefinitions.factory.js
@@ -9,7 +9,8 @@
* generateLookupNodes - Attaches to a form node. Builds an abstract '*.lookup' node with field-specific 'lookup.*' children e.g. {name: 'projects.add.lookup.organizations', ...}
*/
-export default ['$injector', '$stateExtender', '$log', 'i18n', function($injector, $stateExtender, $log, i18n) {
+export default ['$injector', '$stateExtender', '$log', 'i18n',
+function($injector, $stateExtender, $log, i18n) {
return {
/**
* @ngdoc method
@@ -557,7 +558,11 @@ export default ['$injector', '$stateExtender', '$log', 'i18n', function($injecto
function buildListNodes(field) {
let states = [];
- if(field.iterator === 'notification'){
+ if(field.iterator === 'group'){
+ states.push(field.stateGeneratorFunction(field, formStateDefinition, params));
+ states = _.flatten(states);
+ }
+ else if(field.iterator === 'notification'){
states.push(buildNotificationState(field));
states = _.flatten(states);
}