Adds source data to job list and job details view

This commit is contained in:
Alex Corey 2021-08-19 14:11:44 -04:00
parent 86390152bc
commit 48a044cc68
7 changed files with 158 additions and 17 deletions

View File

@ -12,7 +12,7 @@ import useSelected from 'hooks/useSelected';
import useExpanded from 'hooks/useExpanded';
import { isJobRunning, getJobModel } from 'util/jobs';
import { getQSConfig, parseQueryString } from 'util/qs';
import { UnifiedJobsAPI } from 'api';
import { UnifiedJobsAPI, InventorySourcesAPI } from 'api';
import AlertModal from '../AlertModal';
import DatalistToolbar from '../DataListToolbar';
import ErrorDetail from '../ErrorDetail';
@ -41,7 +41,13 @@ function JobList({ defaultParams, showTypeColumn = false }) {
const { me } = useConfig();
const location = useLocation();
const {
result: { results, count, relatedSearchableKeys, searchableKeys },
result: {
results,
count,
relatedSearchableKeys,
searchableKeys,
inventorySourceChoices,
},
error: contentError,
isLoading,
request: fetchJobs,
@ -49,13 +55,28 @@ function JobList({ defaultParams, showTypeColumn = false }) {
useCallback(
async () => {
const params = parseQueryString(qsConfig, location.search);
const [response, actionsResponse] = await Promise.all([
const [
response,
actionsResponse,
{
data: {
actions: {
GET: {
source: { choices },
},
},
},
},
] = await Promise.all([
UnifiedJobsAPI.read({ ...params }),
UnifiedJobsAPI.readOptions(),
InventorySourcesAPI.readOptions(),
]);
return {
results: response.data.results,
count: response.data.count,
inventorySourceChoices: choices,
relatedSearchableKeys: (
actionsResponse?.data?.related_search_fields || []
).map((val) => val.slice(0, -8)),
@ -69,6 +90,7 @@ function JobList({ defaultParams, showTypeColumn = false }) {
{
results: [],
count: 0,
inventorySourceChoices: [],
relatedSearchableKeys: [],
searchableKeys: [],
}
@ -264,6 +286,7 @@ function JobList({ defaultParams, showTypeColumn = false }) {
renderRow={(job, index) => (
<JobListItem
key={job.id}
inventorySourceLabels={inventorySourceChoices}
job={job}
isExpanded={expanded.some((row) => row.id === job.id)}
onExpand={() => handleExpand(job)}

View File

@ -8,6 +8,7 @@ import {
SystemJobsAPI,
UnifiedJobsAPI,
WorkflowJobsAPI,
InventorySourcesAPI,
} from 'api';
import {
mountWithContexts,
@ -140,6 +141,20 @@ describe('<JobList />', () => {
related_search_fields: [],
},
});
InventorySourcesAPI.readOptions.mockResolvedValue({
data: {
actions: {
GET: {
source: {
choices: [
['scm', 'Sourced from Project'],
['file', 'File, Directory or Script'],
],
},
},
},
},
});
debug = global.console.debug; // eslint-disable-line prefer-destructuring
global.console.debug = () => {};
});

View File

@ -28,6 +28,7 @@ function JobListItem({
onSelect,
showTypeColumn = false,
isSuperUser = false,
inventorySourceLabels,
}) {
const labelId = `check-action-${job.id}`;
@ -151,6 +152,16 @@ function JobListItem({
<Td colSpan={showTypeColumn ? 6 : 5}>
<ExpandableRowContent>
<DetailList>
{job.type === 'inventory_update' &&
inventorySourceLabels.length > 0 && (
<Detail
dataCy="job-inventory-source-type"
label={t`Source`}
value={inventorySourceLabels.map(([string, label]) =>
string === job.source ? label : null
)}
/>
)}
<LaunchedByDetail job={job} />
{job_template && (
<Detail

View File

@ -61,6 +61,34 @@ describe('<JobListItem />', () => {
expect(wrapper.find('LaunchButton').length).toBe(1);
});
test('should render souce data in expanded view', () => {
wrapper = mountWithContexts(
<table>
<tbody>
<JobListItem
isExpanded
inventorySourceLabels={[
['scm', 'Sourced from Project'],
['file', 'File, Directory or Script'],
]}
job={{
...mockJob,
type: 'inventory_update',
source: 'scm',
summary_fields: { user_capabilities: { start: false } },
}}
detailUrl={`/jobs/playbook/${mockJob.id}`}
onSelect={() => {}}
isSelected={false}
/>
</tbody>
</table>
);
expect(wrapper.find('ExpandableRowContent')).toHaveLength(1);
expect(
wrapper.find('dd[data-cy="job-inventory-source-type-value"]').text()
).toBe('Sourced from Project');
});
test('launch button hidden from users without launch capabilities', () => {
wrapper = mountWithContexts(
<table>

View File

@ -11,6 +11,7 @@ import {
import { t } from '@lingui/macro';
import { CaretLeftIcon } from '@patternfly/react-icons';
import { Card, PageSection } from '@patternfly/react-core';
import { InventorySourcesAPI } from 'api';
import ContentError from 'components/ContentError';
import ContentLoading from 'components/ContentLoading';
import RoutedTabs from 'components/RoutedTabs';
@ -41,7 +42,12 @@ function Job({ setBreadcrumb }) {
isLoading,
error,
request: fetchJob,
result: { jobDetail, eventRelatedSearchableKeys, eventSearchableKeys },
result: {
jobDetail,
eventRelatedSearchableKeys,
eventSearchableKeys,
inventorySourceChoices,
},
} = useRequest(
useCallback(async () => {
let eventOptions = {};
@ -63,9 +69,16 @@ function Job({ setBreadcrumb }) {
jobDetailData.summary_fields.credentials = results;
}
setBreadcrumb(jobDetailData);
let choices;
if (jobDetailData.type === 'inventory_update') {
choices = await InventorySourcesAPI.readOptions();
}
return {
inventorySourceChoices:
choices?.data?.actions?.GET?.source?.choices || [],
jobDetail: jobDetailData,
eventRelatedSearchableKeys: (
eventOptions?.related_search_fields || []
@ -77,6 +90,7 @@ function Job({ setBreadcrumb }) {
}, [id, type, setBreadcrumb]),
{
jobDetail: null,
inventorySourceChoices: [],
eventRelatedSearchableKeys: [],
eventSearchableKeys: [],
}
@ -145,7 +159,10 @@ function Job({ setBreadcrumb }) {
key={job.type === 'workflow_job' ? 'workflow-details' : 'details'}
path="/jobs/:typeSegment/:id/details"
>
<JobDetail job={job} />
<JobDetail
job={job}
inventorySourceLabels={inventorySourceChoices}
/>
</Route>,
<Route key="output" path="/jobs/:typeSegment/:id/output">
{job.type === 'workflow_job' ? (

View File

@ -50,7 +50,7 @@ const VERBOSITY = {
4: '4 (Connection Debug)',
};
function JobDetail({ job }) {
function JobDetail({ job, inventorySourceLabels }) {
const { me } = useConfig();
const {
created_by,
@ -203,17 +203,28 @@ function JobDetail({ job }) {
/>
)}
{inventory_source && (
<Detail
dataCy="job-inventory-source"
label={t`Inventory Source`}
value={
<Link
to={`/inventories/inventory/${inventory.id}/sources/${inventory_source.id}`}
>
{inventory_source.name}
</Link>
}
/>
<>
<Detail
dataCy="job-inventory-source"
label={t`Inventory Source`}
value={
<Link
to={`/inventories/inventory/${inventory.id}/sources/${inventory_source.id}`}
>
{inventory_source.name}
</Link>
}
/>
{inventorySourceLabels.length > 0 && (
<Detail
dataCy="job-inventory-source-type"
label={t`Source`}
value={inventorySourceLabels.map(([string, label]) =>
string === job.source ? label : null
)}
/>
)}
</>
)}
{inventory_source && inventory_source.source === 'scm' && (
<Detail

View File

@ -143,6 +143,42 @@ describe('<JobDetail />', () => {
assertDetail('Job Type', 'Run Command');
});
test('should display source data', () => {
wrapper = mountWithContexts(
<JobDetail
job={{
...mockJobData,
source: 'scm',
type: 'inventory_update',
module_name: 'command',
module_args: 'echo hello_world',
summary_fields: {
...mockJobData.summary_fields,
inventory_source: { id: 1, name: 'Inventory Source' },
credential: {
id: 2,
name: 'Machine cred',
description: '',
kind: 'ssh',
cloud: false,
kubernetes: false,
credential_type_id: 1,
},
source_workflow_job: {
id: 1234,
name: 'Test Source Workflow',
},
},
}}
inventorySourceLabels={[
['scm', 'Sourced from Project'],
['file', 'File, Directory or Script'],
]}
/>
);
assertDetail('Source', 'Sourced from Project');
});
test('should show schedule that launched workflow job', async () => {
wrapper = mountWithContexts(
<JobDetail