mirror of
https://github.com/ansible/awx.git
synced 2026-02-28 08:18:43 -03:30
Adds source data to job list and job details view
This commit is contained in:
@@ -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)}
|
||||||
|
|||||||
@@ -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 = () => {};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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' ? (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user