Merge pull request #6058 from marshmalien/5890-jt-completed-jobs-list

Add Completed Job list tab to multiple resources

Reviewed-by: Alex Corey <Alex.swansboro@gmail.com>
             https://github.com/AlexSCorey
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-02-27 09:13:22 +00:00
committed by GitHub
22 changed files with 336 additions and 278 deletions

View File

@@ -2,8 +2,17 @@ import React, { useState, useEffect, useCallback } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Card, PageSection } from '@patternfly/react-core';
import { Card } from '@patternfly/react-core';
import AlertModal from '@components/AlertModal';
import DatalistToolbar from '@components/DataListToolbar';
import ErrorDetail from '@components/ErrorDetail';
import PaginatedDataList, {
ToolbarDeleteButton,
} from '@components/PaginatedDataList';
import useRequest, { useDeleteItems } from '@util/useRequest';
import { getQSConfig, parseQueryString } from '@util/qs';
import JobListItem from './JobListItem';
import { import {
AdHocCommandsAPI, AdHocCommandsAPI,
InventoryUpdatesAPI, InventoryUpdatesAPI,
@@ -13,16 +22,6 @@ import {
UnifiedJobsAPI, UnifiedJobsAPI,
WorkflowJobsAPI, WorkflowJobsAPI,
} from '@api'; } from '@api';
import AlertModal from '@components/AlertModal';
import DatalistToolbar from '@components/DataListToolbar';
import ErrorDetail from '@components/ErrorDetail';
import PaginatedDataList, {
ToolbarDeleteButton,
} from '@components/PaginatedDataList';
import useRequest, { useDeleteItems } from '@util/useRequest';
import { getQSConfig, parseQueryString } from '@util/qs';
import JobListItem from './JobListItem';
const QS_CONFIG = getQSConfig( const QS_CONFIG = getQSConfig(
'job', 'job',
@@ -30,12 +29,11 @@ const QS_CONFIG = getQSConfig(
page: 1, page: 1,
page_size: 20, page_size: 20,
order_by: '-finished', order_by: '-finished',
not__launch_type: 'sync',
}, },
['page', 'page_size', 'id'] ['page', 'page_size']
); );
function JobList({ i18n }) { function JobList({ i18n, defaultParams, showTypeColumn = false }) {
const [selected, setSelected] = useState([]); const [selected, setSelected] = useState([]);
const location = useLocation(); const location = useLocation();
@@ -47,14 +45,16 @@ function JobList({ i18n }) {
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
const params = parseQueryString(QS_CONFIG, location.search); const params = parseQueryString(QS_CONFIG, location.search);
const { const {
data: { count, results }, data: { count, results },
} = await UnifiedJobsAPI.read(params); } = await UnifiedJobsAPI.read({ ...params, ...defaultParams });
return { return {
itemCount: count, itemCount: count,
jobs: results, jobs: results,
}; };
}, [location]), }, [location]), // eslint-disable-line react-hooks/exhaustive-deps
{ {
jobs: [], jobs: [],
itemCount: 0, itemCount: 0,
@@ -119,7 +119,7 @@ function JobList({ i18n }) {
}; };
return ( return (
<PageSection> <>
<Card> <Card>
<PaginatedDataList <PaginatedDataList
contentError={contentError} contentError={contentError}
@@ -225,9 +225,8 @@ function JobList({ i18n }) {
renderItem={job => ( renderItem={job => (
<JobListItem <JobListItem
key={job.id} key={job.id}
value={job.name}
job={job} job={job}
detailUrl={`${location.pathname}/${job}/${job.id}`} showTypeColumn={showTypeColumn}
onSelect={() => handleSelect(job)} onSelect={() => handleSelect(job)}
isSelected={selected.some(row => row.id === job.id)} isSelected={selected.some(row => row.id === job.id)}
/> />
@@ -243,7 +242,7 @@ function JobList({ i18n }) {
{i18n._(t`Failed to delete one or more jobs.`)} {i18n._(t`Failed to delete one or more jobs.`)}
<ErrorDetail error={deletionError} /> <ErrorDetail error={deletionError} />
</AlertModal> </AlertModal>
</PageSection> </>
); );
} }

View File

@@ -0,0 +1,90 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Button,
DataListAction,
DataListCell,
DataListCheck,
DataListItem,
DataListItemRow,
DataListItemCells,
Tooltip,
} from '@patternfly/react-core';
import { RocketIcon } from '@patternfly/react-icons';
import LaunchButton from '@components/LaunchButton';
import StatusIcon from '@components/StatusIcon';
import { toTitleCase } from '@util/strings';
import { formatDateString } from '@util/dates';
import { JOB_TYPE_URL_SEGMENTS } from '@constants';
function JobListItem({
i18n,
job,
isSelected,
onSelect,
showTypeColumn = false,
}) {
const labelId = `check-action-${job.id}`;
return (
<DataListItem aria-labelledby={labelId} id={`${job.id}`}>
<DataListItemRow>
<DataListCheck
id={`select-job-${job.id}`}
checked={isSelected}
onChange={onSelect}
aria-labelledby={labelId}
/>
<DataListItemCells
dataListCells={[
<DataListCell key="status" isFilled={false}>
{job.status && <StatusIcon status={job.status} />}
</DataListCell>,
<DataListCell key="name">
<span>
<Link to={`/jobs/${JOB_TYPE_URL_SEGMENTS[job.type]}/${job.id}`}>
<b>
{job.id} &mdash; {job.name}
</b>
</Link>
</span>
</DataListCell>,
...(showTypeColumn
? [
<DataListCell key="type" aria-label="type">
{toTitleCase(job.type)}
</DataListCell>,
]
: []),
<DataListCell key="finished">
{formatDateString(job.finished)}
</DataListCell>,
]}
/>
{job.type !== 'system_job' &&
job.summary_fields?.user_capabilities?.start && (
<DataListAction
aria-label="actions"
aria-labelledby={labelId}
id={labelId}
>
<Tooltip content={i18n._(t`Relaunch Job`)} position="top">
<LaunchButton resource={job}>
{({ handleRelaunch }) => (
<Button variant="plain" onClick={handleRelaunch}>
<RocketIcon />
</Button>
)}
</LaunchButton>
</Tooltip>
</DataListAction>
)}
</DataListItemRow>
</DataListItem>
);
}
export { JobListItem as _JobListItem };
export default withI18n()(JobListItem);

View File

@@ -0,0 +1,82 @@
import React from 'react';
import { createMemoryHistory } from 'history';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import JobListItem from './JobListItem';
const mockJob = {
id: 123,
type: 'job',
url: '/api/v2/jobs/123/',
summary_fields: {
user_capabilities: {
delete: true,
start: true,
},
},
created: '2019-08-08T19:24:05.344276Z',
modified: '2019-08-08T19:24:18.162949Z',
name: 'Demo Job Template',
job_type: 'run',
started: '2019-08-08T19:24:18.329589Z',
finished: '2019-08-08T19:24:50.119995Z',
status: 'successful',
};
describe('<JobListItem />', () => {
let wrapper;
beforeEach(() => {
const history = createMemoryHistory({
initialEntries: ['/jobs'],
});
wrapper = mountWithContexts(
<JobListItem job={mockJob} isSelected onSelect={() => {}} />,
{ context: { router: { history } } }
);
});
afterEach(() => {
wrapper.unmount();
});
test('initially renders successfully', () => {
expect(wrapper.find('JobListItem').length).toBe(1);
});
test('launch button shown to users with launch capabilities', () => {
expect(wrapper.find('LaunchButton').length).toBe(1);
});
test('launch button hidden from users without launch capabilities', () => {
wrapper = mountWithContexts(
<JobListItem
job={{
...mockJob,
summary_fields: { user_capabilities: { start: false } },
}}
detailUrl={`/jobs/playbook/${mockJob.id}`}
onSelect={() => {}}
isSelected={false}
/>
);
expect(wrapper.find('LaunchButton').length).toBe(0);
});
test('should hide type column when showTypeColumn is false', () => {
expect(wrapper.find('DataListCell[aria-label="type"]').length).toBe(0);
});
test('should show type column when showTypeColumn is true', () => {
wrapper = mountWithContexts(
<JobListItem
job={mockJob}
showTypeColumn
isSelected
onSelect={() => {}}
/>
);
expect(wrapper.find('DataListCell[aria-label="type"]').length).toBe(1);
});
});

View File

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

View File

@@ -17,11 +17,11 @@ import CardCloseButton from '@components/CardCloseButton';
import RoutedTabs from '@components/RoutedTabs'; import RoutedTabs from '@components/RoutedTabs';
import ContentError from '@components/ContentError'; import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading'; import ContentLoading from '@components/ContentLoading';
import JobList from '@components/JobList';
import HostFacts from './HostFacts'; import HostFacts from './HostFacts';
import HostDetail from './HostDetail'; import HostDetail from './HostDetail';
import HostEdit from './HostEdit'; import HostEdit from './HostEdit';
import HostGroups from './HostGroups'; import HostGroups from './HostGroups';
import HostCompletedJobs from './HostCompletedJobs';
import { HostsAPI } from '@api'; import { HostsAPI } from '@api';
function Host({ inventory, i18n, setBreadcrumb }) { function Host({ inventory, i18n, setBreadcrumb }) {
@@ -181,11 +181,15 @@ function Host({ inventory, i18n, setBreadcrumb }) {
render={() => <HostGroups host={host} />} render={() => <HostGroups host={host} />}
/> />
)} )}
{host && ( {host?.id && (
<Route <Route
path="/hosts/:id/completed_jobs" path={[
render={() => <HostCompletedJobs host={host} />} '/hosts/:id/completed_jobs',
/> '/inventories/inventory/:id/hosts/:hostId/completed_jobs',
]}
>
<JobList defaultParams={{ job__hosts: host.id }} />
</Route>
)} )}
<Route <Route
key="not-found" key="not-found"

View File

@@ -1,10 +0,0 @@
import React, { Component } from 'react';
import { CardBody } from '@components/Card';
class HostCompletedJobs extends Component {
render() {
return <CardBody>Coming soon :)</CardBody>;
}
}
export default HostCompletedJobs;

View File

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

View File

@@ -64,24 +64,25 @@ class Inventories extends Component {
[`/inventories/${inventoryKind}/${inventory.id}/hosts/add`]: i18n._( [`/inventories/${inventoryKind}/${inventory.id}/hosts/add`]: i18n._(
t`Create New Host` t`Create New Host`
), ),
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
nestedResource.id}`]: i18n._(
t`${nestedResource && nestedResource.name}`
),
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource && [`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
nestedResource.id}/edit`]: i18n._(t`Edit Details`), nestedResource.id}/edit`]: i18n._(t`Edit Details`),
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource && [`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
nestedResource.id}/details`]: i18n._(t`Host Details`), nestedResource.id}/details`]: i18n._(t`Host Details`),
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource && [`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
nestedResource.id}`]: i18n._( nestedResource.id}/completed_jobs`]: i18n._(t`Completed Jobs`),
t`${nestedResource && nestedResource.name}`
),
[`/inventories/${inventoryKind}/${inventory.id}/groups/add`]: i18n._( [`/inventories/${inventoryKind}/${inventory.id}/groups/add`]: i18n._(
t`Create New Group` t`Create New Group`
), ),
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
nestedResource.id}`]: `${nestedResource && nestedResource.name}`,
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource && [`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
nestedResource.id}/edit`]: i18n._(t`Edit Details`), nestedResource.id}/edit`]: i18n._(t`Edit Details`),
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource && [`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
nestedResource.id}/details`]: i18n._(t`Group Details`), nestedResource.id}/details`]: i18n._(t`Group Details`),
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
nestedResource.id}`]: `${nestedResource && nestedResource.name}`,
}; };
this.setState({ breadcrumbConfig }); this.setState({ breadcrumbConfig });
}; };

