Easier review workflow output (#12459)

* Adds new tab component and positions it properly on screen

* Adds filtering, and navigation to node outputs
This commit is contained in:
Alex Corey 2022-08-08 16:13:51 -04:00 committed by GitHub
parent 279cebcef3
commit 2c9a0444e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 211 additions and 43 deletions

View File

@ -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 (
<Tabs
activeKey={getActiveTabId()}
@ -43,10 +57,11 @@ function RoutedTabs({ tabsArray }) {
aria-label={typeof tab.name === 'string' ? tab.name : null}
eventKey={tab.id}
key={tab.id}
href={`#${tab.link}`}
href={!tab.hasstyle && `#${tab.link}`}
title={<TabTitleText>{tab.name}</TabTitleText>}
aria-controls=""
ouiaId={`${tab.name}-tab`}
hasstyle={tab.hasstyle}
/>
))}
</Tabs>
@ -57,7 +72,6 @@ RoutedTabs.propTypes = {
tabsArray: arrayOf(
shape({
id: number.isRequired,
link: string.isRequired,
name: oneOfType([string.isRequired, node.isRequired]),
})
).isRequired,

View File

@ -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 (
<Select
key={`${id}`}
variant={SelectVariant.typeaheadMulti}
menuAppendTo={parentRef?.current}
onToggle={() => {
setIsOpen(!isOpen);
}}
selections={filterBy}
onSelect={(e, v) => {
if (v !== 'Failed' && v !== 'Successful') return;
handleFilter(v);
}}
isOpen={isOpen}
isGrouped
hasInlineFilter
placeholderText={t`Workflow Job 1/${relevantResults.length}`}
chipGroupComponent={
<ChipGroup numChips={1} totalChips={1}>
<Chip key={filterBy} onClick={() => handleFilter(filterBy)}>
{[filterBy]}
</Chip>
</ChipGroup>
}
>
{[
<SelectGroup label={t`Workflow Statuses`} key="status">
<SelectOption
description={t`Filter by failed jobs`}
key="failed"
value={t`Failed`}
itemCount={numFailedJobs}
/>
<SelectOption
description={t`Filter by successful jobs`}
key="successful"
value={t`Successful`}
itemCount={numSuccessJobs}
/>
</SelectGroup>,
<SelectGroup label={t`Workflow Nodes`} key="nodes">
{sortedJobs?.map((node) => (
<SelectOption
key={node.id}
to={`/jobs/${
JOB_URL_SEGMENT_MAP[
node.summary_fields.unified_job_template.unified_job_type
]
}/${node.summary_fields.job?.id}/output`}
component={Link}
value={node.summary_fields.unified_job_template.name}
>
{stringIsUUID(node.identifier)
? node.summary_fields.unified_job_template.name
: node.identifier}
</SelectOption>
))}
</SelectGroup>,
]}
</Select>
);
}
export default WorkflowOutputNavigation;

View File

@ -0,0 +1 @@
export { default } from './WorkflowOutputNavigation';

View File

@ -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: (
<WorkflowOutputNavigation parentRef={ref} relatedJobs={relatedJobs} />
),
link: undefined,
id: 2,
hasstyle: 'margin-left: auto',
});
}
if (isLoading) {
return (
@ -147,46 +170,53 @@ function Job({ setBreadcrumb }) {
return (
<PageSection>
<Card>
<RoutedTabs tabsArray={tabsArray} />
<Switch>
<Redirect from="/jobs/system/:id" to="/jobs/management/:id" exact />
<Redirect
from="/jobs/:typeSegment/:id"
to="/jobs/:typeSegment/:id/output"
exact
<div ref={ref}>
<Card>
<RoutedTabs
isWorkflow={match.url.startsWith('/jobs/workflow')}
tabsArray={tabsArray}
/>
{job && [
<Route
key={job.type === 'workflow_job' ? 'workflow-details' : 'details'}
path="/jobs/:typeSegment/:id/details"
>
<JobDetail
job={job}
inventorySourceLabels={inventorySourceChoices}
/>
</Route>,
<Route key="output" path="/jobs/:typeSegment/:id/output">
{job.type === 'workflow_job' ? (
<WorkflowOutput job={job} />
) : (
<JobOutput
<Switch>
<Redirect from="/jobs/system/:id" to="/jobs/management/:id" exact />
<Redirect
from="/jobs/:typeSegment/:id"
to="/jobs/:typeSegment/:id/output"
exact
/>
{job && [
<Route
key={
job.type === 'workflow_job' ? 'workflow-details' : 'details'
}
path="/jobs/:typeSegment/:id/details"
>
<JobDetail
job={job}
eventRelatedSearchableKeys={eventRelatedSearchableKeys}
eventSearchableKeys={eventSearchableKeys}
inventorySourceLabels={inventorySourceChoices}
/>
)}
</Route>,
<Route key="not-found" path="*">
<ContentError isNotFound>
<Link to={`/jobs/${typeSegment}/${id}/details`}>
{t`View Job Details`}
</Link>
</ContentError>
</Route>,
]}
</Switch>
</Card>
</Route>,
<Route key="output" path="/jobs/:typeSegment/:id/output">
{job.type === 'workflow_job' ? (
<WorkflowOutput job={job} />
) : (
<JobOutput
job={job}
eventRelatedSearchableKeys={eventRelatedSearchableKeys}
eventSearchableKeys={eventSearchableKeys}
/>
)}
</Route>,
<Route key="not-found" path="*">
<ContentError isNotFound>
<Link to={`/jobs/${typeSegment}/${id}/details`}>
{t`View Job Details`}
</Link>
</ContentError>
</Route>,
]}
</Switch>
</Card>
</div>
</PageSection>
);
}