mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 19:10:07 -03:30
convert JobList to PaginatedTable
This commit is contained in:
parent
dfa65225d9
commit
da16785201
@ -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}
|
||||
|
||||
@ -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} — {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} — {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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user