mirror of
https://github.com/ansible/awx.git
synced 2026-03-02 09:18:48 -03:30
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:
@@ -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>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
90
awx/ui_next/src/components/JobList/JobListItem.jsx
Normal file
90
awx/ui_next/src/components/JobList/JobListItem.jsx
Normal 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} — {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);
|
||||||
82
awx/ui_next/src/components/JobList/JobListItem.test.jsx
Normal file
82
awx/ui_next/src/components/JobList/JobListItem.test.jsx
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
1
awx/ui_next/src/components/JobList/index.js
Normal file
1
awx/ui_next/src/components/JobList/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './JobList';
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from './HostCompletedJobs';
|
|
||||||
@@ -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 });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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="*"
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from './InventoryCompletedJobs';
|
|
||||||
@@ -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="*"
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from './SmartInventoryCompletedJobs';
|
|
||||||
@@ -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} — {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);
|
|
||||||
@@ -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 } } }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { default as JobList } from './JobList';
|
|
||||||
export { default as JobListItem } from './JobListItem';
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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="*"
|
||||||
|
|||||||
@@ -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 });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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="*"
|
||||||
|
|||||||
Reference in New Issue
Block a user