diff --git a/src/api/index.js b/src/api/index.js
index 57429507bb..c87e753f2b 100644
--- a/src/api/index.js
+++ b/src/api/index.js
@@ -7,6 +7,7 @@ import Organizations from './models/Organizations';
import Root from './models/Root';
import Teams from './models/Teams';
import UnifiedJobTemplates from './models/UnifiedJobTemplates';
+import UnifiedJobs from './models/UnifiedJobs';
import Users from './models/Users';
import WorkflowJobTemplates from './models/WorkflowJobTemplates';
@@ -19,6 +20,7 @@ const OrganizationsAPI = new Organizations();
const RootAPI = new Root();
const TeamsAPI = new Teams();
const UnifiedJobTemplatesAPI = new UnifiedJobTemplates();
+const UnifiedJobsAPI = new UnifiedJobs();
const UsersAPI = new Users();
const WorkflowJobTemplatesAPI = new WorkflowJobTemplates();
@@ -32,6 +34,7 @@ export {
RootAPI,
TeamsAPI,
UnifiedJobTemplatesAPI,
+ UnifiedJobsAPI,
UsersAPI,
WorkflowJobTemplatesAPI
};
diff --git a/src/api/models/UnifiedJobs.js b/src/api/models/UnifiedJobs.js
new file mode 100644
index 0000000000..0ad142201c
--- /dev/null
+++ b/src/api/models/UnifiedJobs.js
@@ -0,0 +1,10 @@
+import Base from '../Base';
+
+class UnifiedJobs extends Base {
+ constructor (http) {
+ super(http);
+ this.baseUrl = '/api/v2/unified_jobs/';
+ }
+}
+
+export default UnifiedJobs;
diff --git a/src/screens/Job/Job.jsx b/src/screens/Job/Job.jsx
index ea3af16373..a64be2a894 100644
--- a/src/screens/Job/Job.jsx
+++ b/src/screens/Job/Job.jsx
@@ -13,7 +13,7 @@ import RoutedTabs from '@components/RoutedTabs';
import JobDetail from './JobDetail';
import JobOutput from './JobOutput';
-export class Job extends Component {
+class Job extends Component {
constructor (props) {
super(props);
@@ -24,22 +24,22 @@ export class Job extends Component {
isInitialized: false
};
- this.fetchJob = this.fetchJob.bind(this);
+ this.loadJob = this.loadJob.bind(this);
}
async componentDidMount () {
- await this.fetchJob();
+ await this.loadJob();
this.setState({ isInitialized: true });
}
async componentDidUpdate (prevProps) {
const { location } = this.props;
if (location !== prevProps.location) {
- await this.fetchJob();
+ await this.loadJob();
}
}
- async fetchJob () {
+ async loadJob () {
const {
match,
setBreadcrumb,
@@ -153,3 +153,4 @@ export class Job extends Component {
}
export default withI18n()(withRouter(Job));
+export { Job as _Job };
diff --git a/src/screens/Job/JobList/JobList.jsx b/src/screens/Job/JobList/JobList.jsx
new file mode 100644
index 0000000000..598546d178
--- /dev/null
+++ b/src/screens/Job/JobList/JobList.jsx
@@ -0,0 +1,182 @@
+import React, { Component } from 'react';
+import { withRouter } from 'react-router-dom';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import {
+ Card,
+ PageSection,
+ PageSectionVariants,
+} from '@patternfly/react-core';
+
+import { UnifiedJobsAPI } from '@api';
+import AlertModal from '@components/AlertModal';
+import DatalistToolbar from '@components/DataListToolbar';
+import PaginatedDataList, {
+ ToolbarDeleteButton
+} from '@components/PaginatedDataList';
+import { getQSConfig, parseNamespacedQueryString } from '@util/qs';
+
+import JobListItem from './JobListItem';
+
+const QS_CONFIG = getQSConfig('job', {
+ page: 1,
+ page_size: 20,
+ order_by: '-finished',
+ not__launch_type: 'sync',
+});
+
+class JobsList extends Component {
+ constructor (props) {
+ super(props);
+
+ this.state = {
+ hasContentLoading: true,
+ hasContentError: false,
+ deletionError: false,
+ selected: [],
+ jobs: [],
+ itemCount: 0,
+ };
+ this.loadJobs = this.loadJobs.bind(this);
+ this.handleSelectAll = this.handleSelectAll.bind(this);
+ this.handleSelect = this.handleSelect.bind(this);
+ this.handleDelete = this.handleDelete.bind(this);
+ this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this);
+ }
+
+ componentDidMount () {
+ this.loadJobs();
+ }
+
+ componentDidUpdate (prevProps) {
+ const { location } = this.props;
+ if (location !== prevProps.location) {
+ this.loadJobs();
+ }
+ }
+
+ handleDeleteErrorClose () {
+ this.setState({ deletionError: false });
+ }
+
+ handleSelectAll (isSelected) {
+ const { jobs } = this.state;
+ const selected = isSelected ? [...jobs] : [];
+ this.setState({ selected });
+ }
+
+ handleSelect (item) {
+ const { selected } = this.state;
+ if (selected.some(s => s.id === item.id)) {
+ this.setState({ selected: selected.filter(s => s.id !== item.id) });
+ } else {
+ this.setState({ selected: selected.concat(item) });
+ }
+ }
+
+ async handleDelete () {
+ const { selected } = this.state;
+ this.setState({ hasContentLoading: true, deletionError: false });
+ try {
+ await Promise.all(selected.map(({ id }) => UnifiedJobsAPI.destroy(id)));
+ } catch (err) {
+ this.setState({ deletionError: true });
+ } finally {
+ await this.loadJobs();
+ }
+ }
+
+ async loadJobs () {
+ const { location } = this.props;
+ const params = parseNamespacedQueryString(QS_CONFIG, location.search);
+
+ this.setState({ hasContentError: false, hasContentLoading: true });
+ try {
+ const { data: { count, results } } = await UnifiedJobsAPI.read(params);
+ this.setState({
+ itemCount: count,
+ jobs: results,
+ selected: [],
+ });
+ } catch (err) {
+ this.setState({ hasContentError: true });
+ } finally {
+ this.setState({ hasContentLoading: false });
+ }
+ }
+
+ render () {
+ const {
+ hasContentError,
+ hasContentLoading,
+ deletionError,
+ jobs,
+ itemCount,
+ selected,
+ } = this.state;
+ const {
+ match,
+ i18n
+ } = this.props;
+ const { medium } = PageSectionVariants;
+ const isAllSelected = selected.length === jobs.length;
+ const itemName = i18n._(t`Job`);
+ return (
+
+
+ (
+
+ ]}
+ />
+ )}
+ renderItem={(job) => (
+ this.handleSelect(job)}
+ isSelected={selected.some(row => row.id === job.id)}
+ />
+ )}
+ />
+
+
+ {i18n._(t`Failed to delete one or more jobs.`)}
+
+
+ );
+ }
+}
+
+export { JobsList as _JobsList };
+export default withI18n()(withRouter(JobsList));
diff --git a/src/screens/Job/JobList/JobListItem.jsx b/src/screens/Job/JobList/JobListItem.jsx
new file mode 100644
index 0000000000..3ec61565ce
--- /dev/null
+++ b/src/screens/Job/JobList/JobListItem.jsx
@@ -0,0 +1,62 @@
+import React, { Component } from 'react';
+import { Link } from 'react-router-dom';
+import {
+ DataListItem,
+ DataListItemRow,
+ DataListItemCells,
+ DataListCheck,
+ DataListCell as PFDataListCell,
+} from '@patternfly/react-core';
+import styled from 'styled-components';
+
+import VerticalSeparator from '@components/VerticalSeparator';
+import { toTitleCase } from '@util/strings';
+
+const DataListCell = styled(PFDataListCell)`
+ display: flex;
+ align-items: center;
+ @media screen and (min-width: 768px) {
+ padding-bottom: 0;
+ }
+`;
+
+class JobListItem extends Component {
+ render () {
+ const {
+ job,
+ isSelected,
+ onSelect,
+ } = this.props;
+
+ return (
+
+
+
+
+
+
+
+ {job.name}
+
+
+ ,
+ {toTitleCase(job.type)},
+ {job.finished},
+ ]}
+ />
+
+
+ );
+ }
+}
+export { JobListItem as _JobListItem };
+export default JobListItem;
diff --git a/src/screens/Job/JobList/index.js b/src/screens/Job/JobList/index.js
new file mode 100644
index 0000000000..cf71a63a21
--- /dev/null
+++ b/src/screens/Job/JobList/index.js
@@ -0,0 +1,3 @@
+export { default as JobList } from './JobList';
+export { default as JobListItem } from './JobListItem';
+
diff --git a/src/screens/Job/Jobs.jsx b/src/screens/Job/Jobs.jsx
index f7a61b82c0..6f4a1208a8 100644
--- a/src/screens/Job/Jobs.jsx
+++ b/src/screens/Job/Jobs.jsx
@@ -5,7 +5,8 @@ import { t } from '@lingui/macro';
import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs';
-import { Job } from '.';
+import Job from './Job';
+import JobList from './JobList/JobList';
class Jobs extends Component {
constructor (props) {
@@ -47,6 +48,17 @@ class Jobs extends Component {
breadcrumbConfig={breadcrumbConfig}
/>
+ (
+
+ )}
+ />
(
@@ -63,4 +75,5 @@ class Jobs extends Component {
}
}
+export { Jobs as _Jobs };
export default withI18n()(withRouter(Jobs));