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

View File

@@ -8,6 +8,7 @@ import {
SystemJobsAPI, SystemJobsAPI,
UnifiedJobsAPI, UnifiedJobsAPI,
WorkflowJobsAPI, WorkflowJobsAPI,
InventorySourcesAPI,
} from 'api'; } from 'api';
import { import {
mountWithContexts, mountWithContexts,
@@ -140,6 +141,20 @@ describe('<JobList />', () => {
related_search_fields: [], 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 debug = global.console.debug; // eslint-disable-line prefer-destructuring
global.console.debug = () => {}; global.console.debug = () => {};
}); });

View File

@@ -28,6 +28,7 @@ function JobListItem({
onSelect, onSelect,
showTypeColumn = false, showTypeColumn = false,
isSuperUser = false, isSuperUser = false,
inventorySourceLabels,
}) { }) {
const labelId = `check-action-${job.id}`; const labelId = `check-action-${job.id}`;
@@ -151,6 +152,16 @@ function JobListItem({
<Td colSpan={showTypeColumn ? 6 : 5}> <Td colSpan={showTypeColumn ? 6 : 5}>
<ExpandableRowContent> <ExpandableRowContent>
<DetailList> <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} /> <LaunchedByDetail job={job} />
{job_template && ( {job_template && (
<Detail <Detail

View File

@@ -61,6 +61,34 @@ describe('<JobListItem />', () => {
expect(wrapper.find('LaunchButton').length).toBe(1); 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', () => { test('launch button hidden from users without launch capabilities', () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<table> <table>

View File

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

View File

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

View File

@@ -143,6 +143,42 @@ describe('<JobDetail />', () => {
assertDetail('Job Type', 'Run Command'); 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 () => { test('should show schedule that launched workflow job', async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<JobDetail <JobDetail