View File

@@ -1,7 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { Card, CardActions, PageSection } from '@patternfly/react-core';
import { import {
Switch, Switch,
Route, Route,
@@ -10,20 +9,21 @@ import {
useLocation, useLocation,
useRouteMatch, useRouteMatch,
} from 'react-router-dom'; } from 'react-router-dom';
import { Card, CardActions, PageSection } from '@patternfly/react-core';
import { TabbedCardHeader } from '@components/Card'; import { TabbedCardHeader } from '@components/Card';
import CardCloseButton from '@components/CardCloseButton'; import CardCloseButton from '@components/CardCloseButton';
import ContentError from '@components/ContentError'; import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading';
import JobList from '@components/JobList';
import RoutedTabs from '@components/RoutedTabs'; import RoutedTabs from '@components/RoutedTabs';
import { ResourceAccessList } from '@components/ResourceAccessList'; import { ResourceAccessList } from '@components/ResourceAccessList';
import ContentLoading from '@components/ContentLoading';
import InventoryDetail from './InventoryDetail'; import InventoryDetail from './InventoryDetail';
import InventoryEdit from './InventoryEdit';
import InventoryGroups from './InventoryGroups'; import InventoryGroups from './InventoryGroups';
import InventoryCompletedJobs from './InventoryCompletedJobs'; import InventoryHosts from './InventoryHosts/InventoryHosts';
import InventorySources from './InventorySources'; import InventorySources from './InventorySources';
import { InventoriesAPI } from '@api'; import { InventoriesAPI } from '@api';
import InventoryEdit from './InventoryEdit';
import InventoryHosts from './InventoryHosts/InventoryHosts';
function Inventory({ i18n, setBreadcrumb }) { function Inventory({ i18n, setBreadcrumb }) {
const [contentError, setContentError] = useState(null); const [contentError, setContentError] = useState(null);
@@ -172,8 +172,17 @@ function Inventory({ i18n, setBreadcrumb }) {
<Route <Route
key="completed_jobs" key="completed_jobs"
path="/inventories/inventory/:id/completed_jobs" path="/inventories/inventory/:id/completed_jobs"
render={() => <InventoryCompletedJobs inventory={inventory} />} >
/>, <JobList
defaultParams={{
or__job__inventory: inventory.id,
or__adhoccommand__inventory: inventory.id,
or__inventoryupdate__inventory_source__inventory:
inventory.id,
or__workflowjob__inventory: inventory.id,
}}
/>
</Route>,
<Route <Route
key="not-found" key="not-found"
path="*" path="*"

View File

@@ -1,10 +0,0 @@
import React, { Component } from 'react';
import { CardBody } from '@components/Card';
class InventoryCompletedJobs extends Component {
render() {
return <CardBody>Coming soon :)</CardBody>;
}
}
export default InventoryCompletedJobs;

View File

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

View File

@@ -6,11 +6,11 @@ import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
import { TabbedCardHeader } from '@components/Card'; import { TabbedCardHeader } from '@components/Card';
import CardCloseButton from '@components/CardCloseButton'; import CardCloseButton from '@components/CardCloseButton';
import ContentError from '@components/ContentError'; import ContentError from '@components/ContentError';
import JobList from '@components/JobList';
import RoutedTabs from '@components/RoutedTabs'; import RoutedTabs from '@components/RoutedTabs';
import { ResourceAccessList } from '@components/ResourceAccessList'; import { ResourceAccessList } from '@components/ResourceAccessList';
import SmartInventoryDetail from './SmartInventoryDetail'; import SmartInventoryDetail from './SmartInventoryDetail';
import SmartInventoryHosts from './SmartInventoryHosts'; import SmartInventoryHosts from './SmartInventoryHosts';
import SmartInventoryCompletedJobs from './SmartInventoryCompletedJobs';
import { InventoriesAPI } from '@api'; import { InventoriesAPI } from '@api';
import SmartInventoryEdit from './SmartInventoryEdit'; import SmartInventoryEdit from './SmartInventoryEdit';
@@ -149,10 +149,17 @@ class SmartInventory extends Component {
<Route <Route
key="completed_jobs" key="completed_jobs"
path="/inventories/smart_inventory/:id/completed_jobs" path="/inventories/smart_inventory/:id/completed_jobs"
render={() => ( >
<SmartInventoryCompletedJobs inventory={inventory} /> <JobList
)} defaultParams={{
/>, or__job__inventory: inventory.id,
or__adhoccommand__inventory: inventory.id,
or__inventoryupdate__inventory_source__inventory:
inventory.id,
or__workflowjob__inventory: inventory.id,
}}
/>
</Route>,
<Route <Route
key="not-found" key="not-found"
path="*" path="*"

View File

@@ -1,10 +0,0 @@
import React, { Component } from 'react';
import { CardBody } from '@components/Card';
class SmartInventoryCompletedJobs extends Component {
render() {
return <CardBody>Coming soon :)</CardBody>;
}
}
export default SmartInventoryCompletedJobs;

View File

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

View File

@@ -1,82 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Button,
DataListAction,
DataListCell,
DataListCheck,
DataListItem,
DataListItemRow,
DataListItemCells,
Tooltip,
} from '@patternfly/react-core';
import { RocketIcon } from '@patternfly/react-icons';
import LaunchButton from '@components/LaunchButton';
import StatusIcon from '@components/StatusIcon';
import { toTitleCase } from '@util/strings';
import { formatDateString } from '@util/dates';
import { JOB_TYPE_URL_SEGMENTS } from '@constants';
class JobListItem extends Component {
render() {
const { i18n, job, isSelected, onSelect } = this.props;
const labelId = `check-action-${job.id}`;
return (
<DataListItem aria-labelledby={labelId} id={`${job.id}`}>
<DataListItemRow>
<DataListCheck
id={`select-job-${job.id}`}
checked={isSelected}
onChange={onSelect}
aria-labelledby={labelId}
/>
<DataListItemCells
dataListCells={[
<DataListCell key="status" isFilled={false}>
{job.status && <StatusIcon status={job.status} />}
</DataListCell>,
<DataListCell key="name">
<span>
<Link
to={`/jobs/${JOB_TYPE_URL_SEGMENTS[job.type]}/${job.id}`}
>
<b>
{job.id} &mdash; {job.name}
</b>
</Link>
</span>
</DataListCell>,
<DataListCell key="type">{toTitleCase(job.type)}</DataListCell>,
<DataListCell key="finished">
{formatDateString(job.finished)}
</DataListCell>,
]}
/>
{job.type !== 'system_job' &&
job.summary_fields?.user_capabilities?.start && (
<DataListAction
aria-label="actions"
aria-labelledby={labelId}
id={labelId}
>
<Tooltip content={i18n._(t`Relaunch Job`)} position="top">
<LaunchButton resource={job}>
{({ handleRelaunch }) => (
<Button variant="plain" onClick={handleRelaunch}>
<RocketIcon />
</Button>
)}
</LaunchButton>
</Tooltip>
</DataListAction>
)}
</DataListItemRow>
</DataListItem>
);
}
}
export { JobListItem as _JobListItem };
export default withI18n()(JobListItem);

View File

@@ -1,32 +0,0 @@
import React from 'react';
import { createMemoryHistory } from 'history';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import JobListItem from './JobListItem';
describe('<JobListItem />', () => {
test('initially renders succesfully', () => {
const history = createMemoryHistory({
initialEntries: ['/jobs'],
});
mountWithContexts(
<JobListItem
job={{
id: 1,
name: 'Job',
type: 'project update',
summary_fields: {
user_capabilities: {
start: true,
},
},
}}
detailUrl="/organization/1"
isSelected
onSelect={() => {}}
/>,
{ context: { router: { history } } }
);
});
});

View File

@@ -1,2 +0,0 @@
export { default as JobList } from './JobList';
export { default as JobListItem } from './JobListItem';

View File

@@ -1,96 +1,89 @@
import React, { Component, Fragment } from 'react'; import React, { useState, useCallback } from 'react';
import { Route, withRouter, Switch } from 'react-router-dom'; import {
Route,
Switch,
useHistory,
useLocation,
useRouteMatch,
} from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { PageSection } from '@patternfly/react-core';
import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs'; import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs';
import Job from './Job'; import Job from './Job';
import JobTypeRedirect from './JobTypeRedirect'; import JobTypeRedirect from './JobTypeRedirect';
import JobList from './JobList/JobList'; import JobList from '@components/JobList';
import { JOB_TYPE_URL_SEGMENTS } from '@constants'; import { JOB_TYPE_URL_SEGMENTS } from '@constants';
class Jobs extends Component { function Jobs({ i18n }) {
constructor(props) { const history = useHistory();
super(props); const location = useLocation();
const match = useRouteMatch();
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
'/jobs': i18n._(t`Jobs`),
});
const { i18n } = props; const buildBreadcrumbConfig = useCallback(
job => {
if (!job) {
return;
}
this.state = { const type = JOB_TYPE_URL_SEGMENTS[job.type];
breadcrumbConfig: { setBreadcrumbConfig({
'/jobs': i18n._(t`Jobs`), '/jobs': i18n._(t`Jobs`),
}, [`/jobs/${type}/${job.id}`]: `${job.name}`,
}; [`/jobs/${type}/${job.id}/output`]: i18n._(t`Output`),
} [`/jobs/${type}/${job.id}/details`]: i18n._(t`Details`),
});
},
[i18n]
);
setBreadcrumbConfig = job => { return (
const { i18n } = this.props; <>
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
if (!job) { <Switch>
return; <Route exact path={match.path}>
} <PageSection>
<JobList
const type = JOB_TYPE_URL_SEGMENTS[job.type]; showTypeColumn
const breadcrumbConfig = { defaultParams={{ not__launch_type: 'sync' }}
'/jobs': i18n._(t`Jobs`), />
[`/jobs/${type}/${job.id}`]: `${job.name}`, </PageSection>
[`/jobs/${type}/${job.id}/output`]: i18n._(t`Output`), </Route>
[`/jobs/${type}/${job.id}/details`]: i18n._(t`Details`), <Route
}; path={`${match.path}/:id/details`}
render={({ match: m }) => (
this.setState({ breadcrumbConfig }); <JobTypeRedirect id={m.params.id} path={m.path} view="details" />
}; )}
/>
render() { <Route
const { match, history, location } = this.props; path={`${match.path}/:id/output`}
const { breadcrumbConfig } = this.state; render={({ match: m }) => (
<JobTypeRedirect id={m.params.id} path={m.path} view="output" />
return ( )}
<Fragment> />
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} /> <Route
<Switch> path={`${match.path}/:type/:id`}
<Route render={() => (
exact <Job
path={match.path} history={history}
render={() => ( location={location}
<JobList setBreadcrumb={buildBreadcrumbConfig}
history={history} />
location={location} )}
setBreadcrumb={this.setBreadcrumbConfig} />
/> <Route
)} path={`${match.path}/:id`}
/> render={({ match: m }) => (
<Route <JobTypeRedirect id={m.params.id} path={m.path} />
path={`${match.path}/:id/details`} )}
render={({ match: m }) => ( />
<JobTypeRedirect id={m.params.id} path={m.path} view="details" /> </Switch>
)} </>
/> );
<Route
path={`${match.path}/:id/output`}
render={({ match: m }) => (
<JobTypeRedirect id={m.params.id} path={m.path} view="output" />
)}
/>
<Route
path={`${match.path}/:type/:id`}
render={() => (
<Job
history={history}
location={location}
setBreadcrumb={this.setBreadcrumbConfig}
/>
)}
/>
<Route
path={`${match.path}/:id`}
render={({ match: m }) => (
<JobTypeRedirect id={m.params.id} path={m.path} />
)}
/>
</Switch>
</Fragment>
);
}
} }
export { Jobs as _Jobs }; export { Jobs as _Jobs };
export default withI18n()(withRouter(Jobs)); export default withI18n()(Jobs);

