diff --git a/awx/ui_next/src/api/models/Groups.js b/awx/ui_next/src/api/models/Groups.js
index 019ba0ea94..4c19a44572 100644
--- a/awx/ui_next/src/api/models/Groups.js
+++ b/awx/ui_next/src/api/models/Groups.js
@@ -4,6 +4,12 @@ class Groups extends Base {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/groups/';
+
+ this.readAllHosts = this.readAllHosts.bind(this);
+ }
+
+ readAllHosts(id, params) {
+ return this.http.get(`${this.baseUrl}${id}/all_hosts/`, { params });
}
}
diff --git a/awx/ui_next/src/api/models/Inventories.js b/awx/ui_next/src/api/models/Inventories.js
index 08640173d4..858e7a390a 100644
--- a/awx/ui_next/src/api/models/Inventories.js
+++ b/awx/ui_next/src/api/models/Inventories.js
@@ -35,6 +35,10 @@ class Inventories extends InstanceGroupsMixin(Base) {
return this.http.options(`${this.baseUrl}${id}/groups/`);
}
+ readHostsOptions(id) {
+ return this.http.options(`${this.baseUrl}${id}/hosts/`);
+ }
+
promoteGroup(inventoryId, groupId) {
return this.http.post(`${this.baseUrl}${inventoryId}/groups/`, {
id: groupId,
diff --git a/awx/ui_next/src/screens/Inventory/Inventories.jsx b/awx/ui_next/src/screens/Inventory/Inventories.jsx
index d7361690f6..d6bfa12f74 100644
--- a/awx/ui_next/src/screens/Inventory/Inventories.jsx
+++ b/awx/ui_next/src/screens/Inventory/Inventories.jsx
@@ -32,8 +32,10 @@ class Inventories extends Component {
if (!inventory) {
return;
}
+
const inventoryKind =
inventory.kind === 'smart' ? 'smart_inventory' : 'inventory';
+
const breadcrumbConfig = {
'/inventories': i18n._(t`Inventories`),
'/inventories/inventory/add': i18n._(t`Create New Inventory`),
@@ -65,9 +67,7 @@ class Inventories extends Component {
t`Create New Host`
),
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
- nestedResource.id}`]: i18n._(
- t`${nestedResource && nestedResource.name}`
- ),
+ nestedResource.id}`]: `${nestedResource && nestedResource.name}`,
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
nestedResource.id}/edit`]: i18n._(t`Edit Details`),
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
@@ -83,6 +83,10 @@ class Inventories extends Component {
nestedResource.id}/edit`]: i18n._(t`Edit Details`),
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
nestedResource.id}/details`]: i18n._(t`Group Details`),
+ [`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
+ nestedResource.id}`]: `${nestedResource && nestedResource.name}`,
+ [`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
+ nestedResource.id}/nested_hosts`]: i18n._(t`Hosts`),
};
this.setState({ breadcrumbConfig });
};
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx
index ef5b0a7995..b1d3734a5f 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx
@@ -19,6 +19,8 @@ import ContentLoading from '@components/ContentLoading';
import { TabbedCardHeader } from '@components/Card';
import InventoryGroupEdit from '../InventoryGroupEdit/InventoryGroupEdit';
import InventoryGroupDetail from '../InventoryGroupDetail/InventoryGroupDetail';
+import InventoryGroupHosts from '../InventoryGroupHosts';
+
import { GroupsAPI } from '@api';
function InventoryGroup({ i18n, setBreadcrumb, inventory }) {
@@ -142,6 +144,12 @@ function InventoryGroup({ i18n, setBreadcrumb, inventory }) {
}}
/>,
]}
+
+
+
+ {i18n._(t`Add New Host`)}
+ ,
+
+ {i18n._(t`Add Existing Host`)}
+ ,
+ ];
+
+ return (
+ setIsOpen(prevState => !prevState)}
+ >
+ {i18n._(t`Add`)}
+
+ }
+ dropdownItems={dropdownItems}
+ />
+ );
+}
+
+AddHostDropdown.propTypes = {
+ onAddNew: func.isRequired,
+ onAddExisting: func.isRequired,
+};
+
+export default withI18n()(AddHostDropdown);
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/AddHostDropdown.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/AddHostDropdown.test.jsx
new file mode 100644
index 0000000000..c72e505ea2
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/AddHostDropdown.test.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import AddHostDropdown from './AddHostDropdown';
+
+describe('', () => {
+ let wrapper;
+ let dropdownToggle;
+ const onAddNew = jest.fn();
+ const onAddExisting = jest.fn();
+
+ beforeEach(() => {
+ wrapper = mountWithContexts(
+
+ );
+ dropdownToggle = wrapper.find('DropdownToggle button');
+ });
+
+ test('should initially render a closed dropdown', () => {
+ expect(wrapper.find('DropdownItem').length).toBe(0);
+ });
+
+ test('should render two dropdown items', () => {
+ dropdownToggle.simulate('click');
+ expect(wrapper.find('DropdownItem').length).toBe(2);
+ });
+
+ test('should close when button re-clicked', () => {
+ dropdownToggle.simulate('click');
+ expect(wrapper.find('DropdownItem').length).toBe(2);
+ dropdownToggle.simulate('click');
+ expect(wrapper.find('DropdownItem').length).toBe(0);
+ });
+});
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx
new file mode 100644
index 0000000000..237c9e5c93
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx
@@ -0,0 +1,162 @@
+import React, { useEffect, useCallback, useState } from 'react';
+import { useHistory, useLocation, useParams } from 'react-router-dom';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import { getQSConfig, parseQueryString } from '@util/qs';
+import { GroupsAPI, InventoriesAPI } from '@api';
+
+import AlertModal from '@components/AlertModal';
+import DataListToolbar from '@components/DataListToolbar';
+import PaginatedDataList from '@components/PaginatedDataList';
+import useRequest from '@util/useRequest';
+import InventoryGroupHostListItem from './InventoryGroupHostListItem';
+import AddHostDropdown from './AddHostDropdown';
+
+const QS_CONFIG = getQSConfig('host', {
+ page: 1,
+ page_size: 20,
+ order_by: 'name',
+});
+
+function InventoryGroupHostList({ i18n }) {
+ const [selected, setSelected] = useState([]);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const { id: inventoryId, groupId } = useParams();
+ const location = useLocation();
+ const history = useHistory();
+
+ const {
+ result: { hosts, hostCount, actions },
+ error: contentError,
+ isLoading,
+ request: fetchHosts,
+ } = useRequest(
+ useCallback(async () => {
+ const params = parseQueryString(QS_CONFIG, location.search);
+ const [response, actionsResponse] = await Promise.all([
+ GroupsAPI.readAllHosts(groupId, params),
+ InventoriesAPI.readHostsOptions(inventoryId),
+ ]);
+
+ return {
+ hosts: response.data.results,
+ hostCount: response.data.count,
+ actions: actionsResponse.data.actions,
+ };
+ }, [groupId, inventoryId, location.search]),
+ {
+ hosts: [],
+ hostCount: 0,
+ }
+ );
+
+ useEffect(() => {
+ fetchHosts();
+ }, [fetchHosts]);
+
+ const handleSelectAll = isSelected => {
+ setSelected(isSelected ? [...hosts] : []);
+ };
+
+ const handleSelect = row => {
+ if (selected.some(s => s.id === row.id)) {
+ setSelected(selected.filter(s => s.id !== row.id));
+ } else {
+ setSelected(selected.concat(row));
+ }
+ };
+
+ const isAllSelected = selected.length > 0 && selected.length === hosts.length;
+ const canAdd =
+ actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
+ const addFormUrl = `/inventories/inventory/${inventoryId}/groups/${groupId}/nested_hosts/add`;
+
+ return (
+ <>
+ (
+ setIsModalOpen(true)}
+ onAddNew={() => history.push(addFormUrl)}
+ />,
+ ]
+ : []),
+ // TODO HOST DISASSOCIATE BUTTON
+ ]}
+ />
+ )}
+ renderItem={o => (
+ row.id === o.id)}
+ onSelect={() => handleSelect(o)}
+ />
+ )}
+ emptyStateControls={
+ canAdd && (
+ setIsModalOpen(true)}
+ onAddNew={() => history.push(addFormUrl)}
+ />
+ )
+ }
+ />
+
+ {/* DISASSOCIATE HOST MODAL PLACEHOLDER */}
+
+ {isModalOpen && (
+ setIsModalOpen(false)}
+ >
+ {/* ADD/ASSOCIATE HOST MODAL PLACEHOLDER */}
+ {i18n._(t`Host Select Modal`)}
+
+ )}
+ >
+ );
+}
+
+export default withI18n()(InventoryGroupHostList);
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.test.jsx
new file mode 100644
index 0000000000..8345964e40
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.test.jsx
@@ -0,0 +1,164 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { createMemoryHistory } from 'history';
+import { GroupsAPI, InventoriesAPI } from '@api';
+import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
+import InventoryGroupHostList from './InventoryGroupHostList';
+import mockHosts from '../shared/data.hosts.json';
+
+jest.mock('@api/models/Groups');
+jest.mock('@api/models/Inventories');
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: () => ({
+ id: 1,
+ groupId: 2,
+ }),
+}));
+
+describe('', () => {
+ let wrapper;
+
+ beforeEach(async () => {
+ GroupsAPI.readAllHosts.mockResolvedValue({
+ data: { ...mockHosts },
+ });
+ InventoriesAPI.readHostsOptions.mockResolvedValue({
+ data: {
+ actions: {
+ GET: {},
+ POST: {},
+ },
+ },
+ });
+ 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('InventoryGroupHostList').length).toBe(1);
+ });
+
+ test('should fetch inventory group hosts from api and render them in the list', () => {
+ expect(GroupsAPI.readAllHosts).toHaveBeenCalled();
+ expect(InventoriesAPI.readHostsOptions).toHaveBeenCalled();
+ expect(wrapper.find('InventoryGroupHostListItem').length).toBe(3);
+ });
+
+ test('should check and uncheck the row item', async () => {
+ expect(
+ wrapper.find('DataListCheck[id="select-host-2"]').props().checked
+ ).toBe(false);
+ await act(async () => {
+ wrapper.find('DataListCheck[id="select-host-2"]').invoke('onChange')();
+ });
+ wrapper.update();
+ expect(
+ wrapper.find('DataListCheck[id="select-host-2"]').props().checked
+ ).toBe(true);
+ await act(async () => {
+ wrapper.find('DataListCheck[id="select-host-2"]').invoke('onChange')();
+ });
+ wrapper.update();
+ expect(
+ wrapper.find('DataListCheck[id="select-host-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 add dropdown button according to permissions', async () => {
+ expect(wrapper.find('AddHostDropdown').length).toBe(1);
+ InventoriesAPI.readHostsOptions.mockResolvedValueOnce({
+ data: {
+ actions: {
+ GET: {},
+ },
+ },
+ });
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ expect(wrapper.find('AddHostDropdown').length).toBe(0);
+ });
+
+ test('should show associate host modal when adding an existing host', () => {
+ const dropdownToggle = wrapper.find(
+ 'DropdownToggle button[aria-label="add host"]'
+ );
+ dropdownToggle.simulate('click');
+ wrapper
+ .find('DropdownItem[aria-label="add existing host"]')
+ .simulate('click');
+ expect(wrapper.find('AlertModal').length).toBe(1);
+ wrapper.find('ModalBoxCloseButton').simulate('click');
+ expect(wrapper.find('AlertModal').length).toBe(0);
+ });
+
+ test('should navigate to host add form when adding a new host', async () => {
+ GroupsAPI.readAllHosts.mockResolvedValue({
+ data: { ...mockHosts },
+ });
+ InventoriesAPI.readHostsOptions.mockResolvedValue({
+ data: {
+ actions: {
+ GET: {},
+ POST: {},
+ },
+ },
+ });
+ const history = createMemoryHistory();
+ await act(async () => {
+ wrapper = mountWithContexts(, {
+ context: {
+ router: { history },
+ },
+ });
+ });
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
+ const dropdownToggle = wrapper.find(
+ 'DropdownToggle button[aria-label="add host"]'
+ );
+ dropdownToggle.simulate('click');
+ wrapper.find('DropdownItem[aria-label="add new host"]').simulate('click');
+ expect(history.location.pathname).toEqual(
+ '/inventories/inventory/1/groups/2/nested_hosts/add'
+ );
+ });
+
+ test('should show content error when api throws error on initial render', async () => {
+ InventoriesAPI.readHostsOptions.mockImplementationOnce(() =>
+ Promise.reject(new Error())
+ );
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ await waitForElement(wrapper, 'ContentError', el => el.length === 1);
+ });
+});
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostListItem.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostListItem.jsx
new file mode 100644
index 0000000000..a234b9597f
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostListItem.jsx
@@ -0,0 +1,98 @@
+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,
+ DataListCell,
+ DataListCheck,
+ DataListItem,
+ DataListItemCells,
+ DataListItemRow,
+ Tooltip,
+} from '@patternfly/react-core';
+import { PencilAltIcon } from '@patternfly/react-icons';
+import HostToggle from '@components/HostToggle';
+import Sparkline from '@components/Sparkline';
+import { Host } from '@types';
+import styled from 'styled-components';
+
+const DataListAction = styled(_DataListAction)`
+ align-items: center;
+ display: grid;
+ grid-gap: 24px;
+ grid-template-columns: min-content 40px;
+`;
+
+function InventoryGroupHostListItem({
+ i18n,
+ detailUrl,
+ editUrl,
+ host,
+ isSelected,
+ onSelect,
+}) {
+ const recentPlaybookJobs = host.summary_fields.recent_jobs.map(job => ({
+ ...job,
+ type: 'job',
+ }));
+
+ const labelId = `check-action-${host.id}`;
+
+ return (
+
+
+
+
+
+ {host.name}
+
+ ,
+
+
+ ,
+ ]}
+ />
+
+
+ {host.summary_fields.user_capabilities?.edit && (
+
+
+
+ )}
+
+
+
+ );
+}
+
+InventoryGroupHostListItem.propTypes = {
+ detailUrl: string.isRequired,
+ editUrl: string.isRequired,
+ host: Host.isRequired,
+ isSelected: bool.isRequired,
+ onSelect: func.isRequired,
+};
+
+export default withI18n()(InventoryGroupHostListItem);
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostListItem.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostListItem.test.jsx
new file mode 100644
index 0000000000..830bd5540e
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostListItem.test.jsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import InventoryGroupHostListItem from './InventoryGroupHostListItem';
+import mockHosts from '../shared/data.hosts.json';
+
+jest.mock('@api');
+
+describe('', () => {
+ let wrapper;
+ const mockHost = mockHosts.results[0];
+
+ beforeEach(() => {
+ wrapper = mountWithContexts(
+ {}}
+ />
+ );
+ });
+
+ afterEach(() => {
+ wrapper.unmount();
+ });
+
+ test('should display expected row item content', () => {
+ expect(
+ wrapper
+ .find('DataListCell')
+ .first()
+ .text()
+ ).toBe('.host-000001.group-00000.dummy');
+ expect(wrapper.find('Sparkline').length).toBe(1);
+ expect(wrapper.find('HostToggle').length).toBe(1);
+ });
+
+ test('edit button shown to users with edit capabilities', () => {
+ expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
+ });
+
+ test('edit button hidden from users without edit capabilities', () => {
+ const copyMockHost = Object.assign({}, mockHost);
+ copyMockHost.summary_fields.user_capabilities.edit = false;
+ wrapper = mountWithContexts(
+ {}}
+ />
+ );
+ expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
+ });
+});
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHosts.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHosts.jsx
new file mode 100644
index 0000000000..dc3da57781
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHosts.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import { Switch, Route } from 'react-router-dom';
+import InventoryGroupHostList from './InventoryGroupHostList';
+
+function InventoryGroupHosts() {
+ return (
+
+ {/* Route to InventoryGroupHostAddForm */}
+
+
+
+
+ );
+}
+
+export default InventoryGroupHosts;
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHosts.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHosts.test.jsx
new file mode 100644
index 0000000000..0a4cecc19d
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHosts.test.jsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import { act } from 'react-dom/test-utils';
+import { createMemoryHistory } from 'history';
+import InventoryGroupHosts from './InventoryGroupHosts';
+
+jest.mock('@api');
+
+describe('', () => {
+ let wrapper;
+
+ test('initially renders successfully', async () => {
+ const history = createMemoryHistory({
+ initialEntries: ['/inventories/inventory/1/groups/1/nested_hosts'],
+ });
+
+ await act(async () => {
+ wrapper = mountWithContexts(, {
+ context: {
+ router: { history, route: { location: history.location } },
+ },
+ });
+ });
+ expect(wrapper.length).toBe(1);
+ expect(wrapper.find('InventoryGroupHostList').length).toBe(1);
+ });
+});
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/index.js b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/index.js
new file mode 100644
index 0000000000..58e24ac90e
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/index.js
@@ -0,0 +1 @@
+export { default } from './InventoryGroupHosts';
diff --git a/awx/ui_next/src/screens/Inventory/shared/data.hosts.json b/awx/ui_next/src/screens/Inventory/shared/data.hosts.json
new file mode 100644
index 0000000000..07c6ef7d9f
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/shared/data.hosts.json
@@ -0,0 +1,393 @@
+
+{
+ "count": 3,
+ "results": [
+ {
+ "id": 2,
+ "type": "host",
+ "url": "/api/v2/hosts/2/",
+ "related": {
+ "created_by": "/api/v2/users/10/",
+ "modified_by": "/api/v2/users/19/",
+ "variable_data": "/api/v2/hosts/2/variable_data/",
+ "groups": "/api/v2/hosts/2/groups/",
+ "all_groups": "/api/v2/hosts/2/all_groups/",
+ "job_events": "/api/v2/hosts/2/job_events/",
+ "job_host_summaries": "/api/v2/hosts/2/job_host_summaries/",
+ "activity_stream": "/api/v2/hosts/2/activity_stream/",
+ "inventory_sources": "/api/v2/hosts/2/inventory_sources/",
+ "smart_inventories": "/api/v2/hosts/2/smart_inventories/",
+ "ad_hoc_commands": "/api/v2/hosts/2/ad_hoc_commands/",
+ "ad_hoc_command_events": "/api/v2/hosts/2/ad_hoc_command_events/",
+ "insights": "/api/v2/hosts/2/insights/",
+ "ansible_facts": "/api/v2/hosts/2/ansible_facts/",
+ "inventory": "/api/v2/inventories/2/",
+ "last_job": "/api/v2/jobs/236/",
+ "last_job_host_summary": "/api/v2/job_host_summaries/2202/"
+ },
+ "summary_fields": {
+ "inventory": {
+ "id": 2,
+ "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": 2,
+ "kind": ""
+ },
+ "last_job": {
+ "id": 236,
+ "name": " Job Template 1 Project 0",
+ "description": "",
+ "finished": "2020-02-26T03:15:21.471439Z",
+ "status": "successful",
+ "failed": false,
+ "job_template_id": 18,
+ "job_template_name": " Job Template 1 Project 0"
+ },
+ "last_job_host_summary": {
+ "id": 2202,
+ "failed": false
+ },
+ "created_by": {
+ "id": 10,
+ "username": "user-3",
+ "first_name": "",
+ "last_name": ""
+ },
+ "modified_by": {
+ "id": 19,
+ "username": "all",
+ "first_name": "",
+ "last_name": ""
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true
+ },
+ "groups": {
+ "count": 2,
+ "results": [
+ {
+ "id": 1,
+ "name": " Group 1 Inventory 0"
+ },
+ {
+ "id": 2,
+ "name": " Group 2 Inventory 0"
+ }
+ ]
+ },
+ "recent_jobs": [
+ {
+ "id": 236,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-26T03:15:21.471439Z"
+ },
+ {
+ "id": 232,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T21:20:33.593789Z"
+ },
+ {
+ "id": 229,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T16:19:46.364134Z"
+ },
+ {
+ "id": 228,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T16:18:54.138363Z"
+ },
+ {
+ "id": 225,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T15:55:32.247652Z"
+ }
+ ]
+ },
+ "created": "2020-02-24T15:10:58.922179Z",
+ "modified": "2020-02-26T21:52:43.428530Z",
+ "name": ".host-000001.group-00000.dummy",
+ "description": "",
+ "inventory": 2,
+ "enabled": false,
+ "instance_id": "",
+ "variables": "",
+ "has_active_failures": false,
+ "has_inventory_sources": false,
+ "last_job": 236,
+ "last_job_host_summary": 2202,
+ "insights_system_id": null,
+ "ansible_facts_modified": null
+ },
+ {
+ "id": 3,
+ "type": "host",
+ "url": "/api/v2/hosts/3/",
+ "related": {
+ "created_by": "/api/v2/users/11/",
+ "modified_by": "/api/v2/users/1/",
+ "variable_data": "/api/v2/hosts/3/variable_data/",
+ "groups": "/api/v2/hosts/3/groups/",
+ "all_groups": "/api/v2/hosts/3/all_groups/",
+ "job_events": "/api/v2/hosts/3/job_events/",
+ "job_host_summaries": "/api/v2/hosts/3/job_host_summaries/",
+ "activity_stream": "/api/v2/hosts/3/activity_stream/",
+ "inventory_sources": "/api/v2/hosts/3/inventory_sources/",
+ "smart_inventories": "/api/v2/hosts/3/smart_inventories/",
+ "ad_hoc_commands": "/api/v2/hosts/3/ad_hoc_commands/",
+ "ad_hoc_command_events": "/api/v2/hosts/3/ad_hoc_command_events/",
+ "insights": "/api/v2/hosts/3/insights/",
+ "ansible_facts": "/api/v2/hosts/3/ansible_facts/",
+ "inventory": "/api/v2/inventories/2/",
+ "last_job": "/api/v2/jobs/236/",
+ "last_job_host_summary": "/api/v2/job_host_summaries/2195/"
+ },
+ "summary_fields": {
+ "inventory": {
+ "id": 2,
+ "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": 2,
+ "kind": ""
+ },
+ "last_job": {
+ "id": 236,
+ "name": " Job Template 1 Project 0",
+ "description": "",
+ "finished": "2020-02-26T03:15:21.471439Z",
+ "status": "successful",
+ "failed": false,
+ "job_template_id": 18,
+ "job_template_name": " Job Template 1 Project 0"
+ },
+ "last_job_host_summary": {
+ "id": 2195,
+ "failed": false
+ },
+ "created_by": {
+ "id": 11,
+ "username": "user-4",
+ "first_name": "",
+ "last_name": ""
+ },
+ "modified_by": {
+ "id": 1,
+ "username": "admin",
+ "first_name": "",
+ "last_name": ""
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true
+ },
+ "groups": {
+ "count": 2,
+ "results": [
+ {
+ "id": 1,
+ "name": " Group 1 Inventory 0"
+ },
+ {
+ "id": 2,
+ "name": " Group 2 Inventory 0"
+ }
+ ]
+ },
+ "recent_jobs": [
+ {
+ "id": 236,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-26T03:15:21.471439Z"
+ },
+ {
+ "id": 232,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T21:20:33.593789Z"
+ },
+ {
+ "id": 229,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T16:19:46.364134Z"
+ },
+ {
+ "id": 228,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T16:18:54.138363Z"
+ },
+ {
+ "id": 225,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T15:55:32.247652Z"
+ }
+ ]
+ },
+ "created": "2020-02-24T15:10:58.945113Z",
+ "modified": "2020-02-27T03:43:43.635871Z",
+ "name": ".host-000002.group-00000.dummy",
+ "description": "",
+ "inventory": 2,
+ "enabled": false,
+ "instance_id": "",
+ "variables": "",
+ "has_active_failures": false,
+ "has_inventory_sources": false,
+ "last_job": 236,
+ "last_job_host_summary": 2195,
+ "insights_system_id": null,
+ "ansible_facts_modified": null
+ },
+ {
+ "id": 4,
+ "type": "host",
+ "url": "/api/v2/hosts/4/",
+ "related": {
+ "created_by": "/api/v2/users/12/",
+ "modified_by": "/api/v2/users/1/",
+ "variable_data": "/api/v2/hosts/4/variable_data/",
+ "groups": "/api/v2/hosts/4/groups/",
+ "all_groups": "/api/v2/hosts/4/all_groups/",
+ "job_events": "/api/v2/hosts/4/job_events/",
+ "job_host_summaries": "/api/v2/hosts/4/job_host_summaries/",
+ "activity_stream": "/api/v2/hosts/4/activity_stream/",
+ "inventory_sources": "/api/v2/hosts/4/inventory_sources/",
+ "smart_inventories": "/api/v2/hosts/4/smart_inventories/",
+ "ad_hoc_commands": "/api/v2/hosts/4/ad_hoc_commands/",
+ "ad_hoc_command_events": "/api/v2/hosts/4/ad_hoc_command_events/",
+ "insights": "/api/v2/hosts/4/insights/",
+ "ansible_facts": "/api/v2/hosts/4/ansible_facts/",
+ "inventory": "/api/v2/inventories/2/",
+ "last_job": "/api/v2/jobs/236/",
+ "last_job_host_summary": "/api/v2/job_host_summaries/2192/"
+ },
+ "summary_fields": {
+ "inventory": {
+ "id": 2,
+ "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": 2,
+ "kind": ""
+ },
+ "last_job": {
+ "id": 236,
+ "name": " Job Template 1 Project 0",
+ "description": "",
+ "finished": "2020-02-26T03:15:21.471439Z",
+ "status": "successful",
+ "failed": false,
+ "job_template_id": 18,
+ "job_template_name": " Job Template 1 Project 0"
+ },
+ "last_job_host_summary": {
+ "id": 2192,
+ "failed": false
+ },
+ "created_by": {
+ "id": 12,
+ "username": "user-5",
+ "first_name": "",
+ "last_name": ""
+ },
+ "modified_by": {
+ "id": 1,
+ "username": "admin",
+ "first_name": "",
+ "last_name": ""
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true
+ },
+ "groups": {
+ "count": 2,
+ "results": [
+ {
+ "id": 1,
+ "name": " Group 1 Inventory 0"
+ },
+ {
+ "id": 2,
+ "name": " Group 2 Inventory 0"
+ }
+ ]
+ },
+ "recent_jobs": [
+ {
+ "id": 236,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-26T03:15:21.471439Z"
+ },
+ {
+ "id": 232,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T21:20:33.593789Z"
+ },
+ {
+ "id": 229,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T16:19:46.364134Z"
+ },
+ {
+ "id": 228,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T16:18:54.138363Z"
+ },
+ {
+ "id": 225,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T15:55:32.247652Z"
+ }
+ ]
+ },
+ "created": "2020-02-24T15:10:58.962312Z",
+ "modified": "2020-02-27T03:43:45.528882Z",
+ "name": ".host-000003.group-00000.dummy",
+ "description": "",
+ "inventory": 2,
+ "enabled": false,
+ "instance_id": "",
+ "variables": "",
+ "has_active_failures": false,
+ "has_inventory_sources": false,
+ "last_job": 236,
+ "last_job_host_summary": 2192,
+ "insights_system_id": null,
+ "ansible_facts_modified": null
+ }
+ ]
+}