convert inventory groups list to tables

This commit is contained in:
Keith J. Grant
2021-04-27 11:38:46 -07:00
parent fe0ad30245
commit 83ceacf588
5 changed files with 130 additions and 107 deletions

View File

@@ -1,67 +1,57 @@
import React from 'react'; import React from 'react';
import { bool, func, number, oneOfType, string } from 'prop-types'; import { bool, func, number, oneOfType, string } from 'prop-types';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { import { Button } from '@patternfly/react-core';
Button, import { Tr, Td } from '@patternfly/react-table';
DataListAction,
DataListCheck,
DataListItem,
DataListItemCells,
DataListItemRow,
Tooltip,
} from '@patternfly/react-core';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { PencilAltIcon } from '@patternfly/react-icons'; import { PencilAltIcon } from '@patternfly/react-icons';
import DataListCell from '../../../components/DataListCell'; import { ActionsTd, ActionItem } from '../../../components/PaginatedTable';
import { Group } from '../../../types'; import { Group } from '../../../types';
function InventoryGroupItem({ group, inventoryId, isSelected, onSelect }) { function InventoryGroupItem({
group,
inventoryId,
isSelected,
onSelect,
rowIndex,
}) {
const labelId = `check-action-${group.id}`; const labelId = `check-action-${group.id}`;
const detailUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/details`; const detailUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/details`;
const editUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/edit`; const editUrl = `/inventories/inventory/${inventoryId}/groups/${group.id}/edit`;
return ( return (
<DataListItem key={group.id} aria-labelledby={labelId} id={`${group.id}`}> <Tr id={`group-row=${group.id}`}>
<DataListItemRow> <Td
<DataListCheck select={{
aria-labelledby={labelId} rowIndex,
id={`select-group-${group.id}`} isSelected,
checked={isSelected} onSelect,
onChange={onSelect} }}
/> />
<DataListItemCells <Td id={labelId} dataLabel={t`Name`}>
dataListCells={[ <Link to={`${detailUrl}`} id={labelId}>
<DataListCell key="name"> <b>{group.name}</b>
<Link to={`${detailUrl}`} id={labelId}> </Link>
<b>{group.name}</b> </Td>
</Link> <ActionsTd dataLabel={t`Actions`} gridColumns="auto 40px">
</DataListCell>, <ActionItem
]} visible={group.summary_fields.user_capabilities.edit}
/> tooltip={t`Edit group`}
<DataListAction
aria-label={t`actions`}
aria-labelledby={labelId}
id={labelId}
> >
{group.summary_fields.user_capabilities.edit && ( <Button
<Tooltip content={t`Edit Group`} position="top"> ouiaId={`${group.id}-edit-button`}
<Button aria-label={t`Edit Group`}
ouiaId={`${group.id}-edit-button`} variant="plain"
aria-label={t`Edit Group`} component={Link}
variant="plain" to={editUrl}
component={Link} >
to={editUrl} <PencilAltIcon />
> </Button>
<PencilAltIcon /> </ActionItem>
</Button> </ActionsTd>
</Tooltip> </Tr>
)}
</DataListAction>
</DataListItemRow>
</DataListItem>
); );
} }

View File

@@ -18,12 +18,16 @@ describe('<InventoryGroupItem />', () => {
beforeEach(() => { beforeEach(() => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<InventoryGroupItem <table>
group={mockGroup} <tbody>
inventoryId={1} <InventoryGroupItem
isSelected={false} group={mockGroup}
onSelect={() => {}} inventoryId={1}
/> isSelected={false}
onSelect={() => {}}
/>
</tbody>
</table>
); );
}); });
@@ -40,12 +44,16 @@ describe('<InventoryGroupItem />', () => {
copyMockGroup.summary_fields.user_capabilities.edit = false; copyMockGroup.summary_fields.user_capabilities.edit = false;
wrapper = mountWithContexts( wrapper = mountWithContexts(
<InventoryGroupItem <table>
group={copyMockGroup} <tbody>
inventoryId={1} <InventoryGroupItem
isSelected={false} group={copyMockGroup}
onSelect={() => {}} inventoryId={1}
/> isSelected={false}
onSelect={() => {}}
/>
</tbody>
</table>
); );
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy(); expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
}); });

View File

@@ -8,9 +8,11 @@ import useSelected from '../../../util/useSelected';
import useRequest from '../../../util/useRequest'; import useRequest from '../../../util/useRequest';
import { InventoriesAPI } from '../../../api'; import { InventoriesAPI } from '../../../api';
import DataListToolbar from '../../../components/DataListToolbar'; import DataListToolbar from '../../../components/DataListToolbar';
import PaginatedDataList, { import PaginatedTable, {
ToolbarAddButton, HeaderRow,
} from '../../../components/PaginatedDataList'; HeaderCell,
} from '../../../components/PaginatedTable';
import { ToolbarAddButton } from '../../../components/PaginatedDataList';
import InventoryGroupItem from './InventoryGroupItem'; import InventoryGroupItem from './InventoryGroupItem';
import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal'; import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal';
@@ -104,7 +106,7 @@ function InventoryGroupsList() {
return ( return (
<> <>
<PaginatedDataList <PaginatedTable
contentError={contentError} contentError={contentError}
hasContentLoading={isLoading || isAdHocLaunchLoading} hasContentLoading={isLoading || isAdHocLaunchLoading}
items={groups} items={groups}
@@ -135,21 +137,22 @@ function InventoryGroupsList() {
key: 'modified_by__username__icontains', key: 'modified_by__username__icontains',
}, },
]} ]}
toolbarSortColumns={[
{
name: t`Name`,
key: 'name',
},
]}
toolbarSearchableKeys={searchableKeys} toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys} toolbarRelatedSearchableKeys={relatedSearchableKeys}
renderItem={item => ( headerRow={
<HeaderRow qsConfig={QS_CONFIG}>
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
<HeaderCell>{t`Actions`}</HeaderCell>
</HeaderRow>
}
renderRow={(item, index) => (
<InventoryGroupItem <InventoryGroupItem
key={item.id} key={item.id}
group={item} group={item}
inventoryId={inventoryId} inventoryId={inventoryId}
isSelected={selected.some(row => row.id === item.id)} isSelected={selected.some(row => row.id === item.id)}
onSelect={() => handleSelect(item)} onSelect={() => handleSelect(item)}
rowIndex={index}
/> />
)} )}
renderToolbar={props => ( renderToolbar={props => (

View File

@@ -93,15 +93,12 @@ describe('<InventoryGroupsList />', () => {
}); });
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
}); });
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
wrapper.unmount(); wrapper.unmount();
}); });
test('initially renders successfully', () => {
expect(wrapper.find('InventoryGroupsList').length).toBe(1);
});
test('should fetch groups from api and render them in the list', async () => { test('should fetch groups from api and render them in the list', async () => {
expect(InventoriesAPI.readGroups).toHaveBeenCalled(); expect(InventoriesAPI.readGroups).toHaveBeenCalled();
expect(wrapper.find('InventoryGroupItem').length).toBe(3); expect(wrapper.find('InventoryGroupItem').length).toBe(3);
@@ -109,52 +106,71 @@ describe('<InventoryGroupsList />', () => {
test('should check and uncheck the row item', async () => { test('should check and uncheck the row item', async () => {
expect( expect(
wrapper.find('DataListCheck[id="select-group-1"]').props().checked wrapper
.find('.pf-c-table__check')
.first()
.find('input')
.props().checked
).toBe(false); ).toBe(false);
await act(async () => { await act(async () => {
wrapper.find('DataListCheck[id="select-group-1"]').invoke('onChange')( wrapper
true .find('.pf-c-table__check')
); .first()
.find('input')
.invoke('onChange')(true);
}); });
wrapper.update(); wrapper.update();
expect( expect(
wrapper.find('DataListCheck[id="select-group-1"]').props().checked wrapper
.find('.pf-c-table__check')
.first()
.find('input')
.props().checked
).toBe(true); ).toBe(true);
await act(async () => { await act(async () => {
wrapper.find('DataListCheck[id="select-group-1"]').invoke('onChange')( wrapper
false .find('.pf-c-table__check')
); .first()
.find('input')
.invoke('onChange')(false);
}); });
wrapper.update(); wrapper.update();
expect( expect(
wrapper.find('DataListCheck[id="select-group-1"]').props().checked wrapper
.find('.pf-c-table__check')
.first()
.find('input')
.props().checked
).toBe(false); ).toBe(false);
}); });
test('should check all row items when select all is checked', async () => { test('should check all row items when select all is checked', async () => {
wrapper.find('DataListCheck').forEach(el => { expect.assertions(9);
expect(el.props().checked).toBe(false); wrapper.find('.pf-c-table__check').forEach(el => {
expect(el.find('input').props().checked).toBe(false);
}); });
await act(async () => { await act(async () => {
wrapper.find('Checkbox#select-all').invoke('onChange')(true); wrapper.find('Checkbox#select-all').invoke('onChange')(true);
}); });
wrapper.update(); wrapper.update();
wrapper.find('DataListCheck').forEach(el => { wrapper.find('.pf-c-table__check').forEach(el => {
expect(el.props().checked).toBe(true); expect(el.find('input').props().checked).toBe(true);
}); });
await act(async () => { await act(async () => {
wrapper.find('Checkbox#select-all').invoke('onChange')(false); wrapper.find('Checkbox#select-all').invoke('onChange')(false);
}); });
wrapper.update(); wrapper.update();
wrapper.find('DataListCheck').forEach(el => { wrapper.find('.pf-c-table__check').forEach(el => {
expect(el.props().checked).toBe(false); expect(el.find('input').props().checked).toBe(false);
}); });
}); });
}); });
describe('<InventoryGroupsList/> error handling', () => { describe('<InventoryGroupsList/> error handling', () => {
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
InventoriesAPI.readGroups.mockResolvedValue({ InventoriesAPI.readGroups.mockResolvedValue({
data: { data: {
@@ -182,10 +198,12 @@ describe('<InventoryGroupsList/> error handling', () => {
}) })
); );
}); });
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
wrapper.unmount(); wrapper.unmount();
}); });
test('should show content error when api throws error on initial render', async () => { test('should show content error when api throws error on initial render', async () => {
InventoriesAPI.readGroupsOptions.mockImplementationOnce(() => InventoriesAPI.readGroupsOptions.mockImplementationOnce(() =>
Promise.reject(new Error()) Promise.reject(new Error())
@@ -213,7 +231,11 @@ describe('<InventoryGroupsList/> error handling', () => {
waitForElement(wrapper, 'ContentEmpty', el => el.length === 0); waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
await act(async () => { await act(async () => {
wrapper.find('DataListCheck[id="select-group-1"]').invoke('onChange')(); wrapper
.find('.pf-c-table__check')
.first()
.find('input')
.invoke('onChange')();
}); });
wrapper.update(); wrapper.update();
await act(async () => { await act(async () => {

View File

@@ -1,12 +1,11 @@
import React from 'react'; import React from 'react';
import { string, bool, func } from 'prop-types'; import { string, bool, func } from 'prop-types';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Button, Tooltip } from '@patternfly/react-core'; import { Button } from '@patternfly/react-core';
import { Tr, Td } from '@patternfly/react-table'; import { Tr, Td } from '@patternfly/react-table';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { PencilAltIcon } from '@patternfly/react-icons'; import { PencilAltIcon } from '@patternfly/react-icons';
import { ActionsTd } from '../../../components/PaginatedTable'; import { ActionsTd, ActionItem } from '../../../components/PaginatedTable';
import HostToggle from '../../../components/HostToggle'; import HostToggle from '../../../components/HostToggle';
import { Host } from '../../../types'; import { Host } from '../../../types';
@@ -37,18 +36,19 @@ function InventoryHostItem({
</Td> </Td>
<ActionsTd dataLabel={t`Actions`} gridColumns="auto 40px"> <ActionsTd dataLabel={t`Actions`} gridColumns="auto 40px">
<HostToggle host={host} /> <HostToggle host={host} />
{host.summary_fields.user_capabilities?.edit ? ( <ActionItem
<Tooltip content={t`Edit Host`} position="top"> visible={host.summary_fields.user_capabilities?.edit}
<Button tooltip={t`Edit host`}
ouiaId={`${host.id}-edit-button`} >
variant="plain" <Button
component={Link} ouiaId={`${host.id}-edit-button`}
to={`${editUrl}`} variant="plain"
> component={Link}
<PencilAltIcon /> to={`${editUrl}`}
</Button> >
</Tooltip> <PencilAltIcon />
) : null} </Button>
</ActionItem>
</ActionsTd> </ActionsTd>
</Tr> </Tr>
); );