diff --git a/awx/ui/src/components/RoutedTabs/RoutedTabs.js b/awx/ui/src/components/RoutedTabs/RoutedTabs.js index f68d6d733b..01c2afdfc4 100644 --- a/awx/ui/src/components/RoutedTabs/RoutedTabs.js +++ b/awx/ui/src/components/RoutedTabs/RoutedTabs.js @@ -1,7 +1,22 @@ import React from 'react'; import { shape, string, number, arrayOf, node, oneOfType } from 'prop-types'; -import { Tab, Tabs, TabTitleText } from '@patternfly/react-core'; +import { + Tab as PFTab, + Tabs as PFTabs, + TabTitleText, +} from '@patternfly/react-core'; import { useHistory, useLocation } from 'react-router-dom'; +import styled from 'styled-components'; + +const Tabs = styled(PFTabs)` + & > ul { + flex-grow: 1; + } +`; + +const Tab = styled(PFTab)` + ${(props) => props.hasstyle && `${props.hasstyle}`} +`; function RoutedTabs({ tabsArray }) { const history = useHistory(); @@ -31,7 +46,6 @@ function RoutedTabs({ tabsArray }) { history.push(link); } }; - return ( {tab.name}} aria-controls="" ouiaId={`${tab.name}-tab`} + hasstyle={tab.hasstyle} /> ))} @@ -57,7 +72,6 @@ RoutedTabs.propTypes = { tabsArray: arrayOf( shape({ id: number.isRequired, - link: string.isRequired, name: oneOfType([string.isRequired, node.isRequired]), }) ).isRequired, diff --git a/awx/ui/src/components/WorkflowOutputNavigation/WorkflowOutputNavigation.js b/awx/ui/src/components/WorkflowOutputNavigation/WorkflowOutputNavigation.js new file mode 100644 index 0000000000..e3dafc1a67 --- /dev/null +++ b/awx/ui/src/components/WorkflowOutputNavigation/WorkflowOutputNavigation.js @@ -0,0 +1,123 @@ +import React, { useState } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { t } from '@lingui/macro'; +import { + Select, + SelectOption, + SelectGroup, + SelectVariant, + Chip, +} from '@patternfly/react-core'; +import ChipGroup from 'components/ChipGroup'; +import { stringIsUUID } from 'util/strings'; + +const JOB_URL_SEGMENT_MAP = { + job: 'playbook', + project_update: 'project', + system_job: 'management', + system: 'system_job', + inventory_update: 'inventory', + workflow_job: 'workflow', +}; + +function WorkflowOutputNavigation({ relatedJobs, parentRef }) { + const { id } = useParams(); + + const relevantResults = relatedJobs.filter( + ({ + job: jobId, + summary_fields: { + unified_job_template: { unified_job_type }, + }, + }) => jobId && `${jobId}` !== id && unified_job_type !== 'workflow_approval' + ); + + const [isOpen, setIsOpen] = useState(false); + const [filterBy, setFilterBy] = useState(); + const [sortedJobs, setSortedJobs] = useState(relevantResults); + + const handleFilter = (v) => { + if (filterBy === v) { + setSortedJobs(relevantResults); + setFilterBy(); + } else { + setFilterBy(v); + setSortedJobs( + relevantResults.filter( + (node) => + node.summary_fields.job.status === v.toLowerCase() && + `${node.job}` !== id + ) + ); + } + }; + + const numSuccessJobs = relevantResults.filter( + (node) => node.summary_fields.job.status === 'successful' + ).length; + const numFailedJobs = relevantResults.length - numSuccessJobs; + + return ( + + ); +} + +export default WorkflowOutputNavigation; diff --git a/awx/ui/src/components/WorkflowOutputNavigation/index.js b/awx/ui/src/components/WorkflowOutputNavigation/index.js new file mode 100644 index 0000000000..d21e032087 --- /dev/null +++ b/awx/ui/src/components/WorkflowOutputNavigation/index.js @@ -0,0 +1 @@ +export { default } from './WorkflowOutputNavigation'; diff --git a/awx/ui/src/screens/Job/Job.js b/awx/ui/src/screens/Job/Job.js index 5ac0a3c347..3e60c14c71 100644 --- a/awx/ui/src/screens/Job/Job.js +++ b/awx/ui/src/screens/Job/Job.js @@ -1,4 +1,4 @@ -import React, { useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback, useRef } from 'react'; import { Route, Switch, @@ -18,6 +18,7 @@ import RoutedTabs from 'components/RoutedTabs'; import { getSearchableKeys } from 'components/PaginatedTable'; import useRequest from 'hooks/useRequest'; import { getJobModel } from 'util/jobs'; +import WorkflowOutputNavigation from 'components/WorkflowOutputNavigation'; import JobDetail from './JobDetail'; import JobOutput from './JobOutput'; import { WorkflowOutput } from './WorkflowOutput'; @@ -49,10 +50,12 @@ function Job({ setBreadcrumb }) { eventRelatedSearchableKeys, eventSearchableKeys, inventorySourceChoices, + relatedJobs, }, } = useRequest( useCallback(async () => { let eventOptions = {}; + let relatedJobData = {}; const { data: jobDetailData } = await getJobModel(type).readDetail(id); if (type !== 'workflow_job') { const { data: jobEventOptions } = await getJobModel( @@ -60,6 +63,14 @@ function Job({ setBreadcrumb }) { ).readEventOptions(id); eventOptions = jobEventOptions; } + if (jobDetailData.related.source_workflow_job) { + const { + data: { results }, + } = await getJobModel('workflow_job').readNodes( + jobDetailData.summary_fields.source_workflow_job.id + ); + relatedJobData = results; + } if ( jobDetailData?.summary_fields?.credentials?.find( (cred) => cred.kind === 'vault' @@ -82,6 +93,7 @@ function Job({ setBreadcrumb }) { inventorySourceChoices: choices?.data?.actions?.GET?.source?.choices || [], jobDetail: jobDetailData, + relatedJobs: relatedJobData, eventRelatedSearchableKeys: ( eventOptions?.related_search_fields || [] ).map((val) => val.slice(0, -8)), @@ -93,6 +105,7 @@ function Job({ setBreadcrumb }) { inventorySourceChoices: [], eventRelatedSearchableKeys: [], eventSearchableKeys: [], + relatedJobs: [], } ); @@ -101,7 +114,7 @@ function Job({ setBreadcrumb }) { }, [fetchJob]); const job = useWsJob(jobDetail); - + const ref = useRef(null); const tabsArray = [ { name: ( @@ -117,6 +130,16 @@ function Job({ setBreadcrumb }) { { name: t`Details`, link: `${match.url}/details`, id: 0 }, { name: t`Output`, link: `${match.url}/output`, id: 1 }, ]; + if (relatedJobs?.length > 0) { + tabsArray.push({ + name: ( + + ), + link: undefined, + id: 2, + hasstyle: 'margin-left: auto', + }); + } if (isLoading) { return ( @@ -147,46 +170,53 @@ function Job({ setBreadcrumb }) { return ( - - - - - + + - {job && [ - - - , - - {job.type === 'workflow_job' ? ( - - ) : ( - + + + {job && [ + + - )} - , - - - - {t`View Job Details`} - - - , - ]} - - + , + + {job.type === 'workflow_job' ? ( + + ) : ( + + )} + , + + + + {t`View Job Details`} + + + , + ]} + + + ); }