View File

@@ -3,15 +3,17 @@ import { t } from '@lingui/macro';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { Card, CardActions, PageSection } from '@patternfly/react-core'; import { Card, CardActions, PageSection } from '@patternfly/react-core';
import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom'; import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
import { TabbedCardHeader } from '@components/Card'; import { TabbedCardHeader } from '@components/Card';
import CardCloseButton from '@components/CardCloseButton'; import CardCloseButton from '@components/CardCloseButton';
import ContentError from '@components/ContentError'; import ContentError from '@components/ContentError';
import JobList from '@components/JobList';
import NotificationList from '@components/NotificationList'; import NotificationList from '@components/NotificationList';
import RoutedTabs from '@components/RoutedTabs'; import RoutedTabs from '@components/RoutedTabs';
import { ResourceAccessList } from '@components/ResourceAccessList'; import { ResourceAccessList } from '@components/ResourceAccessList';
import JobTemplateDetail from './JobTemplateDetail'; import JobTemplateDetail from './JobTemplateDetail';
import { JobTemplatesAPI, OrganizationsAPI } from '@api';
import JobTemplateEdit from './JobTemplateEdit'; import JobTemplateEdit from './JobTemplateEdit';
import { JobTemplatesAPI, OrganizationsAPI } from '@api';
class Template extends Component { class Template extends Component {
constructor(props) { constructor(props) {
@@ -109,7 +111,7 @@ class Template extends Component {
}, },
{ {
name: i18n._(t`Completed Jobs`), name: i18n._(t`Completed Jobs`),
link: '/home', link: `${match.url}/completed_jobs`,
}, },
{ {
name: i18n._(t`Survey`), name: i18n._(t`Survey`),
@@ -203,6 +205,11 @@ class Template extends Component {
)} )}
/> />
)} )}
{template?.id && (
<Route path="/templates/:templateType/:id/completed_jobs">
<JobList defaultParams={{ job__job_template: template.id }} />
</Route>
)}
<Route <Route
key="not-found" key="not-found"
path="*" path="*"

