mirror of
https://github.com/ansible/awx.git
synced 2026-03-21 19:07:39 -02:30
Add Inventory Group Host list
This commit is contained in:
@@ -4,6 +4,12 @@ class Groups extends Base {
|
|||||||
constructor(http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/groups/';
|
this.baseUrl = '/api/v2/groups/';
|
||||||
|
|
||||||
|
this.readAllHosts = this.readAllHosts.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
readAllHosts(id, params) {
|
||||||
|
return this.http.get(`${this.baseUrl}${id}/all_hosts/`, { params });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ class Inventories extends InstanceGroupsMixin(Base) {
|
|||||||
return this.http.options(`${this.baseUrl}${id}/groups/`);
|
return this.http.options(`${this.baseUrl}${id}/groups/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readHostsOptions(id) {
|
||||||
|
return this.http.options(`${this.baseUrl}${id}/hosts/`);
|
||||||
|
}
|
||||||
|
|
||||||
promoteGroup(inventoryId, groupId) {
|
promoteGroup(inventoryId, groupId) {
|
||||||
return this.http.post(`${this.baseUrl}${inventoryId}/groups/`, {
|
return this.http.post(`${this.baseUrl}${inventoryId}/groups/`, {
|
||||||
id: groupId,
|
id: groupId,
|
||||||
|
|||||||
@@ -32,8 +32,10 @@ class Inventories extends Component {
|
|||||||
if (!inventory) {
|
if (!inventory) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inventoryKind =
|
const inventoryKind =
|
||||||
inventory.kind === 'smart' ? 'smart_inventory' : 'inventory';
|
inventory.kind === 'smart' ? 'smart_inventory' : 'inventory';
|
||||||
|
|
||||||
const breadcrumbConfig = {
|
const breadcrumbConfig = {
|
||||||
'/inventories': i18n._(t`Inventories`),
|
'/inventories': i18n._(t`Inventories`),
|
||||||
'/inventories/inventory/add': i18n._(t`Create New Inventory`),
|
'/inventories/inventory/add': i18n._(t`Create New Inventory`),
|
||||||
@@ -65,9 +67,7 @@ class Inventories extends Component {
|
|||||||
t`Create New Host`
|
t`Create New Host`
|
||||||
),
|
),
|
||||||
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
|
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
|
||||||
nestedResource.id}`]: i18n._(
|
nestedResource.id}`]: `${nestedResource && nestedResource.name}`,
|
||||||
t`${nestedResource && nestedResource.name}`
|
|
||||||
),
|
|
||||||
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
|
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
|
||||||
nestedResource.id}/edit`]: i18n._(t`Edit Details`),
|
nestedResource.id}/edit`]: i18n._(t`Edit Details`),
|
||||||
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
|
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
|
||||||
@@ -83,6 +83,10 @@ class Inventories extends Component {
|
|||||||
nestedResource.id}/edit`]: i18n._(t`Edit Details`),
|
nestedResource.id}/edit`]: i18n._(t`Edit Details`),
|
||||||
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
|
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
|
||||||
nestedResource.id}/details`]: i18n._(t`Group Details`),
|
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 });
|
this.setState({ breadcrumbConfig });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import ContentLoading from '@components/ContentLoading';
|
|||||||
import { TabbedCardHeader } from '@components/Card';
|
import { TabbedCardHeader } from '@components/Card';
|
||||||
import InventoryGroupEdit from '../InventoryGroupEdit/InventoryGroupEdit';
|
import InventoryGroupEdit from '../InventoryGroupEdit/InventoryGroupEdit';
|
||||||
import InventoryGroupDetail from '../InventoryGroupDetail/InventoryGroupDetail';
|
import InventoryGroupDetail from '../InventoryGroupDetail/InventoryGroupDetail';
|
||||||
|
import InventoryGroupHosts from '../InventoryGroupHosts';
|
||||||
|
|
||||||
import { GroupsAPI } from '@api';
|
import { GroupsAPI } from '@api';
|
||||||
|
|
||||||
function InventoryGroup({ i18n, setBreadcrumb, inventory }) {
|
function InventoryGroup({ i18n, setBreadcrumb, inventory }) {
|
||||||
@@ -142,6 +144,12 @@ function InventoryGroup({ i18n, setBreadcrumb, inventory }) {
|
|||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
|
<Route
|
||||||
|
key="hosts"
|
||||||
|
path="/inventories/inventory/:id/groups/:groupId/nested_hosts"
|
||||||
|
>
|
||||||
|
<InventoryGroupHosts />
|
||||||
|
</Route>
|
||||||
<Route
|
<Route
|
||||||
key="not-found"
|
key="not-found"
|
||||||
path="*"
|
path="*"
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { func } from 'prop-types';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import {
|
||||||
|
Dropdown,
|
||||||
|
DropdownItem,
|
||||||
|
DropdownPosition,
|
||||||
|
DropdownToggle,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
|
function AddHostDropdown({ i18n, onAddNew, onAddExisting }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const dropdownItems = [
|
||||||
|
<DropdownItem
|
||||||
|
key="add-new"
|
||||||
|
aria-label="add new host"
|
||||||
|
component="button"
|
||||||
|
onClick={onAddNew}
|
||||||
|
>
|
||||||
|
{i18n._(t`Add New Host`)}
|
||||||
|
</DropdownItem>,
|
||||||
|
<DropdownItem
|
||||||
|
key="add-existing"
|
||||||
|
aria-label="add existing host"
|
||||||
|
component="button"
|
||||||
|
onClick={onAddExisting}
|
||||||
|
>
|
||||||
|
{i18n._(t`Add Existing Host`)}
|
||||||
|
</DropdownItem>,
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
isOpen={isOpen}
|
||||||
|
position={DropdownPosition.right}
|
||||||
|
toggle={
|
||||||
|
<DropdownToggle
|
||||||
|
id="add-host-dropdown"
|
||||||
|
aria-label="add host"
|
||||||
|
isPrimary
|
||||||
|
onToggle={() => setIsOpen(prevState => !prevState)}
|
||||||
|
>
|
||||||
|
{i18n._(t`Add`)}
|
||||||
|
</DropdownToggle>
|
||||||
|
}
|
||||||
|
dropdownItems={dropdownItems}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddHostDropdown.propTypes = {
|
||||||
|
onAddNew: func.isRequired,
|
||||||
|
onAddExisting: func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withI18n()(AddHostDropdown);
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
|
import AddHostDropdown from './AddHostDropdown';
|
||||||
|
|
||||||
|
describe('<AddHostDropdown />', () => {
|
||||||
|
let wrapper;
|
||||||
|
let dropdownToggle;
|
||||||
|
const onAddNew = jest.fn();
|
||||||
|
const onAddExisting = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<AddHostDropdown onAddNew={onAddNew} onAddExisting={onAddExisting} />
|
||||||
|
);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<PaginatedDataList
|
||||||
|
contentError={contentError}
|
||||||
|
hasContentLoading={isLoading}
|
||||||
|
items={hosts}
|
||||||
|
itemCount={hostCount}
|
||||||
|
pluralizedItemName={i18n._(t`Hosts`)}
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
onRowClick={handleSelect}
|
||||||
|
toolbarSearchColumns={[
|
||||||
|
{
|
||||||
|
name: i18n._(t`Name`),
|
||||||
|
key: 'name',
|
||||||
|
isDefault: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Created By (Username)`),
|
||||||
|
key: 'created_by__username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Modified By (Username)`),
|
||||||
|
key: 'modified_by__username',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
toolbarSortColumns={[
|
||||||
|
{
|
||||||
|
name: i18n._(t`Name`),
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
renderToolbar={props => (
|
||||||
|
<DataListToolbar
|
||||||
|
{...props}
|
||||||
|
showSelectAll
|
||||||
|
isAllSelected={isAllSelected}
|
||||||
|
onSelectAll={handleSelectAll}
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
additionalControls={[
|
||||||
|
...(canAdd
|
||||||
|
? [
|
||||||
|
<AddHostDropdown
|
||||||
|
onAddExisting={() => setIsModalOpen(true)}
|
||||||
|
onAddNew={() => history.push(addFormUrl)}
|
||||||
|
/>,
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
// TODO HOST DISASSOCIATE BUTTON
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderItem={o => (
|
||||||
|
<InventoryGroupHostListItem
|
||||||
|
key={o.id}
|
||||||
|
host={o}
|
||||||
|
detailUrl={`/inventories/inventory/${inventoryId}/hosts/${o.id}/details`}
|
||||||
|
editUrl={`/inventories/inventory/${inventoryId}/hosts/${o.id}/edit`}
|
||||||
|
isSelected={selected.some(row => row.id === o.id)}
|
||||||
|
onSelect={() => handleSelect(o)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
emptyStateControls={
|
||||||
|
canAdd && (
|
||||||
|
<AddHostDropdown
|
||||||
|
onAddExisting={() => setIsModalOpen(true)}
|
||||||
|
onAddNew={() => history.push(addFormUrl)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* DISASSOCIATE HOST MODAL PLACEHOLDER */}
|
||||||
|
|
||||||
|
{isModalOpen && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
variant="info"
|
||||||
|
title={i18n._(t`Select Hosts`)}
|
||||||
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
>
|
||||||
|
{/* ADD/ASSOCIATE HOST MODAL PLACEHOLDER */}
|
||||||
|
{i18n._(t`Host Select Modal`)}
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(InventoryGroupHostList);
|
||||||
@@ -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('<InventoryGroupHostList />', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
GroupsAPI.readAllHosts.mockResolvedValue({
|
||||||
|
data: { ...mockHosts },
|
||||||
|
});
|
||||||
|
InventoriesAPI.readHostsOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: {},
|
||||||
|
POST: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<InventoryGroupHostList />);
|
||||||
|
});
|
||||||
|
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(<InventoryGroupHostList />);
|
||||||
|
});
|
||||||
|
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(<InventoryGroupHostList />, {
|
||||||
|
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(<InventoryGroupHostList />);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 (
|
||||||
|
<DataListItem key={host.id} aria-labelledby={labelId} id={`${host.id}`}>
|
||||||
|
<DataListItemRow>
|
||||||
|
<DataListCheck
|
||||||
|
id={`select-host-${host.id}`}
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={onSelect}
|
||||||
|
aria-labelledby={labelId}
|
||||||
|
/>
|
||||||
|
<DataListItemCells
|
||||||
|
dataListCells={[
|
||||||
|
<DataListCell key="name">
|
||||||
|
<Link to={`${detailUrl}`}>
|
||||||
|
<b>{host.name}</b>
|
||||||
|
</Link>
|
||||||
|
</DataListCell>,
|
||||||
|
<DataListCell key="recentJobs">
|
||||||
|
<Sparkline jobs={recentPlaybookJobs} />
|
||||||
|
</DataListCell>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<DataListAction
|
||||||
|
aria-label="actions"
|
||||||
|
aria-labelledby={labelId}
|
||||||
|
id={labelId}
|
||||||
|
>
|
||||||
|
<HostToggle css="grid-column: 1" host={host} />
|
||||||
|
{host.summary_fields.user_capabilities?.edit && (
|
||||||
|
<Tooltip content={i18n._(t`Edit Host`)} position="top">
|
||||||
|
<Button
|
||||||
|
css="grid-column: 2"
|
||||||
|
variant="plain"
|
||||||
|
component={Link}
|
||||||
|
to={`${editUrl}`}
|
||||||
|
>
|
||||||
|
<PencilAltIcon />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</DataListAction>
|
||||||
|
</DataListItemRow>
|
||||||
|
</DataListItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
InventoryGroupHostListItem.propTypes = {
|
||||||
|
detailUrl: string.isRequired,
|
||||||
|
editUrl: string.isRequired,
|
||||||
|
host: Host.isRequired,
|
||||||
|
isSelected: bool.isRequired,
|
||||||
|
onSelect: func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withI18n()(InventoryGroupHostListItem);
|
||||||
@@ -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('<InventoryGroupHostListItem />', () => {
|
||||||
|
let wrapper;
|
||||||
|
const mockHost = mockHosts.results[0];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<InventoryGroupHostListItem
|
||||||
|
detailUrl="/host/1"
|
||||||
|
editUrl="/host/1"
|
||||||
|
host={mockHost}
|
||||||
|
isSelected={false}
|
||||||
|
onSelect={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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(
|
||||||
|
<InventoryGroupHostListItem
|
||||||
|
detailUrl="/host/1"
|
||||||
|
editUrl="/host/1"
|
||||||
|
host={mockHost}
|
||||||
|
isSelected={false}
|
||||||
|
onSelect={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Switch, Route } from 'react-router-dom';
|
||||||
|
import InventoryGroupHostList from './InventoryGroupHostList';
|
||||||
|
|
||||||
|
function InventoryGroupHosts() {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
{/* Route to InventoryGroupHostAddForm */}
|
||||||
|
<Route path="/inventories/inventory/:id/groups/:groupId/nested_hosts">
|
||||||
|
<InventoryGroupHostList />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InventoryGroupHosts;
|
||||||
@@ -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('<InventoryGroupHosts />', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
test('initially renders successfully', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/inventories/inventory/1/groups/1/nested_hosts'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<InventoryGroupHosts />, {
|
||||||
|
context: {
|
||||||
|
router: { history, route: { location: history.location } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(wrapper.length).toBe(1);
|
||||||
|
expect(wrapper.find('InventoryGroupHostList').length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './InventoryGroupHosts';
|
||||||
393
awx/ui_next/src/screens/Inventory/shared/data.hosts.json
Normal file
393
awx/ui_next/src/screens/Inventory/shared/data.hosts.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user