diff --git a/awx/ui/src/api/models/Hosts.js b/awx/ui/src/api/models/Hosts.js
index 5fa8cee698..0422407da9 100644
--- a/awx/ui/src/api/models/Hosts.js
+++ b/awx/ui/src/api/models/Hosts.js
@@ -20,6 +20,10 @@ class Hosts extends Base {
return this.http.get(`${this.baseUrl}${id}/all_groups/`, { params });
}
+ readGroups(id, params) {
+ return this.http.get(`${this.baseUrl}${id}/groups/`, { params });
+ }
+
readGroupsOptions(id) {
return this.http.options(`${this.baseUrl}${id}/groups/`);
}
diff --git a/awx/ui/src/screens/Inventory/InventoryHosts/InventoryHostItem.js b/awx/ui/src/screens/Inventory/InventoryHosts/InventoryHostItem.js
index 9998dc12a4..3b84a80562 100644
--- a/awx/ui/src/screens/Inventory/InventoryHosts/InventoryHostItem.js
+++ b/awx/ui/src/screens/Inventory/InventoryHosts/InventoryHostItem.js
@@ -1,13 +1,17 @@
-import React from 'react';
+import React, { useCallback } from 'react';
import { string, bool, func } from 'prop-types';
import { t } from '@lingui/macro';
-import { Button } from '@patternfly/react-core';
import { Tr, Td } from '@patternfly/react-table';
import { Link } from 'react-router-dom';
import { PencilAltIcon } from '@patternfly/react-icons';
-import { ActionsTd, ActionItem, TdBreakWord } from 'components/PaginatedTable';
-
+import { Button, Chip } from '@patternfly/react-core';
+import { HostsAPI } from 'api';
+import AlertModal from 'components/AlertModal';
+import ChipGroup from 'components/ChipGroup';
+import ErrorDetail from 'components/ErrorDetail';
import HostToggle from 'components/HostToggle';
+import { ActionsTd, ActionItem, TdBreakWord } from 'components/PaginatedTable';
+import useRequest, { useDismissableError } from 'hooks/useRequest';
import { Host } from 'types';
function InventoryHostItem({
@@ -19,45 +23,106 @@ function InventoryHostItem({
rowIndex,
}) {
const labelId = `check-action-${host.id}`;
+ const initialGroups = host?.summary_fields?.groups ?? {
+ results: [],
+ count: 0,
+ };
+
+ const {
+ error,
+ request: fetchRelatedGroups,
+ result: relatedGroups,
+ } = useRequest(
+ useCallback(async (hostId) => {
+ const { data } = await HostsAPI.readGroups(hostId);
+ return data.results;
+ }, []),
+ initialGroups.results
+ );
+
+ const { error: dismissableError, dismissError } = useDismissableError(error);
+
+ const handleOverflowChipClick = (hostId) => {
+ if (relatedGroups.length === initialGroups.count) {
+ return;
+ }
+ fetchRelatedGroups(hostId);
+ };
return (
-
- |
-
-
- {host.name}
-
-
-
- {host.description}
-
-
-
-
+
+ |
+
+
+ {host.name}
+
+
+
-
-
-
-
+ {relatedGroups.map((group) => (
+
+ {group.name}
+
+ ))}
+
+
+
+
+
+
+
+
+
+ {dismissableError && (
+
+ {t`Failed to load related groups.`}
+
+
+ )}
+ >
);
}
diff --git a/awx/ui/src/screens/Inventory/InventoryHosts/InventoryHostItem.test.js b/awx/ui/src/screens/Inventory/InventoryHosts/InventoryHostItem.test.js
index a343264d53..86ebf42ccc 100644
--- a/awx/ui/src/screens/Inventory/InventoryHosts/InventoryHostItem.test.js
+++ b/awx/ui/src/screens/Inventory/InventoryHosts/InventoryHostItem.test.js
@@ -1,6 +1,21 @@
import React from 'react';
-import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
+import { Router } from 'react-router-dom';
+import {
+ render,
+ fireEvent,
+ screen,
+ waitFor,
+ within,
+} from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { HostsAPI } from 'api';
+import { i18n } from '@lingui/core';
+import { en } from 'make-plural/plurals';
import InventoryHostItem from './InventoryHostItem';
+import { createMemoryHistory } from 'history';
+import english from '../../../locales/en/messages';
+
+jest.mock('api');
const mockHost = {
id: 1,
@@ -24,58 +39,194 @@ const mockHost = {
finished: '2020-02-26T22:38:41.037991Z',
},
],
+ groups: {
+ count: 1,
+ results: [
+ {
+ id: 11,
+ name: 'group_11',
+ },
+ ],
+ },
},
};
describe('', () => {
- let wrapper;
+ const history = createMemoryHistory({
+ initialEntries: ['/inventories/inventory/1/hosts'],
+ });
- beforeEach(() => {
- wrapper = mountWithContexts(
+ const getChips = (currentScreen) => {
+ const list = currentScreen.getByRole('list', {
+ name: 'Related Groups',
+ });
+ const { getAllByRole } = within(list);
+ const items = getAllByRole('listitem');
+ return items.map((item) => item.textContent);
+ };
+
+ const Component = (props) => (
+
{}}
+ editUrl={`/inventories/inventory/1/hosts/1/edit`}
host={mockHost}
+ isSelected={false}
+ onSelect={() => {}}
+ {...props}
/>
- );
+
+ );
+
+ beforeEach(() => {
+ i18n.loadLocaleData({ en: { plurals: en } });
+ i18n.load({ en: english });
+ i18n.activate('en');
});
test('should display expected details', () => {
- expect(wrapper.find('InventoryHostItem').length).toBe(1);
- expect(wrapper.find('Td[dataLabel="Name"]').find('Link').prop('to')).toBe(
+ render();
+
+ expect(screen.getByRole('cell', { name: 'Bar' })).toBeInTheDocument();
+ expect(
+ screen.getByRole('checkbox', { name: 'Toggle host' })
+ ).toBeInTheDocument();
+ expect(screen.getByRole('link', { name: 'Host 1' })).toHaveAttribute(
+ 'href',
'/host/1'
);
- expect(wrapper.find('Td[dataLabel="Description"]').text()).toBe('Bar');
- });
+ expect(screen.getByRole('link', { name: 'Edit host' })).toHaveAttribute(
+ 'href',
+ '/inventories/inventory/1/hosts/1/edit'
+ );
- test('edit button shown to users with edit capabilities', () => {
- expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
+ const relatedGroupChips = getChips(screen);
+ expect(relatedGroupChips).toEqual(['group_11']);
});
test('edit button hidden from users without edit capabilities', () => {
const copyMockHost = { ...mockHost };
copyMockHost.summary_fields.user_capabilities.edit = false;
- wrapper = mountWithContexts(
-
-
- {}}
- host={copyMockHost}
- />
-
-
- );
- expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
+
+ render();
+ expect(screen.queryByText('Edit host')).toBeNull();
});
- test('should display host toggle', () => {
- expect(wrapper.find('HostToggle').length).toBe(1);
+ test('should show and hide related groups on overflow button click', async () => {
+ const copyMockHost = { ...mockHost };
+ const mockGroups = [
+ {
+ id: 1,
+ name: 'group_1',
+ },
+ {
+ id: 2,
+ name: 'group_2',
+ },
+ {
+ id: 3,
+ name: 'group_3',
+ },
+ {
+ id: 4,
+ name: 'group_4',
+ },
+ {
+ id: 5,
+ name: 'group_5',
+ },
+ {
+ id: 6,
+ name: 'group_6',
+ },
+ ];
+ copyMockHost.summary_fields.groups = {
+ count: 6,
+ results: mockGroups.slice(0, 5),
+ };
+ HostsAPI.readGroups.mockReturnValue({
+ data: {
+ results: mockGroups,
+ },
+ });
+
+ render();
+
+ const initialRelatedGroupChips = getChips(screen);
+ expect(initialRelatedGroupChips).toEqual([
+ 'group_1',
+ 'group_2',
+ 'group_3',
+ 'group_4',
+ '2 more',
+ ]);
+
+ const overflowGroupsButton = screen.queryByText('2 more');
+ fireEvent.click(overflowGroupsButton);
+
+ await waitFor(() => expect(HostsAPI.readGroups).toHaveBeenCalledWith(1));
+
+ const expandedRelatedGroupChips = getChips(screen);
+ expect(expandedRelatedGroupChips).toEqual([
+ 'group_1',
+ 'group_2',
+ 'group_3',
+ 'group_4',
+ 'group_5',
+ 'group_6',
+ 'Show less',
+ ]);
+
+ const collapseGroupsButton = await screen.findByText('Show less');
+ fireEvent.click(collapseGroupsButton);
+
+ const collapsedRelatedGroupChips = getChips(screen);
+ expect(collapsedRelatedGroupChips).toEqual(initialRelatedGroupChips);
+ });
+
+ test('should show error modal when related groups api request fails', async () => {
+ const copyMockHost = { ...mockHost };
+ const mockGroups = [
+ {
+ id: 1,
+ name: 'group_1',
+ },
+ {
+ id: 2,
+ name: 'group_2',
+ },
+ {
+ id: 3,
+ name: 'group_3',
+ },
+ {
+ id: 4,
+ name: 'group_4',
+ },
+ {
+ id: 5,
+ name: 'group_5',
+ },
+ {
+ id: 6,
+ name: 'group_6',
+ },
+ ];
+ copyMockHost.summary_fields.groups = {
+ count: 6,
+ results: mockGroups.slice(0, 5),
+ };
+ HostsAPI.readGroups.mockRejectedValueOnce(new Error());
+
+ render();
+ await waitFor(() => {
+ const overflowGroupsButton = screen.queryByText('2 more');
+ fireEvent.click(overflowGroupsButton);
+ });
+ expect(screen.getByRole('dialog', { name: 'Alert modal Error!' }));
});
});
diff --git a/awx/ui/src/screens/Inventory/InventoryHosts/InventoryHostList.js b/awx/ui/src/screens/Inventory/InventoryHosts/InventoryHostList.js
index 3b8bbe5fe0..b1a3d6e720 100644
--- a/awx/ui/src/screens/Inventory/InventoryHosts/InventoryHostList.js
+++ b/awx/ui/src/screens/Inventory/InventoryHosts/InventoryHostList.js
@@ -137,6 +137,7 @@ function InventoryHostList() {
{t`Name`}
{t`Description`}
+ {t`Related Groups`}
{t`Actions`}
}