diff --git a/awx/ui_next/src/components/JobList/JobList.jsx b/awx/ui_next/src/components/JobList/JobList.jsx index ead8916fe8..500b3272e4 100644 --- a/awx/ui_next/src/components/JobList/JobList.jsx +++ b/awx/ui_next/src/components/JobList/JobList.jsx @@ -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 ( <> - + {i18n._(t`Name`)} + {i18n._(t`Status`)} + {showTypeColumn && {i18n._(t`Type`)}} + {i18n._(t`Start Time`)} + + {i18n._(t`Finish Time`)} + + + } 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={[ )} - renderItem={job => ( + renderRow={job => ( - - - - {job.status && } - , - - - - - {job.id} — {job.name} - - - - , - ...(showTypeColumn - ? [ - - {jobTypes[job.type]} - , - ] - : []), - - {job.finished ? formatDateString(job.finished) : ''} - , - ]} - /> - + + + + + + {job.id} — {job.name} + + + + + + {job.status && } + + {showTypeColumn && ( + {jobTypes[job.type]} + )} + {formatDateString(job.started)} + + {job.finished ? formatDateString(job.finished) : ''} + + + - {job.type !== 'system_job' && - job.summary_fields?.user_capabilities?.start ? ( - - - {({ handleRelaunch }) => ( - - )} - - - ) : ( - '' - )} - - - + + {({ handleRelaunch }) => ( + + )} + + + + ); } diff --git a/awx/ui_next/src/components/JobList/JobListItem.test.jsx b/awx/ui_next/src/components/JobList/JobListItem.test.jsx index fc453c3be4..6b9a9c3ae4 100644 --- a/awx/ui_next/src/components/JobList/JobListItem.test.jsx +++ b/awx/ui_next/src/components/JobList/JobListItem.test.jsx @@ -32,7 +32,11 @@ describe('', () => { initialEntries: ['/jobs'], }); wrapper = mountWithContexts( - {}} />, + + + {}} /> + +
, { context: { router: { history } } } ); }); @@ -51,32 +55,40 @@ describe('', () => { test('launch button hidden from users without launch capabilities', () => { wrapper = mountWithContexts( - {}} - isSelected={false} - /> + + + {}} + isSelected={false} + /> + +
); 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( - {}} - /> + + + {}} + /> + +
); - expect(wrapper.find('DataListCell[aria-label="type"]').length).toBe(1); + expect(wrapper.find('Td[dataLabel="Type"]').length).toBe(1); }); }); diff --git a/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx b/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx index 14c5c0b8ee..d1914ffae8 100644 --- a/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx +++ b/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx @@ -47,12 +47,15 @@ export default function HeaderRow({ qsConfig, children }) { - {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, + }) )} diff --git a/awx/ui_next/src/components/PaginatedTable/HeaderRow.test.jsx b/awx/ui_next/src/components/PaginatedTable/HeaderRow.test.jsx index ec1124c4b7..fa53baac60 100644 --- a/awx/ui_next/src/components/PaginatedTable/HeaderRow.test.jsx +++ b/awx/ui_next/src/components/PaginatedTable/HeaderRow.test.jsx @@ -62,4 +62,20 @@ describe('', () => { 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( + + + One + {nope && Hidden} + Two + +
+ ); + + const cells = wrapper.find('Th'); + expect(cells).toHaveLength(3); + }); });