Add ConfirmationModal common component (#383)

* Add ConfirmationModal common component

* Update specs
This commit is contained in:
Oleg Shur 2021-04-20 16:48:50 +03:00 committed by GitHub
parent ab57772329
commit 9d62670eef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 236 additions and 76 deletions

View file

@ -37,9 +37,11 @@ const List: React.FC<ListProps> = ({
return ( return (
<div className="section"> <div className="section">
<Breadcrumb>All Connectors</Breadcrumb> <Breadcrumb>All Connectors</Breadcrumb>
<div className="box has-background-danger has-text-centered has-text-light"> <article className="message is-warning">
Kafka Connect section is under construction. <div className="message-body">
</div> Kafka Connect section is under construction.
</div>
</article>
<MetricsWrapper> <MetricsWrapper>
<Indicator <Indicator
className="level-left is-one-third" className="level-left is-one-third"

View file

@ -9,6 +9,7 @@ import { deleteConnector } from 'redux/actions';
import Dropdown from 'components/common/Dropdown/Dropdown'; import Dropdown from 'components/common/Dropdown/Dropdown';
import DropdownDivider from 'components/common/Dropdown/DropdownDivider'; import DropdownDivider from 'components/common/Dropdown/DropdownDivider';
import DropdownItem from 'components/common/Dropdown/DropdownItem'; import DropdownItem from 'components/common/Dropdown/DropdownItem';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import StatusTag from '../StatusTag'; import StatusTag from '../StatusTag';
export interface ListItemProps { export interface ListItemProps {
@ -30,11 +31,16 @@ const ListItem: React.FC<ListItemProps> = ({
}, },
}) => { }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [
isDeleteConnectorConfirmationVisible,
setDeleteConnectorConfirmationVisible,
] = React.useState(false);
const handleDelete = React.useCallback(() => { const handleDelete = React.useCallback(() => {
if (clusterName && connect && name) { if (clusterName && connect && name) {
dispatch(deleteConnector(clusterName, connect, name)); dispatch(deleteConnector(clusterName, connect, name));
} }
setDeleteConnectorConfirmationVisible(false);
}, [clusterName, connect, name]); }, [clusterName, connect, name]);
const runningTasks = React.useMemo(() => { const runningTasks = React.useMemo(() => {
@ -67,20 +73,31 @@ const ListItem: React.FC<ListItemProps> = ({
</span> </span>
)} )}
</td> </td>
<td className="has-text-right"> <td>
<Dropdown <div className="has-text-right">
label={ <Dropdown
<span className="icon"> label={
<i className="fas fa-cog" /> <span className="icon">
</span> <i className="fas fa-cog" />
} </span>
right }
right
>
<DropdownDivider />
<DropdownItem
onClick={() => setDeleteConnectorConfirmationVisible(true)}
>
<span className="has-text-danger">Remove Connector</span>
</DropdownItem>
</Dropdown>
</div>
<ConfirmationModal
isOpen={isDeleteConnectorConfirmationVisible}
onCancel={() => setDeleteConnectorConfirmationVisible(false)}
onConfirm={handleDelete}
> >
<DropdownDivider /> Are you sure want to remove <b>{name}</b> connector?
<DropdownItem onClick={handleDelete}> </ConfirmationModal>
<span className="has-text-danger">Remove Connector</span>
</DropdownItem>
</Dropdown>
</td> </td>
</tr> </tr>
); );

View file

@ -8,6 +8,11 @@ import ListItem, { ListItemProps } from '../ListItem';
const store = configureStore(); const store = configureStore();
jest.mock(
'components/common/ConfirmationModal/ConfirmationModal',
() => 'mock-ConfirmationModal'
);
describe('Connectors ListItem', () => { describe('Connectors ListItem', () => {
const connector = connectorsPayload[0]; const connector = connectorsPayload[0];
const setupWrapper = (props: Partial<ListItemProps> = {}) => ( const setupWrapper = (props: Partial<ListItemProps> = {}) => (
@ -57,7 +62,12 @@ describe('Connectors ListItem', () => {
it('handles delete', () => { it('handles delete', () => {
const wrapper = mount(setupWrapper()); const wrapper = mount(setupWrapper());
wrapper.find('DropdownItem a').last().simulate('click'); expect(wrapper.find('mock-ConfirmationModal').prop('isOpen')).toBeFalsy();
wrapper.find('DropdownItem').last().simulate('click');
const modal = wrapper.find('mock-ConfirmationModal');
expect(modal.prop('isOpen')).toBeTruthy();
modal.simulate('cancel');
expect(wrapper.find('mock-ConfirmationModal').prop('isOpen')).toBeFalsy();
}); });
it('matches snapshot', () => { it('matches snapshot', () => {

View file

@ -110,77 +110,90 @@ exports[`Connectors ListItem matches snapshot 1`] = `
2 2
</span> </span>
</td> </td>
<td <td>
className="has-text-right" <div
> className="has-text-right"
<Dropdown
label={
<span
className="icon"
>
<i
className="fas fa-cog"
/>
</span>
}
right={true}
> >
<div <Dropdown
className="dropdown is-right" label={
<span
className="icon"
>
<i
className="fas fa-cog"
/>
</span>
}
right={true}
> >
<div <div
className="dropdown-trigger" className="dropdown is-right"
>
<button
aria-controls="dropdown-menu"
aria-haspopup="true"
className="button is-small"
onClick={[Function]}
type="button"
>
<span
className="icon"
>
<i
className="fas fa-cog"
/>
</span>
</button>
</div>
<div
className="dropdown-menu"
id="dropdown-menu"
role="menu"
> >
<div <div
className="dropdown-content has-text-left" className="dropdown-trigger"
> >
<DropdownDivider> <button
<hr aria-controls="dropdown-menu"
className="dropdown-divider" aria-haspopup="true"
/> className="button is-small"
</DropdownDivider>
<DropdownItem
onClick={[Function]} onClick={[Function]}
type="button"
> >
<a <span
className="dropdown-item is-link" className="icon"
href="#end"
onClick={[Function]}
role="menuitem"
type="button"
> >
<span <i
className="has-text-danger" className="fas fa-cog"
/>
</span>
</button>
</div>
<div
className="dropdown-menu"
id="dropdown-menu"
role="menu"
>
<div
className="dropdown-content has-text-left"
>
<DropdownDivider>
<hr
className="dropdown-divider"
/>
</DropdownDivider>
<DropdownItem
onClick={[Function]}
>
<a
className="dropdown-item is-link"
href="#end"
onClick={[Function]}
role="menuitem"
type="button"
> >
Remove Connector <span
</span> className="has-text-danger"
</a> >
</DropdownItem> Remove Connector
</span>
</a>
</DropdownItem>
</div>
</div> </div>
</div> </div>
</div> </Dropdown>
</Dropdown> </div>
<mock-ConfirmationModal
isOpen={false}
onCancel={[Function]}
onConfirm={[Function]}
>
Are you sure want to remove
<b>
hdfs-source-connector
</b>
connector?
</mock-ConfirmationModal>
</td> </td>
</tr> </tr>
</ListItem> </ListItem>

View file

@ -0,0 +1,50 @@
import React from 'react';
export interface ConfirmationModalProps {
isOpen?: boolean;
title?: React.ReactNode;
onConfirm(): void;
onCancel(): void;
}
const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
isOpen,
children,
title,
onCancel,
onConfirm,
}) => {
if (!isOpen) return null;
return (
<div className="modal is-active">
<div className="modal-background" onClick={onCancel} aria-hidden="true" />
<div className="modal-card">
<header className="modal-card-head">
<p className="modal-card-title">{title || 'Confirm the action'}</p>
<button
onClick={onCancel}
type="button"
className="delete"
aria-label="close"
/>
</header>
<section className="modal-card-body">{children}</section>
<footer className="modal-card-foot is-justify-content-flex-end">
<button
onClick={onConfirm}
type="button"
className="button is-danger"
>
Confirm
</button>
<button onClick={onCancel} type="button" className="button">
Cancel
</button>
</footer>
</div>
</div>
);
};
export default ConfirmationModal;

View file

@ -0,0 +1,68 @@
import { mount, ReactWrapper } from 'enzyme';
import React from 'react';
import ConfirmationModal, {
ConfirmationModalProps,
} from '../ConfirmationModal';
const confirmMock = jest.fn();
const cancelMock = jest.fn();
const body = 'Please Confirm the action!';
describe('ConfiramationModal', () => {
const setupWrapper = (props: Partial<ConfirmationModalProps> = {}) => (
<ConfirmationModal onCancel={cancelMock} onConfirm={confirmMock} {...props}>
{body}
</ConfirmationModal>
);
it('renders nothing', () => {
const wrapper = mount(setupWrapper({ isOpen: false }));
expect(wrapper.exists(ConfirmationModal)).toBeTruthy();
expect(wrapper.exists('.modal.is-active')).toBeFalsy();
});
it('renders modal', () => {
const wrapper = mount(setupWrapper({ isOpen: true }));
expect(wrapper.exists(ConfirmationModal)).toBeTruthy();
expect(wrapper.exists('.modal.is-active')).toBeTruthy();
expect(wrapper.find('.modal-card-body').text()).toEqual(body);
});
it('renders modal with default header', () => {
const wrapper = mount(setupWrapper({ isOpen: true }));
expect(wrapper.find('.modal-card-title').text()).toEqual(
'Confirm the action'
);
});
it('renders modal with custom header', () => {
const title = 'My Custom Header';
const wrapper = mount(setupWrapper({ isOpen: true, title }));
expect(wrapper.find('.modal-card-title').text()).toEqual(title);
});
it('handles onConfirm when user clicks confirm button', () => {
const wrapper = mount(setupWrapper({ isOpen: true }));
expect(wrapper.find('.modal-card-foot button').length).toEqual(2);
const cancelBtn = wrapper.find('.modal-card-foot button').at(0);
expect(cancelBtn.text()).toEqual('Confirm');
cancelBtn.simulate('click');
expect(cancelMock).toHaveBeenCalledTimes(0);
expect(confirmMock).toHaveBeenCalledTimes(1);
});
describe('cancellation', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
wrapper = mount(setupWrapper({ isOpen: true }));
});
it('handles onCancel when user clicks on modal-background', () => {
wrapper.find('.modal-background').simulate('click');
expect(cancelMock).toHaveBeenCalledTimes(1);
expect(confirmMock).toHaveBeenCalledTimes(0);
});
it('handles onCancel when user clicks on Cancel button', () => {
expect(wrapper.find('.modal-card-foot button').length).toEqual(2);
const cancelBtn = wrapper.find('.modal-card-foot button').at(1);
expect(cancelBtn.text()).toEqual('Cancel');
cancelBtn.simulate('click');
expect(cancelMock).toHaveBeenCalledTimes(1);
expect(confirmMock).toHaveBeenCalledTimes(0);
});
});
});