Convert more lists to tables

- Smart Inventory Hosts List
- Organization EE List
- Organization Teams list
This commit is contained in:
Keith J. Grant
2021-06-11 15:41:09 -07:00
committed by Shane McDonald
parent c6fa85036e
commit 3db92ca668
11 changed files with 170 additions and 195 deletions

View File

@@ -3,7 +3,10 @@ import { useLocation } from 'react-router-dom';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import DataListToolbar from '../../../components/DataListToolbar'; import DataListToolbar from '../../../components/DataListToolbar';
import PaginatedDataList from '../../../components/PaginatedDataList'; import PaginatedTable, {
HeaderRow,
HeaderCell,
} from '../../../components/PaginatedTable';
import SmartInventoryHostListItem from './SmartInventoryHostListItem'; import SmartInventoryHostListItem from './SmartInventoryHostListItem';
import useRequest from '../../../util/useRequest'; import useRequest from '../../../util/useRequest';
import useSelected from '../../../util/useSelected'; import useSelected from '../../../util/useSelected';
@@ -44,9 +47,13 @@ function SmartInventoryHostList({ inventory }) {
} }
); );
const { selected, isAllSelected, handleSelect, setSelected } = useSelected( const {
hosts selected,
); isAllSelected,
handleSelect,
clearSelected,
selectAll,
} = useSelected(hosts);
useEffect(() => { useEffect(() => {
fetchHosts(); fetchHosts();
@@ -54,14 +61,14 @@ function SmartInventoryHostList({ inventory }) {
return ( return (
<> <>
<PaginatedDataList <PaginatedTable
contentError={contentError} contentError={contentError}
hasContentLoading={isLoading || isAdHocLaunchLoading} hasContentLoading={isLoading || isAdHocLaunchLoading}
items={hosts} items={hosts}
itemCount={count} itemCount={count}
pluralizedItemName={t`Hosts`} pluralizedItemName={t`Hosts`}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
onRowClick={handleSelect} clearSelected={clearSelected}
toolbarSearchColumns={[ toolbarSearchColumns={[
{ {
name: t`Name`, name: t`Name`,
@@ -77,20 +84,12 @@ function SmartInventoryHostList({ inventory }) {
key: 'modified_by__username', key: 'modified_by__username',
}, },
]} ]}
toolbarSortColumns={[
{
name: t`Name`,
key: 'name',
},
]}
renderToolbar={props => ( renderToolbar={props => (
<DataListToolbar <DataListToolbar
{...props} {...props}
showSelectAll showSelectAll
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
onSelectAll={isSelected => onSelectAll={selectAll}
setSelected(isSelected ? [...hosts] : [])
}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={ additionalControls={
inventory?.summary_fields?.user_capabilities?.adhoc inventory?.summary_fields?.user_capabilities?.adhoc
@@ -105,13 +104,21 @@ function SmartInventoryHostList({ inventory }) {
} }
/> />
)} )}
renderItem={host => ( headerRow={
<HeaderRow qsConfig={QS_CONFIG}>
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
<HeaderCell>{t`Recent jobs`}</HeaderCell>
<HeaderCell>{t`Inventory`}</HeaderCell>
</HeaderRow>
}
renderRow={(host, index) => (
<SmartInventoryHostListItem <SmartInventoryHostListItem
key={host.id} key={host.id}
host={host} host={host}
detailUrl={`/inventories/smart_inventory/${inventory.id}/hosts/${host.id}/details`} detailUrl={`/inventories/smart_inventory/${inventory.id}/hosts/${host.id}/details`}
isSelected={selected.some(row => row.id === host.id)} isSelected={selected.some(row => row.id === host.id)}
onSelect={() => handleSelect(host)} onSelect={() => handleSelect(host)}
rowIndex={index}
/> />
)} )}
/> />

View File

@@ -50,18 +50,19 @@ describe('<SmartInventoryHostList />', () => {
}); });
test('should select and deselect all items', async () => { test('should select and deselect all items', async () => {
expect.assertions(6);
act(() => { act(() => {
wrapper.find('DataListToolbar').invoke('onSelectAll')(true); wrapper.find('DataListToolbar').invoke('onSelectAll')(true);
}); });
wrapper.update(); wrapper.update();
wrapper.find('DataListCheck').forEach(el => { wrapper.find('.pf-c-table__check input').forEach(el => {
expect(el.props().checked).toEqual(true); expect(el.props().checked).toEqual(true);
}); });
act(() => { act(() => {
wrapper.find('DataListToolbar').invoke('onSelectAll')(false); wrapper.find('DataListToolbar').invoke('onSelectAll')(false);
}); });
wrapper.update(); wrapper.update();
wrapper.find('DataListCheck').forEach(el => { wrapper.find('.pf-c-table__check input').forEach(el => {
expect(el.props().checked).toEqual(false); expect(el.props().checked).toEqual(false);
}); });
}); });

View File

@@ -5,57 +5,45 @@ import { string, bool, func } from 'prop-types';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import 'styled-components/macro'; import 'styled-components/macro';
import { import { Tr, Td } from '@patternfly/react-table';
DataListCheck,
DataListItem,
DataListItemCells,
DataListItemRow,
} from '@patternfly/react-core';
import DataListCell from '../../../components/DataListCell';
import Sparkline from '../../../components/Sparkline'; import Sparkline from '../../../components/Sparkline';
import { Host } from '../../../types'; import { Host } from '../../../types';
function SmartInventoryHostListItem({ detailUrl, host, isSelected, onSelect }) { function SmartInventoryHostListItem({
detailUrl,
host,
isSelected,
onSelect,
rowIndex,
}) {
const recentPlaybookJobs = host.summary_fields.recent_jobs.map(job => ({ const recentPlaybookJobs = host.summary_fields.recent_jobs.map(job => ({
...job, ...job,
type: 'job', type: 'job',
})); }));
const labelId = `check-action-${host.id}`;
return ( return (
<DataListItem key={host.id} aria-labelledby={labelId} id={`${host.id}`}> <Tr id={`host-row-${host.id}`}>
<DataListItemRow> <Td
<DataListCheck select={{
id={`select-host-${host.id}`} rowIndex,
checked={isSelected} isSelected,
onChange={onSelect} onSelect,
aria-labelledby={labelId} }}
/> />
<DataListItemCells <Td dataLabel={t`Name`}>
dataListCells={[ <Link to={`${detailUrl}`}>{host.name}</Link>
<DataListCell key="name"> </Td>
<Link to={`${detailUrl}`}> <Td dataLabel={t`Recent jobs`}>
<b>{host.name}</b> <Sparkline jobs={recentPlaybookJobs} />
</Link> </Td>
</DataListCell>, <Td dataLabel={t`Inventory`}>
<DataListCell key="recentJobs"> <Link
<Sparkline jobs={recentPlaybookJobs} /> to={`/inventories/inventory/${host.summary_fields.inventory.id}/details`}
</DataListCell>, >
<DataListCell key="inventory"> {host.summary_fields.inventory.name}
<> </Link>
<b css="margin-right: 24px">{t`Inventory`}</b> </Td>
<Link </Tr>
to={`/inventories/inventory/${host.summary_fields.inventory.id}/details`}
>
{host.summary_fields.inventory.name}
</Link>
</>
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
); );
} }

View File

@@ -24,12 +24,16 @@ describe('<SmartInventoryHostListItem />', () => {
beforeEach(() => { beforeEach(() => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<SmartInventoryHostListItem <table>
detailUrl="/inventories/smart_inventory/1/hosts/2" <tbody>
host={mockHost} <SmartInventoryHostListItem
isSelected={false} detailUrl="/inventories/smart_inventory/1/hosts/2"
onSelect={() => {}} host={mockHost}
/> isSelected={false}
onSelect={() => {}}
/>
</tbody>
</table>
); );
}); });
@@ -38,10 +42,10 @@ describe('<SmartInventoryHostListItem />', () => {
}); });
test('should render expected row cells', () => { test('should render expected row cells', () => {
const cells = wrapper.find('DataListCell'); const cells = wrapper.find('Td');
expect(cells).toHaveLength(3); expect(cells).toHaveLength(4);
expect(cells.at(0).text()).toEqual('Host Two'); expect(cells.at(1).text()).toEqual('Host Two');
expect(cells.at(1).find('Sparkline').length).toEqual(1); expect(cells.at(2).find('Sparkline').length).toEqual(1);
expect(cells.at(2).text()).toContain('Inv 1'); expect(cells.at(3).text()).toEqual('Inv 1');
}); });
}); });

View File

@@ -7,7 +7,10 @@ import { Card } from '@patternfly/react-core';
import { OrganizationsAPI } from '../../../api'; import { OrganizationsAPI } from '../../../api';
import { getQSConfig, parseQueryString } from '../../../util/qs'; import { getQSConfig, parseQueryString } from '../../../util/qs';
import useRequest from '../../../util/useRequest'; import useRequest from '../../../util/useRequest';
import PaginatedDataList from '../../../components/PaginatedDataList'; import PaginatedTable, {
HeaderRow,
HeaderCell,
} from '../../../components/PaginatedTable';
import DatalistToolbar from '../../../components/DataListToolbar'; import DatalistToolbar from '../../../components/DataListToolbar';
import OrganizationExecEnvListItem from './OrganizationExecEnvListItem'; import OrganizationExecEnvListItem from './OrganizationExecEnvListItem';
@@ -69,7 +72,7 @@ function OrganizationExecEnvList({ organization }) {
return ( return (
<> <>
<Card> <Card>
<PaginatedDataList <PaginatedTable
contentError={contentError} contentError={contentError}
hasContentLoading={isLoading} hasContentLoading={isLoading}
items={executionEnvironments} items={executionEnvironments}
@@ -98,32 +101,21 @@ function OrganizationExecEnvList({ organization }) {
key: 'modified_by__username__icontains', key: 'modified_by__username__icontains',
}, },
]} ]}
toolbarSortColumns={[
{
name: t`Name`,
key: 'name',
},
{
name: t`Image`,
key: 'image',
},
{
name: t`Created`,
key: 'created',
},
{
name: t`Modified`,
key: 'modified',
},
]}
renderToolbar={props => ( renderToolbar={props => (
<DatalistToolbar {...props} qsConfig={QS_CONFIG} /> <DatalistToolbar {...props} qsConfig={QS_CONFIG} />
)} )}
renderItem={executionEnvironment => ( headerRow={
<HeaderRow qsConfig={QS_CONFIG} isSelectable={false}>
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
<HeaderCell sortKey="image">{t`Image`}</HeaderCell>
</HeaderRow>
}
renderRow={(executionEnvironment, index) => (
<OrganizationExecEnvListItem <OrganizationExecEnvListItem
key={executionEnvironment.id} key={executionEnvironment.id}
executionEnvironment={executionEnvironment} executionEnvironment={executionEnvironment}
detailUrl={`/execution_environments/${executionEnvironment.id}`} detailUrl={`/execution_environments/${executionEnvironment.id}`}
rowIndex={index}
/> />
)} )}
/> />

