Skip to content

Commit

Permalink
Refs #36867 - Add bulk modal with bulk params
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremylenz committed Oct 27, 2023
1 parent d99858a commit a879797
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Modal, Button, ModalVariant } from '@patternfly/react-core';
import { Modal, Button, ModalVariant, Checkbox } from '@patternfly/react-core';
import { translate as __ } from '../../common/I18n';
import { closeConfirmModal, selectConfirmModal } from './slice';

Expand All @@ -14,9 +14,11 @@ const ConfirmModal = () => {
onCancel,
modalProps,
isWarning,
isDireWarning,
} = useSelector(selectConfirmModal);

const dispatch = useDispatch();
const [direWarningChecked, setDireWarningChecked] = useState(false);

const closeModal = () => dispatch(closeConfirmModal());

Expand All @@ -36,6 +38,7 @@ const ConfirmModal = () => {
variant={isWarning ? 'danger' : 'primary'}
onClick={handleConfirm}
ouiaId="btn-modal-confirm"
isDisabled={isDireWarning && !direWarningChecked}
>
{confirmButtonText || __('Confirm')}
</Button>,
Expand All @@ -49,6 +52,16 @@ const ConfirmModal = () => {
</Button>,
];

const direWarningCheckbox = (
<Checkbox
id="dire-warning-checkbox"
ouiaId="dire-warning-checkbox"
label={__('I understand that this action cannot be undone.')}
isChecked={direWarningChecked}
onChange={val => setDireWarningChecked(val)}
/>
);

if (!isOpen) return null;

return (
Expand All @@ -64,7 +77,10 @@ const ConfirmModal = () => {
titleIconVariant={isWarning ? 'warning' : null}
{...modalProps}
>
{message}
<>
{message}
{isDireWarning && direWarningCheckbox}
</>
</Modal>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const confirmModalSlice = createSlice({
onConfirm = noop,
onCancel = noop,
isWarning = false,
isDireWarning = false,
confirmButtonText = null,
modalProps = {},
} = action.payload;
Expand All @@ -25,6 +26,7 @@ const confirmModalSlice = createSlice({
onCancel,
modalProps,
isWarning,
isDireWarning,
confirmButtonText,
};
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { visit } from '../../../../foreman_navigation';
import { foremanUrl } from '../../../common/helpers';
import { sprintf, translate as __ } from '../../../common/I18n';
import { openConfirmModal } from '../../ConfirmModal';
import { APIActions } from '../../../redux/API';
import './bulkDeleteModal.scss';

export const bulkDeleteHosts = ({
bulkParams,
selectedCount,
destroyVmOnHostDelete,
}) => dispatch => {
const successToast = () => sprintf(__('%s hosts deleted'), selectedCount);
const errorToast = ({ message }) => message;
const url = foremanUrl(`/api/hosts/bulk_destroy?search=${bulkParams}`);

// TODO: Replace with a checkbox instead of a global setting for cascade host destroy
const cascadeMessage = () =>
destroyVmOnHostDelete
? __(
'For hosts with compute resources, this will delete the VM and its disks.'
)
: __(
'For hosts with compute resources, VMs and their disks will not be deleted.'
);

dispatch(
openConfirmModal({
isWarning: true,
isDireWarning: true,
title: (
<FormattedMessage
defaultMessage="Delete {count, plural, one {{singular}} other {{plural}}}?"
values={{
count: selectedCount,
singular: __('host'),
plural: __('hosts'),
}}
id="bulk-delete-host-count"
/>
),
confirmButtonText: __('Delete'),
onConfirm: () =>
dispatch(
APIActions.delete({
url,
key: `BULK-HOSTS-DELETE`,
successToast,
errorToast,
handleSuccess: () => visit(foremanUrl('/hosts')),
})
),
message: (
<FormattedMessage
id="bulk-delete-hosts"
values={{
hostsCount: (
<strong>
<FormattedMessage
defaultMessage="{count, plural, one {# {singular}} other {# {plural}}}"
values={{
count: selectedCount,
singular: __('host'),
plural: __('hosts'),
}}
id="bulk-delete-host-count"
/>
</strong>
),
cascade: cascadeMessage(),
settings: (
<a href={foremanUrl('/settings?search=destroy')}>
{__('Provisioning settings')}
</a>
),
br: <br />,
}}
defaultMessage={__(
'{hostsCount} will be deleted. This action is irreversible. {br}{br} {cascade} {br}{br} This behavior can be changed via global setting "Destroy associated VM on host delete" in {settings}.{br}{br}'
)}
/>
),
})
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#app-confirm-modal {
min-height: 21rem;
.pf-c-check label {
font-size: 14px;
position: relative;
top: 2px;
}
}
42 changes: 28 additions & 14 deletions webpack/assets/javascripts/react_app/components/HostsIndex/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { createContext } from 'react';
import { useHistory } from 'react-router-dom';
import PropTypes from 'prop-types';
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import { Td } from '@patternfly/react-table';
Expand All @@ -15,6 +16,8 @@ import SelectAllCheckbox from '../PF4/TableIndexPage/Table/SelectAllCheckbox';
import { getPageStats } from '../PF4/TableIndexPage/Table/helpers';
import { deleteHost } from '../HostDetails/ActionsBar/actions';
import { useForemanSettings } from '../../Root/Context/ForemanContext';
import { getURIsearch } from '../../common/urlHelpers';
import { bulkDeleteHosts } from './BulkActions/bulkDelete';

export const ForemanHostsIndexActionsBarContext = createContext({});

Expand All @@ -26,7 +29,15 @@ const HostsIndex = () => {
isSorted: true,
},
};
const defaultParams = { search: '' };

const history = useHistory();
const { location: { search: historySearch } = {} } = history || {};
const urlParams = new URLSearchParams(historySearch);
const urlParamsSearch = urlParams.get('search') || '';
const searchFromUrl = urlParamsSearch || getURIsearch();
const initialSearchQuery = apiSearchQuery || searchFromUrl || '';

const defaultParams = { search: initialSearchQuery };

const response = useAPI('get', `${HOSTS_API_PATH}?include_permissions=true`, {
key: API_REQUEST_KEY,
Expand All @@ -37,6 +48,7 @@ const HostsIndex = () => {
response: {
search: apiSearchQuery,
results,
subtotal,
total,
per_page: perPage,
page,
Expand All @@ -45,10 +57,14 @@ const HostsIndex = () => {

const { pageRowCount } = getPageStats({ total, page, perPage });

const { fetchBulkParams, ...selectAllOptions } = useBulkSelect({
const {
fetchBulkParams,
updateSearchQuery,
...selectAllOptions
} = useBulkSelect({
results,
metadata: { total, page },
initialSearchQuery: apiSearchQuery || '',
metadata: { total, page, selectable: subtotal },
initialSearchQuery,
});

const {
Expand All @@ -60,8 +76,6 @@ const HostsIndex = () => {
areAllRowsOnPageSelected,
areAllRowsSelected,
isSelected,
selectedResults,
selectAllMode,
} = selectAllOptions;

const selectionToolbar = (
Expand Down Expand Up @@ -102,23 +116,22 @@ const HostsIndex = () => {
const deleteHostHandler = ({ hostName, computeId }) =>
dispatch(deleteHost(hostName, computeId, destroyVmOnHostDelete));
const handleBulkDelete = () => {
// eslint-disable-next-line camelcase
const selectedHosts = selectedResults.map(
({ id, name: hostName, compute_id: computeId }) => ({
id,
hostName,
computeId,
const bulkParams = fetchBulkParams();
dispatch(
bulkDeleteHosts({
bulkParams,
selectedCount,
destroyVmOnHostDelete,
})
);
deleteHostHandler(selectedHosts[0]);
};

const dropdownItems = [
<DropdownItem
ouiaId="delete=hosts-dropdown-item"
key="delete=hosts-dropdown-item"
onClick={handleBulkDelete}
isDisabled={selectAllMode || selectedCount === 0}
isDisabled={selectedCount === 0}
icon={<TrashIcon />}
>
{__('Delete')}
Expand Down Expand Up @@ -160,6 +173,7 @@ const HostsIndex = () => {
showCheckboxes
rowSelectTd={RowSelectTd}
rowKebabItems={rowKebabItems}
updateSearchQuery={updateSearchQuery}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export const useBulkSelect = ({
const fetchBulkParams = ({
idColumnName = idColumn,
selectAllQuery = '',
}) => {
} = {}) => {
const searchQueryWithExclusionSet = () => {
const query = [
searchQuery,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const TableIndexPage = ({
showCheckboxes,
rowSelectTd,
rowKebabItems,
updateSearchQuery,
}) => {
const history = useHistory();
const { location: { search: historySearch } = {} } = history || {};
Expand Down Expand Up @@ -154,6 +155,7 @@ const TableIndexPage = ({
const setSearch = newSearch => {
const uri = new URI();
uri.setSearch(newSearch);
updateSearchQuery(newSearch.search);
history.push({ search: uri.search() });
setParamsAndAPI({ ...params, ...newSearch });
};
Expand Down Expand Up @@ -324,6 +326,7 @@ TableIndexPage.propTypes = {
rowSelectTd: PropTypes.func,
showCheckboxes: PropTypes.bool,
rowKebabItems: PropTypes.func,
updateSearchQuery: PropTypes.func,
};

TableIndexPage.defaultProps = {
Expand All @@ -350,6 +353,7 @@ TableIndexPage.defaultProps = {
showCheckboxes: false,
replacementResponse: null,
rowKebabItems: noop,
updateSearchQuery: noop,
};

export default TableIndexPage;

0 comments on commit a879797

Please sign in to comment.