diff --git a/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx b/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx
index 4252788cf0..cb6a5cc518 100644
--- a/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx
+++ b/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx
@@ -107,7 +107,11 @@ function DisassociateButton({
>
{modalNote && {modalNote}}
-
{i18n._(t`This action will disassociate the following:`)}
+
+ {i18n._(
+ t`This action will disassociate the following and any of their descendents:`
+ )}
+
{itemsToDisassociate.map(item => (
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx
index 9818bd43de..f9fcdb9fa7 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx
@@ -17,6 +17,7 @@ import ContentLoading from '../../../components/ContentLoading';
import InventoryGroupEdit from '../InventoryGroupEdit/InventoryGroupEdit';
import InventoryGroupDetail from '../InventoryGroupDetail/InventoryGroupDetail';
import InventoryGroupHosts from '../InventoryGroupHosts';
+import InventoryGroupsRelatedGroup from '../InventoryRelatedGroups';
import { GroupsAPI } from '../../../api';
@@ -129,6 +130,12 @@ function InventoryGroup({ i18n, setBreadcrumb, inventory }) {
>
,
+
+
+ ,
]}
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx
index 22ee9b9338..228a1fb9cf 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx
@@ -25,7 +25,7 @@ import DisassociateButton from '../../../components/DisassociateButton';
import { Kebabified } from '../../../contexts/Kebabified';
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
import InventoryGroupHostListItem from './InventoryGroupHostListItem';
-import AddHostDropdown from './AddHostDropdown';
+import AddHostDropdown from '../shared/AddDropdown';
const QS_CONFIG = getQSConfig('host', {
page: 1,
@@ -216,6 +216,9 @@ function InventoryGroupHostList({ i18n }) {
key="associate"
onAddExisting={() => setIsModalOpen(true)}
onAddNew={() => history.push(addFormUrl)}
+ newTitle={i18n._(t`Add new host`)}
+ existingTitle={i18n._(t`Add existing host`)}
+ label={i18n._(t`host`)}
/>,
]
: []),
@@ -283,6 +286,9 @@ function InventoryGroupHostList({ i18n }) {
key="associate"
onAddExisting={() => setIsModalOpen(true)}
onAddNew={() => history.push(addFormUrl)}
+ newTitle={i18n._(t`Add new host`)}
+ existingTitle={i18n._(t`Add existing host`)}
+ label={i18n._(t`host`)}
/>
)
}
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.test.jsx
index 59550792a5..729cd7017a 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.test.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.test.jsx
@@ -131,7 +131,7 @@ describe('', () => {
});
test('should show add dropdown button according to permissions', async () => {
- expect(wrapper.find('AddHostDropdown').length).toBe(1);
+ expect(wrapper.find('AddDropdown').length).toBe(1);
InventoriesAPI.readHostsOptions.mockResolvedValueOnce({
data: {
actions: {
@@ -143,7 +143,7 @@ describe('', () => {
wrapper = mountWithContexts();
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
- expect(wrapper.find('AddHostDropdown').length).toBe(0);
+ expect(wrapper.find('AddDropdown').length).toBe(0);
});
test('expected api calls are made for multi-delete', async () => {
diff --git a/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.jsx b/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.jsx
new file mode 100644
index 0000000000..0ea82d41c2
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.jsx
@@ -0,0 +1,246 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import {
+ Button,
+ Tooltip,
+ DropdownItem,
+ ToolbarItem,
+} from '@patternfly/react-core';
+import { useParams, useLocation, useHistory } from 'react-router-dom';
+
+import { GroupsAPI, InventoriesAPI } from '../../../api';
+import useRequest from '../../../util/useRequest';
+import { getQSConfig, parseQueryString, mergeParams } from '../../../util/qs';
+import useSelected from '../../../util/useSelected';
+
+import DataListToolbar from '../../../components/DataListToolbar';
+import PaginatedDataList from '../../../components/PaginatedDataList';
+import InventoryGroupRelatedGroupListItem from './InventoryRelatedGroupListItem';
+import AddDropdown from '../shared/AddDropdown';
+import { Kebabified } from '../../../contexts/Kebabified';
+import AdHocCommandsButton from '../../../components/AdHocCommands/AdHocCommands';
+import AssociateModal from '../../../components/AssociateModal';
+import DisassociateButton from '../../../components/DisassociateButton';
+
+const QS_CONFIG = getQSConfig('group', {
+ page: 1,
+ page_size: 20,
+ order_by: 'name',
+});
+function InventoryRelatedGroupList({ i18n, inventoryGroup }) {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const { id: inventoryId, groupId } = useParams();
+ const location = useLocation();
+ const history = useHistory();
+ const {
+ request: fetchRelated,
+ result: {
+ groups,
+ itemCount,
+ relatedSearchableKeys,
+ searchableKeys,
+ canAdd,
+ },
+ isLoading,
+ error: contentError,
+ } = useRequest(
+ useCallback(async () => {
+ const params = parseQueryString(QS_CONFIG, location.search);
+ const [response, actions] = await Promise.all([
+ GroupsAPI.readChildren(groupId, params),
+ InventoriesAPI.readGroupsOptions(inventoryId),
+ ]);
+
+ return {
+ groups: response.data.results,
+ itemCount: response.data.count,
+ relatedSearchableKeys: (
+ actions?.data?.related_search_fields || []
+ ).map(val => val.slice(0, -8)),
+ searchableKeys: Object.keys(actions.data.actions?.GET || {}).filter(
+ key => actions.data.actions?.GET[key].filterable
+ ),
+ canAdd:
+ actions.data.actions &&
+ Object.prototype.hasOwnProperty.call(actions.data.actions, 'POST'),
+ };
+ }, [groupId, location.search, inventoryId]),
+ { groups: [], itemCount: 0, canAdd: false }
+ );
+ useEffect(() => {
+ fetchRelated();
+ }, [fetchRelated]);
+
+ const fetchGroupsToAssociate = useCallback(
+ params => {
+ return InventoriesAPI.readGroups(
+ inventoryId,
+ mergeParams(params, { not__id: inventoryId, not__parents: inventoryId })
+ );
+ },
+ [inventoryId]
+ );
+
+ const fetchGroupsOptions = useCallback(
+ () => InventoriesAPI.readGroupsOptions(inventoryId),
+ [inventoryId]
+ );
+
+ const { selected, isAllSelected, handleSelect, setSelected } = useSelected(
+ groups
+ );
+
+ const addFormUrl = `/home`;
+
+ return (
+ <>
+ (
+
+ setSelected(isSelected ? [...groups] : [])
+ }
+ qsConfig={QS_CONFIG}
+ additionalControls={[
+ ...(canAdd
+ ? [
+ setIsModalOpen(true)}
+ onAddNew={() => history.push(addFormUrl)}
+ newTitle={i18n._(t`Add new group`)}
+ existingTitle={i18n._(t`Add existing group`)}
+ label={i18n._(t`group`)}
+ />,
+ ]
+ : []),
+
+ {({ isKebabified }) =>
+ isKebabified ? (
+
+ {({ openAdHocCommands, isDisabled }) => (
+
+ {i18n._(t`Run command`)}
+
+ )}
+
+ ) : (
+
+
+
+ {({ openAdHocCommands, isDisabled }) => (
+
+ )}
+
+
+
+ )
+ }
+ ,
+ {}}
+ itemsToDisassociate={selected}
+ modalTitle={i18n._(t`Disassociate related group(s)?`)}
+ />,
+ ]}
+ />
+ )}
+ renderItem={o => (
+ row.id === o.id)}
+ onSelect={() => handleSelect(o)}
+ />
+ )}
+ emptyStateControls={
+ canAdd && (
+ setIsModalOpen(true)}
+ onAddNew={() => history.push(addFormUrl)}
+ newTitle={i18n._(t`Add new group`)}
+ existingTitle={i18n._(t`Add existing group`)}
+ label={i18n._(t`group`)}
+ />
+ )
+ }
+ />
+ {isModalOpen && (
+ {}}
+ onClose={() => setIsModalOpen(false)}
+ title={i18n._(t`Select Groups`)}
+ />
+ )}
+ >
+ );
+}
+export default withI18n()(InventoryRelatedGroupList);
diff --git a/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.test.jsx
new file mode 100644
index 0000000000..834e0738ec
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.test.jsx
@@ -0,0 +1,148 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+
+import { GroupsAPI, InventoriesAPI } from '../../../api';
+import {
+ mountWithContexts,
+ waitForElement,
+} from '../../../../testUtils/enzymeHelpers';
+import InventoryRelatedGroupList from './InventoryRelatedGroupList';
+import mockRelatedGroups from '../shared/data.relatedGroups.json';
+
+jest.mock('../../../api/models/Groups');
+jest.mock('../../../api/models/Inventories');
+jest.mock('../../../api/models/CredentialTypes');
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: () => ({
+ id: 1,
+ groupId: 2,
+ }),
+}));
+
+describe('', () => {
+ let wrapper;
+
+ beforeEach(async () => {
+ GroupsAPI.readChildren.mockResolvedValue({
+ data: { ...mockRelatedGroups },
+ });
+ InventoriesAPI.readGroupsOptions.mockResolvedValue({
+ data: {
+ actions: {
+ GET: {},
+ POST: {},
+ },
+ related_search_fields: [
+ 'parents__search',
+ 'inventory__search',
+ 'inventory_sources__search',
+ 'created_by__search',
+ 'children__search',
+ 'modified_by__search',
+ 'hosts__search',
+ ],
+ },
+ });
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ wrapper.unmount();
+ });
+
+ test('initially renders successfully ', () => {
+ expect(wrapper.find('InventoryRelatedGroupList').length).toBe(1);
+ });
+
+ test('should fetch inventory group hosts from api and render them in the list', () => {
+ expect(GroupsAPI.readChildren).toHaveBeenCalled();
+ expect(InventoriesAPI.readGroupsOptions).toHaveBeenCalled();
+ expect(wrapper.find('InventoryRelatedGroupListItem').length).toBe(3);
+ });
+
+ test('should check and uncheck the row item', async () => {
+ expect(
+ wrapper.find('DataListCheck[id="select-group-2"]').props().checked
+ ).toBe(false);
+ await act(async () => {
+ wrapper.find('DataListCheck[id="select-group-2"]').invoke('onChange')();
+ });
+ wrapper.update();
+ expect(
+ wrapper.find('DataListCheck[id="select-group-2"]').props().checked
+ ).toBe(true);
+ await act(async () => {
+ wrapper.find('DataListCheck[id="select-group-2"]').invoke('onChange')();
+ });
+ wrapper.update();
+ expect(
+ wrapper.find('DataListCheck[id="select-group-2"]').props().checked
+ ).toBe(false);
+ });
+
+ test('should check all row items when select all is checked', async () => {
+ wrapper.find('DataListCheck').forEach(el => {
+ expect(el.props().checked).toBe(false);
+ });
+ await act(async () => {
+ wrapper.find('Checkbox#select-all').invoke('onChange')(true);
+ });
+ wrapper.update();
+ wrapper.find('DataListCheck').forEach(el => {
+ expect(el.props().checked).toBe(true);
+ });
+ await act(async () => {
+ wrapper.find('Checkbox#select-all').invoke('onChange')(false);
+ });
+ wrapper.update();
+ wrapper.find('DataListCheck').forEach(el => {
+ expect(el.props().checked).toBe(false);
+ });
+ });
+
+ test('should show content error when api throws error on initial render', async () => {
+ GroupsAPI.readChildren.mockResolvedValueOnce({
+ data: { ...mockRelatedGroups },
+ });
+ InventoriesAPI.readGroupsOptions.mockImplementationOnce(() =>
+ Promise.reject(new Error())
+ );
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ await waitForElement(wrapper, 'ContentError', el => el.length === 1);
+ });
+
+ test('should show add dropdown button according to permissions', async () => {
+ GroupsAPI.readChildren.mockResolvedValueOnce({
+ data: { ...mockRelatedGroups },
+ });
+ InventoriesAPI.readGroupsOptions.mockResolvedValueOnce({
+ data: {
+ actions: {
+ GET: {},
+ },
+ related_search_fields: [
+ 'parents__search',
+ 'inventory__search',
+ 'inventory_sources__search',
+ 'created_by__search',
+ 'children__search',
+ 'modified_by__search',
+ 'hosts__search',
+ ],
+ },
+ });
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ expect(wrapper.find('AddDropdown').length).toBe(0);
+ });
+});
diff --git a/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupListItem.jsx b/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupListItem.jsx
new file mode 100644
index 0000000000..67f543c9a7
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupListItem.jsx
@@ -0,0 +1,90 @@
+import 'styled-components/macro';
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { string, bool, func } from 'prop-types';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+
+import {
+ Button,
+ DataListAction as _DataListAction,
+ DataListCheck,
+ DataListItem,
+ DataListItemCells,
+ DataListItemRow,
+ Tooltip,
+} from '@patternfly/react-core';
+import { PencilAltIcon } from '@patternfly/react-icons';
+import styled from 'styled-components';
+import DataListCell from '../../../components/DataListCell';
+
+import { Group } from '../../../types';
+
+const DataListAction = styled(_DataListAction)`
+ align-items: center;
+ display: grid;
+ grid-gap: 24px;
+ grid-template-columns: min-content 40px;
+`;
+
+function InventoryRelatedGroupListItem({
+ i18n,
+ detailUrl,
+ editUrl,
+ group,
+ isSelected,
+ onSelect,
+}) {
+ const labelId = `check-action-${group.id}`;
+
+ return (
+
+
+
+
+
+ {group.name}
+
+ ,
+ ]}
+ />
+
+ {group.summary_fields.user_capabilities?.edit && (
+
+
+
+ )}
+
+
+
+ );
+}
+
+InventoryRelatedGroupListItem.propTypes = {
+ detailUrl: string.isRequired,
+ editUrl: string.isRequired,
+ group: Group.isRequired,
+ isSelected: bool.isRequired,
+ onSelect: func.isRequired,
+};
+
+export default withI18n()(InventoryRelatedGroupListItem);
diff --git a/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupListItem.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupListItem.test.jsx
new file mode 100644
index 0000000000..f52eaad866
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupListItem.test.jsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
+import InventoryRelatedGroupListItem from './InventoryRelatedGroupListItem';
+import mockRelatedGroups from '../shared/data.relatedGroups.json';
+
+jest.mock('../../../api');
+
+describe('', () => {
+ let wrapper;
+ const mockGroup = mockRelatedGroups.results[0];
+
+ beforeEach(() => {
+ wrapper = mountWithContexts(
+ {}}
+ />
+ );
+ });
+
+ afterEach(() => {
+ wrapper.unmount();
+ });
+
+ test('should display expected row item content', () => {
+ expect(
+ wrapper
+ .find('DataListCell')
+ .first()
+ .text()
+ ).toBe(' Group 2 Inventory 0');
+ });
+
+ test('edit button shown to users with edit capabilities', () => {
+ expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
+ });
+
+ test('edit button hidden from users without edit capabilities', () => {
+ wrapper = mountWithContexts(
+ {}}
+ />
+ );
+ expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
+ });
+});
diff --git a/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/index.js b/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/index.js
new file mode 100644
index 0000000000..09833dbe98
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/index.js
@@ -0,0 +1 @@
+export { default } from './InventoryRelatedGroupList';
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/AddHostDropdown.jsx b/awx/ui_next/src/screens/Inventory/shared/AddDropdown.jsx
similarity index 65%
rename from awx/ui_next/src/screens/Inventory/InventoryGroupHosts/AddHostDropdown.jsx
rename to awx/ui_next/src/screens/Inventory/shared/AddDropdown.jsx
index e5393aa50d..86a2704954 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/AddHostDropdown.jsx
+++ b/awx/ui_next/src/screens/Inventory/shared/AddDropdown.jsx
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
-import { func } from 'prop-types';
+import { func, string } from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
@@ -9,25 +9,32 @@ import {
DropdownToggle,
} from '@patternfly/react-core';
-function AddHostDropdown({ i18n, onAddNew, onAddExisting }) {
+function AddDropdown({
+ i18n,
+ onAddNew,
+ onAddExisting,
+ newTitle,
+ existingTitle,
+ label,
+}) {
const [isOpen, setIsOpen] = useState(false);
const dropdownItems = [
- {i18n._(t`Add New Host`)}
+ {newTitle}
,
- {i18n._(t`Add Existing Host`)}
+ {existingTitle}
,
];
@@ -37,8 +44,8 @@ function AddHostDropdown({ i18n, onAddNew, onAddExisting }) {
position={DropdownPosition.right}
toggle={
setIsOpen(prevState => !prevState)}
>
@@ -50,9 +57,12 @@ function AddHostDropdown({ i18n, onAddNew, onAddExisting }) {
);
}
-AddHostDropdown.propTypes = {
+AddDropdown.propTypes = {
onAddNew: func.isRequired,
onAddExisting: func.isRequired,
+ newTitle: string.isRequired,
+ existingTitle: string.isRequired,
+ label: string.isRequired,
};
-export default withI18n()(AddHostDropdown);
+export default withI18n()(AddDropdown);
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/AddHostDropdown.test.jsx b/awx/ui_next/src/screens/Inventory/shared/AddDropdown.test.jsx
similarity index 83%
rename from awx/ui_next/src/screens/Inventory/InventoryGroupHosts/AddHostDropdown.test.jsx
rename to awx/ui_next/src/screens/Inventory/shared/AddDropdown.test.jsx
index 8a7d2c11d4..68a8ab7ef9 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/AddHostDropdown.test.jsx
+++ b/awx/ui_next/src/screens/Inventory/shared/AddDropdown.test.jsx
@@ -1,8 +1,8 @@
import React from 'react';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
-import AddHostDropdown from './AddHostDropdown';
+import AddDropdown from './AddDropdown';
-describe('', () => {
+describe('', () => {
let wrapper;
let dropdownToggle;
const onAddNew = jest.fn();
@@ -10,7 +10,7 @@ describe('', () => {
beforeEach(() => {
wrapper = mountWithContexts(
-
+
);
dropdownToggle = wrapper.find('DropdownToggle button');
});
diff --git a/awx/ui_next/src/screens/Inventory/shared/data.relatedGroups.json b/awx/ui_next/src/screens/Inventory/shared/data.relatedGroups.json
new file mode 100644
index 0000000000..835ab95d79
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/shared/data.relatedGroups.json
@@ -0,0 +1,181 @@
+{
+ "count": 3,
+ "results": [{
+ "id": 2,
+ "type": "group",
+ "url": "/api/v2/groups/2/",
+ "related": {
+ "created_by": "/api/v2/users/10/",
+ "modified_by": "/api/v2/users/14/",
+ "variable_data": "/api/v2/groups/2/variable_data/",
+ "hosts": "/api/v2/groups/2/hosts/",
+ "potential_children": "/api/v2/groups/2/potential_children/",
+ "children": "/api/v2/groups/2/children/",
+ "all_hosts": "/api/v2/groups/2/all_hosts/",
+ "job_events": "/api/v2/groups/2/job_events/",
+ "job_host_summaries": "/api/v2/groups/2/job_host_summaries/",
+ "activity_stream": "/api/v2/groups/2/activity_stream/",
+ "inventory_sources": "/api/v2/groups/2/inventory_sources/",
+ "ad_hoc_commands": "/api/v2/groups/2/ad_hoc_commands/",
+ "inventory": "/api/v2/inventories/1/"
+ },
+ "summary_fields": {
+ "inventory": {
+ "id": 1,
+ "name": " Inventory 1 Org 0",
+ "description": "",
+ "has_active_failures": false,
+ "total_hosts": 33,
+ "hosts_with_active_failures": 0,
+ "total_groups": 4,
+ "has_inventory_sources": false,
+ "total_inventory_sources": 0,
+ "inventory_sources_with_failures": 0,
+ "organization_id": 1,
+ "kind": ""
+ },
+ "created_by": {
+ "id": 10,
+ "username": "user-4",
+ "first_name": "",
+ "last_name": ""
+ },
+ "modified_by": {
+ "id": 14,
+ "username": "user-8",
+ "first_name": "",
+ "last_name": ""
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true,
+ "copy": true
+ }
+ },
+ "created": "2020-09-23T14:30:55.263148Z",
+ "modified": "2020-09-23T14:30:55.263175Z",
+ "name": " Group 2 Inventory 0",
+ "description": "",
+ "inventory": 1,
+ "variables": ""
+ },
+ {
+ "id": 3,
+ "type": "group",
+ "url": "/api/v2/groups/3/",
+ "related": {
+ "created_by": "/api/v2/users/11/",
+ "modified_by": "/api/v2/users/15/",
+ "variable_data": "/api/v2/groups/3/variable_data/",
+ "hosts": "/api/v2/groups/3/hosts/",
+ "potential_children": "/api/v2/groups/3/potential_children/",
+ "children": "/api/v2/groups/3/children/",
+ "all_hosts": "/api/v2/groups/3/all_hosts/",
+ "job_events": "/api/v2/groups/3/job_events/",
+ "job_host_summaries": "/api/v2/groups/3/job_host_summaries/",
+ "activity_stream": "/api/v2/groups/3/activity_stream/",
+ "inventory_sources": "/api/v2/groups/3/inventory_sources/",
+ "ad_hoc_commands": "/api/v2/groups/3/ad_hoc_commands/",
+ "inventory": "/api/v2/inventories/1/"
+ },
+ "summary_fields": {
+ "inventory": {
+ "id": 1,
+ "name": " Inventory 1 Org 0",
+ "description": "",
+ "has_active_failures": false,
+ "total_hosts": 33,
+ "hosts_with_active_failures": 0,
+ "total_groups": 4,
+ "has_inventory_sources": false,
+ "total_inventory_sources": 0,
+ "inventory_sources_with_failures": 0,
+ "organization_id": 1,
+ "kind": ""
+ },
+ "created_by": {
+ "id": 11,
+ "username": "user-5",
+ "first_name": "",
+ "last_name": ""
+ },
+ "modified_by": {
+ "id": 15,
+ "username": "user-9",
+ "first_name": "",
+ "last_name": ""
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true,
+ "copy": true
+ }
+ },
+ "created": "2020-09-23T14:30:55.281583Z",
+ "modified": "2020-09-23T14:30:55.281615Z",
+ "name": " Group 3 Inventory 0",
+ "description": "",
+ "inventory": 1,
+ "variables": ""
+ },
+ {
+ "id": 4,
+ "type": "group",
+ "url": "/api/v2/groups/4/",
+ "related": {
+ "created_by": "/api/v2/users/12/",
+ "modified_by": "/api/v2/users/16/",
+ "variable_data": "/api/v2/groups/4/variable_data/",
+ "hosts": "/api/v2/groups/4/hosts/",
+ "potential_children": "/api/v2/groups/4/potential_children/",
+ "children": "/api/v2/groups/4/children/",
+ "all_hosts": "/api/v2/groups/4/all_hosts/",
+ "job_events": "/api/v2/groups/4/job_events/",
+ "job_host_summaries": "/api/v2/groups/4/job_host_summaries/",
+ "activity_stream": "/api/v2/groups/4/activity_stream/",
+ "inventory_sources": "/api/v2/groups/4/inventory_sources/",
+ "ad_hoc_commands": "/api/v2/groups/4/ad_hoc_commands/",
+ "inventory": "/api/v2/inventories/1/"
+ },
+ "summary_fields": {
+ "inventory": {
+ "id": 1,
+ "name": " Inventory 1 Org 0",
+ "description": "",
+ "has_active_failures": false,
+ "total_hosts": 33,
+ "hosts_with_active_failures": 0,
+ "total_groups": 4,
+ "has_inventory_sources": false,
+ "total_inventory_sources": 0,
+ "inventory_sources_with_failures": 0,
+ "organization_id": 1,
+ "kind": ""
+ },
+ "created_by": {
+ "id": 12,
+ "username": "user-6",
+ "first_name": "",
+ "last_name": ""
+ },
+ "modified_by": {
+ "id": 16,
+ "username": "user-10",
+ "first_name": "",
+ "last_name": ""
+ },
+ "user_capabilities": {
+ "edit": false,
+ "delete": true,
+ "copy": true
+ }
+ },
+ "created": "2020-09-23T14:30:55.293574Z",
+ "modified": "2020-09-23T14:30:55.293603Z",
+ "name": " Group 4 Inventory 0",
+ "description": "",
+ "inventory": 1,
+ "variables": ""
+ }
+ ]
+}