فهرست منبع

Add ConfirmationModal common component (#383)

* Add ConfirmationModal common component

* Update specs
Oleg Shur 4 سال پیش
والد
کامیت
9d62670eef

+ 5 - 3
kafka-ui-react-app/src/components/Connect/List/List.tsx

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

+ 30 - 13
kafka-ui-react-app/src/components/Connect/List/ListItem.tsx

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

+ 11 - 1
kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx

@@ -8,6 +8,11 @@ import ListItem, { ListItemProps } from '../ListItem';
 
 const store = configureStore();
 
+jest.mock(
+  'components/common/ConfirmationModal/ConfirmationModal',
+  () => 'mock-ConfirmationModal'
+);
+
 describe('Connectors ListItem', () => {
   const connector = connectorsPayload[0];
   const setupWrapper = (props: Partial<ListItemProps> = {}) => (
@@ -57,7 +62,12 @@ describe('Connectors ListItem', () => {
 
   it('handles delete', () => {
     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', () => {

+ 70 - 57
kafka-ui-react-app/src/components/Connect/List/__tests__/__snapshots__/ListItem.spec.tsx.snap

@@ -110,77 +110,90 @@ exports[`Connectors ListItem matches snapshot 1`] = `
                   2
                 </span>
               </td>
-              <td
-                className="has-text-right"
-              >
-                <Dropdown
-                  label={
-                    <span
-                      className="icon"
-                    >
-                      <i
-                        className="fas fa-cog"
-                      />
-                    </span>
-                  }
-                  right={true}
+              <td>
+                <div
+                  className="has-text-right"
                 >
-                  <div
-                    className="dropdown is-right"
+                  <Dropdown
+                    label={
+                      <span
+                        className="icon"
+                      >
+                        <i
+                          className="fas fa-cog"
+                        />
+                      </span>
+                    }
+                    right={true}
                   >
                     <div
-                      className="dropdown-trigger"
+                      className="dropdown is-right"
                     >
-                      <button
-                        aria-controls="dropdown-menu"
-                        aria-haspopup="true"
-                        className="button is-small"
-                        onClick={[Function]}
-                        type="button"
+                      <div
+                        className="dropdown-trigger"
                       >
-                        <span
-                          className="icon"
+                        <button
+                          aria-controls="dropdown-menu"
+                          aria-haspopup="true"
+                          className="button is-small"
+                          onClick={[Function]}
+                          type="button"
                         >
-                          <i
-                            className="fas fa-cog"
-                          />
-                        </span>
-                      </button>
-                    </div>
-                    <div
-                      className="dropdown-menu"
-                      id="dropdown-menu"
-                      role="menu"
-                    >
+                          <span
+                            className="icon"
+                          >
+                            <i
+                              className="fas fa-cog"
+                            />
+                          </span>
+                        </button>
+                      </div>
                       <div
-                        className="dropdown-content has-text-left"
+                        className="dropdown-menu"
+                        id="dropdown-menu"
+                        role="menu"
                       >
-                        <DropdownDivider>
-                          <hr
-                            className="dropdown-divider"
-                          />
-                        </DropdownDivider>
-                        <DropdownItem
-                          onClick={[Function]}
+                        <div
+                          className="dropdown-content has-text-left"
                         >
-                          <a
-                            className="dropdown-item is-link"
-                            href="#end"
+                          <DropdownDivider>
+                            <hr
+                              className="dropdown-divider"
+                            />
+                          </DropdownDivider>
+                          <DropdownItem
                             onClick={[Function]}
-                            role="menuitem"
-                            type="button"
                           >
-                            <span
-                              className="has-text-danger"
+                            <a
+                              className="dropdown-item is-link"
+                              href="#end"
+                              onClick={[Function]}
+                              role="menuitem"
+                              type="button"
                             >
-                              Remove Connector
-                            </span>
-                          </a>
-                        </DropdownItem>
+                              <span
+                                className="has-text-danger"
+                              >
+                                Remove Connector
+                              </span>
+                            </a>
+                          </DropdownItem>
+                        </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>
             </tr>
           </ListItem>

+ 50 - 0
kafka-ui-react-app/src/components/common/ConfirmationModal/ConfirmationModal.tsx

@@ -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;

+ 68 - 0
kafka-ui-react-app/src/components/common/ConfirmationModal/__test__/ConfirmationModal.spec.tsx

@@ -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);
+    });
+  });
+});