mirror of
https://github.com/ansible/awx.git
synced 2026-03-01 00:38:45 -03:30
51
awx/ui_next/src/components/DetailList/LaunchedByDetail.jsx
Normal file
51
awx/ui_next/src/components/DetailList/LaunchedByDetail.jsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import Detail from './Detail';
|
||||||
|
|
||||||
|
const getLaunchedByDetails = ({ summary_fields = {}, related = {} }) => {
|
||||||
|
const {
|
||||||
|
created_by: createdBy,
|
||||||
|
job_template: jobTemplate,
|
||||||
|
schedule,
|
||||||
|
} = summary_fields;
|
||||||
|
const { schedule: relatedSchedule } = related;
|
||||||
|
|
||||||
|
if (!createdBy && !schedule) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let link;
|
||||||
|
let value;
|
||||||
|
|
||||||
|
if (createdBy) {
|
||||||
|
link = `/users/${createdBy.id}`;
|
||||||
|
value = createdBy.username;
|
||||||
|
} else if (relatedSchedule && jobTemplate) {
|
||||||
|
link = `/templates/job_template/${jobTemplate.id}/schedules/${schedule.id}`;
|
||||||
|
value = schedule.name;
|
||||||
|
} else {
|
||||||
|
link = null;
|
||||||
|
value = schedule.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { link, value };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LaunchedByDetail({ job, i18n }) {
|
||||||
|
const { value: launchedByValue, link: launchedByLink } =
|
||||||
|
getLaunchedByDetails(job) || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Launched By`)}
|
||||||
|
value={
|
||||||
|
launchedByLink ? (
|
||||||
|
<Link to={`${launchedByLink}`}>{launchedByValue}</Link>
|
||||||
|
) : (
|
||||||
|
launchedByValue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ export { default as DeletedDetail } from './DeletedDetail';
|
|||||||
export { default as UserDateDetail } from './UserDateDetail';
|
export { default as UserDateDetail } from './UserDateDetail';
|
||||||
export { default as DetailBadge } from './DetailBadge';
|
export { default as DetailBadge } from './DetailBadge';
|
||||||
export { default as ArrayDetail } from './ArrayDetail';
|
export { default as ArrayDetail } from './ArrayDetail';
|
||||||
|
export { default as LaunchedByDetail } from './LaunchedByDetail';
|
||||||
/*
|
/*
|
||||||
NOTE: CodeDetail cannot be imported here, as it causes circular
|
NOTE: CodeDetail cannot be imported here, as it causes circular
|
||||||
dependencies in testing environment. Import it directly from
|
dependencies in testing environment. Import it directly from
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { Card } from '@patternfly/react-core';
|
|||||||
import AlertModal from '../AlertModal';
|
import AlertModal from '../AlertModal';
|
||||||
import DatalistToolbar from '../DataListToolbar';
|
import DatalistToolbar from '../DataListToolbar';
|
||||||
import ErrorDetail from '../ErrorDetail';
|
import ErrorDetail from '../ErrorDetail';
|
||||||
import PaginatedDataList, { ToolbarDeleteButton } from '../PaginatedDataList';
|
import { ToolbarDeleteButton } from '../PaginatedDataList';
|
||||||
|
import PaginatedTable, { HeaderRow, HeaderCell } from '../PaginatedTable';
|
||||||
import useRequest, {
|
import useRequest, {
|
||||||
useDeleteItems,
|
useDeleteItems,
|
||||||
useDismissableError,
|
useDismissableError,
|
||||||
@@ -27,7 +28,7 @@ import {
|
|||||||
} from '../../api';
|
} from '../../api';
|
||||||
|
|
||||||
function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
||||||
const QS_CONFIG = getQSConfig(
|
const qsConfig = getQSConfig(
|
||||||
'job',
|
'job',
|
||||||
{
|
{
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -49,7 +50,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
|||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(
|
useCallback(
|
||||||
async () => {
|
async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
const params = parseQueryString(qsConfig, location.search);
|
||||||
const [response, actionsResponse] = await Promise.all([
|
const [response, actionsResponse] = await Promise.all([
|
||||||
UnifiedJobsAPI.read({ ...params }),
|
UnifiedJobsAPI.read({ ...params }),
|
||||||
UnifiedJobsAPI.readOptions(),
|
UnifiedJobsAPI.readOptions(),
|
||||||
@@ -81,7 +82,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
|||||||
// TODO: update QS_CONFIG to be safe for deps array
|
// TODO: update QS_CONFIG to be safe for deps array
|
||||||
const fetchJobsById = useCallback(
|
const fetchJobsById = useCallback(
|
||||||
async ids => {
|
async ids => {
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
const params = parseQueryString(qsConfig, location.search);
|
||||||
params.id__in = ids.join(',');
|
params.id__in = ids.join(',');
|
||||||
const { data } = await UnifiedJobsAPI.read(params);
|
const { data } = await UnifiedJobsAPI.read(params);
|
||||||
return data.results;
|
return data.results;
|
||||||
@@ -89,7 +90,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
|||||||
[location.search] // eslint-disable-line react-hooks/exhaustive-deps
|
[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;
|
const isAllSelected = selected.length === jobs.length && selected.length > 0;
|
||||||
|
|
||||||
@@ -145,7 +146,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
|||||||
);
|
);
|
||||||
}, [selected]),
|
}, [selected]),
|
||||||
{
|
{
|
||||||
qsConfig: QS_CONFIG,
|
qsConfig,
|
||||||
allItemsSelected: isAllSelected,
|
allItemsSelected: isAllSelected,
|
||||||
fetchItems: fetchJobs,
|
fetchItems: fetchJobs,
|
||||||
}
|
}
|
||||||
@@ -176,14 +177,13 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card>
|
<Card>
|
||||||
<PaginatedDataList
|
<PaginatedTable
|
||||||
contentError={contentError}
|
contentError={contentError}
|
||||||
hasContentLoading={isLoading || isDeleteLoading || isCancelLoading}
|
hasContentLoading={isLoading || isDeleteLoading || isCancelLoading}
|
||||||
items={jobs}
|
items={jobs}
|
||||||
itemCount={count}
|
itemCount={count}
|
||||||
pluralizedItemName={i18n._(t`Jobs`)}
|
pluralizedItemName={i18n._(t`Jobs`)}
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={qsConfig}
|
||||||
onRowClick={handleSelect}
|
|
||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
@@ -233,32 +233,17 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
|||||||
key: 'job__limit',
|
key: 'job__limit',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSortColumns={[
|
headerRow={
|
||||||
{
|
<HeaderRow qsConfig={qsConfig} isExpandable>
|
||||||
name: i18n._(t`Finish Time`),
|
<HeaderCell sortKey="name">{i18n._(t`Name`)}</HeaderCell>
|
||||||
key: 'finished',
|
<HeaderCell sortKey="status">{i18n._(t`Status`)}</HeaderCell>
|
||||||
},
|
{showTypeColumn && <HeaderCell>{i18n._(t`Type`)}</HeaderCell>}
|
||||||
{
|
<HeaderCell sortKey="started">{i18n._(t`Start Time`)}</HeaderCell>
|
||||||
name: i18n._(t`ID`),
|
<HeaderCell sortKey="finished">
|
||||||
key: 'id',
|
{i18n._(t`Finish Time`)}
|
||||||
},
|
</HeaderCell>
|
||||||
{
|
</HeaderRow>
|
||||||
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',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
toolbarSearchableKeys={searchableKeys}
|
toolbarSearchableKeys={searchableKeys}
|
||||||
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
renderToolbar={props => (
|
renderToolbar={props => (
|
||||||
@@ -267,7 +252,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
|||||||
showSelectAll
|
showSelectAll
|
||||||
isAllSelected={isAllSelected}
|
isAllSelected={isAllSelected}
|
||||||
onSelectAll={handleSelectAll}
|
onSelectAll={handleSelectAll}
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={qsConfig}
|
||||||
additionalControls={[
|
additionalControls={[
|
||||||
<ToolbarDeleteButton
|
<ToolbarDeleteButton
|
||||||
key="delete"
|
key="delete"
|
||||||
@@ -283,13 +268,14 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderItem={job => (
|
renderRow={(job, index) => (
|
||||||
<JobListItem
|
<JobListItem
|
||||||
key={job.id}
|
key={job.id}
|
||||||
job={job}
|
job={job}
|
||||||
showTypeColumn={showTypeColumn}
|
showTypeColumn={showTypeColumn}
|
||||||
onSelect={() => handleSelect(job)}
|
onSelect={() => handleSelect(job)}
|
||||||
isSelected={selected.some(row => row.id === job.id)}
|
isSelected={selected.some(row => row.id === job.id)}
|
||||||
|
rowIndex={index}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,39 +1,29 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import { Button, Chip } from '@patternfly/react-core';
|
||||||
Button,
|
import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
|
||||||
DataListAction as _DataListAction,
|
|
||||||
DataListCheck,
|
|
||||||
DataListItem,
|
|
||||||
DataListItemRow,
|
|
||||||
DataListItemCells,
|
|
||||||
Tooltip,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { RocketIcon } from '@patternfly/react-icons';
|
import { RocketIcon } from '@patternfly/react-icons';
|
||||||
import styled from 'styled-components';
|
import { ActionsTd, ActionItem } from '../PaginatedTable';
|
||||||
import DataListCell from '../DataListCell';
|
|
||||||
import LaunchButton from '../LaunchButton';
|
import LaunchButton from '../LaunchButton';
|
||||||
import StatusIcon from '../StatusIcon';
|
import StatusLabel from '../StatusLabel';
|
||||||
|
import { DetailList, Detail, LaunchedByDetail } from '../DetailList';
|
||||||
|
import ChipGroup from '../ChipGroup';
|
||||||
|
import CredentialChip from '../CredentialChip';
|
||||||
import { formatDateString } from '../../util/dates';
|
import { formatDateString } from '../../util/dates';
|
||||||
import { JOB_TYPE_URL_SEGMENTS } from '../../constants';
|
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({
|
function JobListItem({
|
||||||
i18n,
|
i18n,
|
||||||
job,
|
job,
|
||||||
|
rowIndex,
|
||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
showTypeColumn = false,
|
showTypeColumn = false,
|
||||||
}) {
|
}) {
|
||||||
const labelId = `check-action-${job.id}`;
|
const labelId = `check-action-${job.id}`;
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
const jobTypes = {
|
const jobTypes = {
|
||||||
project_update: i18n._(t`Source Control Update`),
|
project_update: i18n._(t`Source Control Update`),
|
||||||
@@ -44,67 +34,123 @@ function JobListItem({
|
|||||||
workflow_job: i18n._(t`Workflow Job`),
|
workflow_job: i18n._(t`Workflow Job`),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { credentials, inventory, labels } = job.summary_fields;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataListItem aria-labelledby={labelId} id={`${job.id}`}>
|
<>
|
||||||
<DataListItemRow>
|
<Tr id={`job-row-${job.id}`}>
|
||||||
<DataListCheck
|
<Td
|
||||||
id={`select-job-${job.id}`}
|
expand={{
|
||||||
checked={isSelected}
|
rowIndex: job.id,
|
||||||
onChange={onSelect}
|
isExpanded,
|
||||||
aria-labelledby={labelId}
|
onToggle: () => setIsExpanded(!isExpanded),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<DataListItemCells
|
<Td
|
||||||
dataListCells={[
|
select={{
|
||||||
<DataListCell key="status" isFilled={false}>
|
rowIndex,
|
||||||
{job.status && <StatusIcon status={job.status} />}
|
isSelected,
|
||||||
</DataListCell>,
|
onSelect,
|
||||||
<DataListCell key="name">
|
}}
|
||||||
<span>
|
dataLabel={i18n._(t`Select`)}
|
||||||
<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
|
<Td id={labelId} dataLabel={i18n._(t`Name`)}>
|
||||||
aria-label="actions"
|
<span>
|
||||||
aria-labelledby={labelId}
|
<Link to={`/jobs/${JOB_TYPE_URL_SEGMENTS[job.type]}/${job.id}`}>
|
||||||
id={labelId}
|
<b>
|
||||||
>
|
{job.id} — {job.name}
|
||||||
{job.type !== 'system_job' &&
|
</b>
|
||||||
job.summary_fields?.user_capabilities?.start ? (
|
</Link>
|
||||||
<Tooltip content={i18n._(t`Relaunch Job`)} position="top">
|
</span>
|
||||||
<LaunchButton resource={job}>
|
</Td>
|
||||||
{({ handleRelaunch }) => (
|
<Td dataLabel={i18n._(t`Status`)}>
|
||||||
<Button
|
{job.status && <StatusLabel status={job.status} />}
|
||||||
variant="plain"
|
</Td>
|
||||||
onClick={handleRelaunch}
|
{showTypeColumn && (
|
||||||
aria-label={i18n._(t`Relaunch`)}
|
<Td dataLabel={i18n._(t`Type`)}>{jobTypes[job.type]}</Td>
|
||||||
>
|
)}
|
||||||
<RocketIcon />
|
<Td dataLabel={i18n._(t`Start Time`)}>
|
||||||
</Button>
|
{formatDateString(job.started)}
|
||||||
)}
|
</Td>
|
||||||
</LaunchButton>
|
<Td dataLabel={i18n._(t`Finish Time`)}>
|
||||||
</Tooltip>
|
{job.finished ? formatDateString(job.finished) : ''}
|
||||||
) : (
|
</Td>
|
||||||
''
|
<ActionsTd dataLabel={i18n._(t`Actions`)}>
|
||||||
)}
|
<ActionItem
|
||||||
</DataListAction>
|
visible={
|
||||||
</DataListItemRow>
|
job.type !== 'system_job' &&
|
||||||
</DataListItem>
|
job.summary_fields?.user_capabilities?.start
|
||||||
|
}
|
||||||
|
tooltip={i18n._(t`Relaunch Job`)}
|
||||||
|
>
|
||||||
|
<LaunchButton resource={job}>
|
||||||
|
{({ handleRelaunch }) => (
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
onClick={handleRelaunch}
|
||||||
|
aria-label={i18n._(t`Relaunch`)}
|
||||||
|
>
|
||||||
|
<RocketIcon />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</LaunchButton>
|
||||||
|
</ActionItem>
|
||||||
|
</ActionsTd>
|
||||||
|
</Tr>
|
||||||
|
<Tr isExpanded={isExpanded} id={`expanded-job-row-${job.id}`}>
|
||||||
|
<Td colSpan={2} />
|
||||||
|
<Td colSpan={showTypeColumn ? 5 : 4}>
|
||||||
|
<ExpandableRowContent>
|
||||||
|
<DetailList>
|
||||||
|
<LaunchedByDetail job={job} i18n={i18n} />
|
||||||
|
{credentials && credentials.length > 0 && (
|
||||||
|
<Detail
|
||||||
|
fullWidth
|
||||||
|
label={i18n._(t`Credentials`)}
|
||||||
|
value={
|
||||||
|
<ChipGroup numChips={5} totalChips={credentials.length}>
|
||||||
|
{credentials.map(c => (
|
||||||
|
<CredentialChip key={c.id} credential={c} isReadOnly />
|
||||||
|
))}
|
||||||
|
</ChipGroup>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{labels && labels.count > 0 && (
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Labels`)}
|
||||||
|
value={
|
||||||
|
<ChipGroup numChips={5} totalChips={labels.results.length}>
|
||||||
|
{labels.results.map(l => (
|
||||||
|
<Chip key={l.id} isReadOnly>
|
||||||
|
{l.name}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</ChipGroup>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{inventory && (
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Inventory`)}
|
||||||
|
value={
|
||||||
|
<Link
|
||||||
|
to={
|
||||||
|
inventory.kind === 'smart'
|
||||||
|
? `/inventories/smart_inventory/${inventory.id}`
|
||||||
|
: `/inventories/inventory/${inventory.id}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{inventory.name}
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DetailList>
|
||||||
|
</ExpandableRowContent>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,11 @@ describe('<JobListItem />', () => {
|
|||||||
initialEntries: ['/jobs'],
|
initialEntries: ['/jobs'],
|
||||||
});
|
});
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<JobListItem job={mockJob} isSelected onSelect={() => {}} />,
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<JobListItem job={mockJob} isSelected onSelect={() => {}} />
|
||||||
|
</tbody>
|
||||||
|
</table>,
|
||||||
{ context: { router: { history } } }
|
{ context: { router: { history } } }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -51,32 +55,40 @@ describe('<JobListItem />', () => {
|
|||||||
|
|
||||||
test('launch button hidden from users without launch capabilities', () => {
|
test('launch button hidden from users without launch capabilities', () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<JobListItem
|
<table>
|
||||||
job={{
|
<tbody>
|
||||||
...mockJob,
|
<JobListItem
|
||||||
summary_fields: { user_capabilities: { start: false } },
|
job={{
|
||||||
}}
|
...mockJob,
|
||||||
detailUrl={`/jobs/playbook/${mockJob.id}`}
|
summary_fields: { user_capabilities: { start: false } },
|
||||||
onSelect={() => {}}
|
}}
|
||||||
isSelected={false}
|
detailUrl={`/jobs/playbook/${mockJob.id}`}
|
||||||
/>
|
onSelect={() => {}}
|
||||||
|
isSelected={false}
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
);
|
);
|
||||||
expect(wrapper.find('LaunchButton').length).toBe(0);
|
expect(wrapper.find('LaunchButton').length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should hide type column when showTypeColumn is false', () => {
|
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', () => {
|
test('should show type column when showTypeColumn is true', () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<JobListItem
|
<table>
|
||||||
job={mockJob}
|
<tbody>
|
||||||
showTypeColumn
|
<JobListItem
|
||||||
isSelected
|
job={mockJob}
|
||||||
onSelect={() => {}}
|
showTypeColumn
|
||||||
/>
|
isSelected
|
||||||
|
onSelect={() => {}}
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
);
|
);
|
||||||
expect(wrapper.find('DataListCell[aria-label="type"]').length).toBe(1);
|
expect(wrapper.find('Td[dataLabel="Type"]').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const Th = styled(PFTh)`
|
|||||||
--pf-c-table--cell--Overflow: initial;
|
--pf-c-table--cell--Overflow: initial;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function HeaderRow({ qsConfig, children }) {
|
export default function HeaderRow({ qsConfig, isExpandable, children }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
@@ -41,25 +41,38 @@ export default function HeaderRow({ qsConfig, children }) {
|
|||||||
index: sortKey || qsConfig.defaultParams?.order_by,
|
index: sortKey || qsConfig.defaultParams?.order_by,
|
||||||
direction: params.order_by?.startsWith('-') ? 'desc' : 'asc',
|
direction: params.order_by?.startsWith('-') ? 'desc' : 'asc',
|
||||||
};
|
};
|
||||||
|
const idPrefix = `${qsConfig.namespace}-table-sort`;
|
||||||
|
|
||||||
// empty first Th aligns with checkboxes in table rows
|
// empty first Th aligns with checkboxes in table rows
|
||||||
return (
|
return (
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
|
{isExpandable && <Th />}
|
||||||
<Th />
|
<Th />
|
||||||
{React.Children.map(children, child =>
|
{React.Children.map(
|
||||||
React.cloneElement(child, {
|
children,
|
||||||
onSort,
|
child =>
|
||||||
sortBy,
|
child &&
|
||||||
columnIndex: child.props.sortKey,
|
React.cloneElement(child, {
|
||||||
})
|
onSort,
|
||||||
|
sortBy,
|
||||||
|
columnIndex: child.props.sortKey,
|
||||||
|
idPrefix,
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HeaderCell({ sortKey, onSort, sortBy, columnIndex, children }) {
|
export function HeaderCell({
|
||||||
|
sortKey,
|
||||||
|
onSort,
|
||||||
|
sortBy,
|
||||||
|
columnIndex,
|
||||||
|
idPrefix,
|
||||||
|
children,
|
||||||
|
}) {
|
||||||
const sort = sortKey
|
const sort = sortKey
|
||||||
? {
|
? {
|
||||||
onSort: (event, key, order) => onSort(sortKey, order),
|
onSort: (event, key, order) => onSort(sortKey, order),
|
||||||
@@ -67,5 +80,9 @@ export function HeaderCell({ sortKey, onSort, sortBy, columnIndex, children }) {
|
|||||||
columnIndex,
|
columnIndex,
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
return <Th sort={sort}>{children}</Th>;
|
return (
|
||||||
|
<Th sort={sort} id={sortKey ? `${idPrefix}-${sortKey}` : null}>
|
||||||
|
{children}
|
||||||
|
</Th>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,4 +62,20 @@ describe('<HeaderRow />', () => {
|
|||||||
const cell = wrapper.find('Th').at(2);
|
const cell = wrapper.find('Th').at(2);
|
||||||
expect(cell.prop('sort')).toEqual(null);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
DetailList,
|
DetailList,
|
||||||
Detail,
|
Detail,
|
||||||
UserDateDetail,
|
UserDateDetail,
|
||||||
|
LaunchedByDetail,
|
||||||
} from '../../../components/DetailList';
|
} from '../../../components/DetailList';
|
||||||
import { CardBody, CardActionsRow } from '../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../components/Card';
|
||||||
import ChipGroup from '../../../components/ChipGroup';
|
import ChipGroup from '../../../components/ChipGroup';
|
||||||
@@ -53,35 +54,6 @@ const VERBOSITY = {
|
|||||||
4: '4 (Connection Debug)',
|
4: '4 (Connection Debug)',
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLaunchedByDetails = ({ summary_fields = {}, related = {} }) => {
|
|
||||||
const {
|
|
||||||
created_by: createdBy,
|
|
||||||
job_template: jobTemplate,
|
|
||||||
schedule,
|
|
||||||
} = summary_fields;
|
|
||||||
const { schedule: relatedSchedule } = related;
|
|
||||||
|
|
||||||
if (!createdBy && !schedule) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let link;
|
|
||||||
let value;
|
|
||||||
|
|
||||||
if (createdBy) {
|
|
||||||
link = `/users/${createdBy.id}`;
|
|
||||||
value = createdBy.username;
|
|
||||||
} else if (relatedSchedule && jobTemplate) {
|
|
||||||
link = `/templates/job_template/${jobTemplate.id}/schedules/${schedule.id}`;
|
|
||||||
value = schedule.name;
|
|
||||||
} else {
|
|
||||||
link = null;
|
|
||||||
value = schedule.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { link, value };
|
|
||||||
};
|
|
||||||
|
|
||||||
function JobDetail({ job, i18n }) {
|
function JobDetail({ job, i18n }) {
|
||||||
const {
|
const {
|
||||||
created_by,
|
created_by,
|
||||||
@@ -107,9 +79,6 @@ function JobDetail({ job, i18n }) {
|
|||||||
workflow_job: i18n._(t`Workflow Job`),
|
workflow_job: i18n._(t`Workflow Job`),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { value: launchedByValue, link: launchedByLink } =
|
|
||||||
getLaunchedByDetails(job) || {};
|
|
||||||
|
|
||||||
const deleteJob = async () => {
|
const deleteJob = async () => {
|
||||||
try {
|
try {
|
||||||
switch (job.type) {
|
switch (job.type) {
|
||||||
@@ -207,16 +176,7 @@ function JobDetail({ job, i18n }) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Detail label={i18n._(t`Job Type`)} value={jobTypes[job.type]} />
|
<Detail label={i18n._(t`Job Type`)} value={jobTypes[job.type]} />
|
||||||
<Detail
|
<LaunchedByDetail job={job} i18n={i18n} />
|
||||||
label={i18n._(t`Launched By`)}
|
|
||||||
value={
|
|
||||||
launchedByLink ? (
|
|
||||||
<Link to={`${launchedByLink}`}>{launchedByValue}</Link>
|
|
||||||
) : (
|
|
||||||
launchedByValue
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{inventory && (
|
{inventory && (
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Inventory`)}
|
label={i18n._(t`Inventory`)}
|
||||||
|
|||||||
Reference in New Issue
Block a user