View File

@@ -39,10 +39,13 @@ class Templates extends Component {
[`/templates/${template.type}/${template.id}/edit`]: i18n._( [`/templates/${template.type}/${template.id}/edit`]: i18n._(
t`Edit Details` t`Edit Details`
), ),
[`/templates/${template.type}/${template.id}/access`]: i18n._(t`Access`),
[`/templates/${template.type}/${template.id}/notifications`]: i18n._( [`/templates/${template.type}/${template.id}/notifications`]: i18n._(
t`Notifications` t`Notifications`
), ),
[`/templates/${template.type}/${template.id}/access`]: i18n._(t`Access`), [`/templates/${template.type}/${template.id}/completed_jobs`]: i18n._(
t`Completed Jobs`
),
}; };
this.setState({ breadcrumbConfig }); this.setState({ breadcrumbConfig });
}; };

View File

@@ -8,6 +8,7 @@ import AppendBody from '@components/AppendBody';
import CardCloseButton from '@components/CardCloseButton'; import CardCloseButton from '@components/CardCloseButton';
import ContentError from '@components/ContentError'; import ContentError from '@components/ContentError';
import FullPage from '@components/FullPage'; import FullPage from '@components/FullPage';
import JobList from '@components/JobList';
import RoutedTabs from '@components/RoutedTabs'; import RoutedTabs from '@components/RoutedTabs';
import { WorkflowJobTemplatesAPI, CredentialsAPI } from '@api'; import { WorkflowJobTemplatesAPI, CredentialsAPI } from '@api';
import WorkflowJobTemplateDetail from './WorkflowJobTemplateDetail'; import WorkflowJobTemplateDetail from './WorkflowJobTemplateDetail';
@@ -81,6 +82,7 @@ class WorkflowJobTemplate extends Component {
const tabsArray = [ const tabsArray = [
{ name: i18n._(t`Details`), link: `${match.url}/details` }, { name: i18n._(t`Details`), link: `${match.url}/details` },
{ name: i18n._(t`Visualizer`), link: `${match.url}/visualizer` }, { name: i18n._(t`Visualizer`), link: `${match.url}/visualizer` },
{ name: i18n._(t`Completed Jobs`), link: `${match.url}/completed_jobs` },
]; ];
tabsArray.forEach((tab, n) => { tabsArray.forEach((tab, n) => {
@@ -151,6 +153,15 @@ class WorkflowJobTemplate extends Component {
)} )}
/> />
)} )}
{template?.id && (
<Route path="/templates/workflow_job_template/:id/completed_jobs">
<JobList
defaultParams={{
workflow_job__workflow_job_template: template.id,
}}
/>
</Route>
)}
<Route <Route
key="not-found" key="not-found"
path="*" path="*"