mirror of
https://github.com/ansible/awx.git
synced 2026-02-25 15:06:02 -03:30
Merge pull request #5938 from keithjgrant/4239-pagination-on-delete-followup
Pagination on delete (followup) Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { useLocation, useHistory } 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 { CredentialsAPI } from '@api';
|
import { CredentialsAPI } from '@api';
|
||||||
@@ -11,12 +11,8 @@ import PaginatedDataList, {
|
|||||||
ToolbarAddButton,
|
ToolbarAddButton,
|
||||||
ToolbarDeleteButton,
|
ToolbarDeleteButton,
|
||||||
} from '@components/PaginatedDataList';
|
} from '@components/PaginatedDataList';
|
||||||
import {
|
import useRequest, { useDeleteItems } from '@util/useRequest';
|
||||||
getQSConfig,
|
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||||
parseQueryString,
|
|
||||||
replaceParams,
|
|
||||||
encodeNonDefaultQueryString,
|
|
||||||
} from '@util/qs';
|
|
||||||
import { CredentialListItem } from '.';
|
import { CredentialListItem } from '.';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('credential', {
|
const QS_CONFIG = getQSConfig('credential', {
|
||||||
@@ -26,53 +22,59 @@ const QS_CONFIG = getQSConfig('credential', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function CredentialList({ i18n }) {
|
function CredentialList({ i18n }) {
|
||||||
const [actions, setActions] = useState(null);
|
|
||||||
const [contentError, setContentError] = useState(null);
|
|
||||||
const [credentialCount, setCredentialCount] = useState(0);
|
|
||||||
const [credentials, setCredentials] = useState([]);
|
|
||||||
const [deletionError, setDeletionError] = useState(null);
|
|
||||||
const [hasContentLoading, setHasContentLoading] = useState(true);
|
|
||||||
const [selected, setSelected] = useState([]);
|
const [selected, setSelected] = useState([]);
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
const loadCredentials = async ({ search }) => {
|
const {
|
||||||
const params = parseQueryString(QS_CONFIG, search);
|
result: { credentials, credentialCount, actions },
|
||||||
setContentError(null);
|
error: contentError,
|
||||||
setHasContentLoading(true);
|
isLoading,
|
||||||
try {
|
request: fetchCredentials,
|
||||||
const [
|
} = useRequest(
|
||||||
{
|
useCallback(async () => {
|
||||||
data: { count, results },
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
},
|
const [creds, credActions] = await Promise.all([
|
||||||
{
|
|
||||||
data: { actions: optionActions },
|
|
||||||
},
|
|
||||||
] = await Promise.all([
|
|
||||||
CredentialsAPI.read(params),
|
CredentialsAPI.read(params),
|
||||||
loadCredentialActions(),
|
CredentialsAPI.readOptions(),
|
||||||
]);
|
]);
|
||||||
|
return {
|
||||||
setCredentials(results);
|
credentials: creds.data.results,
|
||||||
setCredentialCount(count);
|
credentialCount: creds.data.count,
|
||||||
setActions(optionActions);
|
actions: credActions.data.actions,
|
||||||
} catch (error) {
|
};
|
||||||
setContentError(error);
|
}, [location]),
|
||||||
} finally {
|
{
|
||||||
setHasContentLoading(false);
|
credentials: [],
|
||||||
|
credentialCount: 0,
|
||||||
|
actions: {},
|
||||||
}
|
}
|
||||||
};
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadCredentials(location);
|
fetchCredentials();
|
||||||
}, [location]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [fetchCredentials]);
|
||||||
|
|
||||||
const loadCredentialActions = () => {
|
const isAllSelected =
|
||||||
if (actions) {
|
selected.length > 0 && selected.length === credentials.length;
|
||||||
return Promise.resolve({ data: { actions } });
|
const {
|
||||||
|
isLoading: isDeleteLoading,
|
||||||
|
deleteItems: deleteCredentials,
|
||||||
|
deletionError,
|
||||||
|
clearDeletionError,
|
||||||
|
} = useDeleteItems(
|
||||||
|
useCallback(async () => {
|
||||||
|
return Promise.all(selected.map(({ id }) => CredentialsAPI.destroy(id)));
|
||||||
|
}, [selected]),
|
||||||
|
{
|
||||||
|
qsConfig: QS_CONFIG,
|
||||||
|
allItemsSelected: isAllSelected,
|
||||||
|
fetchItems: fetchCredentials,
|
||||||
}
|
}
|
||||||
return CredentialsAPI.readOptions();
|
);
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
await deleteCredentials();
|
||||||
|
setSelected([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAll = isSelected => {
|
const handleSelectAll = isSelected => {
|
||||||
@@ -87,45 +89,15 @@ function CredentialList({ i18n }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
setHasContentLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.all(
|
|
||||||
selected.map(credential => CredentialsAPI.destroy(credential.id))
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
setDeletionError(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustPagination();
|
|
||||||
setSelected([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const adjustPagination = () => {
|
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
|
||||||
if (params.page > 1 && selected.length === credentials.length) {
|
|
||||||
const newParams = encodeNonDefaultQueryString(
|
|
||||||
QS_CONFIG,
|
|
||||||
replaceParams(params, { page: params.page - 1 })
|
|
||||||
);
|
|
||||||
history.push(`${location.pathname}?${newParams}`);
|
|
||||||
} else {
|
|
||||||
loadCredentials(location);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const canAdd =
|
const canAdd =
|
||||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||||
const isAllSelected =
|
|
||||||
selected.length > 0 && selected.length === credentials.length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
contentError={contentError}
|
contentError={contentError}
|
||||||
hasContentLoading={hasContentLoading}
|
hasContentLoading={isLoading || isDeleteLoading}
|
||||||
items={credentials}
|
items={credentials}
|
||||||
itemCount={credentialCount}
|
itemCount={credentialCount}
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
@@ -165,7 +137,7 @@ function CredentialList({ i18n }) {
|
|||||||
isOpen={deletionError}
|
isOpen={deletionError}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
title={i18n._(t`Error!`)}
|
title={i18n._(t`Error!`)}
|
||||||
onClose={() => setDeletionError(null)}
|
onClose={clearDeletionError}
|
||||||
>
|
>
|
||||||
{i18n._(t`Failed to delete one or more credentials.`)}
|
{i18n._(t`Failed to delete one or more credentials.`)}
|
||||||
<ErrorDetail error={deletionError} />
|
<ErrorDetail error={deletionError} />
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ import { getQSConfig, parseQueryString } from '@util/qs';
|
|||||||
import AddDropDownButton from '@components/AddDropDownButton';
|
import AddDropDownButton from '@components/AddDropDownButton';
|
||||||
import InventoryListItem from './InventoryListItem';
|
import InventoryListItem from './InventoryListItem';
|
||||||
|
|
||||||
// The type value in const QS_CONFIG below does not have a space between job_inventory and
|
|
||||||
// workflow_job_inventory so the params sent to the API match what the api expects.
|
|
||||||
const QS_CONFIG = getQSConfig('inventory', {
|
const QS_CONFIG = getQSConfig('inventory', {
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 20,
|
page_size: 20,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { withRouter } 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, PageSection } from '@patternfly/react-core';
|
||||||
@@ -19,6 +19,7 @@ import ErrorDetail from '@components/ErrorDetail';
|
|||||||
import PaginatedDataList, {
|
import PaginatedDataList, {
|
||||||
ToolbarDeleteButton,
|
ToolbarDeleteButton,
|
||||||
} from '@components/PaginatedDataList';
|
} from '@components/PaginatedDataList';
|
||||||
|
import useRequest, { useDeleteItems } from '@util/useRequest';
|
||||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||||
|
|
||||||
import JobListItem from './JobListItem';
|
import JobListItem from './JobListItem';
|
||||||
@@ -34,257 +35,216 @@ const QS_CONFIG = getQSConfig(
|
|||||||
['page', 'page_size', 'id']
|
['page', 'page_size', 'id']
|
||||||
);
|
);
|
||||||
|
|
||||||
class JobList extends Component {
|
function JobList({ i18n }) {
|
||||||
constructor(props) {
|
const [selected, setSelected] = useState([]);
|
||||||
super(props);
|
const location = useLocation();
|
||||||
|
|
||||||
this.state = {
|
const {
|
||||||
hasContentLoading: true,
|
result: { jobs, itemCount },
|
||||||
deletionError: null,
|
error: contentError,
|
||||||
contentError: null,
|
isLoading,
|
||||||
selected: [],
|
request: fetchJobs,
|
||||||
jobs: [],
|
} = useRequest(
|
||||||
itemCount: 0,
|
useCallback(async () => {
|
||||||
};
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
this.loadJobs = this.loadJobs.bind(this);
|
|
||||||
this.handleSelectAll = this.handleSelectAll.bind(this);
|
|
||||||
this.handleSelect = this.handleSelect.bind(this);
|
|
||||||
this.handleJobDelete = this.handleJobDelete.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: null });
|
|
||||||
}
|
|
||||||
|
|
||||||
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 handleJobDelete() {
|
|
||||||
const { selected, itemCount } = this.state;
|
|
||||||
this.setState({ hasContentLoading: true });
|
|
||||||
try {
|
|
||||||
await Promise.all(
|
|
||||||
selected.map(({ type, id }) => {
|
|
||||||
let deletePromise;
|
|
||||||
switch (type) {
|
|
||||||
case 'job':
|
|
||||||
deletePromise = JobsAPI.destroy(id);
|
|
||||||
break;
|
|
||||||
case 'ad_hoc_command':
|
|
||||||
deletePromise = AdHocCommandsAPI.destroy(id);
|
|
||||||
break;
|
|
||||||
case 'system_job':
|
|
||||||
deletePromise = SystemJobsAPI.destroy(id);
|
|
||||||
break;
|
|
||||||
case 'project_update':
|
|
||||||
deletePromise = ProjectUpdatesAPI.destroy(id);
|
|
||||||
break;
|
|
||||||
case 'inventory_update':
|
|
||||||
deletePromise = InventoryUpdatesAPI.destroy(id);
|
|
||||||
break;
|
|
||||||
case 'workflow_job':
|
|
||||||
deletePromise = WorkflowJobsAPI.destroy(id);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return deletePromise;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this.setState({ itemCount: itemCount - selected.length });
|
|
||||||
} catch (err) {
|
|
||||||
this.setState({ deletionError: err });
|
|
||||||
} finally {
|
|
||||||
await this.loadJobs();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadJobs() {
|
|
||||||
const { location } = this.props;
|
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
|
||||||
|
|
||||||
this.setState({ contentError: null, hasContentLoading: true });
|
|
||||||
try {
|
|
||||||
const {
|
const {
|
||||||
data: { count, results },
|
data: { count, results },
|
||||||
} = await UnifiedJobsAPI.read(params);
|
} = await UnifiedJobsAPI.read(params);
|
||||||
this.setState({
|
return {
|
||||||
itemCount: count,
|
itemCount: count,
|
||||||
jobs: results,
|
jobs: results,
|
||||||
selected: [],
|
};
|
||||||
});
|
}, [location]),
|
||||||
} catch (err) {
|
{
|
||||||
this.setState({ contentError: err });
|
jobs: [],
|
||||||
} finally {
|
itemCount: 0,
|
||||||
this.setState({ hasContentLoading: false });
|
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
|
|
||||||
render() {
|
useEffect(() => {
|
||||||
const {
|
fetchJobs();
|
||||||
contentError,
|
}, [fetchJobs]);
|
||||||
hasContentLoading,
|
|
||||||
deletionError,
|
const isAllSelected = selected.length === jobs.length && selected.length > 0;
|
||||||
jobs,
|
const {
|
||||||
itemCount,
|
isLoading: isDeleteLoading,
|
||||||
selected,
|
deleteItems: deleteJobs,
|
||||||
} = this.state;
|
deletionError,
|
||||||
const { match, i18n } = this.props;
|
clearDeletionError,
|
||||||
const isAllSelected =
|
} = useDeleteItems(
|
||||||
selected.length === jobs.length && selected.length > 0;
|
useCallback(async () => {
|
||||||
return (
|
return Promise.all(
|
||||||
<PageSection>
|
selected.map(({ type, id }) => {
|
||||||
<Card>
|
switch (type) {
|
||||||
<PaginatedDataList
|
case 'job':
|
||||||
contentError={contentError}
|
return JobsAPI.destroy(id);
|
||||||
hasContentLoading={hasContentLoading}
|
case 'ad_hoc_command':
|
||||||
items={jobs}
|
return AdHocCommandsAPI.destroy(id);
|
||||||
itemCount={itemCount}
|
case 'system_job':
|
||||||
pluralizedItemName="Jobs"
|
return SystemJobsAPI.destroy(id);
|
||||||
qsConfig={QS_CONFIG}
|
case 'project_update':
|
||||||
onRowClick={this.handleSelect}
|
return ProjectUpdatesAPI.destroy(id);
|
||||||
toolbarSearchColumns={[
|
case 'inventory_update':
|
||||||
{
|
return InventoryUpdatesAPI.destroy(id);
|
||||||
name: i18n._(t`Name`),
|
case 'workflow_job':
|
||||||
key: 'name',
|
return WorkflowJobsAPI.destroy(id);
|
||||||
isDefault: true,
|
default:
|
||||||
},
|
return null;
|
||||||
{
|
}
|
||||||
name: i18n._(t`ID`),
|
})
|
||||||
key: 'id',
|
);
|
||||||
},
|
}, [selected]),
|
||||||
{
|
{
|
||||||
name: i18n._(t`Label Name`),
|
qsConfig: QS_CONFIG,
|
||||||
key: 'labels__name',
|
allItemsSelected: isAllSelected,
|
||||||
},
|
fetchItems: fetchJobs,
|
||||||
{
|
}
|
||||||
name: i18n._(t`Job Type`),
|
);
|
||||||
key: `type`,
|
|
||||||
options: [
|
const handleJobDelete = async () => {
|
||||||
[`project_update`, i18n._(t`SCM Update`)],
|
await deleteJobs();
|
||||||
[`inventory_update`, i18n._(t`Inventory Sync`)],
|
setSelected([]);
|
||||||
[`job`, i18n._(t`Playbook Run`)],
|
};
|
||||||
[`ad_hoc_command`, i18n._(t`Command`)],
|
|
||||||
[`system_job`, i18n._(t`Management Job`)],
|
const handleSelectAll = isSelected => {
|
||||||
[`workflow_job`, i18n._(t`Workflow Job`)],
|
setSelected(isSelected ? [...jobs] : []);
|
||||||
],
|
};
|
||||||
},
|
|
||||||
{
|
const handleSelect = item => {
|
||||||
name: i18n._(t`Launched By (Username)`),
|
if (selected.some(s => s.id === item.id)) {
|
||||||
key: 'created_by__username',
|
setSelected(selected.filter(s => s.id !== item.id));
|
||||||
},
|
} else {
|
||||||
{
|
setSelected(selected.concat(item));
|
||||||
name: i18n._(t`Status`),
|
}
|
||||||
key: 'status',
|
};
|
||||||
options: [
|
|
||||||
[`new`, i18n._(t`New`)],
|
return (
|
||||||
[`pending`, i18n._(t`Pending`)],
|
<PageSection>
|
||||||
[`waiting`, i18n._(t`Waiting`)],
|
<Card>
|
||||||
[`running`, i18n._(t`Running`)],
|
<PaginatedDataList
|
||||||
[`successful`, i18n._(t`Successful`)],
|
contentError={contentError}
|
||||||
[`failed`, i18n._(t`Failed`)],
|
hasContentLoading={isLoading || isDeleteLoading}
|
||||||
[`error`, i18n._(t`Error`)],
|
items={jobs}
|
||||||
[`canceled`, i18n._(t`Canceled`)],
|
itemCount={itemCount}
|
||||||
],
|
pluralizedItemName="Jobs"
|
||||||
},
|
qsConfig={QS_CONFIG}
|
||||||
{
|
onRowClick={handleSelect}
|
||||||
name: i18n._(t`Limit`),
|
toolbarSearchColumns={[
|
||||||
key: 'job__limit',
|
{
|
||||||
},
|
name: i18n._(t`Name`),
|
||||||
]}
|
key: 'name',
|
||||||
toolbarSortColumns={[
|
isDefault: true,
|
||||||
{
|
},
|
||||||
name: i18n._(t`Finish Time`),
|
{
|
||||||
key: 'finished',
|
name: i18n._(t`ID`),
|
||||||
},
|
key: 'id',
|
||||||
{
|
},
|
||||||
name: i18n._(t`ID`),
|
{
|
||||||
key: 'id',
|
name: i18n._(t`Label Name`),
|
||||||
},
|
key: 'labels__name',
|
||||||
{
|
},
|
||||||
name: i18n._(t`Launched By`),
|
{
|
||||||
key: 'created_by__id',
|
name: i18n._(t`Job Type`),
|
||||||
},
|
key: `type`,
|
||||||
{
|
options: [
|
||||||
name: i18n._(t`Name`),
|
[`project_update`, i18n._(t`SCM Update`)],
|
||||||
key: 'name',
|
[`inventory_update`, i18n._(t`Inventory Sync`)],
|
||||||
},
|
[`job`, i18n._(t`Playbook Run`)],
|
||||||
{
|
[`ad_hoc_command`, i18n._(t`Command`)],
|
||||||
name: i18n._(t`Project`),
|
[`system_job`, i18n._(t`Management Job`)],
|
||||||
key: 'unified_job_template__project__id',
|
[`workflow_job`, i18n._(t`Workflow Job`)],
|
||||||
},
|
],
|
||||||
{
|
},
|
||||||
name: i18n._(t`Start Time`),
|
{
|
||||||
key: 'started',
|
name: i18n._(t`Launched By (Username)`),
|
||||||
},
|
key: 'created_by__username',
|
||||||
]}
|
},
|
||||||
renderToolbar={props => (
|
{
|
||||||
<DatalistToolbar
|
name: i18n._(t`Status`),
|
||||||
{...props}
|
key: 'status',
|
||||||
showSelectAll
|
options: [
|
||||||
showExpandCollapse
|
[`new`, i18n._(t`New`)],
|
||||||
isAllSelected={isAllSelected}
|
[`pending`, i18n._(t`Pending`)],
|
||||||
onSelectAll={this.handleSelectAll}
|
[`waiting`, i18n._(t`Waiting`)],
|
||||||
qsConfig={QS_CONFIG}
|
[`running`, i18n._(t`Running`)],
|
||||||
additionalControls={[
|
[`successful`, i18n._(t`Successful`)],
|
||||||
<ToolbarDeleteButton
|
[`failed`, i18n._(t`Failed`)],
|
||||||
key="delete"
|
[`error`, i18n._(t`Error`)],
|
||||||
onDelete={this.handleJobDelete}
|
[`canceled`, i18n._(t`Canceled`)],
|
||||||
itemsToDelete={selected}
|
],
|
||||||
pluralizedItemName="Jobs"
|
},
|
||||||
/>,
|
{
|
||||||
]}
|
name: i18n._(t`Limit`),
|
||||||
/>
|
key: 'job__limit',
|
||||||
)}
|
},
|
||||||
renderItem={job => (
|
]}
|
||||||
<JobListItem
|
toolbarSortColumns={[
|
||||||
key={job.id}
|
{
|
||||||
value={job.name}
|
name: i18n._(t`Finish Time`),
|
||||||
job={job}
|
key: 'finished',
|
||||||
detailUrl={`${match.url}/${job}/${job.id}`}
|
},
|
||||||
onSelect={() => this.handleSelect(job)}
|
{
|
||||||
isSelected={selected.some(row => row.id === job.id)}
|
name: i18n._(t`ID`),
|
||||||
/>
|
key: 'id',
|
||||||
)}
|
},
|
||||||
/>
|
{
|
||||||
</Card>
|
name: i18n._(t`Launched By`),
|
||||||
<AlertModal
|
key: 'created_by__id',
|
||||||
isOpen={deletionError}
|
},
|
||||||
variant="danger"
|
{
|
||||||
title={i18n._(t`Error!`)}
|
name: i18n._(t`Name`),
|
||||||
onClose={this.handleDeleteErrorClose}
|
key: 'name',
|
||||||
>
|
},
|
||||||
{i18n._(t`Failed to delete one or more jobs.`)}
|
{
|
||||||
<ErrorDetail error={deletionError} />
|
name: i18n._(t`Project`),
|
||||||
</AlertModal>
|
key: 'unified_job_template__project__id',
|
||||||
</PageSection>
|
},
|
||||||
);
|
{
|
||||||
}
|
name: i18n._(t`Start Time`),
|
||||||
|
key: 'started',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
renderToolbar={props => (
|
||||||
|
<DatalistToolbar
|
||||||
|
{...props}
|
||||||
|
showSelectAll
|
||||||
|
showExpandCollapse
|
||||||
|
isAllSelected={isAllSelected}
|
||||||
|
onSelectAll={handleSelectAll}
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
additionalControls={[
|
||||||
|
<ToolbarDeleteButton
|
||||||
|
key="delete"
|
||||||
|
onDelete={handleJobDelete}
|
||||||
|
itemsToDelete={selected}
|
||||||
|
pluralizedItemName="Jobs"
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderItem={job => (
|
||||||
|
<JobListItem
|
||||||
|
key={job.id}
|
||||||
|
value={job.name}
|
||||||
|
job={job}
|
||||||
|
detailUrl={`${location.pathname}/${job}/${job.id}`}
|
||||||
|
onSelect={() => handleSelect(job)}
|
||||||
|
isSelected={selected.some(row => row.id === job.id)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
<AlertModal
|
||||||
|
isOpen={deletionError}
|
||||||
|
variant="danger"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={clearDeletionError}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to delete one or more jobs.`)}
|
||||||
|
<ErrorDetail error={deletionError} />
|
||||||
|
</AlertModal>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { JobList as _JobList };
|
export default withI18n()(JobList);
|
||||||
export default withI18n()(withRouter(JobList));
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AdHocCommandsAPI,
|
AdHocCommandsAPI,
|
||||||
InventoryUpdatesAPI,
|
InventoryUpdatesAPI,
|
||||||
@@ -87,58 +87,118 @@ UnifiedJobsAPI.read.mockResolvedValue({
|
|||||||
data: { count: 3, results: mockResults },
|
data: { count: 3, results: mockResults },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function waitForLoaded(wrapper) {
|
||||||
|
return waitForElement(
|
||||||
|
wrapper,
|
||||||
|
'JobList',
|
||||||
|
el => el.find('ContentLoading').length === 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
describe('<JobList />', () => {
|
describe('<JobList />', () => {
|
||||||
test('initially renders succesfully', async done => {
|
test('initially renders succesfully', async () => {
|
||||||
const wrapper = mountWithContexts(<JobList />);
|
let wrapper;
|
||||||
await waitForElement(
|
await act(async () => {
|
||||||
wrapper,
|
wrapper = mountWithContexts(<JobList />);
|
||||||
'JobList',
|
});
|
||||||
el => el.state('jobs').length === 6
|
await waitForLoaded(wrapper);
|
||||||
);
|
expect(wrapper.find('JobListItem')).toHaveLength(6);
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('select makes expected state updates', async done => {
|
test('should select and un-select items', async () => {
|
||||||
const [mockItem] = mockResults;
|
const [mockItem] = mockResults;
|
||||||
const wrapper = mountWithContexts(<JobList />);
|
let wrapper;
|
||||||
await waitForElement(wrapper, 'JobListItem', el => el.length === 6);
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<JobList />);
|
||||||
|
});
|
||||||
|
await waitForLoaded(wrapper);
|
||||||
|
|
||||||
wrapper
|
act(() => {
|
||||||
.find('JobListItem')
|
wrapper
|
||||||
.first()
|
.find('JobListItem')
|
||||||
.prop('onSelect')(mockItem);
|
.first()
|
||||||
expect(wrapper.find('JobList').state('selected').length).toEqual(1);
|
.invoke('onSelect')(mockItem);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('JobListItem')
|
||||||
|
.first()
|
||||||
|
.prop('isSelected')
|
||||||
|
).toEqual(true);
|
||||||
|
expect(
|
||||||
|
wrapper.find('ToolbarDeleteButton').prop('itemsToDelete')
|
||||||
|
).toHaveLength(1);
|
||||||
|
|
||||||
wrapper
|
act(() => {
|
||||||
.find('JobListItem')
|
wrapper
|
||||||
.first()
|
.find('JobListItem')
|
||||||
.prop('onSelect')(mockItem);
|
.first()
|
||||||
expect(wrapper.find('JobList').state('selected').length).toEqual(0);
|
.invoke('onSelect')(mockItem);
|
||||||
|
});
|
||||||
done();
|
wrapper.update();
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('JobListItem')
|
||||||
|
.first()
|
||||||
|
.prop('isSelected')
|
||||||
|
).toEqual(false);
|
||||||
|
expect(
|
||||||
|
wrapper.find('ToolbarDeleteButton').prop('itemsToDelete')
|
||||||
|
).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('select-all-delete makes expected state updates and api calls', async done => {
|
test('should select and deselect all', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<JobList />);
|
||||||
|
});
|
||||||
|
await waitForLoaded(wrapper);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('DataListToolbar').invoke('onSelectAll')(true);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
wrapper.find('JobListItem');
|
||||||
|
expect(
|
||||||
|
wrapper.find('ToolbarDeleteButton').prop('itemsToDelete')
|
||||||
|
).toHaveLength(6);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('DataListToolbar').invoke('onSelectAll')(false);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
wrapper.find('JobListItem');
|
||||||
|
expect(
|
||||||
|
wrapper.find('ToolbarDeleteButton').prop('itemsToDelete')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should send all corresponding delete API requests', async () => {
|
||||||
AdHocCommandsAPI.destroy = jest.fn();
|
AdHocCommandsAPI.destroy = jest.fn();
|
||||||
InventoryUpdatesAPI.destroy = jest.fn();
|
InventoryUpdatesAPI.destroy = jest.fn();
|
||||||
JobsAPI.destroy = jest.fn();
|
JobsAPI.destroy = jest.fn();
|
||||||
ProjectUpdatesAPI.destroy = jest.fn();
|
ProjectUpdatesAPI.destroy = jest.fn();
|
||||||
SystemJobsAPI.destroy = jest.fn();
|
SystemJobsAPI.destroy = jest.fn();
|
||||||
WorkflowJobsAPI.destroy = jest.fn();
|
WorkflowJobsAPI.destroy = jest.fn();
|
||||||
const wrapper = mountWithContexts(<JobList />);
|
let wrapper;
|
||||||
await waitForElement(wrapper, 'JobListItem', el => el.length === 6);
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<JobList />);
|
||||||
|
});
|
||||||
|
await waitForLoaded(wrapper);
|
||||||
|
|
||||||
wrapper.find('DataListToolbar').prop('onSelectAll')(true);
|
act(() => {
|
||||||
expect(wrapper.find('JobList').state('selected').length).toEqual(6);
|
wrapper.find('DataListToolbar').invoke('onSelectAll')(true);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
wrapper.find('JobListItem');
|
||||||
|
expect(
|
||||||
|
wrapper.find('ToolbarDeleteButton').prop('itemsToDelete')
|
||||||
|
).toHaveLength(6);
|
||||||
|
|
||||||
wrapper.find('DataListToolbar').prop('onSelectAll')(false);
|
await act(async () => {
|
||||||
expect(wrapper.find('JobList').state('selected').length).toEqual(0);
|
wrapper.find('ToolbarDeleteButton').invoke('onDelete')();
|
||||||
|
});
|
||||||
wrapper.find('DataListToolbar').prop('onSelectAll')(true);
|
|
||||||
expect(wrapper.find('JobList').state('selected').length).toEqual(6);
|
|
||||||
|
|
||||||
wrapper.find('ToolbarDeleteButton').prop('onDelete')();
|
|
||||||
expect(AdHocCommandsAPI.destroy).toHaveBeenCalledTimes(1);
|
expect(AdHocCommandsAPI.destroy).toHaveBeenCalledTimes(1);
|
||||||
expect(InventoryUpdatesAPI.destroy).toHaveBeenCalledTimes(1);
|
expect(InventoryUpdatesAPI.destroy).toHaveBeenCalledTimes(1);
|
||||||
expect(JobsAPI.destroy).toHaveBeenCalledTimes(1);
|
expect(JobsAPI.destroy).toHaveBeenCalledTimes(1);
|
||||||
@@ -146,12 +206,12 @@ describe('<JobList />', () => {
|
|||||||
expect(SystemJobsAPI.destroy).toHaveBeenCalledTimes(1);
|
expect(SystemJobsAPI.destroy).toHaveBeenCalledTimes(1);
|
||||||
expect(WorkflowJobsAPI.destroy).toHaveBeenCalledTimes(1);
|
expect(WorkflowJobsAPI.destroy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
done();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('error is shown when job not successfully deleted from api', async done => {
|
test('error is shown when job not successfully deleted from api', async () => {
|
||||||
JobsAPI.destroy.mockRejectedValue(
|
JobsAPI.destroy.mockImplementation(() => {
|
||||||
new Error({
|
throw new Error({
|
||||||
response: {
|
response: {
|
||||||
config: {
|
config: {
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
@@ -159,21 +219,29 @@ describe('<JobList />', () => {
|
|||||||
},
|
},
|
||||||
data: 'An error occurred',
|
data: 'An error occurred',
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
);
|
|
||||||
const wrapper = mountWithContexts(<JobList />);
|
|
||||||
wrapper.find('JobList').setState({
|
|
||||||
jobs: mockResults,
|
|
||||||
itemCount: 6,
|
|
||||||
selected: mockResults.slice(1, 2),
|
|
||||||
});
|
});
|
||||||
wrapper.find('ToolbarDeleteButton').prop('onDelete')();
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<JobList />);
|
||||||
|
});
|
||||||
|
await waitForLoaded(wrapper);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find('JobListItem')
|
||||||
|
.at(1)
|
||||||
|
.invoke('onSelect')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('ToolbarDeleteButton').invoke('onDelete')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
await waitForElement(
|
await waitForElement(
|
||||||
wrapper,
|
wrapper,
|
||||||
'Modal',
|
'Modal',
|
||||||
el => el.props().isOpen === true && el.props().title === 'Error!'
|
el => el.props().isOpen === true && el.props().title === 'Error!'
|
||||||
);
|
);
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { useLocation, useHistory, useRouteMatch } from 'react-router-dom';
|
import { 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 { Card, PageSection } from '@patternfly/react-core';
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
|
|
||||||
import { OrganizationsAPI } from '@api';
|
import { OrganizationsAPI } from '@api';
|
||||||
import useRequest from '@util/useRequest';
|
import useRequest, { useDeleteItems } from '@util/useRequest';
|
||||||
import AlertModal from '@components/AlertModal';
|
import AlertModal from '@components/AlertModal';
|
||||||
import DataListToolbar from '@components/DataListToolbar';
|
import DataListToolbar from '@components/DataListToolbar';
|
||||||
import ErrorDetail from '@components/ErrorDetail';
|
import ErrorDetail from '@components/ErrorDetail';
|
||||||
@@ -13,13 +13,7 @@ import PaginatedDataList, {
|
|||||||
ToolbarAddButton,
|
ToolbarAddButton,
|
||||||
ToolbarDeleteButton,
|
ToolbarDeleteButton,
|
||||||
} from '@components/PaginatedDataList';
|
} from '@components/PaginatedDataList';
|
||||||
import {
|
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||||
getQSConfig,
|
|
||||||
parseQueryString,
|
|
||||||
replaceParams,
|
|
||||||
encodeNonDefaultQueryString,
|
|
||||||
} from '@util/qs';
|
|
||||||
|
|
||||||
import OrganizationListItem from './OrganizationListItem';
|
import OrganizationListItem from './OrganizationListItem';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('organization', {
|
const QS_CONFIG = getQSConfig('organization', {
|
||||||
@@ -30,11 +24,9 @@ const QS_CONFIG = getQSConfig('organization', {
|
|||||||
|
|
||||||
function OrganizationsList({ i18n }) {
|
function OrganizationsList({ i18n }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useHistory();
|
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
|
|
||||||
const [selected, setSelected] = useState([]);
|
const [selected, setSelected] = useState([]);
|
||||||
const [deletionError, setDeletionError] = useState(null);
|
|
||||||
|
|
||||||
const addUrl = `${match.url}/add`;
|
const addUrl = `${match.url}/add`;
|
||||||
|
|
||||||
@@ -63,58 +55,40 @@ function OrganizationsList({ i18n }) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
|
||||||
isLoading: isDeleteLoading,
|
|
||||||
error: dError,
|
|
||||||
request: deleteOrganizations,
|
|
||||||
} = useRequest(
|
|
||||||
useCallback(async () => {
|
|
||||||
return Promise.all(
|
|
||||||
selected.map(({ id }) => OrganizationsAPI.destroy(id))
|
|
||||||
);
|
|
||||||
}, [selected])
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (dError) {
|
|
||||||
setDeletionError(dError);
|
|
||||||
}
|
|
||||||
}, [dError]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchOrganizations();
|
fetchOrganizations();
|
||||||
}, [fetchOrganizations]);
|
}, [fetchOrganizations]);
|
||||||
|
|
||||||
|
const isAllSelected =
|
||||||
|
selected.length === organizations.length && selected.length > 0;
|
||||||
|
const {
|
||||||
|
isLoading: isDeleteLoading,
|
||||||
|
deleteItems: deleteOrganizations,
|
||||||
|
deletionError,
|
||||||
|
clearDeletionError,
|
||||||
|
} = useDeleteItems(
|
||||||
|
useCallback(async () => {
|
||||||
|
return Promise.all(
|
||||||
|
selected.map(({ id }) => OrganizationsAPI.destroy(id))
|
||||||
|
);
|
||||||
|
}, [selected]),
|
||||||
|
{
|
||||||
|
qsConfig: QS_CONFIG,
|
||||||
|
allItemsSelected: isAllSelected,
|
||||||
|
fetchItems: fetchOrganizations,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const handleOrgDelete = async () => {
|
const handleOrgDelete = async () => {
|
||||||
await deleteOrganizations();
|
await deleteOrganizations();
|
||||||
await adjustPagination();
|
|
||||||
setSelected([]);
|
setSelected([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const adjustPagination = () => {
|
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
|
||||||
if (params.page > 1 && selected.length === organizations.length) {
|
|
||||||
const newParams = encodeNonDefaultQueryString(
|
|
||||||
QS_CONFIG,
|
|
||||||
replaceParams(params, { page: params.page - 1 })
|
|
||||||
);
|
|
||||||
history.push(`${location.pathname}?${newParams}`);
|
|
||||||
} else {
|
|
||||||
fetchOrganizations();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasContentLoading = isDeleteLoading || isOrgsLoading;
|
const hasContentLoading = isDeleteLoading || isOrgsLoading;
|
||||||
const canAdd = actions && actions.POST;
|
const canAdd = actions && actions.POST;
|
||||||
const isAllSelected =
|
|
||||||
selected.length === organizations.length && selected.length > 0;
|
|
||||||
|
|
||||||
const handleSelectAll = isSelected => {
|
const handleSelectAll = isSelected => {
|
||||||
if (isSelected) {
|
setSelected(isSelected ? [...organizations] : []);
|
||||||
setSelected(organizations);
|
|
||||||
} else {
|
|
||||||
setSelected([]);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelect = row => {
|
const handleSelect = row => {
|
||||||
@@ -197,7 +171,7 @@ function OrganizationsList({ i18n }) {
|
|||||||
isOpen={deletionError}
|
isOpen={deletionError}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
title={i18n._(t`Error!`)}
|
title={i18n._(t`Error!`)}
|
||||||
onClose={() => setDeletionError(null)}
|
onClose={clearDeletionError}
|
||||||
>
|
>
|
||||||
{i18n._(t`Failed to delete one or more organizations.`)}
|
{i18n._(t`Failed to delete one or more organizations.`)}
|
||||||
<ErrorDetail error={deletionError} />
|
<ErrorDetail error={deletionError} />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import { useParams, useLocation } from 'react-router-dom';
|
import { useParams, 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';
|
||||||
@@ -15,6 +15,7 @@ import ErrorDetail from '@components/ErrorDetail';
|
|||||||
import PaginatedDataList, {
|
import PaginatedDataList, {
|
||||||
ToolbarDeleteButton,
|
ToolbarDeleteButton,
|
||||||
} from '@components/PaginatedDataList';
|
} from '@components/PaginatedDataList';
|
||||||
|
import useRequest, { useDeleteItems } from '@util/useRequest';
|
||||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||||
|
|
||||||
import AddDropDownButton from '@components/AddDropDownButton';
|
import AddDropDownButton from '@components/AddDropDownButton';
|
||||||
@@ -31,82 +32,80 @@ const QS_CONFIG = getQSConfig('template', {
|
|||||||
|
|
||||||
function TemplateList({ i18n }) {
|
function TemplateList({ i18n }) {
|
||||||
const { id: projectId } = useParams();
|
const { id: projectId } = useParams();
|
||||||
const { pathname, search } = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const [deletionError, setDeletionError] = useState(null);
|
|
||||||
const [contentError, setContentError] = useState(null);
|
|
||||||
const [hasContentLoading, setHasContentLoading] = useState(true);
|
|
||||||
const [jtActions, setJTActions] = useState(null);
|
|
||||||
const [wfjtActions, setWFJTActions] = useState(null);
|
|
||||||
const [count, setCount] = useState(0);
|
|
||||||
const [templates, setTemplates] = useState([]);
|
|
||||||
const [selected, setSelected] = useState([]);
|
const [selected, setSelected] = useState([]);
|
||||||
|
|
||||||
useEffect(
|
const {
|
||||||
() => {
|
result: { templates, count, jtActions, wfjtActions },
|
||||||
const loadTemplates = async () => {
|
error: contentError,
|
||||||
const params = {
|
isLoading,
|
||||||
...parseQueryString(QS_CONFIG, search),
|
request: fetchTemplates,
|
||||||
};
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
let jtOptionsPromise;
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
if (jtActions) {
|
if (location.pathname.startsWith('/projects') && projectId) {
|
||||||
jtOptionsPromise = Promise.resolve({
|
params.jobtemplate__project = projectId;
|
||||||
data: { actions: jtActions },
|
}
|
||||||
});
|
const results = await Promise.all([
|
||||||
} else {
|
UnifiedJobTemplatesAPI.read(params),
|
||||||
jtOptionsPromise = JobTemplatesAPI.readOptions();
|
JobTemplatesAPI.readOptions(),
|
||||||
}
|
WorkflowJobTemplatesAPI.readOptions(),
|
||||||
|
]);
|
||||||
let wfjtOptionsPromise;
|
return {
|
||||||
if (wfjtActions) {
|
templates: results[0].data.results,
|
||||||
wfjtOptionsPromise = Promise.resolve({
|
count: results[0].data.count,
|
||||||
data: { actions: wfjtActions },
|
jtActions: results[1].data.actions,
|
||||||
});
|
wfjtActions: results[2].data.actions,
|
||||||
} else {
|
|
||||||
wfjtOptionsPromise = WorkflowJobTemplatesAPI.readOptions();
|
|
||||||
}
|
|
||||||
if (pathname.startsWith('/projects') && projectId) {
|
|
||||||
params.jobtemplate__project = projectId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const promises = Promise.all([
|
|
||||||
UnifiedJobTemplatesAPI.read(params),
|
|
||||||
jtOptionsPromise,
|
|
||||||
wfjtOptionsPromise,
|
|
||||||
]);
|
|
||||||
setDeletionError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [
|
|
||||||
{
|
|
||||||
data: { count: itemCount, results },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: { actions: jobTemplateActions },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: { actions: workFlowJobTemplateActions },
|
|
||||||
},
|
|
||||||
] = await promises;
|
|
||||||
setJTActions(jobTemplateActions);
|
|
||||||
setWFJTActions(workFlowJobTemplateActions);
|
|
||||||
setCount(itemCount);
|
|
||||||
setTemplates(results);
|
|
||||||
setHasContentLoading(false);
|
|
||||||
} catch (err) {
|
|
||||||
setContentError(err);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
loadTemplates();
|
}, [location, projectId]),
|
||||||
},
|
{
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
templates: [],
|
||||||
[pathname, search, count, projectId]
|
count: 0,
|
||||||
|
jtActions: {},
|
||||||
|
wfjtActions: {},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTemplates();
|
||||||
|
}, [fetchTemplates]);
|
||||||
|
|
||||||
|
const isAllSelected =
|
||||||
|
selected.length === templates.length && selected.length > 0;
|
||||||
|
const {
|
||||||
|
isLoading: isDeleteLoading,
|
||||||
|
deleteItems: deleteTemplates,
|
||||||
|
deletionError,
|
||||||
|
clearDeletionError,
|
||||||
|
} = useDeleteItems(
|
||||||
|
useCallback(async () => {
|
||||||
|
return Promise.all(
|
||||||
|
selected.map(({ type, id }) => {
|
||||||
|
if (type === 'job_template') {
|
||||||
|
return JobTemplatesAPI.destroy(id);
|
||||||
|
}
|
||||||
|
if (type === 'workflow_job_template') {
|
||||||
|
return WorkflowJobTemplatesAPI.destroy(id);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [selected]),
|
||||||
|
{
|
||||||
|
qsConfig: QS_CONFIG,
|
||||||
|
allItemsSelected: isAllSelected,
|
||||||
|
fetchItems: fetchTemplates,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTemplateDelete = async () => {
|
||||||
|
await deleteTemplates();
|
||||||
|
setSelected([]);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSelectAll = isSelected => {
|
const handleSelectAll = isSelected => {
|
||||||
const selectedItems = isSelected ? [...templates] : [];
|
setSelected(isSelected ? [...templates] : []);
|
||||||
setSelected(selectedItems);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelect = template => {
|
const handleSelect = template => {
|
||||||
@@ -117,26 +116,6 @@ function TemplateList({ i18n }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTemplateDelete = async () => {
|
|
||||||
setHasContentLoading(true);
|
|
||||||
try {
|
|
||||||
await Promise.all(
|
|
||||||
selected.map(({ type, id }) => {
|
|
||||||
let deletePromise;
|
|
||||||
if (type === 'job_template') {
|
|
||||||
deletePromise = JobTemplatesAPI.destroy(id);
|
|
||||||
} else if (type === 'workflow_job_template') {
|
|
||||||
deletePromise = WorkflowJobTemplatesAPI.destroy(id);
|
|
||||||
}
|
|
||||||
return deletePromise;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
setCount(count - selected.length);
|
|
||||||
} catch (err) {
|
|
||||||
setDeletionError(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const canAddJT =
|
const canAddJT =
|
||||||
jtActions && Object.prototype.hasOwnProperty.call(jtActions, 'POST');
|
jtActions && Object.prototype.hasOwnProperty.call(jtActions, 'POST');
|
||||||
const canAddWFJT =
|
const canAddWFJT =
|
||||||
@@ -154,8 +133,6 @@ function TemplateList({ i18n }) {
|
|||||||
url: `/templates/workflow_job_template/add/`,
|
url: `/templates/workflow_job_template/add/`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const isAllSelected =
|
|
||||||
selected.length === templates.length && selected.length > 0;
|
|
||||||
const addButton = (
|
const addButton = (
|
||||||
<AddDropDownButton key="add" dropdownItems={addButtonOptions} />
|
<AddDropDownButton key="add" dropdownItems={addButtonOptions} />
|
||||||
);
|
);
|
||||||
@@ -164,7 +141,7 @@ function TemplateList({ i18n }) {
|
|||||||
<Card>
|
<Card>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
contentError={contentError}
|
contentError={contentError}
|
||||||
hasContentLoading={hasContentLoading}
|
hasContentLoading={isLoading || isDeleteLoading}
|
||||||
items={templates}
|
items={templates}
|
||||||
itemCount={count}
|
itemCount={count}
|
||||||
pluralizedItemName={i18n._(t`Templates`)}
|
pluralizedItemName={i18n._(t`Templates`)}
|
||||||
@@ -247,7 +224,7 @@ function TemplateList({ i18n }) {
|
|||||||
key={template.id}
|
key={template.id}
|
||||||
value={template.name}
|
value={template.name}
|
||||||
template={template}
|
template={template}
|
||||||
detailUrl={`${pathname}/${template.type}/${template.id}`}
|
detailUrl={`${location.pathname}/${template.type}/${template.id}`}
|
||||||
onSelect={() => handleSelect(template)}
|
onSelect={() => handleSelect(template)}
|
||||||
isSelected={selected.some(row => row.id === template.id)}
|
isSelected={selected.some(row => row.id === template.id)}
|
||||||
/>
|
/>
|
||||||
@@ -259,7 +236,7 @@ function TemplateList({ i18n }) {
|
|||||||
isOpen={deletionError}
|
isOpen={deletionError}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
title={i18n._(t`Error!`)}
|
title={i18n._(t`Error!`)}
|
||||||
onClose={() => setDeletionError(null)}
|
onClose={clearDeletionError}
|
||||||
>
|
>
|
||||||
{i18n._(t`Failed to delete one or more templates.`)}
|
{i18n._(t`Failed to delete one or more templates.`)}
|
||||||
<ErrorDetail error={deletionError} />
|
<ErrorDetail error={deletionError} />
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||||
|
import { useLocation, useHistory } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
parseQueryString,
|
||||||
|
replaceParams,
|
||||||
|
encodeNonDefaultQueryString,
|
||||||
|
} from './qs';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The useRequest hook accepts a request function and returns an object with
|
* The useRequest hook accepts a request function and returns an object with
|
||||||
@@ -47,3 +53,40 @@ export default function useRequest(makeRequest, initialValue) {
|
|||||||
}, [makeRequest]),
|
}, [makeRequest]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useDeleteItems(
|
||||||
|
makeRequest,
|
||||||
|
{ qsConfig, allItemsSelected, fetchItems }
|
||||||
|
) {
|
||||||
|
const location = useLocation();
|
||||||
|
const history = useHistory();
|
||||||
|
const [showError, setShowError] = useState(false);
|
||||||
|
const { error, isLoading, request } = useRequest(makeRequest, null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) {
|
||||||
|
setShowError(true);
|
||||||
|
}
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
const deleteItems = async () => {
|
||||||
|
await request();
|
||||||
|
const params = parseQueryString(qsConfig, location.search);
|
||||||
|
if (params.page > 1 && allItemsSelected) {
|
||||||
|
const newParams = encodeNonDefaultQueryString(
|
||||||
|
qsConfig,
|
||||||
|
replaceParams(params, { page: params.page - 1 })
|
||||||
|
);
|
||||||
|
history.push(`${location.pathname}?${newParams}`);
|
||||||
|
} else {
|
||||||
|
fetchItems();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading,
|
||||||
|
deleteItems,
|
||||||
|
deletionError: showError && error,
|
||||||
|
clearDeletionError: () => setShowError(false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user