diff --git a/awx/ui_next/src/screens/Job/JobList/JobList.jsx b/awx/ui_next/src/components/JobList/JobList.jsx
similarity index 94%
rename from awx/ui_next/src/screens/Job/JobList/JobList.jsx
rename to awx/ui_next/src/components/JobList/JobList.jsx
index 94bb99b73e..f03fa5ac89 100644
--- a/awx/ui_next/src/screens/Job/JobList/JobList.jsx
+++ b/awx/ui_next/src/components/JobList/JobList.jsx
@@ -2,8 +2,17 @@ import React, { useState, useEffect, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { withI18n } from '@lingui/react';
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 {
AdHocCommandsAPI,
InventoryUpdatesAPI,
@@ -13,16 +22,6 @@ import {
UnifiedJobsAPI,
WorkflowJobsAPI,
} 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(
'job',
@@ -30,12 +29,11 @@ const QS_CONFIG = getQSConfig(
page: 1,
page_size: 20,
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 location = useLocation();
@@ -47,14 +45,16 @@ function JobList({ i18n }) {
} = useRequest(
useCallback(async () => {
const params = parseQueryString(QS_CONFIG, location.search);
+
const {
data: { count, results },
- } = await UnifiedJobsAPI.read(params);
+ } = await UnifiedJobsAPI.read({ ...params, ...defaultParams });
+
return {
itemCount: count,
jobs: results,
};
- }, [location]),
+ }, [location]), // eslint-disable-line react-hooks/exhaustive-deps
{
jobs: [],
itemCount: 0,
@@ -119,7 +119,7 @@ function JobList({ i18n }) {
};
return (
-
+ <>
(
handleSelect(job)}
isSelected={selected.some(row => row.id === job.id)}
/>
@@ -243,7 +242,7 @@ function JobList({ i18n }) {
{i18n._(t`Failed to delete one or more jobs.`)}
-
+ >
);
}
diff --git a/awx/ui_next/src/screens/Job/JobList/JobList.test.jsx b/awx/ui_next/src/components/JobList/JobList.test.jsx
similarity index 100%
rename from awx/ui_next/src/screens/Job/JobList/JobList.test.jsx
rename to awx/ui_next/src/components/JobList/JobList.test.jsx
diff --git a/awx/ui_next/src/components/JobList/JobListItem.jsx b/awx/ui_next/src/components/JobList/JobListItem.jsx
new file mode 100644
index 0000000000..a577c605b8
--- /dev/null
+++ b/awx/ui_next/src/components/JobList/JobListItem.jsx
@@ -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 (
+
+
+
+
+ {job.status && }
+ ,
+
+
+
+
+ {job.id} — {job.name}
+
+
+
+ ,
+ ...(showTypeColumn
+ ? [
+
+ {toTitleCase(job.type)}
+ ,
+ ]
+ : []),
+
+ {formatDateString(job.finished)}
+ ,
+ ]}
+ />
+ {job.type !== 'system_job' &&
+ job.summary_fields?.user_capabilities?.start && (
+
+
+
+ {({ handleRelaunch }) => (
+
+ )}
+
+
+
+ )}
+
+
+ );
+}
+
+export { JobListItem as _JobListItem };
+export default withI18n()(JobListItem);
diff --git a/awx/ui_next/src/components/JobList/JobListItem.test.jsx b/awx/ui_next/src/components/JobList/JobListItem.test.jsx
new file mode 100644
index 0000000000..d087b23b95
--- /dev/null
+++ b/awx/ui_next/src/components/JobList/JobListItem.test.jsx
@@ -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('', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ const history = createMemoryHistory({
+ initialEntries: ['/jobs'],
+ });
+ wrapper = mountWithContexts(
+ {}} />,
+ { 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(
+ {}}
+ 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(
+ {}}
+ />
+ );
+ expect(wrapper.find('DataListCell[aria-label="type"]').length).toBe(1);
+ });
+});
diff --git a/awx/ui_next/src/components/JobList/index.js b/awx/ui_next/src/components/JobList/index.js
new file mode 100644
index 0000000000..35e78bb71e
--- /dev/null
+++ b/awx/ui_next/src/components/JobList/index.js
@@ -0,0 +1 @@
+export { default } from './JobList';
diff --git a/awx/ui_next/src/screens/Host/Host.jsx b/awx/ui_next/src/screens/Host/Host.jsx
index 57670658bc..37096fc024 100644
--- a/awx/ui_next/src/screens/Host/Host.jsx
+++ b/awx/ui_next/src/screens/Host/Host.jsx
@@ -17,11 +17,11 @@ import CardCloseButton from '@components/CardCloseButton';
import RoutedTabs from '@components/RoutedTabs';
import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading';
+import JobList from '@components/JobList';
import HostFacts from './HostFacts';
import HostDetail from './HostDetail';
import HostEdit from './HostEdit';
import HostGroups from './HostGroups';
-import HostCompletedJobs from './HostCompletedJobs';
import { HostsAPI } from '@api';
function Host({ inventory, i18n, setBreadcrumb }) {
@@ -181,11 +181,15 @@ function Host({ inventory, i18n, setBreadcrumb }) {
render={() => }
/>
)}
- {host && (
+ {host?.id && (
}
- />
+ path={[
+ '/hosts/:id/completed_jobs',
+ '/inventories/inventory/:id/hosts/:hostId/completed_jobs',
+ ]}
+ >
+
+
)}
Coming soon :);
- }
-}
-
-export default HostCompletedJobs;
diff --git a/awx/ui_next/src/screens/Host/HostCompletedJobs/index.js b/awx/ui_next/src/screens/Host/HostCompletedJobs/index.js
deleted file mode 100644
index 9dbe868803..0000000000
--- a/awx/ui_next/src/screens/Host/HostCompletedJobs/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './HostCompletedJobs';
diff --git a/awx/ui_next/src/screens/Inventory/Inventories.jsx b/awx/ui_next/src/screens/Inventory/Inventories.jsx
index 0df1e9370d..d7361690f6 100644
--- a/awx/ui_next/src/screens/Inventory/Inventories.jsx
+++ b/awx/ui_next/src/screens/Inventory/Inventories.jsx
@@ -64,24 +64,25 @@ class Inventories extends Component {
[`/inventories/${inventoryKind}/${inventory.id}/hosts/add`]: i18n._(
t`Create New Host`
),
+ [`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
+ nestedResource.id}`]: i18n._(
+ t`${nestedResource && nestedResource.name}`
+ ),
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
nestedResource.id}/edit`]: i18n._(t`Edit Details`),
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
nestedResource.id}/details`]: i18n._(t`Host Details`),
[`/inventories/${inventoryKind}/${inventory.id}/hosts/${nestedResource &&
- nestedResource.id}`]: i18n._(
- t`${nestedResource && nestedResource.name}`
- ),
-
+ nestedResource.id}/completed_jobs`]: i18n._(t`Completed Jobs`),
[`/inventories/${inventoryKind}/${inventory.id}/groups/add`]: i18n._(
t`Create New Group`
),
+ [`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
+ nestedResource.id}`]: `${nestedResource && nestedResource.name}`,
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
nestedResource.id}/edit`]: i18n._(t`Edit Details`),
[`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
nestedResource.id}/details`]: i18n._(t`Group Details`),
- [`/inventories/${inventoryKind}/${inventory.id}/groups/${nestedResource &&
- nestedResource.id}`]: `${nestedResource && nestedResource.name}`,
};
this.setState({ breadcrumbConfig });
};
diff --git a/awx/ui_next/src/screens/Inventory/Inventory.jsx b/awx/ui_next/src/screens/Inventory/Inventory.jsx
index e28ffedc02..4056a418b2 100644
--- a/awx/ui_next/src/screens/Inventory/Inventory.jsx
+++ b/awx/ui_next/src/screens/Inventory/Inventory.jsx
@@ -1,7 +1,6 @@
import React, { useEffect, useState } from 'react';
import { t } from '@lingui/macro';
import { withI18n } from '@lingui/react';
-import { Card, CardActions, PageSection } from '@patternfly/react-core';
import {
Switch,
Route,
@@ -10,20 +9,21 @@ import {
useLocation,
useRouteMatch,
} from 'react-router-dom';
+
+import { Card, CardActions, PageSection } from '@patternfly/react-core';
import { TabbedCardHeader } from '@components/Card';
import CardCloseButton from '@components/CardCloseButton';
import ContentError from '@components/ContentError';
+import ContentLoading from '@components/ContentLoading';
+import JobList from '@components/JobList';
import RoutedTabs from '@components/RoutedTabs';
import { ResourceAccessList } from '@components/ResourceAccessList';
-import ContentLoading from '@components/ContentLoading';
import InventoryDetail from './InventoryDetail';
-
+import InventoryEdit from './InventoryEdit';
import InventoryGroups from './InventoryGroups';
-import InventoryCompletedJobs from './InventoryCompletedJobs';
+import InventoryHosts from './InventoryHosts/InventoryHosts';
import InventorySources from './InventorySources';
import { InventoriesAPI } from '@api';
-import InventoryEdit from './InventoryEdit';
-import InventoryHosts from './InventoryHosts/InventoryHosts';
function Inventory({ i18n, setBreadcrumb }) {
const [contentError, setContentError] = useState(null);
@@ -172,8 +172,17 @@ function Inventory({ i18n, setBreadcrumb }) {
}
- />,
+ >
+
+ ,
Coming soon :);
- }
-}
-
-export default InventoryCompletedJobs;
diff --git a/awx/ui_next/src/screens/Inventory/InventoryCompletedJobs/index.js b/awx/ui_next/src/screens/Inventory/InventoryCompletedJobs/index.js
deleted file mode 100644
index 17e7b7be0b..0000000000
--- a/awx/ui_next/src/screens/Inventory/InventoryCompletedJobs/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './InventoryCompletedJobs';
diff --git a/awx/ui_next/src/screens/Inventory/SmartInventory.jsx b/awx/ui_next/src/screens/Inventory/SmartInventory.jsx
index 50907083d1..2a6a1c8ca3 100644
--- a/awx/ui_next/src/screens/Inventory/SmartInventory.jsx
+++ b/awx/ui_next/src/screens/Inventory/SmartInventory.jsx
@@ -6,11 +6,11 @@ import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
import { TabbedCardHeader } from '@components/Card';
import CardCloseButton from '@components/CardCloseButton';
import ContentError from '@components/ContentError';
+import JobList from '@components/JobList';
import RoutedTabs from '@components/RoutedTabs';
import { ResourceAccessList } from '@components/ResourceAccessList';
import SmartInventoryDetail from './SmartInventoryDetail';
import SmartInventoryHosts from './SmartInventoryHosts';
-import SmartInventoryCompletedJobs from './SmartInventoryCompletedJobs';
import { InventoriesAPI } from '@api';
import SmartInventoryEdit from './SmartInventoryEdit';
@@ -149,10 +149,17 @@ class SmartInventory extends Component {
(
-
- )}
- />,
+ >
+
+ ,
Coming soon :);
- }
-}
-
-export default SmartInventoryCompletedJobs;
diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryCompletedJobs/index.js b/awx/ui_next/src/screens/Inventory/SmartInventoryCompletedJobs/index.js
deleted file mode 100644
index 9c7ec9bc49..0000000000
--- a/awx/ui_next/src/screens/Inventory/SmartInventoryCompletedJobs/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './SmartInventoryCompletedJobs';
diff --git a/awx/ui_next/src/screens/Job/JobList/JobListItem.jsx b/awx/ui_next/src/screens/Job/JobList/JobListItem.jsx
deleted file mode 100644
index 28649f1edc..0000000000
--- a/awx/ui_next/src/screens/Job/JobList/JobListItem.jsx
+++ /dev/null
@@ -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 (
-
-
-
-
- {job.status && }
- ,
-
-
-
-
- {job.id} — {job.name}
-
-
-
- ,
- {toTitleCase(job.type)},
-
- {formatDateString(job.finished)}
- ,
- ]}
- />
- {job.type !== 'system_job' &&
- job.summary_fields?.user_capabilities?.start && (
-
-
-
- {({ handleRelaunch }) => (
-
- )}
-
-
-
- )}
-
-
- );
- }
-}
-export { JobListItem as _JobListItem };
-export default withI18n()(JobListItem);
diff --git a/awx/ui_next/src/screens/Job/JobList/JobListItem.test.jsx b/awx/ui_next/src/screens/Job/JobList/JobListItem.test.jsx
deleted file mode 100644
index d29514e9d9..0000000000
--- a/awx/ui_next/src/screens/Job/JobList/JobListItem.test.jsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import React from 'react';
-import { createMemoryHistory } from 'history';
-
-import { mountWithContexts } from '@testUtils/enzymeHelpers';
-
-import JobListItem from './JobListItem';
-
-describe('', () => {
- test('initially renders succesfully', () => {
- const history = createMemoryHistory({
- initialEntries: ['/jobs'],
- });
- mountWithContexts(
- {}}
- />,
- { context: { router: { history } } }
- );
- });
-});
diff --git a/awx/ui_next/src/screens/Job/JobList/index.js b/awx/ui_next/src/screens/Job/JobList/index.js
deleted file mode 100644
index 5b0caebb8a..0000000000
--- a/awx/ui_next/src/screens/Job/JobList/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as JobList } from './JobList';
-export { default as JobListItem } from './JobListItem';
diff --git a/awx/ui_next/src/screens/Job/Jobs.jsx b/awx/ui_next/src/screens/Job/Jobs.jsx
index 7c710614d8..b5008c3423 100644
--- a/awx/ui_next/src/screens/Job/Jobs.jsx
+++ b/awx/ui_next/src/screens/Job/Jobs.jsx
@@ -1,96 +1,89 @@
-import React, { Component, Fragment } from 'react';
-import { Route, withRouter, Switch } from 'react-router-dom';
+import React, { useState, useCallback } from 'react';
+import {
+ Route,
+ Switch,
+ useHistory,
+ useLocation,
+ useRouteMatch,
+} from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
+import { PageSection } from '@patternfly/react-core';
import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs';
import Job from './Job';
import JobTypeRedirect from './JobTypeRedirect';
-import JobList from './JobList/JobList';
+import JobList from '@components/JobList';
import { JOB_TYPE_URL_SEGMENTS } from '@constants';
-class Jobs extends Component {
- constructor(props) {
- super(props);
+function Jobs({ i18n }) {
+ const history = useHistory();
+ 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 = {
- breadcrumbConfig: {
+ const type = JOB_TYPE_URL_SEGMENTS[job.type];
+ setBreadcrumbConfig({
'/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 => {
- const { i18n } = this.props;
-
- if (!job) {
- return;
- }
-
- const type = JOB_TYPE_URL_SEGMENTS[job.type];
- const breadcrumbConfig = {
- '/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`),
- };
-
- this.setState({ breadcrumbConfig });
- };
-
- render() {
- const { match, history, location } = this.props;
- const { breadcrumbConfig } = this.state;
-
- return (
-
-
-
- (
-
- )}
- />
- (
-
- )}
- />
- (
-
- )}
- />
- (
-
- )}
- />
- (
-
- )}
- />
-
-
- );
- }
+ return (
+ <>
+
+
+
+
+
+
+
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+
+ >
+ );
}
export { Jobs as _Jobs };
-export default withI18n()(withRouter(Jobs));
+export default withI18n()(Jobs);
diff --git a/awx/ui_next/src/screens/Template/Template.jsx b/awx/ui_next/src/screens/Template/Template.jsx
index c8863edb1e..b46365aae4 100644
--- a/awx/ui_next/src/screens/Template/Template.jsx
+++ b/awx/ui_next/src/screens/Template/Template.jsx
@@ -3,15 +3,17 @@ import { t } from '@lingui/macro';
import { withI18n } from '@lingui/react';
import { Card, CardActions, PageSection } from '@patternfly/react-core';
import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
+
import { TabbedCardHeader } from '@components/Card';
import CardCloseButton from '@components/CardCloseButton';
import ContentError from '@components/ContentError';
+import JobList from '@components/JobList';
import NotificationList from '@components/NotificationList';
import RoutedTabs from '@components/RoutedTabs';
import { ResourceAccessList } from '@components/ResourceAccessList';
import JobTemplateDetail from './JobTemplateDetail';
-import { JobTemplatesAPI, OrganizationsAPI } from '@api';
import JobTemplateEdit from './JobTemplateEdit';
+import { JobTemplatesAPI, OrganizationsAPI } from '@api';
class Template extends Component {
constructor(props) {
@@ -109,7 +111,7 @@ class Template extends Component {
},
{
name: i18n._(t`Completed Jobs`),
- link: '/home',
+ link: `${match.url}/completed_jobs`,
},
{
name: i18n._(t`Survey`),
@@ -203,6 +205,11 @@ class Template extends Component {
)}
/>
)}
+ {template?.id && (
+
+
+
+ )}
{
@@ -151,6 +153,15 @@ class WorkflowJobTemplate extends Component {
)}
/>
)}
+ {template?.id && (
+
+
+
+ )}