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`}
+
+
+ ,
+ ]}
+
+
+
);
}