View File

@@ -3,42 +3,18 @@ import { string } from 'prop-types';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { import { Tr, Td } from '@patternfly/react-table';
DataListItem,
DataListItemRow,
DataListItemCells,
} from '@patternfly/react-core';
import DataListCell from '../../../components/DataListCell';
import { ExecutionEnvironment } from '../../../types'; import { ExecutionEnvironment } from '../../../types';
function OrganizationExecEnvListItem({ executionEnvironment, detailUrl }) { function OrganizationExecEnvListItem({ executionEnvironment, detailUrl }) {
const labelId = `check-action-${executionEnvironment.id}`;
return ( return (
<DataListItem <Tr id={`ee-row-${executionEnvironment.id}`}>
key={executionEnvironment.id} <Td dataLabel={t`Name`}>
aria-labelledby={labelId} <Link to={`${detailUrl}`}>{executionEnvironment.name}</Link>
id={`${executionEnvironment.id}`} </Td>
> <Td dataLabel={t`Image`}>{executionEnvironment.image}</Td>
<DataListItemRow> </Tr>
<DataListItemCells
dataListCells={[
<DataListCell key="name" aria-label={t`Execution environment name`}>
<Link to={`${detailUrl}`}>
<b>{executionEnvironment.name}</b>
</Link>
</DataListCell>,
<DataListCell
key="image"
aria-label={t`Execution environment image`}
>
{executionEnvironment.image}
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
); );
} }

View File

@@ -19,10 +19,14 @@ describe('<OrganizationExecEnvListItem/>', () => {
test('should mount successfully', async () => { test('should mount successfully', async () => {
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<OrganizationExecEnvListItem <table>
executionEnvironment={executionEnvironment} <tbody>
detailUrl="execution_environments/1/details" <OrganizationExecEnvListItem
/> executionEnvironment={executionEnvironment}
detailUrl="execution_environments/1/details"
/>
</tbody>
</table>
); );
}); });
expect(wrapper.find('OrganizationExecEnvListItem').length).toBe(1); expect(wrapper.find('OrganizationExecEnvListItem').length).toBe(1);
@@ -31,15 +35,20 @@ describe('<OrganizationExecEnvListItem/>', () => {
test('should render the proper data', async () => { test('should render the proper data', async () => {
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<OrganizationExecEnvListItem <table>
executionEnvironment={executionEnvironment} <tbody>
detailUrl="execution_environments/1/details" <OrganizationExecEnvListItem
/> executionEnvironment={executionEnvironment}
detailUrl="execution_environments/1/details"
/>
</tbody>
</table>
); );
}); });
expect( expect(
wrapper wrapper
.find('DataListCell[aria-label="Execution environment image"]') .find('Td')
.at(1)
.text() .text()
).toBe(executionEnvironment.image); ).toBe(executionEnvironment.image);
}); });

