Forráskód Böngészése

New Confirmation Messages modal (#2376)

* New Confirmation Messages

* Fix #2348

* Fix codesmels

* fix #2242

* fix #2347
Oleg Shur 2 éve
szülő
commit
2d82b9c0a9
40 módosított fájl, 579 hozzáadás és 947 törlés
  1. 77 71
      kafka-ui-react-app/src/components/App.tsx
  2. 25 33
      kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx
  3. 5 10
      kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx
  4. 2 2
      kafka-ui-react-app/src/components/Connect/Details/Config/__test__/Config.spec.tsx
  5. 2 2
      kafka-ui-react-app/src/components/Connect/Details/Overview/__tests__/Overview.spec.tsx
  6. 12 14
      kafka-ui-react-app/src/components/Connect/List/ListItem.tsx
  7. 0 44
      kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx
  8. 3 13
      kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx
  9. 5 4
      kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/Details.spec.tsx
  10. 7 14
      kafka-ui-react-app/src/components/Schemas/Details/Details.tsx
  11. 26 40
      kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.tsx
  12. 5 2
      kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/__test__/GlobalSchemaSelector.spec.tsx
  13. 38 64
      kafka-ui-react-app/src/components/Topics/List/ActionsCell/ActionsCell.tsx
  14. 41 59
      kafka-ui-react-app/src/components/Topics/List/List.tsx
  15. 2 2
      kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx
  16. 34 58
      kafka-ui-react-app/src/components/Topics/Topic/Details/Details.tsx
  17. 0 7
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.styled.ts
  18. 5 22
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/SavedFilters.tsx
  19. 16 9
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/SavedFilters.spec.tsx
  20. 23 18
      kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/Details.spec.tsx
  21. 27 58
      kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZone.tsx
  22. 1 7
      kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZoneContainer.ts
  23. 2 71
      kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/__test__/DangerZone.spec.tsx
  24. 26 17
      kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx
  25. 18 2
      kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx
  26. 1 0
      kafka-ui-react-app/src/components/common/Alert/Alert.styled.ts
  27. 52 47
      kafka-ui-react-app/src/components/common/ConfirmationModal/ConfirmationModal.styled.tsx
  28. 21 43
      kafka-ui-react-app/src/components/common/ConfirmationModal/ConfirmationModal.tsx
  29. 0 101
      kafka-ui-react-app/src/components/common/ConfirmationModal/__test__/ConfirmationModal.spec.tsx
  30. 1 0
      kafka-ui-react-app/src/components/common/Dropdown/Dropdown.styled.ts
  31. 11 1
      kafka-ui-react-app/src/components/common/Dropdown/DropdownItem.tsx
  32. 0 61
      kafka-ui-react-app/src/components/common/MultiSelect/__test__/MultiSelect.styled.spec.tsx
  33. 39 0
      kafka-ui-react-app/src/components/contexts/ConfirmContext.tsx
  34. 1 1
      kafka-ui-react-app/src/lib/hooks/__tests__/useDataSaver.spec.tsx
  35. 14 0
      kafka-ui-react-app/src/lib/hooks/useConfirm.ts
  36. 14 7
      kafka-ui-react-app/src/lib/testHelpers.tsx
  37. 1 1
      kafka-ui-react-app/src/redux/reducers/topicMessages/topicMessagesSlice.ts
  38. 0 6
      kafka-ui-react-app/src/redux/reducers/topics/__test__/selectors.spec.ts
  39. 0 20
      kafka-ui-react-app/src/redux/reducers/topics/selectors.ts
  40. 22 16
      kafka-ui-react-app/src/redux/reducers/topics/topicsSlice.ts

+ 77 - 71
kafka-ui-react-app/src/components/App.tsx

@@ -18,6 +18,9 @@ import Logo from 'components/common/Logo/Logo';
 import GitIcon from 'components/common/Icons/GitIcon';
 import DiscordIcon from 'components/common/Icons/DiscordIcon';
 
+import { ConfirmContextProvider } from './contexts/ConfirmContext';
+import ConfirmationModal from './common/ConfirmationModal/ConfirmationModal';
+
 const queryClient = new QueryClient({
   defaultOptions: {
     queries: {
@@ -44,84 +47,87 @@ const App: React.FC = () => {
   return (
     <QueryClientProvider client={queryClient}>
       <ThemeProvider theme={theme}>
-        <GlobalCSS />
-        <S.Layout>
-          <S.Navbar role="navigation" aria-label="Page Header">
-            <S.NavbarBrand>
+        <ConfirmContextProvider>
+          <GlobalCSS />
+          <S.Layout>
+            <S.Navbar role="navigation" aria-label="Page Header">
               <S.NavbarBrand>
-                <S.NavbarBurger
-                  onClick={onBurgerClick}
-                  onKeyDown={onBurgerClick}
-                  role="button"
-                  tabIndex={0}
-                  aria-label="burger"
-                >
-                  <S.Span role="separator" />
-                  <S.Span role="separator" />
-                  <S.Span role="separator" />
-                </S.NavbarBurger>
+                <S.NavbarBrand>
+                  <S.NavbarBurger
+                    onClick={onBurgerClick}
+                    onKeyDown={onBurgerClick}
+                    role="button"
+                    tabIndex={0}
+                    aria-label="burger"
+                  >
+                    <S.Span role="separator" />
+                    <S.Span role="separator" />
+                    <S.Span role="separator" />
+                  </S.NavbarBurger>
 
-                <S.Hyperlink to="/">
-                  <Logo />
-                  UI for Apache Kafka
-                </S.Hyperlink>
+                  <S.Hyperlink to="/">
+                    <Logo />
+                    UI for Apache Kafka
+                  </S.Hyperlink>
 
-                <S.NavbarItem>
-                  {GIT_TAG && <Version tag={GIT_TAG} commit={GIT_COMMIT} />}
-                </S.NavbarItem>
+                  <S.NavbarItem>
+                    {GIT_TAG && <Version tag={GIT_TAG} commit={GIT_COMMIT} />}
+                  </S.NavbarItem>
+                </S.NavbarBrand>
               </S.NavbarBrand>
-            </S.NavbarBrand>
-            <S.NavbarSocial>
-              <S.LogoutLink href="/logout">
-                <S.LogoutButton buttonType="primary" buttonSize="M">
-                  Log out
-                </S.LogoutButton>
-              </S.LogoutLink>
-              <S.SocialLink
-                href="https://github.com/provectus/kafka-ui"
-                target="_blank"
-              >
-                <GitIcon />
-              </S.SocialLink>
-              <S.SocialLink
-                href="https://discord.com/invite/4DWzD7pGE5"
-                target="_blank"
-              >
-                <DiscordIcon />
-              </S.SocialLink>
-            </S.NavbarSocial>
-          </S.Navbar>
+              <S.NavbarSocial>
+                <S.LogoutLink href="/logout">
+                  <S.LogoutButton buttonType="primary" buttonSize="M">
+                    Log out
+                  </S.LogoutButton>
+                </S.LogoutLink>
+                <S.SocialLink
+                  href="https://github.com/provectus/kafka-ui"
+                  target="_blank"
+                >
+                  <GitIcon />
+                </S.SocialLink>
+                <S.SocialLink
+                  href="https://discord.com/invite/4DWzD7pGE5"
+                  target="_blank"
+                >
+                  <DiscordIcon />
+                </S.SocialLink>
+              </S.NavbarSocial>
+            </S.Navbar>
 
-          <S.Container>
-            <S.Sidebar aria-label="Sidebar" $visible={isSidebarVisible}>
-              <Suspense fallback={<PageLoader />}>
-                <Nav />
-              </Suspense>
-            </S.Sidebar>
-            <S.Overlay
-              $visible={isSidebarVisible}
-              onClick={closeSidebar}
-              onKeyDown={closeSidebar}
-              tabIndex={-1}
-              aria-hidden="true"
-              aria-label="Overlay"
-            />
-            <Routes>
-              {['/', '/ui', '/ui/clusters'].map((path) => (
+            <S.Container>
+              <S.Sidebar aria-label="Sidebar" $visible={isSidebarVisible}>
+                <Suspense fallback={<PageLoader />}>
+                  <Nav />
+                </Suspense>
+              </S.Sidebar>
+              <S.Overlay
+                $visible={isSidebarVisible}
+                onClick={closeSidebar}
+                onKeyDown={closeSidebar}
+                tabIndex={-1}
+                aria-hidden="true"
+                aria-label="Overlay"
+              />
+              <Routes>
+                {['/', '/ui', '/ui/clusters'].map((path) => (
+                  <Route
+                    key="Home" // optional: avoid full re-renders on route changes
+                    path={path}
+                    element={<Dashboard />}
+                  />
+                ))}
                 <Route
-                  key="Home" // optional: avoid full re-renders on route changes
-                  path={path}
-                  element={<Dashboard />}
+                  path={getNonExactPath(clusterPath())}
+                  element={<ClusterPage />}
                 />
-              ))}
-              <Route
-                path={getNonExactPath(clusterPath())}
-                element={<ClusterPage />}
-              />
-            </Routes>
-          </S.Container>
-          <Toaster position="bottom-right" />
-        </S.Layout>
+              </Routes>
+            </S.Container>
+            <Toaster position="bottom-right" />
+          </S.Layout>
+          <ConfirmationModal />
+        </ConfirmContextProvider>
       </ThemeProvider>
     </QueryClientProvider>
   );

+ 25 - 33
kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx

@@ -4,7 +4,6 @@ import { useNavigate } from 'react-router-dom';
 import { useIsMutating } from '@tanstack/react-query';
 import { ConnectorState, ConnectorAction } from 'generated-sources';
 import useAppParams from 'lib/hooks/useAppParams';
-import useModal from 'lib/hooks/useModal';
 import {
   useConnector,
   useDeleteConnector,
@@ -15,8 +14,8 @@ import {
   clusterConnectorsPath,
   RouterParamsClusterConnectConnector,
 } from 'lib/paths';
-import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import { Button } from 'components/common/Button/Button';
+import { useConfirm } from 'lib/hooks/useConfirm';
 
 const ConnectorActionsWrapperStyled = styled.div`
   display: flex;
@@ -32,22 +31,24 @@ const Actions: React.FC = () => {
   const isMutating = mutationsNumber > 0;
 
   const { data: connector } = useConnector(routerProps);
-
-  const {
-    isOpen: isDeleteConnectorConfirmationOpen,
-    setClose: setDeleteConnectorConfirmationClose,
-    setOpen: setDeleteConnectorConfirmationOpen,
-  } = useModal();
+  const confirm = useConfirm();
 
   const deleteConnectorMutation = useDeleteConnector(routerProps);
-  const deleteConnectorHandler = async () => {
-    try {
-      await deleteConnectorMutation.mutateAsync();
-      navigate(clusterConnectorsPath(routerProps.clusterName));
-    } catch {
-      // do not redirect
-    }
-  };
+  const deleteConnectorHandler = () =>
+    confirm(
+      <>
+        Are you sure you want to remove <b>{routerProps.connectorName}</b>{' '}
+        connector?
+      </>,
+      async () => {
+        try {
+          await deleteConnectorMutation.mutateAsync();
+          navigate(clusterConnectorsPath(routerProps.clusterName));
+        } catch {
+          // do not redirect
+        }
+      }
+    );
 
   const stateMutation = useUpdateConnectorState(routerProps);
   const restartConnectorHandler = () =>
@@ -71,7 +72,7 @@ const Actions: React.FC = () => {
           onClick={pauseConnectorHandler}
           disabled={isMutating}
         >
-          <span>Pause</span>
+          Pause
         </Button>
       )}
 
@@ -83,7 +84,7 @@ const Actions: React.FC = () => {
           onClick={resumeConnectorHandler}
           disabled={isMutating}
         >
-          <span>Resume</span>
+          Resume
         </Button>
       )}
 
@@ -94,7 +95,7 @@ const Actions: React.FC = () => {
         onClick={restartConnectorHandler}
         disabled={isMutating}
       >
-        <span>Restart Connector</span>
+        Restart Connector
       </Button>
       <Button
         buttonSize="M"
@@ -103,7 +104,7 @@ const Actions: React.FC = () => {
         onClick={restartAllTasksHandler}
         disabled={isMutating}
       >
-        <span>Restart All Tasks</span>
+        Restart All Tasks
       </Button>
       <Button
         buttonSize="M"
@@ -112,7 +113,7 @@ const Actions: React.FC = () => {
         onClick={restartFailedTasksHandler}
         disabled={isMutating}
       >
-        <span>Restart Failed Tasks</span>
+        Restart Failed Tasks
       </Button>
       <Button
         buttonSize="M"
@@ -125,27 +126,18 @@ const Actions: React.FC = () => {
           routerProps.connectorName
         )}
       >
-        <span>Edit Config</span>
+        Edit Config
       </Button>
 
       <Button
         buttonSize="M"
         buttonType="secondary"
         type="button"
-        onClick={setDeleteConnectorConfirmationOpen}
+        onClick={deleteConnectorHandler}
         disabled={isMutating}
       >
-        <span>Delete</span>
+        Delete
       </Button>
-      <ConfirmationModal
-        isOpen={isDeleteConnectorConfirmationOpen}
-        onCancel={setDeleteConnectorConfirmationClose}
-        onConfirm={deleteConnectorHandler}
-        isConfirming={isMutating}
-      >
-        Are you sure you want to remove <b>{routerProps.connectorName}</b>{' '}
-        connector?
-      </ConfirmationModal>
     </ConnectorActionsWrapperStyled>
   );
 };

+ 5 - 10
kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx

@@ -3,7 +3,7 @@ import { render, WithRoute } from 'lib/testHelpers';
 import { clusterConnectConnectorPath } from 'lib/paths';
 import Actions from 'components/Connect/Details/Actions/Actions';
 import { ConnectorAction, ConnectorState } from 'generated-sources';
-import { screen } from '@testing-library/react';
+import { screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import {
   useConnector,
@@ -27,11 +27,6 @@ jest.mock('lib/hooks/api/kafkaConnect', () => ({
   useUpdateConnectorState: jest.fn(),
 }));
 
-jest.mock(
-  'components/common/ConfirmationModal/ConfirmationModal',
-  () => 'mock-ConfirmationModal'
-);
-
 const expectActionButtonsExists = () => {
   expect(screen.getByText('Restart Connector')).toBeInTheDocument();
   expect(screen.getByText('Restart All Tasks')).toBeInTheDocument();
@@ -116,10 +111,10 @@ describe('Actions', () => {
 
       it('opens confirmation modal when delete button clicked', async () => {
         renderComponent();
-        userEvent.click(screen.getByRole('button', { name: 'Delete' }));
-        expect(
-          screen.getByText(/Are you sure you want to remove/i)
-        ).toHaveAttribute('isopen', 'true');
+        await waitFor(() =>
+          userEvent.click(screen.getByRole('button', { name: 'Delete' }))
+        );
+        expect(screen.getByRole('dialog')).toBeInTheDocument();
       });
 
       it('calls restartConnector when restart button clicked', () => {

+ 2 - 2
kafka-ui-react-app/src/components/Connect/Details/Config/__test__/Config.spec.tsx

@@ -32,8 +32,8 @@ describe('Config', () => {
 
   it('is empty when no config', () => {
     (useConnectorConfig as jest.Mock).mockImplementation(() => ({}));
-    const { container } = renderComponent();
-    expect(container).toBeEmptyDOMElement();
+    renderComponent();
+    expect(screen.queryByText('mock-Editor')).not.toBeInTheDocument();
   });
 
   it('renders editor', () => {

+ 2 - 2
kafka-ui-react-app/src/components/Connect/Details/Overview/__tests__/Overview.spec.tsx

@@ -19,8 +19,8 @@ describe('Overview', () => {
       data: undefined,
     }));
 
-    const { container } = render(<Overview />);
-    expect(container).toBeEmptyDOMElement();
+    render(<Overview />);
+    expect(screen.queryByText('Worker')).not.toBeInTheDocument();
   });
 
   describe('when connector is loaded', () => {

+ 12 - 14
kafka-ui-react-app/src/components/Connect/List/ListItem.tsx

@@ -3,13 +3,12 @@ import { FullConnectorInfo } from 'generated-sources';
 import { clusterConnectConnectorPath, clusterTopicPath } from 'lib/paths';
 import { ClusterName } from 'redux/interfaces';
 import { Link, NavLink } from 'react-router-dom';
-import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import { Tag } from 'components/common/Tag/Tag.styled';
 import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled';
 import getTagColor from 'components/common/Tag/getTagColor';
-import useModal from 'lib/hooks/useModal';
 import { useDeleteConnector } from 'lib/hooks/api/kafkaConnect';
 import { Dropdown, DropdownItem } from 'components/common/Dropdown';
+import { useConfirm } from 'lib/hooks/useConfirm';
 
 import * as S from './List.styled';
 
@@ -31,16 +30,22 @@ const ListItem: React.FC<ListItemProps> = ({
     failedTasksCount,
   },
 }) => {
-  const { isOpen, setClose, setOpen } = useModal();
+  const confirm = useConfirm();
   const deleteMutation = useDeleteConnector({
     clusterName,
     connectName: connect,
     connectorName: name,
   });
 
-  const handleDelete = async () => {
-    await deleteMutation.mutateAsync();
-    setClose();
+  const handleDelete = () => {
+    confirm(
+      <>
+        Are you sure want to remove <b>{name}</b> connector?
+      </>,
+      async () => {
+        await deleteMutation.mutateAsync();
+      }
+    );
   };
 
   const runningTasks = React.useMemo(() => {
@@ -78,18 +83,11 @@ const ListItem: React.FC<ListItemProps> = ({
       <td>
         <div>
           <Dropdown>
-            <DropdownItem onClick={setOpen} danger>
+            <DropdownItem onClick={handleDelete} danger>
               Remove Connector
             </DropdownItem>
           </Dropdown>
         </div>
-        <ConfirmationModal
-          isOpen={isOpen}
-          onCancel={setClose}
-          onConfirm={handleDelete}
-        >
-          Are you sure want to remove <b>{name}</b> connector?
-        </ConfirmationModal>
       </td>
     </tr>
   );

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

@@ -1,16 +1,9 @@
 import React from 'react';
 import { connectors } from 'lib/fixtures/kafkaConnect';
 import ListItem, { ListItemProps } from 'components/Connect/List/ListItem';
-import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import { screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
 import { render } from 'lib/testHelpers';
 
-jest.mock(
-  'components/common/ConfirmationModal/ConfirmationModal',
-  () => 'mock-ConfirmationModal'
-);
-
 describe('Connectors ListItem', () => {
   const connector = connectors[0];
   const setupWrapper = (props: Partial<ListItemProps> = {}) => (
@@ -21,25 +14,6 @@ describe('Connectors ListItem', () => {
     </table>
   );
 
-  const onCancel = jest.fn();
-  const onConfirm = jest.fn();
-  const confirmationModal = (props: Partial<ListItemProps> = {}) => (
-    <ConfirmationModal onCancel={onCancel} onConfirm={onConfirm}>
-      <button type="button" id="cancel" onClick={onCancel}>
-        Cancel
-      </button>
-      {props.clusterName ? (
-        <button type="button" id="delete" onClick={onConfirm}>
-          Confirm
-        </button>
-      ) : (
-        <button type="button" id="delete">
-          Confirm
-        </button>
-      )}
-    </ConfirmationModal>
-  );
-
   it('renders item', () => {
     render(setupWrapper());
     expect(screen.getAllByRole('cell')[6]).toHaveTextContent('2 of 2');
@@ -76,22 +50,4 @@ describe('Connectors ListItem', () => {
     );
     expect(screen.getAllByRole('cell')[6]).toHaveTextContent('');
   });
-
-  it('handles cancel', async () => {
-    render(confirmationModal());
-    userEvent.click(screen.getByText('Cancel'));
-    expect(onCancel).toHaveBeenCalled();
-  });
-
-  it('handles delete', () => {
-    render(confirmationModal({ clusterName: 'test' }));
-    userEvent.click(screen.getByText('Confirm'));
-    expect(onConfirm).toHaveBeenCalled();
-  });
-
-  it('handles delete when clusterName is not present', () => {
-    render(confirmationModal({ clusterName: undefined }));
-    userEvent.click(screen.getByText('Confirm'));
-    expect(onConfirm).toHaveBeenCalledTimes(0);
-  });
 });

+ 3 - 13
kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx

@@ -6,7 +6,6 @@ import {
   ClusterGroupParam,
 } from 'lib/paths';
 import PageLoader from 'components/common/PageLoader/PageLoader';
-import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import ClusterContext from 'components/contexts/ClusterContext';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import * as Metrics from 'components/common/Metrics';
@@ -38,17 +37,14 @@ const Details: React.FC = () => {
   const isDeleted = useAppSelector(getIsConsumerGroupDeleted);
   const isFetched = useAppSelector(getAreConsumerGroupDetailsFulfilled);
 
-  const [isConfirmationModalVisible, setIsConfirmationModalVisible] =
-    React.useState<boolean>(false);
-
   React.useEffect(() => {
     dispatch(fetchConsumerGroupDetails({ clusterName, consumerGroupID }));
   }, [clusterName, consumerGroupID, dispatch]);
 
   const onDelete = () => {
-    setIsConfirmationModalVisible(false);
     dispatch(deleteConsumerGroup({ clusterName, consumerGroupID }));
   };
+
   React.useEffect(() => {
     if (isDeleted) {
       navigate('../');
@@ -73,7 +69,8 @@ const Details: React.FC = () => {
             <Dropdown>
               <DropdownItem onClick={onResetOffsets}>Reset offset</DropdownItem>
               <DropdownItem
-                onClick={() => setIsConfirmationModalVisible(true)}
+                confirm="Are you sure you want to delete this consumer group?"
+                onClick={onDelete}
                 danger
               >
                 Delete consumer group
@@ -119,13 +116,6 @@ const Details: React.FC = () => {
           ))}
         </tbody>
       </Table>
-      <ConfirmationModal
-        isOpen={isConfirmationModalVisible}
-        onCancel={() => setIsConfirmationModalVisible(false)}
-        onConfirm={onDelete}
-      >
-        Are you sure you want to delete this consumer group?
-      </ConfirmationModal>
     </div>
   );
 };

+ 5 - 4
kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/Details.spec.tsx

@@ -97,12 +97,13 @@ describe('Details component', () => {
         `/api/clusters/${clusterName}/consumer-groups/${groupId}`,
         200
       );
-      await act(() => {
-        userEvent.click(screen.getByText('Submit'));
+      await waitFor(() => {
+        userEvent.click(screen.getByRole('button', { name: 'Confirm' }));
       });
       expect(deleteConsumerGroupMock.called()).toBeTruthy();
-      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
-      expect(mockNavigate).toHaveBeenLastCalledWith('../');
+
+      await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
+      await waitFor(() => expect(mockNavigate).toHaveBeenLastCalledWith('../'));
     });
   });
 });

+ 7 - 14
kafka-ui-react-app/src/components/Schemas/Details/Details.tsx

@@ -6,7 +6,6 @@ import {
   clusterSchemaSchemaComparePageRelativePath,
 } from 'lib/paths';
 import ClusterContext from 'components/contexts/ClusterContext';
-import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import { Button } from 'components/common/Button/Button';
@@ -38,10 +37,6 @@ const Details: React.FC = () => {
   const dispatch = useAppDispatch();
   const { isReadOnly } = React.useContext(ClusterContext);
   const { clusterName, subject } = useAppParams<ClusterSubjectParam>();
-  const [
-    isDeleteSchemaConfirmationVisible,
-    setDeleteSchemaConfirmationVisible,
-  ] = React.useState(false);
 
   React.useEffect(() => {
     dispatch(fetchLatestSchema({ clusterName, subject }));
@@ -62,7 +57,7 @@ const Details: React.FC = () => {
   const isFetched = useAppSelector(getAreSchemaLatestFulfilled);
   const areVersionsFetched = useAppSelector(getAreSchemaVersionsFulfilled);
 
-  const onDelete = async () => {
+  const deleteHandler = async () => {
     try {
       await schemasApiClient.deleteSchema({
         clusterName,
@@ -101,19 +96,17 @@ const Details: React.FC = () => {
             </Button>
             <Dropdown>
               <DropdownItem
-                onClick={() => setDeleteSchemaConfirmationVisible(true)}
+                confirm={
+                  <>
+                    Are you sure want to remove <b>{subject}</b> schema?
+                  </>
+                }
+                onClick={deleteHandler}
                 danger
               >
                 Remove schema
               </DropdownItem>
             </Dropdown>
-            <ConfirmationModal
-              isOpen={isDeleteSchemaConfirmationVisible}
-              onCancel={() => setDeleteSchemaConfirmationVisible(false)}
-              onConfirm={onDelete}
-            >
-              Are you sure want to remove <b>{subject}</b> schema?
-            </ConfirmationModal>
           </>
         )}
       </PageHeading>

+ 26 - 40
kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.tsx

@@ -1,5 +1,4 @@
 import React from 'react';
-import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import Select from 'components/common/Select/Select';
 import { CompatibilityLevelCompatibilityEnum } from 'generated-sources';
 import { useAppDispatch } from 'lib/hooks/redux';
@@ -10,6 +9,7 @@ import { fetchSchemas } from 'redux/reducers/schemas/schemasSlice';
 import { ClusterNameRoute } from 'lib/paths';
 import { schemasApiClient } from 'lib/api';
 import { showServerError } from 'lib/errorHandling';
+import { useConfirm } from 'lib/hooks/useConfirm';
 
 import * as S from './GlobalSchemaSelector.styled';
 
@@ -18,17 +18,12 @@ const GlobalSchemaSelector: React.FC = () => {
   const dispatch = useAppDispatch();
   const [searchText] = useSearch();
   const { page, perPage } = usePagination();
+  const confirm = useConfirm();
 
   const [currentCompatibilityLevel, setCurrentCompatibilityLevel] =
     React.useState<CompatibilityLevelCompatibilityEnum | undefined>();
-  const [nextCompatibilityLevel, setNextCompatibilityLevel] = React.useState<
-    CompatibilityLevelCompatibilityEnum | undefined
-  >();
 
   const [isFetching, setIsFetching] = React.useState(false);
-  const [isUpdating, setIsUpdating] = React.useState(false);
-  const [isConfirmationVisible, setIsConfirmationVisible] =
-    React.useState(false);
 
   React.useEffect(() => {
     const fetchData = async () => {
@@ -49,29 +44,30 @@ const GlobalSchemaSelector: React.FC = () => {
   }, [clusterName]);
 
   const handleChangeCompatibilityLevel = (level: string | number) => {
-    setNextCompatibilityLevel(level as CompatibilityLevelCompatibilityEnum);
-    setIsConfirmationVisible(true);
-  };
-
-  const handleUpdateCompatibilityLevel = async () => {
-    setIsUpdating(true);
-    if (nextCompatibilityLevel) {
-      try {
-        await schemasApiClient.updateGlobalSchemaCompatibilityLevel({
-          clusterName,
-          compatibilityLevel: { compatibility: nextCompatibilityLevel },
-        });
-        setCurrentCompatibilityLevel(nextCompatibilityLevel);
-        setNextCompatibilityLevel(undefined);
-        setIsConfirmationVisible(false);
-        dispatch(
-          fetchSchemas({ clusterName, page, perPage, search: searchText })
-        );
-      } catch (e) {
-        showServerError(e as Response);
+    const nextLevel = level as CompatibilityLevelCompatibilityEnum;
+    confirm(
+      <>
+        Are you sure you want to update the global compatibility level and set
+        it to <b>{nextLevel}</b>? This may affect the compatibility levels of
+        the schemas.
+      </>,
+      async () => {
+        try {
+          await schemasApiClient.updateGlobalSchemaCompatibilityLevel({
+            clusterName,
+            compatibilityLevel: {
+              compatibility: nextLevel,
+            },
+          });
+          setCurrentCompatibilityLevel(nextLevel);
+          dispatch(
+            fetchSchemas({ clusterName, page, perPage, search: searchText })
+          );
+        } catch (e) {
+          showServerError(e as Response);
+        }
       }
-    }
-    setIsUpdating(false);
+    );
   };
 
   if (!currentCompatibilityLevel) return null;
@@ -84,21 +80,11 @@ const GlobalSchemaSelector: React.FC = () => {
         defaultValue={currentCompatibilityLevel}
         minWidth="200px"
         onChange={handleChangeCompatibilityLevel}
-        disabled={isFetching || isUpdating || isConfirmationVisible}
+        disabled={isFetching}
         options={Object.keys(CompatibilityLevelCompatibilityEnum).map(
           (level) => ({ value: level, label: level })
         )}
       />
-      <ConfirmationModal
-        isOpen={isConfirmationVisible}
-        onCancel={() => setIsConfirmationVisible(false)}
-        onConfirm={handleUpdateCompatibilityLevel}
-        isConfirming={isUpdating}
-      >
-        Are you sure you want to update the global compatibility level and set
-        it to <b>{nextCompatibilityLevel}</b>? This may affect the compatibility
-        levels of the schemas.
-      </ConfirmationModal>
     </S.Wrapper>
   );
 };

+ 5 - 2
kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/__test__/GlobalSchemaSelector.spec.tsx

@@ -86,7 +86,7 @@ describe('GlobalSchemaSelector', () => {
       200
     );
     await waitFor(() => {
-      userEvent.click(screen.getByText('Submit'));
+      userEvent.click(screen.getByRole('button', { name: 'Confirm' }));
     });
     await waitFor(() => expect(putNewCompatibilityMock.called()).toBeTruthy());
     await waitFor(() => expect(getSchemasMock.called()).toBeTruthy());
@@ -94,6 +94,9 @@ describe('GlobalSchemaSelector', () => {
     await waitFor(() =>
       expect(screen.queryByText('Confirm the action')).not.toBeInTheDocument()
     );
-    expectOptionIsSelected(CompatibilityLevelCompatibilityEnum.FORWARD);
+
+    await waitFor(() =>
+      expectOptionIsSelected(CompatibilityLevelCompatibilityEnum.FORWARD)
+    );
   });
 });

+ 38 - 64
kafka-ui-react-app/src/components/Topics/List/ActionsCell/ActionsCell.tsx

@@ -5,13 +5,11 @@ import {
   TopicColumnsToSort,
 } from 'generated-sources';
 import { useAppDispatch } from 'lib/hooks/redux';
-import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import { TableCellProps } from 'components/common/SmartTable/TableColumn';
 import { TopicWithDetailedInfo } from 'redux/interfaces';
 import ClusterContext from 'components/contexts/ClusterContext';
 import * as S from 'components/Topics/List/List.styled';
 import { ClusterNameRoute } from 'lib/paths';
-import useModal from 'lib/hooks/useModal';
 import useAppParams from 'lib/hooks/useAppParams';
 import {
   deleteTopic,
@@ -46,85 +44,61 @@ const ActionsCell: React.FC<
   const dispatch = useAppDispatch();
   const { clusterName } = useAppParams<ClusterNameRoute>();
 
-  const {
-    isOpen: isDeleteTopicModalOpen,
-    setClose: closeDeleteTopicModal,
-    setOpen: openDeleteTopicModal,
-  } = useModal(false);
-
-  const {
-    isOpen: isRecreateTopicModalOpen,
-    setClose: closeRecreateTopicModal,
-    setOpen: openRecreateTopicModal,
-  } = useModal(false);
-
-  const {
-    isOpen: isClearMessagesModalOpen,
-    setClose: closeClearMessagesModal,
-    setOpen: openClearMessagesModal,
-  } = useModal(false);
-
   const isHidden = internal || isReadOnly || !hovered;
 
   const deleteTopicHandler = () => {
     dispatch(deleteTopic({ clusterName, topicName: name }));
-    closeDeleteTopicModal();
   };
 
   const clearTopicMessagesHandler = () => {
     dispatch(clearTopicMessages({ clusterName, topicName: name }));
     dispatch(fetchTopicsList(topicsListParams));
-    closeClearMessagesModal();
   };
 
   const recreateTopicHandler = () => {
     dispatch(recreateTopic({ clusterName, topicName: name }));
-    closeRecreateTopicModal();
   };
 
   return (
-    <>
-      <S.ActionsContainer>
-        {!isHidden && (
-          <Dropdown>
-            {cleanUpPolicy === CleanUpPolicy.DELETE && (
-              <DropdownItem onClick={openClearMessagesModal} danger>
-                Clear Messages
-              </DropdownItem>
-            )}
-            <DropdownItem onClick={openRecreateTopicModal} danger>
-              Recreate Topic
+    <S.ActionsContainer>
+      {!isHidden && (
+        <Dropdown>
+          {cleanUpPolicy === CleanUpPolicy.DELETE && (
+            <DropdownItem
+              onClick={clearTopicMessagesHandler}
+              confirm="Are you sure want to clear topic messages?"
+              danger
+            >
+              Clear Messages
+            </DropdownItem>
+          )}
+          <DropdownItem
+            onClick={recreateTopicHandler}
+            confirm={
+              <>
+                Are you sure to recreate <b>{name}</b> topic?
+              </>
+            }
+            danger
+          >
+            Recreate Topic
+          </DropdownItem>
+          {isTopicDeletionAllowed && (
+            <DropdownItem
+              onClick={deleteTopicHandler}
+              confirm={
+                <>
+                  Are you sure want to remove <b>{name}</b> topic?
+                </>
+              }
+              danger
+            >
+              Remove Topic
             </DropdownItem>
-            {isTopicDeletionAllowed && (
-              <DropdownItem onClick={openDeleteTopicModal} danger>
-                Remove Topic
-              </DropdownItem>
-            )}
-          </Dropdown>
-        )}
-      </S.ActionsContainer>
-      <ConfirmationModal
-        isOpen={isClearMessagesModalOpen}
-        onCancel={closeClearMessagesModal}
-        onConfirm={clearTopicMessagesHandler}
-      >
-        Are you sure want to clear topic messages?
-      </ConfirmationModal>
-      <ConfirmationModal
-        isOpen={isDeleteTopicModalOpen}
-        onCancel={closeDeleteTopicModal}
-        onConfirm={deleteTopicHandler}
-      >
-        Are you sure want to remove <b>{name}</b> topic?
-      </ConfirmationModal>
-      <ConfirmationModal
-        isOpen={isRecreateTopicModalOpen}
-        onCancel={closeRecreateTopicModal}
-        onConfirm={recreateTopicHandler}
-      >
-        Are you sure to recreate <b>{name}</b> topic?
-      </ConfirmationModal>
-    </>
+          )}
+        </Dropdown>
+      )}
+    </S.ActionsContainer>
   );
 };
 

+ 41 - 59
kafka-ui-react-app/src/components/Topics/List/List.tsx

@@ -14,7 +14,6 @@ import {
 import usePagination from 'lib/hooks/usePagination';
 import ClusterContext from 'components/contexts/ClusterContext';
 import PageLoader from 'components/common/PageLoader/PageLoader';
-import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import {
   GetTopicsRequest,
   SortOrder,
@@ -30,6 +29,7 @@ import { SmartTable } from 'components/common/SmartTable/SmartTable';
 import { TableColumn } from 'components/common/SmartTable/TableColumn';
 import { useTableState } from 'lib/hooks/useTableState';
 import PlusIcon from 'components/common/Icons/PlusIcon';
+import { useConfirm } from 'lib/hooks/useConfirm';
 
 import {
   MessagesCell,
@@ -88,6 +88,7 @@ const List: React.FC<TopicsListProps> = ({
     page || null
   );
   const navigate = useNavigate();
+  const confirm = useConfirm();
 
   const topicsListParams = React.useMemo(
     () => ({
@@ -148,16 +149,6 @@ const List: React.FC<TopicsListProps> = ({
     });
   };
 
-  const [confirmationModal, setConfirmationModal] = React.useState<
-    '' | 'deleteTopics' | 'purgeMessages'
-  >('');
-
-  const [confirmationModalText, setConfirmationModalText] =
-    React.useState<string>('');
-  const closeConfirmationModal = () => {
-    setConfirmationModal('');
-  };
-
   const clearSelectedTopics = () => tableState.toggleSelection(false);
 
   const searchHandler = (searchString: string) => {
@@ -171,16 +162,26 @@ const List: React.FC<TopicsListProps> = ({
       search: `?page=${newPageQuery}&perPage=${perPage || PER_PAGE}`,
     });
   };
-  const deleteOrPurgeConfirmationHandler = () => {
+
+  const deleteTopicsHandler = () => {
     const selectedIds = Array.from(tableState.selectedIds);
-    if (confirmationModal === 'deleteTopics') {
+    confirm('Are you sure you want to remove selected topics?', () => {
       deleteTopics({ clusterName, topicNames: selectedIds });
-    } else {
-      clearTopicsMessages({ clusterName, topicNames: selectedIds });
-    }
-    closeConfirmationModal();
-    clearSelectedTopics();
-    fetchTopicsList(topicsListParams);
+      clearSelectedTopics();
+      fetchTopicsList(topicsListParams);
+    });
+  };
+
+  const purgeTopicsHandler = () => {
+    const selectedIds = Array.from(tableState.selectedIds);
+    confirm(
+      'Are you sure you want to purge messages of selected topics?',
+      () => {
+        clearTopicsMessages({ clusterName, topicNames: selectedIds });
+        clearSelectedTopics();
+        fetchTopicsList(topicsListParams);
+      }
+    );
   };
 
   return (
@@ -220,54 +221,35 @@ const List: React.FC<TopicsListProps> = ({
       ) : (
         <div>
           {tableState.selectedCount > 0 && (
-            <>
-              <ControlPanelWrapper data-testid="delete-buttons">
+            <ControlPanelWrapper data-testid="delete-buttons">
+              <Button
+                buttonSize="M"
+                buttonType="secondary"
+                onClick={deleteTopicsHandler}
+              >
+                Delete selected topics
+              </Button>
+              {tableState.selectedCount === 1 && (
                 <Button
                   buttonSize="M"
                   buttonType="secondary"
-                  onClick={() => {
-                    setConfirmationModal('deleteTopics');
-                    setConfirmationModalText(
-                      'Are you sure you want to remove selected topics?'
-                    );
+                  to={{
+                    pathname: clusterTopicCopyRelativePath,
+                    search: `?${getSelectedTopic()}`,
                   }}
                 >
-                  Delete selected topics
+                  Copy selected topic
                 </Button>
-                {tableState.selectedCount === 1 && (
-                  <Button
-                    buttonSize="M"
-                    buttonType="secondary"
-                    to={{
-                      pathname: clusterTopicCopyRelativePath,
-                      search: `?${getSelectedTopic()}`,
-                    }}
-                  >
-                    Copy selected topic
-                  </Button>
-                )}
+              )}
 
-                <Button
-                  buttonSize="M"
-                  buttonType="secondary"
-                  onClick={() => {
-                    setConfirmationModal('purgeMessages');
-                    setConfirmationModalText(
-                      'Are you sure you want to purge messages of selected topics?'
-                    );
-                  }}
-                >
-                  Purge messages of selected topics
-                </Button>
-              </ControlPanelWrapper>
-              <ConfirmationModal
-                isOpen={confirmationModal !== ''}
-                onCancel={closeConfirmationModal}
-                onConfirm={deleteOrPurgeConfirmationHandler}
+              <Button
+                buttonSize="M"
+                buttonType="secondary"
+                onClick={purgeTopicsHandler}
               >
-                {confirmationModalText}
-              </ConfirmationModal>
-            </>
+                Purge messages of selected topics
+              </Button>
+            </ControlPanelWrapper>
           )}
           <SmartTable
             selectable={!isReadOnly}

+ 2 - 2
kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx

@@ -280,9 +280,9 @@ describe('List', () => {
       )[buttonIndex];
       userEvent.click(buttonClickedElement);
 
-      const modal = screen.getByRole('dialog');
+      const modal = await screen.findByRole('dialog');
       expect(within(modal).getByText(confirmationText)).toBeInTheDocument();
-      userEvent.click(within(modal).getByRole('button', { name: 'Submit' }));
+      userEvent.click(within(modal).getByRole('button', { name: 'Confirm' }));
 
       await waitFor(() => {
         expect(screen.queryByTestId('delete-buttons')).not.toBeInTheDocument();

+ 34 - 58
kafka-ui-react-app/src/components/Topics/Topic/Details/Details.tsx

@@ -10,7 +10,6 @@ import {
   clusterTopicSendMessageRelativePath,
 } from 'lib/paths';
 import ClusterContext from 'components/contexts/ClusterContext';
-import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import { Button } from 'components/common/Button/Button';
 import styled from 'styled-components';
@@ -60,42 +59,22 @@ const Details: React.FC<Props> = ({
   clearTopicMessages,
 }) => {
   const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
-
   const isInternal = useAppSelector((state) =>
     getIsTopicInternal(state, topicName)
   );
   const isDeletePolicy = useAppSelector((state) =>
     getIsTopicDeletePolicy(state, topicName)
   );
-
   const navigate = useNavigate();
+
   const { isReadOnly, isTopicDeletionAllowed } =
     React.useContext(ClusterContext);
 
-  const [isDeleteTopicConfirmationVisible, setDeleteTopicConfirmationVisible] =
-    React.useState(false);
-  const [isClearTopicConfirmationVisible, setClearTopicConfirmationVisible] =
-    React.useState(false);
-  const [
-    isRecreateTopicConfirmationVisible,
-    setRecreateTopicConfirmationVisible,
-  ] = React.useState(false);
   const deleteTopicHandler = () => {
     deleteTopic({ clusterName, topicName });
-    setDeleteTopicConfirmationVisible(false);
     navigate('../..');
   };
 
-  const clearTopicMessagesHandler = () => {
-    clearTopicMessages({ clusterName, topicName });
-    setClearTopicConfirmationVisible(false);
-  };
-
-  const recreateTopicHandler = () => {
-    recreateTopic({ clusterName, topicName });
-    setRecreateTopicConfirmationVisible(false);
-  };
-
   return (
     <div>
       <PageHeading text={topicName}>
@@ -131,26 +110,44 @@ const Details: React.FC<Props> = ({
                         especially important consequences.
                       </DropdownItemHint>
                     </DropdownItem>
+                    {isDeletePolicy && (
+                      <DropdownItem
+                        disabled={!isDeletePolicy}
+                        onClick={() =>
+                          clearTopicMessages({ clusterName, topicName })
+                        }
+                        confirm="Are you sure want to clear topic messages?"
+                        danger
+                      >
+                        Clear messages
+                      </DropdownItem>
+                    )}
                     <DropdownItem
-                      disabled={!isDeletePolicy}
-                      onClick={() => setClearTopicConfirmationVisible(true)}
-                      danger
-                    >
-                      Clear messages
-                    </DropdownItem>
-                    <DropdownItem
-                      onClick={() => setRecreateTopicConfirmationVisible(true)}
+                      onClick={() => recreateTopic({ clusterName, topicName })}
+                      confirm={
+                        <>
+                          Are you sure want to recreate <b>{topicName}</b>{' '}
+                          topic?
+                        </>
+                      }
                       danger
                     >
                       Recreate Topic
                     </DropdownItem>
-                    <DropdownItem
-                      disabled={!isTopicDeletionAllowed}
-                      onClick={() => setDeleteTopicConfirmationVisible(true)}
-                      danger
-                    >
-                      Remove Topic
-                    </DropdownItem>
+                    {isTopicDeletionAllowed && (
+                      <DropdownItem
+                        onClick={deleteTopicHandler}
+                        confirm={
+                          <>
+                            Are you sure want to remove <b>{topicName}</b>{' '}
+                            topic?
+                          </>
+                        }
+                        danger
+                      >
+                        Remove Topic
+                      </DropdownItem>
+                    )}
                   </Dropdown>
                 }
               />
@@ -158,27 +155,6 @@ const Details: React.FC<Props> = ({
           )}
         </HeaderControlsWrapper>
       </PageHeading>
-      <ConfirmationModal
-        isOpen={isDeleteTopicConfirmationVisible}
-        onCancel={() => setDeleteTopicConfirmationVisible(false)}
-        onConfirm={deleteTopicHandler}
-      >
-        Are you sure want to remove <b>{topicName}</b> topic?
-      </ConfirmationModal>
-      <ConfirmationModal
-        isOpen={isClearTopicConfirmationVisible}
-        onCancel={() => setClearTopicConfirmationVisible(false)}
-        onConfirm={clearTopicMessagesHandler}
-      >
-        Are you sure want to clear topic messages?
-      </ConfirmationModal>
-      <ConfirmationModal
-        isOpen={isRecreateTopicConfirmationVisible}
-        onCancel={() => setRecreateTopicConfirmationVisible(false)}
-        onConfirm={recreateTopicHandler}
-      >
-        Are you sure want to recreate <b>{topicName}</b> topic?
-      </ConfirmationModal>
       <Navbar role="navigation">
         <NavLink
           to="."

+ 0 - 7
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.styled.ts

@@ -298,13 +298,6 @@ export const DeleteSavedFilterIcon = styled.div`
   margin-left: 4px;
 `;
 
-export const ConfirmDeletionText = styled.h3`
-  color: ${({ theme }) => theme.modal.deletionTextColor};
-  font-size: 14px;
-  line-height: 20px;
-  padding: 16px 0;
-`;
-
 export const MessageLoading = styled.div.attrs({
   role: 'contentLoader',
 })<MessageLoadingProps>`

+ 5 - 22
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/SavedFilters.tsx

@@ -1,8 +1,7 @@
 import React, { FC } from 'react';
 import { Button } from 'components/common/Button/Button';
-import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
-import useModal from 'lib/hooks/useModal';
 import DeleteIcon from 'components/common/Icons/DeleteIcon';
+import { useConfirm } from 'lib/hooks/useConfirm';
 
 import * as S from './Filters.styled';
 import { MessageFilters } from './Filters';
@@ -24,9 +23,8 @@ const SavedFilters: FC<Props> = ({
   closeModal,
   onGoBack,
 }) => {
-  const { isOpen, setOpen, setClose } = useModal();
-  const [deleteIndex, setDeleteIndex] = React.useState<number>(-1);
   const [selectedFilter, setSelectedFilter] = React.useState(-1);
+  const confirm = useConfirm();
 
   const activeFilter = () => {
     if (selectedFilter > -1) {
@@ -36,26 +34,13 @@ const SavedFilters: FC<Props> = ({
   };
 
   const deleteFilterHandler = (index: number) => {
-    setOpen();
-    setDeleteIndex(index);
+    confirm(<>Are you sure want to remove {filters[index]?.name}?</>, () => {
+      deleteFilter(index);
+    });
   };
 
   return (
     <>
-      <ConfirmationModal
-        isOpen={isOpen}
-        title="Confirm deletion"
-        onConfirm={() => {
-          deleteFilter(deleteIndex);
-          setClose();
-        }}
-        onCancel={setClose}
-        submitBtnText="Delete"
-      >
-        <S.ConfirmDeletionText>
-          Are you sure want to remove {filters[deleteIndex]?.name}?
-        </S.ConfirmDeletionText>
-      </ConfirmationModal>
       <S.BackToCustomText onClick={onGoBack}>
         Back To custom filters
       </S.BackToCustomText>
@@ -86,7 +71,6 @@ const SavedFilters: FC<Props> = ({
           buttonType="secondary"
           type="button"
           onClick={closeModal}
-          disabled={isOpen}
         >
           Cancel
         </Button>
@@ -95,7 +79,6 @@ const SavedFilters: FC<Props> = ({
           buttonType="primary"
           type="button"
           onClick={activeFilter}
-          disabled={isOpen}
         >
           Select filter
         </Button>

+ 16 - 9
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/SavedFilters.spec.tsx

@@ -3,7 +3,12 @@ import SavedFilters, {
   Props,
 } from 'components/Topics/Topic/Details/Messages/Filters/SavedFilters';
 import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/Filters';
-import { screen, within } from '@testing-library/react';
+import {
+  screen,
+  waitFor,
+  waitForElementToBeRemoved,
+  within,
+} from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { render } from 'lib/testHelpers';
 
@@ -11,8 +16,8 @@ jest.mock('components/common/Icons/DeleteIcon', () => () => 'mock-DeleteIcon');
 
 describe('SavedFilter Component', () => {
   const mockFilters: MessageFilters[] = [
-    { name: 'name', code: 'code' },
-    { name: 'name1', code: 'code1' },
+    { name: 'My Filter', code: 'code' },
+    { name: 'One More Filter', code: 'code1' },
   ];
 
   const setUpComponent = (props: Partial<Props> = {}) => {
@@ -125,11 +130,11 @@ describe('SavedFilter Component', () => {
       const modelDialog = screen.getByRole('dialog');
       expect(modelDialog).toBeInTheDocument();
       expect(
-        within(modelDialog).getByText(/Confirm deletion/i)
+        within(modelDialog).getByText('Are you sure want to remove My Filter?')
       ).toBeInTheDocument();
     });
 
-    it('Close Confirmations deletion modal with button', () => {
+    it('Close Confirmations deletion modal with button', async () => {
       setUpComponent({ deleteFilter: deleteMock });
       const savedFilters = getSavedFilters();
       const deleteIcons = screen.getAllByText('mock-DeleteIcon');
@@ -142,11 +147,11 @@ describe('SavedFilter Component', () => {
       const cancelButton = within(modelDialog).getByRole('button', {
         name: /Cancel/i,
       });
-      userEvent.click(cancelButton);
+      await waitFor(() => userEvent.click(cancelButton));
       expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
     });
 
-    it('Delete the saved filter', () => {
+    it('Delete the saved filter', async () => {
       setUpComponent({ deleteFilter: deleteMock });
       const savedFilters = getSavedFilters();
       const deleteIcons = screen.getAllByText('mock-DeleteIcon');
@@ -154,9 +159,11 @@ describe('SavedFilter Component', () => {
       userEvent.hover(savedFilters[0]);
       userEvent.click(deleteIcons[0]);
 
-      userEvent.click(screen.getByRole('button', { name: /Delete/i }));
+      await waitFor(() =>
+        userEvent.click(screen.getByRole('button', { name: 'Confirm' }))
+      );
       expect(deleteMock).toHaveBeenCalledTimes(1);
-      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+      await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
     });
   });
 });

+ 23 - 18
kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/Details.spec.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import { screen } from '@testing-library/react';
+import { screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import ClusterContext from 'components/contexts/ClusterContext';
 import Details from 'components/Topics/Topic/Details/Details';
@@ -97,9 +97,12 @@ describe('Details', () => {
       userEvent.click(openModalButton);
     });
 
-    it('calls deleteTopic on confirm', () => {
-      const submitButton = screen.getAllByText('Submit')[0];
-      userEvent.click(submitButton);
+    it('calls deleteTopic on confirm', async () => {
+      const submitButton = screen.getAllByRole('button', {
+        name: 'Confirm',
+      })[0];
+
+      await waitFor(() => userEvent.click(submitButton));
 
       expect(mockDelete).toHaveBeenCalledWith({
         clusterName: mockClusterName,
@@ -107,10 +110,9 @@ describe('Details', () => {
       });
     });
 
-    it('closes the modal when cancel button is clicked', () => {
+    it('closes the modal when cancel button is clicked', async () => {
       const cancelButton = screen.getAllByText('Cancel')[0];
-      userEvent.click(cancelButton);
-
+      await waitFor(() => userEvent.click(cancelButton));
       expect(cancelButton).not.toBeInTheDocument();
     });
   });
@@ -123,9 +125,11 @@ describe('Details', () => {
       userEvent.click(confirmButton);
     });
 
-    it('it calls clearTopicMessages on confirm', () => {
-      const submitButton = screen.getAllByText('Submit')[0];
-      userEvent.click(submitButton);
+    it('it calls clearTopicMessages on confirm', async () => {
+      const submitButton = screen.getAllByRole('button', {
+        name: 'Confirm',
+      })[0];
+      await waitFor(() => userEvent.click(submitButton));
 
       expect(mockClearTopicMessages).toHaveBeenCalledWith({
         clusterName: mockClusterName,
@@ -133,9 +137,9 @@ describe('Details', () => {
       });
     });
 
-    it('closes the modal when cancel button is clicked', () => {
+    it('closes the modal when cancel button is clicked', async () => {
       const cancelButton = screen.getAllByText('Cancel')[0];
-      userEvent.click(cancelButton);
+      await waitFor(() => userEvent.click(cancelButton));
 
       expect(cancelButton).not.toBeInTheDocument();
     });
@@ -152,14 +156,14 @@ describe('Details', () => {
     });
   });
 
-  it('redirects to the correct route if topic is deleted', () => {
+  it('redirects to the correct route if topic is deleted', async () => {
     setupComponent();
 
     const deleteTopicButton = screen.getByText(/Remove topic/i);
     userEvent.click(deleteTopicButton);
 
-    const submitDeleteButton = screen.getByText(/Submit/i);
-    userEvent.click(submitDeleteButton);
+    const submitDeleteButton = screen.getByRole('button', { name: 'Confirm' });
+    await waitFor(() => userEvent.click(submitDeleteButton));
 
     expect(mockNavigate).toHaveBeenCalledWith('../..');
   });
@@ -184,12 +188,13 @@ describe('Details', () => {
     ).toBeInTheDocument();
   });
 
-  it('calling recreation function after click on Submit button', () => {
+  it('calling recreation function after click on Submit button', async () => {
     setupComponent();
     const recreateTopicButton = screen.getByText(/Recreate topic/i);
     userEvent.click(recreateTopicButton);
-    const confirmBtn = screen.getByRole('button', { name: /submit/i });
-    userEvent.click(confirmBtn);
+    const confirmBtn = screen.getByRole('button', { name: /Confirm/i });
+
+    await waitFor(() => userEvent.click(confirmBtn));
     expect(mockRecreateTopic).toBeCalledTimes(1);
   });
 

+ 27 - 58
kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZone.tsx

@@ -1,6 +1,5 @@
 import { ErrorMessage } from '@hookform/error-message';
 import { Button } from 'components/common/Button/Button';
-import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import Input from 'components/common/Input/Input';
 import { FormError } from 'components/common/Input/Input.styled';
 import { InputLabel } from 'components/common/Input/InputLabel.styled';
@@ -9,14 +8,13 @@ import { FormProvider, useForm } from 'react-hook-form';
 import { RouteParamsClusterTopic } from 'lib/paths';
 import { ClusterName, TopicName } from 'redux/interfaces';
 import useAppParams from 'lib/hooks/useAppParams';
+import { useConfirm } from 'lib/hooks/useConfirm';
 
 import * as S from './DangerZone.styled';
 
 export interface Props {
   defaultPartitions: number;
   defaultReplicationFactor: number;
-  partitionsCountIncreased: boolean;
-  replicationFactorUpdated: boolean;
   updateTopicPartitionsCount: (payload: {
     clusterName: ClusterName;
     topicName: TopicName;
@@ -32,19 +30,10 @@ export interface Props {
 const DangerZone: React.FC<Props> = ({
   defaultPartitions,
   defaultReplicationFactor,
-  partitionsCountIncreased,
-  replicationFactorUpdated,
   updateTopicPartitionsCount,
   updateTopicReplicationFactor,
 }) => {
   const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
-
-  const [isPartitionsConfirmationVisible, setIsPartitionsConfirmationVisible] =
-    React.useState<boolean>(false);
-  const [
-    isReplicationFactorConfirmationVisible,
-    setIsReplicationFactorConfirmationVisible,
-  ] = React.useState<boolean>(false);
   const [partitions, setPartitions] = React.useState<number>(defaultPartitions);
   const [replicationFactor, setReplicationFactor] = React.useState<number>(
     defaultReplicationFactor
@@ -62,6 +51,29 @@ const DangerZone: React.FC<Props> = ({
     },
   });
 
+  const confirm = useConfirm();
+
+  const confirmPartitionsChange = () =>
+    confirm(
+      `Are you sure you want to increase the number of partitions?
+        Do it only if you 100% know what you are doing!`,
+      () =>
+        updateTopicPartitionsCount({
+          clusterName,
+          topicName,
+          partitions: partitionsMethods.getValues('partitions'),
+        })
+    );
+  const confirmReplicationFactorChange = () =>
+    confirm('Are you sure you want to update the replication factor?', () =>
+      updateTopicReplicationFactor({
+        clusterName,
+        topicName,
+        replicationFactor:
+          replicationFactorMethods.getValues('replicationFactor'),
+      })
+    );
+
   const validatePartitions = (data: { partitions: number }) => {
     if (data.partitions < defaultPartitions) {
       partitionsMethods.setError('partitions', {
@@ -70,42 +82,15 @@ const DangerZone: React.FC<Props> = ({
       });
     } else {
       setPartitions(data.partitions);
-      setIsPartitionsConfirmationVisible(true);
+      confirmPartitionsChange();
     }
   };
 
   const validateReplicationFactor = (data: { replicationFactor: number }) => {
     setReplicationFactor(data.replicationFactor);
-    setIsReplicationFactorConfirmationVisible(true);
+    confirmReplicationFactorChange();
   };
 
-  React.useEffect(() => {
-    if (partitionsCountIncreased) {
-      setIsPartitionsConfirmationVisible(false);
-    }
-  }, [partitionsCountIncreased]);
-
-  React.useEffect(() => {
-    if (replicationFactorUpdated) {
-      setIsReplicationFactorConfirmationVisible(false);
-    }
-  }, [replicationFactorUpdated]);
-
-  const partitionsSubmit = () => {
-    updateTopicPartitionsCount({
-      clusterName,
-      topicName,
-      partitions: partitionsMethods.getValues('partitions'),
-    });
-  };
-  const replicationFactorSubmit = () => {
-    updateTopicReplicationFactor({
-      clusterName,
-      topicName,
-      replicationFactor:
-        replicationFactorMethods.getValues('replicationFactor'),
-    });
-  };
   return (
     <S.Wrapper>
       <S.Title>Danger Zone</S.Title>
@@ -148,15 +133,6 @@ const DangerZone: React.FC<Props> = ({
             name="partitions"
           />
         </FormError>
-        <ConfirmationModal
-          isOpen={isPartitionsConfirmationVisible}
-          onCancel={() => setIsPartitionsConfirmationVisible(false)}
-          onConfirm={partitionsSubmit}
-        >
-          Are you sure you want to increase the number of partitions? Do it only
-          if you 100% know what you are doing!
-        </ConfirmationModal>
-
         <FormProvider {...replicationFactorMethods}>
           <S.Form
             onSubmit={replicationFactorMethods.handleSubmit(
@@ -170,6 +146,7 @@ const DangerZone: React.FC<Props> = ({
               </InputLabel>
               <Input
                 id="replicationFactor"
+                inputSize="M"
                 type="number"
                 placeholder="Replication Factor"
                 name="replicationFactor"
@@ -190,20 +167,12 @@ const DangerZone: React.FC<Props> = ({
             </div>
           </S.Form>
         </FormProvider>
-
         <FormError>
           <ErrorMessage
             errors={replicationFactorMethods.formState.errors}
             name="replicationFactor"
           />
         </FormError>
-        <ConfirmationModal
-          isOpen={isReplicationFactorConfirmationVisible}
-          onCancel={() => setIsReplicationFactorConfirmationVisible(false)}
-          onConfirm={replicationFactorSubmit}
-        >
-          Are you sure you want to update the replication factor?
-        </ConfirmationModal>
       </div>
     </S.Wrapper>
   );

+ 1 - 7
kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZoneContainer.ts

@@ -4,10 +4,6 @@ import {
   updateTopicPartitionsCount,
   updateTopicReplicationFactor,
 } from 'redux/reducers/topics/topicsSlice';
-import {
-  getTopicPartitionsCountIncreased,
-  getTopicReplicationFactorUpdated,
-} from 'redux/reducers/topics/selectors';
 
 import DangerZone from './DangerZone';
 
@@ -17,13 +13,11 @@ type OwnProps = {
 };
 
 const mapStateToProps = (
-  state: RootState,
+  _: RootState,
   { defaultPartitions, defaultReplicationFactor }: OwnProps
 ) => ({
   defaultPartitions,
   defaultReplicationFactor,
-  partitionsCountIncreased: getTopicPartitionsCountIncreased(state),
-  replicationFactorUpdated: getTopicReplicationFactorUpdated(state),
 });
 
 const mapDispatchToProps = {

+ 2 - 71
kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/__test__/DangerZone.spec.tsx

@@ -20,8 +20,6 @@ const renderComponent = (props?: Partial<Props>) =>
       <DangerZone
         defaultPartitions={defaultPartitions}
         defaultReplicationFactor={defaultReplicationFactor}
-        partitionsCountIncreased={false}
-        replicationFactorUpdated={false}
         updateTopicPartitionsCount={jest.fn()}
         updateTopicReplicationFactor={jest.fn()}
         {...props}
@@ -33,7 +31,7 @@ const renderComponent = (props?: Partial<Props>) =>
 const clickOnDialogSubmitButton = () => {
   userEvent.click(
     within(screen.getByRole('dialog')).getByRole('button', {
-      name: 'Submit',
+      name: 'Confirm',
     })
   );
 };
@@ -41,7 +39,7 @@ const clickOnDialogSubmitButton = () => {
 const checkDialogThenPressCancel = async () => {
   const dialog = screen.getByRole('dialog');
   expect(screen.getByRole('dialog')).toBeInTheDocument();
-  userEvent.click(within(dialog).getByText(/cancel/i));
+  userEvent.click(within(dialog).getByRole('button', { name: 'Cancel' }));
   await waitFor(() =>
     expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
   );
@@ -174,73 +172,6 @@ describe('DangerZone', () => {
     );
   });
 
-  it('should close any popup if the partitionsCount is Increased ', async () => {
-    renderComponent({ partitionsCountIncreased: true });
-    await waitFor(() =>
-      expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
-    );
-  });
-
-  it('should close any popup if the replicationFactor is Updated', async () => {
-    renderComponent({ replicationFactorUpdated: true });
-    await waitFor(() =>
-      expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
-    );
-  });
-
-  it('should already opened Confirmation popup if partitionsCount is Increased', async () => {
-    const { rerender } = renderComponent();
-    const partitionInput = screen.getByPlaceholderText('Number of partitions');
-    const partitionInputSubmitBtn = screen.getAllByText(/submit/i)[0];
-
-    await waitFor(() => {
-      userEvent.type(partitionInput, '5');
-    });
-
-    userEvent.click(partitionInputSubmitBtn);
-    await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument());
-    rerender(
-      <DangerZone
-        defaultPartitions={defaultPartitions}
-        defaultReplicationFactor={defaultReplicationFactor}
-        partitionsCountIncreased
-        replicationFactorUpdated={false}
-        updateTopicPartitionsCount={jest.fn()}
-        updateTopicReplicationFactor={jest.fn()}
-      />
-    );
-    await waitFor(() =>
-      expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
-    );
-  });
-
-  it('should already opened Confirmation popup if replicationFactor is Increased', async () => {
-    const { rerender } = renderComponent();
-    const replicatorFactorInput =
-      screen.getByPlaceholderText('Replication Factor');
-    const replicatorFactorInputSubmitBtn = screen.getAllByText(/submit/i)[1];
-
-    await waitFor(() => {
-      userEvent.type(replicatorFactorInput, '5');
-    });
-
-    userEvent.click(replicatorFactorInputSubmitBtn);
-    await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument());
-    rerender(
-      <DangerZone
-        defaultPartitions={defaultPartitions}
-        defaultReplicationFactor={defaultReplicationFactor}
-        partitionsCountIncreased={false}
-        replicationFactorUpdated
-        updateTopicPartitionsCount={jest.fn()}
-        updateTopicReplicationFactor={jest.fn()}
-      />
-    );
-    await waitFor(() =>
-      expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
-    );
-  });
-
   it('should close the partitions dialog if he cancel button is pressed', async () => {
     renderComponent();
 

+ 26 - 17
kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx

@@ -15,7 +15,7 @@ jest.mock('react-router-dom', () => ({
   useNavigate: () => mockNavigate,
 }));
 
-const renderComponent = (
+const renderComponent = async (
   props: Partial<Props> = {},
   topic: TopicWithDetailedInfo | null = topicWithInfo
 ) => {
@@ -27,7 +27,7 @@ const renderComponent = (
     topics = getTopicStateFixtures([topic]);
   }
 
-  return render(
+  render(
     <WithRoute path={clusterTopicEditPath()}>
       <Edit
         isFetched
@@ -47,8 +47,8 @@ const renderComponent = (
 describe('Edit Component', () => {
   afterEach(() => {});
 
-  it('renders the Edit Component', () => {
-    renderComponent();
+  it('renders the Edit Component', async () => {
+    await act(() => renderComponent());
 
     expect(
       screen.getByRole('heading', { name: `Edit ${topicName}` })
@@ -58,8 +58,10 @@ describe('Edit Component', () => {
     ).toBeInTheDocument();
   });
 
-  it('should check Edit component renders null is not rendered when topic is not passed', () => {
-    renderComponent({}, { ...topicWithInfo, config: undefined });
+  it('should check Edit component renders null is not rendered when topic is not passed', async () => {
+    await act(() =>
+      renderComponent({}, { ...topicWithInfo, config: undefined })
+    );
     expect(
       screen.queryByRole('heading', { name: `Edit ${topicName}` })
     ).not.toBeInTheDocument();
@@ -68,8 +70,8 @@ describe('Edit Component', () => {
     ).not.toBeInTheDocument();
   });
 
-  it('should check Edit component renders null is not isFetched is false', () => {
-    renderComponent({ isFetched: false });
+  it('should check Edit component renders null is not isFetched is false', async () => {
+    await act(() => renderComponent({ isFetched: false }));
     expect(
       screen.queryByRole('heading', { name: `Edit ${topicName}` })
     ).not.toBeInTheDocument();
@@ -78,10 +80,10 @@ describe('Edit Component', () => {
     ).not.toBeInTheDocument();
   });
 
-  it('should check Edit component renders null is not topic config is not passed is false', () => {
+  it('should check Edit component renders null is not topic config is not passed is false', async () => {
     const modifiedTopic = { ...topicWithInfo };
     modifiedTopic.config = undefined;
-    renderComponent({}, modifiedTopic);
+    await act(() => renderComponent({}, modifiedTopic));
     expect(
       screen.queryByRole('heading', { name: `Edit ${topicName}` })
     ).not.toBeInTheDocument();
@@ -92,7 +94,9 @@ describe('Edit Component', () => {
 
   describe('Edit Component with its topic default and modified values', () => {
     it('should check the default partitions value in the DangerZone', async () => {
-      renderComponent({}, { ...topicWithInfo, partitionCount: 0 });
+      await act(() =>
+        renderComponent({}, { ...topicWithInfo, partitionCount: 0 })
+      );
       // cause topic selector will return falsy
       expect(
         screen.queryByRole('heading', { name: `Edit ${topicName}` })
@@ -103,7 +107,9 @@ describe('Edit Component', () => {
     });
 
     it('should check the default partitions value in the DangerZone', async () => {
-      renderComponent({}, { ...topicWithInfo, replicationFactor: undefined });
+      await act(() =>
+        renderComponent({}, { ...topicWithInfo, replicationFactor: undefined })
+      );
       expect(screen.getByPlaceholderText('Replication Factor')).toHaveValue(
         DEFAULTS.replicationFactor
       );
@@ -114,7 +120,9 @@ describe('Edit Component', () => {
     it('should check the submit functionality when topic updated is false', async () => {
       const updateTopicMock = jest.fn();
 
-      renderComponent({ updateTopic: updateTopicMock }, undefined);
+      await act(() =>
+        renderComponent({ updateTopic: updateTopicMock }, undefined)
+      );
 
       const btn = screen.getAllByText(/Save/i)[0];
 
@@ -134,10 +142,11 @@ describe('Edit Component', () => {
 
     it('should check the submit functionality when topic updated is true', async () => {
       const updateTopicMock = jest.fn();
-
-      renderComponent(
-        { updateTopic: updateTopicMock, isTopicUpdated: true },
-        undefined
+      await act(() =>
+        renderComponent(
+          { updateTopic: updateTopicMock, isTopicUpdated: true },
+          undefined
+        )
       );
 
       const btn = screen.getAllByText(/Save/i)[0];

+ 18 - 2
kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import { useFormContext, Controller } from 'react-hook-form';
 import { NOT_SET, BYTES_IN_GB } from 'lib/constants';
-import { TopicName } from 'redux/interfaces';
+import { ClusterName, TopicName } from 'redux/interfaces';
 import { ErrorMessage } from '@hookform/error-message';
 import Select, { SelectOption } from 'components/common/Select/Select';
 import Input from 'components/common/Input/Input';
@@ -9,6 +9,9 @@ import { Button } from 'components/common/Button/Button';
 import { InputLabel } from 'components/common/Input/InputLabel.styled';
 import { FormError } from 'components/common/Input/Input.styled';
 import { StyledForm } from 'components/common/Form/Form.styled';
+import { clusterTopicsPath } from 'lib/paths';
+import { useNavigate } from 'react-router-dom';
+import useAppParams from 'lib/hooks/useAppParams';
 
 import CustomParams from './CustomParams/CustomParams';
 import TimeToRetain from './TimeToRetain';
@@ -54,7 +57,10 @@ const TopicForm: React.FC<Props> = ({
   const {
     control,
     formState: { errors, isDirty, isValid },
+    reset,
   } = useFormContext();
+  const navigate = useNavigate();
+  const { clusterName } = useAppParams<{ clusterName: ClusterName }>();
   const getCleanUpPolicy =
     CleanupPolicyOptions.find((option: SelectOption) => {
       return (
@@ -68,6 +74,11 @@ const TopicForm: React.FC<Props> = ({
       return option.value === retentionBytes;
     })?.value || RetentionBytesOptions[0].value;
 
+  const onCancel = () => {
+    reset();
+    navigate(clusterTopicsPath(clusterName));
+  };
+
   return (
     <StyledForm onSubmit={onSubmit} aria-label="topic form">
       <fieldset disabled={isSubmitting}>
@@ -228,7 +239,12 @@ const TopicForm: React.FC<Props> = ({
           >
             {isEditing ? 'Save' : 'Create topic'}
           </Button>
-          <Button type="button" buttonType="primary" buttonSize="L">
+          <Button
+            type="button"
+            buttonType="primary"
+            buttonSize="L"
+            onClick={onCancel}
+          >
             Cancel
           </Button>
         </S.ButtonWrapper>

+ 1 - 0
kafka-ui-react-app/src/components/common/Alert/Alert.styled.ts

@@ -4,6 +4,7 @@ import styled from 'styled-components';
 export const Alert = styled.div<{ $type: ToastType }>`
   background-color: ${({ $type, theme }) => theme.alert.color[$type]};
   min-width: 400px;
+  max-width: 600px;
   min-height: 64px;
   border-radius: 8px;
   padding: 12px;

+ 52 - 47
kafka-ui-react-app/src/components/common/ConfirmationModal/ConfirmationModal.styled.tsx

@@ -1,59 +1,64 @@
 import styled, { css } from 'styled-components';
 
-export const ConfirmationModalWrapper = styled.div.attrs({ role: 'dialog' })(
-  ({ theme }) => css`
-    display: flex;
-    align-items: center;
-    flex-direction: column;
-    justify-content: center;
-    overflow: hidden;
-    position: fixed;
-    z-index: 40;
+export const Wrapper = styled.div.attrs({ role: 'dialog' })`
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+  justify-content: center;
+  overflow: hidden;
+  position: fixed;
+  z-index: 40;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  top: 0;
+`;
+
+export const Overlay = styled.div(
+  ({ theme: { modal } }) => css`
+    background-color: ${modal.overlay};
     bottom: 0;
     left: 0;
+    position: absolute;
     right: 0;
     top: 0;
-    & div:first-child {
-      background-color: ${theme.modal.overlay};
-      bottom: 0;
-      left: 0;
-      position: absolute;
-      right: 0;
-      top: 0;
-    }
-
-    & div:last-child {
-      position: absolute;
-      display: flex;
-      flex-direction: column;
-      width: 560px;
-      border-radius: 8px;
-
-      background-color: ${theme.modal.backgroundColor};
-      filter: drop-shadow(0px 4px 16px ${theme.modal.shadow});
+  `
+);
 
-      & > * {
-        padding: 16px;
-        width: 100%;
-      }
+export const Modal = styled.div(
+  ({ theme: { modal } }) => css`
+    position: absolute;
+    display: flex;
+    flex-direction: column;
+    width: 560px;
+    border-radius: 8px;
 
-      & > header {
-        height: 64px;
-        font-size: 20px;
-        text-align: start;
-      }
+    background-color: ${modal.backgroundColor};
+    filter: drop-shadow(0px 4px 16px ${modal.shadow});
+  `
+);
 
-      & > section {
-        border-top: 1px solid ${theme.modal.border.top};
-        border-bottom: 1px solid ${theme.modal.border.bottom};
-      }
+export const Header = styled.div`
+  font-size: 20px;
+  text-align: start;
+  padding: 16px;
+  width: 100%;
+`;
 
-      & > footer {
-        height: 64px;
-        display: flex;
-        justify-content: flex-end;
-        gap: 10px;
-      }
-    }
+export const Content = styled.div(
+  ({ theme: { modal } }) => css`
+    padding: 16px;
+    width: 100%;
+    border-top: 1px solid ${modal.border.top};
+    border-bottom: 1px solid ${modal.border.bottom};
   `
 );
+
+export const Footer = styled.div`
+  height: 64px;
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+  padding: 16px;
+  width: 100%;
+`;

+ 21 - 43
kafka-ui-react-app/src/components/common/ConfirmationModal/ConfirmationModal.tsx

@@ -1,64 +1,42 @@
-import React, { PropsWithChildren } from 'react';
+import React from 'react';
 import { Button } from 'components/common/Button/Button';
+import { ConfirmContext } from 'components/contexts/ConfirmContext';
 
-import { ConfirmationModalWrapper } from './ConfirmationModal.styled';
+import * as S from './ConfirmationModal.styled';
 
-export interface ConfirmationModalProps {
-  isOpen?: boolean;
-  title?: React.ReactNode;
-  onConfirm(): void;
-  onCancel(): void;
-  isConfirming?: boolean;
-  submitBtnText?: string;
-}
+const ConfirmationModal: React.FC = () => {
+  const context = React.useContext(ConfirmContext);
+  const isOpen = context?.content && context?.confirm;
 
-const ConfirmationModal: React.FC<
-  PropsWithChildren<ConfirmationModalProps>
-> = ({
-  isOpen,
-  children,
-  title = 'Confirm the action',
-  onCancel,
-  onConfirm,
-  isConfirming = false,
-  submitBtnText = 'Submit',
-}) => {
-  const cancelHandler = () => {
-    if (!isConfirming) onCancel();
-  };
+  if (!isOpen) return null;
 
-  return isOpen ? (
-    <ConfirmationModalWrapper>
-      <div onClick={cancelHandler} aria-hidden="true" role="button" />
-      <div>
-        <header>
-          <p>{title}</p>
-        </header>
-        <section>{children}</section>
-        <footer>
+  return (
+    <S.Wrapper role="dialog" aria-label="Confirmation Dialog">
+      <S.Overlay onClick={context.cancel} aria-hidden="true" role="button" />
+      <S.Modal>
+        <S.Header>Confirm the action</S.Header>
+        <S.Content>{context.content}</S.Content>
+        <S.Footer>
           <Button
             buttonType="secondary"
             buttonSize="M"
-            onClick={cancelHandler}
+            onClick={context.cancel}
             type="button"
-            disabled={isConfirming}
           >
             Cancel
           </Button>
-
           <Button
             buttonType="primary"
             buttonSize="M"
-            onClick={onConfirm}
+            onClick={context.confirm}
             type="button"
-            disabled={isConfirming}
           >
-            {submitBtnText}
+            Confirm
           </Button>
-        </footer>
-      </div>
-    </ConfirmationModalWrapper>
-  ) : null;
+        </S.Footer>
+      </S.Modal>
+    </S.Wrapper>
+  );
 };
 
 export default ConfirmationModal;

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

@@ -1,101 +0,0 @@
-import React from 'react';
-import ConfirmationModal, {
-  ConfirmationModalProps,
-} from 'components/common/ConfirmationModal/ConfirmationModal';
-import { screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { render } from 'lib/testHelpers';
-
-const confirmMock = jest.fn();
-const cancelMock = jest.fn();
-const body = 'Please Confirm the action!';
-
-describe('ConfirmationModal', () => {
-  const setupWrapper = (props: Partial<ConfirmationModalProps> = {}) => (
-    <ConfirmationModal onCancel={cancelMock} onConfirm={confirmMock} {...props}>
-      {body}
-    </ConfirmationModal>
-  );
-
-  it('renders nothing', () => {
-    render(setupWrapper({ isOpen: false }));
-    expect(screen.queryByText(body)).not.toBeInTheDocument();
-  });
-
-  it('renders modal', () => {
-    render(setupWrapper({ isOpen: true }));
-    expect(screen.getByRole('dialog')).toHaveTextContent(body);
-    expect(screen.getAllByRole('button').length).toEqual(2);
-  });
-  it('renders modal with default header', () => {
-    render(setupWrapper({ isOpen: true }));
-    expect(screen.getByText('Confirm the action')).toBeInTheDocument();
-  });
-  it('renders modal with custom header', () => {
-    const title = 'My Custom Header';
-    render(setupWrapper({ isOpen: true, title }));
-    expect(screen.getByText(title)).toBeInTheDocument();
-  });
-
-  it('Check the text on the submit button default behavior', () => {
-    render(setupWrapper({ isOpen: true }));
-    expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
-  });
-
-  it('handles onConfirm when user clicks confirm button', () => {
-    render(setupWrapper({ isOpen: true }));
-    const confirmBtn = screen.getByRole('button', { name: 'Submit' });
-    userEvent.click(confirmBtn);
-    expect(cancelMock).toHaveBeenCalledTimes(0);
-    expect(confirmMock).toHaveBeenCalledTimes(1);
-  });
-
-  it('Check the text on the submit button', () => {
-    const submitBtnText = 'Submit btn Text';
-    render(setupWrapper({ isOpen: true, submitBtnText }));
-    expect(
-      screen.getByRole('button', { name: submitBtnText })
-    ).toBeInTheDocument();
-  });
-
-  describe('cancellation', () => {
-    describe('when not confirming', () => {
-      beforeEach(() => {
-        render(setupWrapper({ isOpen: true }));
-      });
-
-      it('handles onCancel when user clicks on modal-background', () => {
-        const { container } = render(setupWrapper({ isOpen: true }));
-        userEvent.click(container.children[0].children[0]);
-
-        expect(cancelMock).toHaveBeenCalledTimes(1);
-        expect(confirmMock).toHaveBeenCalledTimes(0);
-      });
-      it('handles onCancel when user clicks on Cancel button', () => {
-        const cancelBtn = screen.getByRole('button', { name: 'Cancel' });
-
-        userEvent.click(cancelBtn);
-        expect(cancelMock).toHaveBeenCalledTimes(1);
-        expect(confirmMock).toHaveBeenCalledTimes(0);
-      });
-    });
-
-    describe('when confirming', () => {
-      beforeEach(() => {
-        render(setupWrapper({ isOpen: true, isConfirming: true }));
-      });
-      it('does not call onCancel when user clicks on modal-background', () => {
-        userEvent.click(screen.getByRole('dialog'));
-        expect(cancelMock).toHaveBeenCalledTimes(0);
-        expect(confirmMock).toHaveBeenCalledTimes(0);
-      });
-
-      it('does not call onCancel when user clicks on Cancel button', () => {
-        const cancelBtn = screen.getByRole('button', { name: 'Cancel' });
-        userEvent.click(cancelBtn);
-        expect(cancelMock).toHaveBeenCalledTimes(0);
-        expect(confirmMock).toHaveBeenCalledTimes(0);
-      });
-    });
-  });
-});

+ 1 - 0
kafka-ui-react-app/src/components/common/Dropdown/Dropdown.styled.ts

@@ -60,6 +60,7 @@ export const DropdownButton = styled.button`
   border: none;
   display: flex;
   cursor: pointer;
+  align-self: center;
 `;
 
 export const DangerItem = styled.div`

+ 11 - 1
kafka-ui-react-app/src/components/common/Dropdown/DropdownItem.tsx

@@ -1,26 +1,36 @@
 import React, { PropsWithChildren } from 'react';
 import { ClickEvent, MenuItem, MenuItemProps } from '@szhsin/react-menu';
+import { useConfirm } from 'lib/hooks/useConfirm';
 
 import * as S from './Dropdown.styled';
 
 interface DropdownItemProps extends PropsWithChildren<MenuItemProps> {
   danger?: boolean;
   onClick?(): void;
+  confirm?: React.ReactNode;
 }
 
 const DropdownItem: React.FC<DropdownItemProps> = ({
   onClick,
   danger,
   children,
+  confirm,
   ...rest
 }) => {
+  const confirmation = useConfirm();
+
   const handleClick = (e: ClickEvent) => {
     if (!onClick) return;
 
     // eslint-disable-next-line no-param-reassign
     e.stopPropagation = true;
     e.syntheticEvent.stopPropagation();
-    onClick();
+
+    if (confirm) {
+      confirmation(confirm, onClick);
+    } else {
+      onClick();
+    }
   };
 
   return (

+ 0 - 61
kafka-ui-react-app/src/components/common/MultiSelect/__test__/MultiSelect.styled.spec.tsx

@@ -1,61 +0,0 @@
-import React from 'react';
-import { render } from 'lib/testHelpers';
-import MultiSelect from 'components/common/MultiSelect/MultiSelect.styled';
-import { ISelectProps } from 'react-multi-select-component/dist/lib/interfaces';
-
-const Option1 = { value: 1, label: 'option 1' };
-const Option2 = { value: 2, label: 'option 2' };
-
-interface IMultiSelectProps extends ISelectProps {
-  minWidth?: string;
-}
-
-const DefaultProps: IMultiSelectProps = {
-  options: [Option1, Option2],
-  labelledBy: 'multi-select',
-  value: [Option1, Option2],
-};
-
-describe('MultiSelect.Styled', () => {
-  const setUpComponent = (props: IMultiSelectProps = DefaultProps) => {
-    const { container } = render(<MultiSelect {...props} />);
-    const multiSelect = container.firstChild;
-    const dropdownContainer = multiSelect?.firstChild?.firstChild;
-
-    return { container, multiSelect, dropdownContainer };
-  };
-
-  it('should have 200px minWidth by default', () => {
-    const { container } = setUpComponent();
-    const multiSelect = container.firstChild;
-
-    expect(multiSelect).toHaveStyle('min-width: 200px');
-  });
-
-  it('should have the provided minWidth in styles', () => {
-    const minWidth = '400px';
-    const { container } = setUpComponent({ ...DefaultProps, minWidth });
-    const multiSelect = container.firstChild;
-
-    expect(multiSelect).toHaveStyle(`min-width: ${minWidth}`);
-  });
-
-  describe('when not disabled', () => {
-    it('should have cursor pointer', () => {
-      const { dropdownContainer } = setUpComponent();
-
-      expect(dropdownContainer).toHaveStyle(`cursor: pointer`);
-    });
-  });
-
-  describe('when disabled', () => {
-    it('should have cursor not-allowed', () => {
-      const { dropdownContainer } = setUpComponent({
-        ...DefaultProps,
-        disabled: true,
-      });
-
-      expect(dropdownContainer).toHaveStyle(`cursor: not-allowed`);
-    });
-  });
-});

+ 39 - 0
kafka-ui-react-app/src/components/contexts/ConfirmContext.tsx

@@ -0,0 +1,39 @@
+import React, { useState } from 'react';
+
+type ConfirmContextType = {
+  content: React.ReactNode;
+  confirm?: () => void;
+  setContent: React.Dispatch<React.SetStateAction<React.ReactNode>>;
+  setConfirm: React.Dispatch<React.SetStateAction<(() => void) | undefined>>;
+  cancel: () => void;
+};
+
+export const ConfirmContext = React.createContext<ConfirmContextType | null>(
+  null
+);
+
+export const ConfirmContextProvider: React.FC<
+  React.PropsWithChildren<unknown>
+> = ({ children }) => {
+  const [content, setContent] = useState<React.ReactNode>(null);
+  const [confirm, setConfirm] = useState<(() => void) | undefined>(undefined);
+
+  const cancel = () => {
+    setContent(null);
+    setConfirm(undefined);
+  };
+
+  return (
+    <ConfirmContext.Provider
+      value={{
+        content,
+        setContent,
+        confirm,
+        setConfirm,
+        cancel,
+      }}
+    >
+      {children}
+    </ConfirmContext.Provider>
+  );
+};

+ 1 - 1
kafka-ui-react-app/src/lib/hooks/__tests__/useDataSaver.spec.tsx

@@ -1,6 +1,6 @@
 import React, { useEffect } from 'react';
-import { render } from 'lib/testHelpers';
 import useDataSaver from 'lib/hooks/useDataSaver';
+import { render } from '@testing-library/react';
 
 describe('useDataSaver hook', () => {
   const content = {

+ 14 - 0
kafka-ui-react-app/src/lib/hooks/useConfirm.ts

@@ -0,0 +1,14 @@
+import { ConfirmContext } from 'components/contexts/ConfirmContext';
+import React, { useContext } from 'react';
+
+export const useConfirm = () => {
+  const context = useContext(ConfirmContext);
+
+  return (message: React.ReactNode, callback: () => void | Promise<void>) => {
+    context?.setContent(message);
+    context?.setConfirm(() => async () => {
+      await callback();
+      context?.cancel();
+    });
+  };
+};

+ 14 - 7
kafka-ui-react-app/src/lib/testHelpers.tsx

@@ -18,6 +18,8 @@ import {
   QueryClientProvider,
   UseQueryResult,
 } from '@tanstack/react-query';
+import { ConfirmContextProvider } from 'components/contexts/ConfirmContext';
+import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 
 interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
   preloadedState?: Partial<RootState>;
@@ -67,13 +69,18 @@ const customRender = (
     children,
   }) => (
     <ThemeProvider theme={theme}>
-      <Provider store={store}>
-        <TestQueryClientProvider>
-          <MemoryRouter initialEntries={initialEntries}>
-            {children}
-          </MemoryRouter>
-        </TestQueryClientProvider>
-      </Provider>
+      <ConfirmContextProvider>
+        <Provider store={store}>
+          <TestQueryClientProvider>
+            <MemoryRouter initialEntries={initialEntries}>
+              <div>
+                {children}
+                <ConfirmationModal />
+              </div>
+            </MemoryRouter>
+          </TestQueryClientProvider>
+        </Provider>
+      </ConfirmContextProvider>
     </ThemeProvider>
   );
   return render(ui, { wrapper: AllTheProviders, ...renderOptions });

+ 1 - 1
kafka-ui-react-app/src/redux/reducers/topicMessages/topicMessagesSlice.ts

@@ -27,7 +27,7 @@ export const clearTopicMessages = createAsyncThunk<
       dispatch(fetchTopicDetails({ clusterName, topicName }));
       showSuccessAlert({
         id: `message-${topicName}-${clusterName}-${partitions}`,
-        message: 'Messages successfully cleared!',
+        message: `${topicName} messages have been successfully cleared!`,
       });
 
       return undefined;

+ 0 - 6
kafka-ui-react-app/src/redux/reducers/topics/__test__/selectors.spec.ts

@@ -20,12 +20,6 @@ describe('Topics selectors', () => {
       expect(selectors.getTopicMessageSchemaFetched(store.getState())).toEqual(
         false
       );
-      expect(
-        selectors.getTopicPartitionsCountIncreased(store.getState())
-      ).toEqual(false);
-      expect(
-        selectors.getTopicReplicationFactorUpdated(store.getState())
-      ).toEqual(false);
       expect(
         selectors.getTopicsConsumerGroupsFetched(store.getState())
       ).toEqual(false);

+ 0 - 20
kafka-ui-react-app/src/redux/reducers/topics/selectors.ts

@@ -11,8 +11,6 @@ import {
   fetchTopicConsumerGroups,
   createTopic,
   deleteTopic,
-  updateTopicPartitionsCount,
-  updateTopicReplicationFactor,
 } from 'redux/reducers/topics/topicsSlice';
 import { AsyncRequestStatus } from 'lib/constants';
 
@@ -90,24 +88,6 @@ export const getTopicMessageSchemaFetched = createSelector(
   (status) => status === AsyncRequestStatus.fulfilled
 );
 
-const getPartitionsCountIncreaseStatus = createFetchingSelector(
-  updateTopicPartitionsCount.typePrefix
-);
-
-export const getTopicPartitionsCountIncreased = createSelector(
-  getPartitionsCountIncreaseStatus,
-  (status) => status === AsyncRequestStatus.fulfilled
-);
-
-const getReplicationFactorUpdateStatus = createFetchingSelector(
-  updateTopicReplicationFactor.typePrefix
-);
-
-export const getTopicReplicationFactorUpdated = createSelector(
-  getReplicationFactorUpdateStatus,
-  (status) => status === AsyncRequestStatus.fulfilled
-);
-
 const getTopicConsumerGroupsStatus = createFetchingSelector(
   fetchTopicConsumerGroups.typePrefix
 );

+ 22 - 16
kafka-ui-react-app/src/redux/reducers/topics/topicsSlice.ts

@@ -274,7 +274,7 @@ export const updateTopicPartitionsCount = createAsyncThunk<
       });
       showSuccessAlert({
         id: `message-${topicName}-${clusterName}-${partitions}`,
-        message: 'Number of partitions successfully increased!',
+        message: 'Number of partitions successfully increased',
       });
       dispatch(fetchTopicDetails({ clusterName, topicName }));
       return undefined;
@@ -294,7 +294,7 @@ export const updateTopicReplicationFactor = createAsyncThunk<
   }
 >(
   'topic/updateTopicReplicationFactor',
-  async (payload, { rejectWithValue }) => {
+  async (payload, { rejectWithValue, dispatch }) => {
     try {
       const { clusterName, topicName, replicationFactor } = payload;
 
@@ -303,7 +303,11 @@ export const updateTopicReplicationFactor = createAsyncThunk<
         topicName,
         replicationFactorChange: { totalReplicationFactor: replicationFactor },
       });
-
+      showSuccessAlert({
+        id: `message-${topicName}-${clusterName}-replicationFactor`,
+        message: 'Replication Factor successfully updated',
+      });
+      dispatch(fetchTopicDetails({ clusterName, topicName }));
       return undefined;
     } catch (err) {
       showServerError(err as Response);
@@ -340,22 +344,24 @@ export const clearTopicsMessages = createAsyncThunk<
     clusterName: ClusterName;
     topicNames: TopicName[];
   }
->('topic/clearTopicsMessages', async (payload, { rejectWithValue }) => {
-  try {
-    const { clusterName, topicNames } = payload;
-
-    topicNames.forEach((topicName) => {
-      clearTopicMessages({ clusterName, topicName });
-    });
+>(
+  'topic/clearTopicsMessages',
+  async (payload, { rejectWithValue, dispatch }) => {
+    try {
+      const { clusterName, topicNames } = payload;
+      topicNames.forEach((topicName) => {
+        dispatch(clearTopicMessages({ clusterName, topicName }));
+      });
 
-    return undefined;
-  } catch (err) {
-    showServerError(err as Response);
-    return rejectWithValue(await getResponse(err as Response));
+      return undefined;
+    } catch (err) {
+      showServerError(err as Response);
+      return rejectWithValue(await getResponse(err as Response));
+    }
   }
-});
+);
 
-export const initialState: TopicsState = {
+const initialState: TopicsState = {
   byName: {},
   allNames: [],
   totalPages: 1,