convert JobList to PaginatedTable

This commit is contained in:
Keith Grant 2020-12-22 13:27:30 -08:00
parent dfa65225d9
commit da16785201
5 changed files with 131 additions and 139 deletions

View File

@ -7,7 +7,8 @@ import { Card } from '@patternfly/react-core';
import AlertModal from '../AlertModal';
import DatalistToolbar from '../DataListToolbar';
import ErrorDetail from '../ErrorDetail';
import PaginatedDataList, { ToolbarDeleteButton } from '../PaginatedDataList';
import { ToolbarDeleteButton } from '../PaginatedDataList';
import PaginatedTable, { HeaderRow, HeaderCell } from '../PaginatedTable';
import useRequest, {
useDeleteItems,
useDismissableError,
@ -27,7 +28,7 @@ import {
} from '../../api';
function JobList({ i18n, defaultParams, showTypeColumn = false }) {
const QS_CONFIG = getQSConfig(
const qsConfig = getQSConfig(
'job',
{
page: 1,
@ -49,7 +50,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
} = useRequest(
useCallback(
async () => {
const params = parseQueryString(QS_CONFIG, location.search);
const params = parseQueryString(qsConfig, location.search);
const [response, actionsResponse] = await Promise.all([
UnifiedJobsAPI.read({ ...params }),
UnifiedJobsAPI.readOptions(),
@ -81,7 +82,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
// TODO: update QS_CONFIG to be safe for deps array
const fetchJobsById = useCallback(
async ids => {
const params = parseQueryString(QS_CONFIG, location.search);
const params = parseQueryString(qsConfig, location.search);
params.id__in = ids.join(',');
const { data } = await UnifiedJobsAPI.read(params);
return data.results;
@ -89,7 +90,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
[location.search] // eslint-disable-line react-hooks/exhaustive-deps
);
const jobs = useWsJobs(results, fetchJobsById, QS_CONFIG);
const jobs = useWsJobs(results, fetchJobsById, qsConfig);
const isAllSelected = selected.length === jobs.length && selected.length > 0;
@ -145,7 +146,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
);
}, [selected]),
{
qsConfig: QS_CONFIG,
qsConfig,
allItemsSelected: isAllSelected,
fetchItems: fetchJobs,
}
@ -176,14 +177,13 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
return (
<>
<Card>
<PaginatedDataList
<PaginatedTable
contentError={contentError}
hasContentLoading={isLoading || isDeleteLoading || isCancelLoading}
items={jobs}
itemCount={count}
pluralizedItemName={i18n._(t`Jobs`)}
qsConfig={QS_CONFIG}
onRowClick={handleSelect}
qsConfig={qsConfig}
toolbarSearchColumns={[
{
name: i18n._(t`Name`),
@ -233,32 +233,17 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
key: 'job__limit',
},
]}
toolbarSortColumns={[
{
name: i18n._(t`Finish Time`),
key: 'finished',
},
{
name: i18n._(t`ID`),
key: 'id',
},
{
name: i18n._(t`Launched By`),
key: 'created_by__id',
},
{
name: i18n._(t`Name`),
key: 'name',
},
{
name: i18n._(t`Project`),
key: 'unified_job_template__project__id',
},
{
name: i18n._(t`Start Time`),
key: 'started',
},
]}
headerRow={
<HeaderRow qsConfig={qsConfig}>
<HeaderCell sortKey="name">{i18n._(t`Name`)}</HeaderCell>
<HeaderCell sortKey="status">{i18n._(t`Status`)}</HeaderCell>
{showTypeColumn && <HeaderCell>{i18n._(t`Type`)}</HeaderCell>}
<HeaderCell sortKey="started">{i18n._(t`Start Time`)}</HeaderCell>
<HeaderCell sortKey="finished">
{i18n._(t`Finish Time`)}
</HeaderCell>
</HeaderRow>
}
toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys}
renderToolbar={props => (
@ -267,7 +252,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
showSelectAll
isAllSelected={isAllSelected}
onSelectAll={handleSelectAll}
qsConfig={QS_CONFIG}
qsConfig={qsConfig}
additionalControls={[
<ToolbarDeleteButton
key="delete"
@ -283,7 +268,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
]}
/>
)}
renderItem={job => (
renderRow={job => (
<JobListItem
key={job.id}
job={job}

View File

@ -2,33 +2,19 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Button,
DataListAction as _DataListAction,
DataListCheck,
DataListItem,
DataListItemRow,
DataListItemCells,
Tooltip,
} from '@patternfly/react-core';
import { Button } from '@patternfly/react-core';
import { Tr, Td } from '@patternfly/react-table';
import { RocketIcon } from '@patternfly/react-icons';
import styled from 'styled-components';
import DataListCell from '../DataListCell';
import { ActionsTd, ActionItem } from '../PaginatedTable';
import LaunchButton from '../LaunchButton';
import StatusIcon from '../StatusIcon';
import StatusLabel from '../StatusLabel';
import { formatDateString } from '../../util/dates';
import { JOB_TYPE_URL_SEGMENTS } from '../../constants';
const DataListAction = styled(_DataListAction)`
align-items: center;
display: grid;
grid-gap: 16px;
grid-template-columns: 40px;
`;
function JobListItem({
i18n,
job,
rowIndex,
isSelected,
onSelect,
showTypeColumn = false,
@ -45,66 +31,56 @@ function JobListItem({
};
return (
<DataListItem aria-labelledby={labelId} id={`${job.id}`}>
<DataListItemRow>
<DataListCheck
id={`select-job-${job.id}`}
checked={isSelected}
onChange={onSelect}
aria-labelledby={labelId}
/>
<DataListItemCells
dataListCells={[
<DataListCell key="status" isFilled={false}>
{job.status && <StatusIcon status={job.status} />}
</DataListCell>,
<DataListCell key="name">
<span>
<Link to={`/jobs/${JOB_TYPE_URL_SEGMENTS[job.type]}/${job.id}`}>
<b>
{job.id} &mdash; {job.name}
</b>
</Link>
</span>
</DataListCell>,
...(showTypeColumn
? [
<DataListCell key="type" aria-label="type">
{jobTypes[job.type]}
</DataListCell>,
]
: []),
<DataListCell key="finished">
{job.finished ? formatDateString(job.finished) : ''}
</DataListCell>,
]}
/>
<DataListAction
aria-label="actions"
aria-labelledby={labelId}
id={labelId}
<Tr id={`job-row-${job.id}`}>
<Td
select={{
rowIndex,
isSelected,
onSelect,
disable: false,
}}
/>
<Td id={labelId} dataLabel={i18n._(t`Name`)}>
<span>
<Link to={`/jobs/${JOB_TYPE_URL_SEGMENTS[job.type]}/${job.id}`}>
<b>
{job.id} &mdash; {job.name}
</b>
</Link>
</span>
</Td>
<Td dataLabel={i18n._(t`Status`)}>
{job.status && <StatusLabel status={job.status} />}
</Td>
{showTypeColumn && (
<Td dataLabel={i18n._(t`Type`)}>{jobTypes[job.type]}</Td>
)}
<Td dataLabel={i18n._(t`Start Time`)}>{formatDateString(job.started)}</Td>
<Td dataLabel={i18n._(t`Finish Time`)}>
{job.finished ? formatDateString(job.finished) : ''}
</Td>
<ActionsTd dataLabel={i18n._(t`Actions`)}>
<ActionItem
visible={
job.type !== 'system_job' &&
job.summary_fields?.user_capabilities?.start
}
tooltip={i18n._(t`Relaunch Job`)}
>
{job.type !== 'system_job' &&
job.summary_fields?.user_capabilities?.start ? (
<Tooltip content={i18n._(t`Relaunch Job`)} position="top">
<LaunchButton resource={job}>
{({ handleRelaunch }) => (
<Button
variant="plain"
onClick={handleRelaunch}
aria-label={i18n._(t`Relaunch`)}
>
<RocketIcon />
</Button>
)}
</LaunchButton>
</Tooltip>
) : (
''
)}
</DataListAction>
</DataListItemRow>
</DataListItem>
<LaunchButton resource={job}>
{({ handleRelaunch }) => (
<Button
variant="plain"
onClick={handleRelaunch}
aria-label={i18n._(t`Relaunch`)}
>
<RocketIcon />
</Button>
)}
</LaunchButton>
</ActionItem>
</ActionsTd>
</Tr>
);
}

View File

@ -32,7 +32,11 @@ describe('<JobListItem />', () => {
initialEntries: ['/jobs'],
});
wrapper = mountWithContexts(
<JobListItem job={mockJob} isSelected onSelect={() => {}} />,
<table>
<tbody>
<JobListItem job={mockJob} isSelected onSelect={() => {}} />
</tbody>
</table>,
{ context: { router: { history } } }
);
});
@ -51,32 +55,40 @@ describe('<JobListItem />', () => {
test('launch button hidden from users without launch capabilities', () => {
wrapper = mountWithContexts(
<JobListItem
job={{
...mockJob,
summary_fields: { user_capabilities: { start: false } },
}}
detailUrl={`/jobs/playbook/${mockJob.id}`}
onSelect={() => {}}
isSelected={false}
/>
<table>
<tbody>
<JobListItem
job={{
...mockJob,
summary_fields: { user_capabilities: { start: false } },
}}
detailUrl={`/jobs/playbook/${mockJob.id}`}
onSelect={() => {}}
isSelected={false}
/>
</tbody>
</table>
);
expect(wrapper.find('LaunchButton').length).toBe(0);
});
test('should hide type column when showTypeColumn is false', () => {
expect(wrapper.find('DataListCell[aria-label="type"]').length).toBe(0);
expect(wrapper.find('Td[dataLabel="Type"]').length).toBe(0);
});
test('should show type column when showTypeColumn is true', () => {
wrapper = mountWithContexts(
<JobListItem
job={mockJob}
showTypeColumn
isSelected
onSelect={() => {}}
/>
<table>
<tbody>
<JobListItem
job={mockJob}
showTypeColumn
isSelected
onSelect={() => {}}
/>
</tbody>
</table>
);
expect(wrapper.find('DataListCell[aria-label="type"]').length).toBe(1);
expect(wrapper.find('Td[dataLabel="Type"]').length).toBe(1);
});
});

View File

@ -47,12 +47,15 @@ export default function HeaderRow({ qsConfig, children }) {
<Thead>
<Tr>
<Th />
{React.Children.map(children, child =>
React.cloneElement(child, {
onSort,
sortBy,
columnIndex: child.props.sortKey,
})
{React.Children.map(
children,
child =>
child &&
React.cloneElement(child, {
onSort,
sortBy,
columnIndex: child.props.sortKey,
})
)}
</Tr>
</Thead>

View File

@ -62,4 +62,20 @@ describe('<HeaderRow />', () => {
const cell = wrapper.find('Th').at(2);
expect(cell.prop('sort')).toEqual(null);
});
test('should handle null children gracefully', async () => {
const nope = false;
const wrapper = mountWithContexts(
<table>
<HeaderRow qsConfig={qsConfig}>
<HeaderCell sortKey="one">One</HeaderCell>
{nope && <HeaderCell>Hidden</HeaderCell>}
<HeaderCell>Two</HeaderCell>
</HeaderRow>
</table>
);
const cells = wrapper.find('Th');
expect(cells).toHaveLength(3);
});
});