View File

@@ -4,7 +4,10 @@ import { useLocation } from 'react-router-dom';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { OrganizationsAPI } from '../../../api'; import { OrganizationsAPI } from '../../../api';
import PaginatedDataList from '../../../components/PaginatedDataList'; import PaginatedTable, {
HeaderRow,
HeaderCell,
} from '../../../components/PaginatedTable';
import { getQSConfig, parseQueryString } from '../../../util/qs'; import { getQSConfig, parseQueryString } from '../../../util/qs';
import useRequest from '../../../util/useRequest'; import useRequest from '../../../util/useRequest';
import OrganizationTeamListItem from './OrganizationTeamListItem'; import OrganizationTeamListItem from './OrganizationTeamListItem';
@@ -54,7 +57,7 @@ function OrganizationTeamList({ id }) {
}, [fetchTeams]); }, [fetchTeams]);
return ( return (
<PaginatedDataList <PaginatedTable
contentError={error} contentError={error}
hasContentLoading={isLoading} hasContentLoading={isLoading}
items={teams} items={teams}
@@ -76,15 +79,15 @@ function OrganizationTeamList({ id }) {
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} isSelectable={false}>
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
<HeaderCell>{t`Actions`}</HeaderCell>
</HeaderRow>
}
renderRow={item => (
<OrganizationTeamListItem <OrganizationTeamListItem
key={item.id} key={item.id}
value={item.name} value={item.name}

View File

@@ -93,7 +93,8 @@ describe('<OrganizationTeamList />', () => {
}); });
}); });
test('should pass fetched teams to PaginatedDatalist', async () => { test('should pass fetched teams to PaginatedTable', async () => {
// expect.assertions(7);
let wrapper; let wrapper;
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
@@ -103,10 +104,8 @@ describe('<OrganizationTeamList />', () => {
await sleep(0); await sleep(0);
wrapper.update(); wrapper.update();
const list = wrapper.find('PaginatedDataList'); const list = wrapper.find('PaginatedTable');
list.find('DataListCell').forEach((el, index) => { expect(list.prop('items')).toEqual(listData.data.results);
expect(el.text()).toBe(listData.data.results[index].name);
});
expect(list.prop('itemCount')).toEqual(listData.data.count); expect(list.prop('itemCount')).toEqual(listData.data.count);
expect(list.prop('qsConfig')).toEqual({ expect(list.prop('qsConfig')).toEqual({
namespace: 'team', namespace: 'team',

View File

@@ -1,58 +1,37 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import { Button } from '@patternfly/react-core';
Button, import { Tr, Td } from '@patternfly/react-table';
DataListAction,
DataListItem,
DataListItemRow,
DataListItemCells,
Tooltip,
} from '@patternfly/react-core';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { PencilAltIcon } from '@patternfly/react-icons'; import { PencilAltIcon } from '@patternfly/react-icons';
import DataListCell from '../../../components/DataListCell'; import { ActionsTd, ActionItem } from '../../../components/PaginatedTable';
function OrganizationTeamListItem({ team, detailUrl }) { function OrganizationTeamListItem({ team, detailUrl }) {
const labelId = `check-action-${team.id}`;
return ( return (
<DataListItem aria-labelledby={labelId} id={`${team.id}`}> <Tr id={`team-row-${team.id}`}>
<DataListItemRow> <Td dataLabel={t`Name`}>
<DataListItemCells <Link to={`${detailUrl}/details`}>{team.name}</Link>
dataListCells={[ </Td>
<DataListCell key="divider"> <ActionsTd dataLabel={t`Actions`}>
<span> <ActionItem
<Link to={`${detailUrl}/details`}> visible={team.summary_fields.user_capabilities.edit}
<b aria-label={t`team name`}>{team.name}</b> tooltip={t`Edit Team`}
</Link>
</span>
</DataListCell>,
]}
/>
<DataListAction
aria-label={t`actions`}
aria-labelledby={labelId}
id={labelId}
> >
{team.summary_fields.user_capabilities.edit && ( <Button
<Tooltip content={t`Edit Team`} position="top"> ouiaId={`${team.id}-edit-button`}
<Button aria-label={t`Edit Team`}
ouiaId={`${team.id}-edit-button`} css="grid-column: 2"
aria-label={t`Edit Team`} variant="plain"
css="grid-column: 2" component={Link}
variant="plain" to={`${detailUrl}/edit`}
component={Link} >
to={`${detailUrl}/edit`} <PencilAltIcon />
> </Button>
<PencilAltIcon /> </ActionItem>
</Button> </ActionsTd>
</Tooltip> </Tr>
)}
</DataListAction>
</DataListItemRow>
</DataListItem>
); );
} }

View File

@@ -17,7 +17,11 @@ describe('<OrganizationTeamListItem />', () => {
test('should mount properly', async () => { test('should mount properly', async () => {
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<OrganizationTeamListItem team={team} detailUrl="/teams/1" /> <table>
<tbody>
<OrganizationTeamListItem team={team} detailUrl="/teams/1" />
</tbody>
</table>
); );
}); });
expect(wrapper.find('OrganizationTeamListItem').length).toBe(1); expect(wrapper.find('OrganizationTeamListItem').length).toBe(1);
@@ -26,10 +30,19 @@ describe('<OrganizationTeamListItem />', () => {
test('should render proper data', async () => { test('should render proper data', async () => {
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<OrganizationTeamListItem team={team} detailUrl="/teams/1" /> <table>
<tbody>
<OrganizationTeamListItem team={team} detailUrl="/teams/1" />
</tbody>
</table>
); );
}); });
expect(wrapper.find(`b[aria-label="team name"]`).text()).toBe('one'); expect(
wrapper
.find(`Td`)
.first()
.text()
).toBe('one');
expect(wrapper.find('PencilAltIcon').length).toBe(1); expect(wrapper.find('PencilAltIcon').length).toBe(1);
}); });
@@ -37,7 +50,11 @@ describe('<OrganizationTeamListItem />', () => {
team.summary_fields.user_capabilities.edit = false; team.summary_fields.user_capabilities.edit = false;
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<OrganizationTeamListItem team={team} detailUrl="/teams/1" /> <table>
<tbody>
<OrganizationTeamListItem team={team} detailUrl="/teams/1" />
</tbody>
</table>
); );
}); });
expect(wrapper.find('PencilAltIcon').length).toBe(0); expect(wrapper.find('PencilAltIcon').length).toBe(0);