Kaynağa Gözat

Refactoring Topics reducer into redux toolkit (#1939)

* WIP: creating topicSlice

* reformating reducer into toolkit slice and fixed tests

* removing unnecessary lines

* removing dismiss alert action and tests for actions

* removing unnecessary code

* adding tests for selectors

* adding test case for new topics

* adding test cases for topic reducer when fulfilled

* removing unnecessary code

* adding test cases for asyncthunks to increase coverage

* adding new test cases to cover topicSlice

* adding clear messsages fulfilled test case

* removing unnecessary code
Robert Azizbekyan 3 yıl önce
ebeveyn
işleme
6d8c6cace0
48 değiştirilmiş dosya ile 1500 ekleme ve 1580 silme
  1. 0 14
      kafka-ui-react-app/package-lock.json
  2. 0 1
      kafka-ui-react-app/package.json
  3. 0 19
      kafka-ui-react-app/src/components/Alerts/Alerts.tsx
  4. 3 15
      kafka-ui-react-app/src/components/Alerts/__tests__/Alerts.spec.tsx
  5. 18 10
      kafka-ui-react-app/src/components/Topics/List/List.tsx
  6. 7 7
      kafka-ui-react-app/src/components/Topics/List/ListContainer.ts
  7. 4 4
      kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx
  8. 7 24
      kafka-ui-react-app/src/components/Topics/New/New.tsx
  9. 52 24
      kafka-ui-react-app/src/components/Topics/New/__test__/New.spec.tsx
  10. 5 5
      kafka-ui-react-app/src/components/Topics/Topic/Details/ConsumerGroups/TopicConsumerGroups.tsx
  11. 1 1
      kafka-ui-react-app/src/components/Topics/Topic/Details/ConsumerGroups/TopicConsumerGroupsContainer.ts
  12. 10 6
      kafka-ui-react-app/src/components/Topics/Topic/Details/Details.tsx
  13. 1 1
      kafka-ui-react-app/src/components/Topics/Topic/Details/DetailsContainer.ts
  14. 5 2
      kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/Settings.tsx
  15. 1 1
      kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/SettingsContainer.ts
  16. 4 4
      kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/Details.spec.tsx
  17. 18 16
      kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZone.tsx
  18. 1 1
      kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZoneContainer.ts
  19. 16 13
      kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx
  20. 4 1
      kafka-ui-react-app/src/components/Topics/Topic/Edit/EditContainer.tsx
  21. 3 2
      kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx
  22. 11 7
      kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/SendMessage.spec.tsx
  23. 5 2
      kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx
  24. 1 1
      kafka-ui-react-app/src/components/Topics/Topic/TopicContainer.tsx
  25. 0 168
      kafka-ui-react-app/src/redux/actions/__test__/actions.spec.ts
  26. 0 295
      kafka-ui-react-app/src/redux/actions/__test__/thunks/topics.spec.ts
  27. 0 94
      kafka-ui-react-app/src/redux/actions/actions.ts
  28. 0 2
      kafka-ui-react-app/src/redux/actions/index.ts
  29. 0 1
      kafka-ui-react-app/src/redux/actions/thunks/index.ts
  30. 0 370
      kafka-ui-react-app/src/redux/actions/thunks/topics.ts
  31. 0 7
      kafka-ui-react-app/src/redux/interfaces/alerts.ts
  32. 0 16
      kafka-ui-react-app/src/redux/interfaces/index.ts
  33. 1 0
      kafka-ui-react-app/src/redux/interfaces/topic.ts
  34. 0 10
      kafka-ui-react-app/src/redux/reducers/alerts/__test__/fixtures.ts
  35. 0 104
      kafka-ui-react-app/src/redux/reducers/alerts/__test__/reducer.spec.ts
  36. 0 25
      kafka-ui-react-app/src/redux/reducers/alerts/__test__/selectors.spec.ts
  37. 0 22
      kafka-ui-react-app/src/redux/reducers/alerts/reducer.ts
  38. 0 9
      kafka-ui-react-app/src/redux/reducers/alerts/selectors.ts
  39. 0 37
      kafka-ui-react-app/src/redux/reducers/alerts/utils.ts
  40. 4 9
      kafka-ui-react-app/src/redux/reducers/index.ts
  41. 0 46
      kafka-ui-react-app/src/redux/reducers/loader/reducer.ts
  42. 0 4
      kafka-ui-react-app/src/redux/reducers/loader/selectors.ts
  43. 800 67
      kafka-ui-react-app/src/redux/reducers/topics/__test__/reducer.spec.ts
  44. 35 0
      kafka-ui-react-app/src/redux/reducers/topics/__test__/selectors.spec.ts
  45. 0 73
      kafka-ui-react-app/src/redux/reducers/topics/reducer.ts
  46. 61 37
      kafka-ui-react-app/src/redux/reducers/topics/selectors.ts
  47. 419 0
      kafka-ui-react-app/src/redux/reducers/topics/topicsSlice.ts
  48. 3 3
      kafka-ui-react-app/src/redux/store/configureStore/mockStoreCreator.ts

+ 0 - 14
kafka-ui-react-app/package-lock.json

@@ -41,7 +41,6 @@
         "redux-thunk": "^2.3.0",
         "redux-thunk": "^2.3.0",
         "sass": "^1.43.4",
         "sass": "^1.43.4",
         "styled-components": "^5.3.1",
         "styled-components": "^5.3.1",
-        "typesafe-actions": "^5.1.0",
         "use-debounce": "^7.0.0",
         "use-debounce": "^7.0.0",
         "uuid": "^8.3.1",
         "uuid": "^8.3.1",
         "yup": "^0.32.9"
         "yup": "^0.32.9"
@@ -27842,14 +27841,6 @@
         "is-typedarray": "^1.0.0"
         "is-typedarray": "^1.0.0"
       }
       }
     },
     },
-    "node_modules/typesafe-actions": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/typesafe-actions/-/typesafe-actions-5.1.0.tgz",
-      "integrity": "sha512-bna6Yi1pRznoo6Bz1cE6btB/Yy8Xywytyfrzu/wc+NFW3ZF0I+2iCGImhBsoYYCOWuICtRO4yHcnDlzgo1AdNg==",
-      "engines": {
-        "node": ">= 4"
-      }
-    },
     "node_modules/typescript": {
     "node_modules/typescript": {
       "version": "4.3.5",
       "version": "4.3.5",
       "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
       "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
@@ -49539,11 +49530,6 @@
         "is-typedarray": "^1.0.0"
         "is-typedarray": "^1.0.0"
       }
       }
     },
     },
-    "typesafe-actions": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/typesafe-actions/-/typesafe-actions-5.1.0.tgz",
-      "integrity": "sha512-bna6Yi1pRznoo6Bz1cE6btB/Yy8Xywytyfrzu/wc+NFW3ZF0I+2iCGImhBsoYYCOWuICtRO4yHcnDlzgo1AdNg=="
-    },
     "typescript": {
     "typescript": {
       "version": "4.3.5",
       "version": "4.3.5",
       "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
       "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",

+ 0 - 1
kafka-ui-react-app/package.json

@@ -37,7 +37,6 @@
     "redux-thunk": "^2.3.0",
     "redux-thunk": "^2.3.0",
     "sass": "^1.43.4",
     "sass": "^1.43.4",
     "styled-components": "^5.3.1",
     "styled-components": "^5.3.1",
-    "typesafe-actions": "^5.1.0",
     "use-debounce": "^7.0.0",
     "use-debounce": "^7.0.0",
     "uuid": "^8.3.1",
     "uuid": "^8.3.1",
     "yup": "^0.32.9"
     "yup": "^0.32.9"

+ 0 - 19
kafka-ui-react-app/src/components/Alerts/Alerts.tsx

@@ -1,6 +1,4 @@
 import React from 'react';
 import React from 'react';
-import { dismissAlert } from 'redux/actions';
-import { getAlerts } from 'redux/reducers/alerts/selectors';
 import { alertDissmissed, selectAll } from 'redux/reducers/alerts/alertsSlice';
 import { alertDissmissed, selectAll } from 'redux/reducers/alerts/alertsSlice';
 import { useAppSelector, useAppDispatch } from 'lib/hooks/redux';
 import { useAppSelector, useAppDispatch } from 'lib/hooks/redux';
 import Alert from 'components/Alerts/Alert';
 import Alert from 'components/Alerts/Alert';
@@ -15,14 +13,6 @@ const Alerts: React.FC = () => {
     [dispatch]
     [dispatch]
   );
   );
 
 
-  const legacyAlerts = useAppSelector(getAlerts);
-  const dismissLegacy = React.useCallback(
-    (id: string) => {
-      dispatch(dismissAlert(id));
-    },
-    [dispatch]
-  );
-
   return (
   return (
     <>
     <>
       {alerts.map(({ id, type, title, message }) => (
       {alerts.map(({ id, type, title, message }) => (
@@ -34,15 +24,6 @@ const Alerts: React.FC = () => {
           onDissmiss={() => dismiss(id)}
           onDissmiss={() => dismiss(id)}
         />
         />
       ))}
       ))}
-      {legacyAlerts.map(({ id, type, title, message }) => (
-        <Alert
-          key={id}
-          type={type}
-          title={title}
-          message={message}
-          onDissmiss={() => dismissLegacy(id)}
-        />
-      ))}
     </>
     </>
   );
   );
 };
 };

+ 3 - 15
kafka-ui-react-app/src/components/Alerts/__tests__/Alerts.spec.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
 import React from 'react';
-import { Action, FailurePayload, ServerResponse } from 'redux/interfaces';
+import { ServerResponse } from 'redux/interfaces';
 import { act, screen } from '@testing-library/react';
 import { act, screen } from '@testing-library/react';
 import Alerts from 'components/Alerts/Alerts';
 import Alerts from 'components/Alerts/Alerts';
 import { render } from 'lib/testHelpers';
 import { render } from 'lib/testHelpers';
@@ -26,35 +26,23 @@ const action: UnknownAsyncThunkRejectedWithValueAction = {
   },
   },
   error: { message: 'Rejected' },
   error: { message: 'Rejected' },
 };
 };
-const alert: FailurePayload = {
-  title: '404 - Not Found',
-  message: 'Item is not found',
-  subject: 'subject',
-};
-const legacyAction: Action = {
-  type: 'CLEAR_TOPIC_MESSAGES__FAILURE',
-  payload: { alert },
-};
 
 
 describe('Alerts', () => {
 describe('Alerts', () => {
   it('renders alerts', async () => {
   it('renders alerts', async () => {
     store.dispatch(action);
     store.dispatch(action);
-    store.dispatch(legacyAction);
 
 
     await act(() => {
     await act(() => {
       render(<Alerts />, { store });
       render(<Alerts />, { store });
     });
     });
 
 
-    expect(screen.getAllByRole('alert').length).toEqual(2);
+    expect(screen.getAllByRole('alert').length).toEqual(1);
 
 
     const dissmissAlertButtons = screen.getAllByRole('button');
     const dissmissAlertButtons = screen.getAllByRole('button');
-    expect(dissmissAlertButtons.length).toEqual(2);
+    expect(dissmissAlertButtons.length).toEqual(1);
 
 
     const dissmissButton = dissmissAlertButtons[0];
     const dissmissButton = dissmissAlertButtons[0];
-    const dissmissLegacyButton = dissmissAlertButtons[1];
 
 
     userEvent.click(dissmissButton);
     userEvent.click(dissmissButton);
-    userEvent.click(dissmissLegacyButton);
 
 
     expect(screen.queryAllByRole('alert').length).toEqual(0);
     expect(screen.queryAllByRole('alert').length).toEqual(0);
   });
   });

+ 18 - 10
kafka-ui-react-app/src/components/Topics/List/List.tsx

@@ -13,7 +13,9 @@ import PageLoader from 'components/common/PageLoader/PageLoader';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import {
 import {
   CleanUpPolicy,
   CleanUpPolicy,
+  DeleteTopicRequest,
   GetTopicsRequest,
   GetTopicsRequest,
+  RecreateTopicRequest,
   SortOrder,
   SortOrder,
   TopicColumnsToSort,
   TopicColumnsToSort,
 } from 'generated-sources';
 } from 'generated-sources';
@@ -45,12 +47,18 @@ export interface TopicsListProps {
   areTopicsFetching: boolean;
   areTopicsFetching: boolean;
   topics: TopicWithDetailedInfo[];
   topics: TopicWithDetailedInfo[];
   totalPages: number;
   totalPages: number;
-  fetchTopicsList(props: GetTopicsRequest): void;
-  deleteTopic(topicName: TopicName, clusterName: ClusterName): void;
-  deleteTopics(topicName: TopicName, clusterNames: ClusterName[]): void;
-  recreateTopic(topicName: TopicName, clusterName: ClusterName): void;
-  clearTopicsMessages(topicName: TopicName, clusterNames: ClusterName[]): void;
-  clearTopicMessages(params: {
+  fetchTopicsList(payload: GetTopicsRequest): void;
+  deleteTopic(payload: DeleteTopicRequest): void;
+  deleteTopics(payload: {
+    clusterName: ClusterName;
+    topicNames: TopicName[];
+  }): void;
+  recreateTopic(payload: RecreateTopicRequest): void;
+  clearTopicsMessages(payload: {
+    clusterName: ClusterName;
+    topicNames: TopicName[];
+  }): void;
+  clearTopicMessages(payload: {
     topicName: TopicName;
     topicName: TopicName;
     clusterName: ClusterName;
     clusterName: ClusterName;
     partitions?: number[];
     partitions?: number[];
@@ -180,9 +188,9 @@ const List: React.FC<TopicsListProps> = ({
   const deleteOrPurgeConfirmationHandler = React.useCallback(() => {
   const deleteOrPurgeConfirmationHandler = React.useCallback(() => {
     const selectedIds = Array.from(tableState.selectedIds);
     const selectedIds = Array.from(tableState.selectedIds);
     if (confirmationModal === 'deleteTopics') {
     if (confirmationModal === 'deleteTopics') {
-      deleteTopics(clusterName, selectedIds);
+      deleteTopics({ clusterName, topicNames: selectedIds });
     } else {
     } else {
-      clearTopicsMessages(clusterName, selectedIds);
+      clearTopicsMessages({ clusterName, topicNames: selectedIds });
     }
     }
     closeConfirmationModal();
     closeConfirmationModal();
     clearSelectedTopics();
     clearSelectedTopics();
@@ -221,7 +229,7 @@ const List: React.FC<TopicsListProps> = ({
       const isHidden = internal || isReadOnly || !hovered;
       const isHidden = internal || isReadOnly || !hovered;
 
 
       const deleteTopicHandler = React.useCallback(() => {
       const deleteTopicHandler = React.useCallback(() => {
-        deleteTopic(clusterName, name);
+        deleteTopic({ clusterName, topicName: name });
       }, [name]);
       }, [name]);
 
 
       const clearTopicMessagesHandler = React.useCallback(() => {
       const clearTopicMessagesHandler = React.useCallback(() => {
@@ -231,7 +239,7 @@ const List: React.FC<TopicsListProps> = ({
       }, [name, fetchTopicsList, topicsListParams]);
       }, [name, fetchTopicsList, topicsListParams]);
 
 
       const recreateTopicHandler = React.useCallback(() => {
       const recreateTopicHandler = React.useCallback(() => {
-        recreateTopic(clusterName, name);
+        recreateTopic({ clusterName, topicName: name });
         closeRecreateTopicModal();
         closeRecreateTopicModal();
       }, [name]);
       }, [name]);
 
 

+ 7 - 7
kafka-ui-react-app/src/components/Topics/List/ListContainer.ts

@@ -1,15 +1,15 @@
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
 import { RootState } from 'redux/interfaces';
 import { RootState } from 'redux/interfaces';
+import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
 import {
 import {
   fetchTopicsList,
   fetchTopicsList,
   deleteTopic,
   deleteTopic,
-  deleteTopics,
   recreateTopic,
   recreateTopic,
+  setTopicsSearch,
+  setTopicsOrderBy,
+  deleteTopics,
   clearTopicsMessages,
   clearTopicsMessages,
-  setTopicsSearchAction,
-  setTopicsOrderByAction,
-} from 'redux/actions';
-import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
+} from 'redux/reducers/topics/topicsSlice';
 import {
 import {
   getTopicList,
   getTopicList,
   getAreTopicsFetching,
   getAreTopicsFetching,
@@ -37,8 +37,8 @@ const mapDispatchToProps = {
   recreateTopic,
   recreateTopic,
   clearTopicsMessages,
   clearTopicsMessages,
   clearTopicMessages,
   clearTopicMessages,
-  setTopicsSearch: setTopicsSearchAction,
-  setTopicsOrderBy: setTopicsOrderByAction,
+  setTopicsSearch,
+  setTopicsOrderBy,
 };
 };
 
 
 export default connect(mapStateToProps, mapDispatchToProps)(List);
 export default connect(mapStateToProps, mapDispatchToProps)(List);

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

@@ -290,10 +290,10 @@ describe('List', () => {
       });
       });
 
 
       expect(mockFn).toBeCalledTimes(1);
       expect(mockFn).toBeCalledTimes(1);
-      expect(mockFn).toBeCalledWith('local', [
-        externalTopicPayload.name,
-        'external.topic2',
-      ]);
+      expect(mockFn).toBeCalledWith({
+        clusterName: 'local',
+        topicNames: [externalTopicPayload.name, 'external.topic2'],
+      });
     };
     };
 
 
     it('triggers the deleteTopics when clicked on the delete button', async () => {
     it('triggers the deleteTopics when clicked on the delete button', async () => {

+ 7 - 24
kafka-ui-react-app/src/components/Topics/New/New.tsx

@@ -1,19 +1,14 @@
 import React from 'react';
 import React from 'react';
-import { ClusterName, TopicFormData, FailurePayload } from 'redux/interfaces';
+import { ClusterName, TopicFormData } from 'redux/interfaces';
 import { useForm, FormProvider } from 'react-hook-form';
 import { useForm, FormProvider } from 'react-hook-form';
 import { clusterTopicPath } from 'lib/paths';
 import { clusterTopicPath } from 'lib/paths';
 import TopicForm from 'components/Topics/shared/Form/TopicForm';
 import TopicForm from 'components/Topics/shared/Form/TopicForm';
-import {
-  formatTopicCreation,
-  topicsApiClient,
-  createTopicAction,
-} from 'redux/actions';
-import { useDispatch } from 'react-redux';
-import { getResponse } from 'lib/errorHandling';
+import { createTopic } from 'redux/reducers/topics/topicsSlice';
 import { useHistory, useLocation, useParams } from 'react-router-dom';
 import { useHistory, useLocation, useParams } from 'react-router-dom';
 import { yupResolver } from '@hookform/resolvers/yup';
 import { yupResolver } from '@hookform/resolvers/yup';
 import { topicFormValidationSchema } from 'lib/yupExtended';
 import { topicFormValidationSchema } from 'lib/yupExtended';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import PageHeading from 'components/common/PageHeading/PageHeading';
+import { useAppDispatch } from 'lib/hooks/redux';
 
 
 interface RouterParams {
 interface RouterParams {
   clusterName: ClusterName;
   clusterName: ClusterName;
@@ -35,9 +30,8 @@ const New: React.FC = () => {
 
 
   const { clusterName } = useParams<RouterParams>();
   const { clusterName } = useParams<RouterParams>();
   const history = useHistory();
   const history = useHistory();
-  const dispatch = useDispatch();
-
   const { search } = useLocation();
   const { search } = useLocation();
+  const dispatch = useAppDispatch();
   const params = new URLSearchParams(search);
   const params = new URLSearchParams(search);
 
 
   const name = params.get(Filters.NAME) || '';
   const name = params.get(Filters.NAME) || '';
@@ -47,21 +41,10 @@ const New: React.FC = () => {
   const cleanUpPolicy = params.get(Filters.CLEANUP_POLICY) || 'Delete';
   const cleanUpPolicy = params.get(Filters.CLEANUP_POLICY) || 'Delete';
 
 
   const onSubmit = async (data: TopicFormData) => {
   const onSubmit = async (data: TopicFormData) => {
-    try {
-      await topicsApiClient.createTopic({
-        clusterName,
-        topicCreation: formatTopicCreation(data),
-      });
-      history.push(clusterTopicPath(clusterName, data.name));
-    } catch (error) {
-      const response = await getResponse(error as Response);
-      const alert: FailurePayload = {
-        subject: ['schema', data.name].join('-'),
-        title: `${response.message}`,
-        response,
-      };
+    const { meta } = await dispatch(createTopic({ clusterName, data }));
 
 
-      dispatch(createTopicAction.failure({ alert }));
+    if (meta.requestStatus === 'fulfilled') {
+      history.push(clusterTopicPath(clusterName, data.name));
     }
     }
   };
   };
 
 

+ 52 - 24
kafka-ui-react-app/src/components/Topics/New/__test__/New.spec.tsx

@@ -3,7 +3,7 @@ import New from 'components/Topics/New/New';
 import { Route, Router } from 'react-router-dom';
 import { Route, Router } from 'react-router-dom';
 import configureStore from 'redux-mock-store';
 import configureStore from 'redux-mock-store';
 import { RootState } from 'redux/interfaces';
 import { RootState } from 'redux/interfaces';
-import { Provider } from 'react-redux';
+import * as redux from 'react-redux';
 import { act, screen, waitFor } from '@testing-library/react';
 import { act, screen, waitFor } from '@testing-library/react';
 import { createMemoryHistory } from 'history';
 import { createMemoryHistory } from 'history';
 import fetchMock from 'fetch-mock-jest';
 import fetchMock from 'fetch-mock-jest';
@@ -15,7 +15,7 @@ import {
 import userEvent from '@testing-library/user-event';
 import userEvent from '@testing-library/user-event';
 import { render } from 'lib/testHelpers';
 import { render } from 'lib/testHelpers';
 
 
-import { createTopicPayload, createTopicResponsePayload } from './fixtures';
+const { Provider } = redux;
 
 
 const mockStore = configureStore();
 const mockStore = configureStore();
 
 
@@ -25,7 +25,6 @@ const topicName = 'test-topic';
 const initialState: Partial<RootState> = {};
 const initialState: Partial<RootState> = {};
 const storeMock = mockStore(initialState);
 const storeMock = mockStore(initialState);
 const historyMock = createMemoryHistory();
 const historyMock = createMemoryHistory();
-const createTopicAPIPath = `/api/clusters/${clusterName}/topics`;
 
 
 const renderComponent = (history = historyMock, store = storeMock) =>
 const renderComponent = (history = historyMock, store = storeMock) =>
   render(
   render(
@@ -96,18 +95,21 @@ describe('New', () => {
   });
   });
 
 
   it('submits valid form', async () => {
   it('submits valid form', async () => {
-    const createTopicAPIPathMock = fetchMock.postOnce(
-      createTopicAPIPath,
-      createTopicResponsePayload,
-      {
-        body: createTopicPayload,
-      }
-    );
+    const useDispatchSpy = jest.spyOn(redux, 'useDispatch');
+    const useDispatchMock = jest.fn(() => ({
+      meta: { requestStatus: 'fulfilled' },
+    })) as jest.Mock;
+    useDispatchSpy.mockReturnValue(useDispatchMock);
+
     const mockedHistory = createMemoryHistory({
     const mockedHistory = createMemoryHistory({
       initialEntries: [clusterTopicNewPath(clusterName)],
       initialEntries: [clusterTopicNewPath(clusterName)],
     });
     });
+
     jest.spyOn(mockedHistory, 'push');
     jest.spyOn(mockedHistory, 'push');
-    renderComponent(mockedHistory);
+
+    await act(() => {
+      renderComponent(mockedHistory);
+    });
 
 
     await waitFor(() => {
     await waitFor(() => {
       userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName);
       userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName);
@@ -119,32 +121,58 @@ describe('New', () => {
         clusterTopicPath(clusterName, topicName)
         clusterTopicPath(clusterName, topicName)
       )
       )
     );
     );
+
+    expect(useDispatchMock).toHaveBeenCalledTimes(1);
     expect(mockedHistory.push).toBeCalledTimes(1);
     expect(mockedHistory.push).toBeCalledTimes(1);
-    expect(createTopicAPIPathMock.called()).toBeTruthy();
   });
   });
 
 
-  it('submits valid form that result in an error', async () => {
-    const createTopicAPIPathMock = fetchMock.postOnce(
-      createTopicAPIPath,
-      { throws: new Error('Something went wrong') },
-      {
-        body: createTopicPayload,
-      }
+  it('does not redirect page when request is not fulfilled', async () => {
+    const useDispatchSpy = jest.spyOn(redux, 'useDispatch');
+    const useDispatchMock = jest.fn(() => ({
+      meta: { requestStatus: 'pending' },
+    })) as jest.Mock;
+    useDispatchSpy.mockReturnValue(useDispatchMock);
+
+    const mockedHistory = createMemoryHistory({
+      initialEntries: [clusterTopicNewPath(clusterName)],
+    });
+
+    jest.spyOn(mockedHistory, 'push');
+
+    await act(() => {
+      renderComponent(mockedHistory);
+    });
+
+    await waitFor(() => {
+      userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName);
+      userEvent.click(screen.getByText(/submit/i));
+    });
+
+    await waitFor(() =>
+      expect(mockedHistory.location.pathname).toBe(
+        clusterTopicNewPath(clusterName)
+      )
     );
     );
+  });
+
+  it('submits valid form that result in an error', async () => {
+    const useDispatchSpy = jest.spyOn(redux, 'useDispatch');
+    const useDispatchMock = jest.fn();
+    useDispatchSpy.mockReturnValue(useDispatchMock);
 
 
-    const mocked = createMemoryHistory({
+    const mockedHistory = createMemoryHistory({
       initialEntries: [clusterTopicNewPath(clusterName)],
       initialEntries: [clusterTopicNewPath(clusterName)],
     });
     });
 
 
-    jest.spyOn(mocked, 'push');
-    renderComponent(mocked);
+    jest.spyOn(mockedHistory, 'push');
+    renderComponent(mockedHistory);
 
 
     await act(() => {
     await act(() => {
       userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName);
       userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName);
       userEvent.click(screen.getByText(/submit/i));
       userEvent.click(screen.getByText(/submit/i));
     });
     });
 
 
-    expect(createTopicAPIPathMock.called()).toBeTruthy();
-    expect(mocked.push).toBeCalledTimes(0);
+    expect(useDispatchMock).toHaveBeenCalledTimes(1);
+    expect(mockedHistory.push).toBeCalledTimes(0);
   });
   });
 });
 });

+ 5 - 5
kafka-ui-react-app/src/components/Topics/Topic/Details/ConsumerGroups/TopicConsumerGroups.tsx

@@ -15,10 +15,10 @@ export interface Props extends Topic, TopicDetails {
   topicName: TopicName;
   topicName: TopicName;
   consumerGroups: ConsumerGroup[];
   consumerGroups: ConsumerGroup[];
   isFetched: boolean;
   isFetched: boolean;
-  fetchTopicConsumerGroups(
-    clusterName: ClusterName,
-    topicName: TopicName
-  ): void;
+  fetchTopicConsumerGroups(payload: {
+    clusterName: ClusterName;
+    topicName: TopicName;
+  }): void;
 }
 }
 
 
 const TopicConsumerGroups: React.FC<Props> = ({
 const TopicConsumerGroups: React.FC<Props> = ({
@@ -29,7 +29,7 @@ const TopicConsumerGroups: React.FC<Props> = ({
   isFetched,
   isFetched,
 }) => {
 }) => {
   React.useEffect(() => {
   React.useEffect(() => {
-    fetchTopicConsumerGroups(clusterName, topicName);
+    fetchTopicConsumerGroups({ clusterName, topicName });
   }, [clusterName, fetchTopicConsumerGroups, topicName]);
   }, [clusterName, fetchTopicConsumerGroups, topicName]);
 
 
   if (!isFetched) {
   if (!isFetched) {

+ 1 - 1
kafka-ui-react-app/src/components/Topics/Topic/Details/ConsumerGroups/TopicConsumerGroupsContainer.ts

@@ -1,7 +1,7 @@
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
 import { RootState, TopicName, ClusterName } from 'redux/interfaces';
 import { RootState, TopicName, ClusterName } from 'redux/interfaces';
 import { withRouter, RouteComponentProps } from 'react-router-dom';
 import { withRouter, RouteComponentProps } from 'react-router-dom';
-import { fetchTopicConsumerGroups } from 'redux/actions';
+import { fetchTopicConsumerGroups } from 'redux/reducers/topics/topicsSlice';
 import TopicConsumerGroups from 'components/Topics/Topic/Details/ConsumerGroups/TopicConsumerGroups';
 import TopicConsumerGroups from 'components/Topics/Topic/Details/ConsumerGroups/TopicConsumerGroups';
 import {
 import {
   getTopicConsumerGroups,
   getTopicConsumerGroups,

+ 10 - 6
kafka-ui-react-app/src/components/Topics/Topic/Details/Details.tsx

@@ -14,7 +14,6 @@ import {
 import ClusterContext from 'components/contexts/ClusterContext';
 import ClusterContext from 'components/contexts/ClusterContext';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import { useDispatch } from 'react-redux';
 import { useDispatch } from 'react-redux';
-import { deleteTopicAction } from 'redux/actions';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import { Button } from 'components/common/Button/Button';
 import { Button } from 'components/common/Button/Button';
 import Dropdown from 'components/common/Dropdown/Dropdown';
 import Dropdown from 'components/common/Dropdown/Dropdown';
@@ -35,8 +34,14 @@ interface Props extends Topic, TopicDetails {
   isInternal: boolean;
   isInternal: boolean;
   isDeleted: boolean;
   isDeleted: boolean;
   isDeletePolicy: boolean;
   isDeletePolicy: boolean;
-  deleteTopic: (clusterName: ClusterName, topicName: TopicName) => void;
-  recreateTopic: (clusterName: ClusterName, topicName: TopicName) => void;
+  deleteTopic: (payload: {
+    clusterName: ClusterName;
+    topicName: TopicName;
+  }) => void;
+  recreateTopic: (payload: {
+    clusterName: ClusterName;
+    topicName: TopicName;
+  }) => void;
   clearTopicMessages(params: {
   clearTopicMessages(params: {
     clusterName: ClusterName;
     clusterName: ClusterName;
     topicName: TopicName;
     topicName: TopicName;
@@ -73,12 +78,11 @@ const Details: React.FC<Props> = ({
     setRecreateTopicConfirmationVisible,
     setRecreateTopicConfirmationVisible,
   ] = React.useState(false);
   ] = React.useState(false);
   const deleteTopicHandler = React.useCallback(() => {
   const deleteTopicHandler = React.useCallback(() => {
-    deleteTopic(clusterName, topicName);
+    deleteTopic({ clusterName, topicName });
   }, [clusterName, topicName, deleteTopic]);
   }, [clusterName, topicName, deleteTopic]);
 
 
   React.useEffect(() => {
   React.useEffect(() => {
     if (isDeleted) {
     if (isDeleted) {
-      dispatch(deleteTopicAction.cancel());
       history.push(clusterTopicsPath(clusterName));
       history.push(clusterTopicsPath(clusterName));
     }
     }
   }, [isDeleted, clusterName, dispatch, history]);
   }, [isDeleted, clusterName, dispatch, history]);
@@ -89,7 +93,7 @@ const Details: React.FC<Props> = ({
   }, [clusterName, topicName, clearTopicMessages]);
   }, [clusterName, topicName, clearTopicMessages]);
 
 
   const recreateTopicHandler = React.useCallback(() => {
   const recreateTopicHandler = React.useCallback(() => {
-    recreateTopic(clusterName, topicName);
+    recreateTopic({ clusterName, topicName });
     setRecreateTopicConfirmationVisible(false);
     setRecreateTopicConfirmationVisible(false);
   }, [recreateTopic, clusterName, topicName]);
   }, [recreateTopic, clusterName, topicName]);
 
 

+ 1 - 1
kafka-ui-react-app/src/components/Topics/Topic/Details/DetailsContainer.ts

@@ -1,7 +1,7 @@
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
 import { ClusterName, RootState, TopicName } from 'redux/interfaces';
 import { ClusterName, RootState, TopicName } from 'redux/interfaces';
 import { withRouter, RouteComponentProps } from 'react-router-dom';
 import { withRouter, RouteComponentProps } from 'react-router-dom';
-import { deleteTopic, recreateTopic } from 'redux/actions';
+import { deleteTopic, recreateTopic } from 'redux/reducers/topics/topicsSlice';
 import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
 import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
 import {
 import {
   getIsTopicDeleted,
   getIsTopicDeleted,

+ 5 - 2
kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/Settings.tsx

@@ -12,7 +12,10 @@ interface Props {
   topicName: TopicName;
   topicName: TopicName;
   config?: TopicConfig[];
   config?: TopicConfig[];
   isFetched: boolean;
   isFetched: boolean;
-  fetchTopicConfig: (clusterName: ClusterName, topicName: TopicName) => void;
+  fetchTopicConfig: (payload: {
+    clusterName: ClusterName;
+    topicName: TopicName;
+  }) => void;
 }
 }
 
 
 const Settings: React.FC<Props> = ({
 const Settings: React.FC<Props> = ({
@@ -23,7 +26,7 @@ const Settings: React.FC<Props> = ({
   config,
   config,
 }) => {
 }) => {
   React.useEffect(() => {
   React.useEffect(() => {
-    fetchTopicConfig(clusterName, topicName);
+    fetchTopicConfig({ clusterName, topicName });
   }, [fetchTopicConfig, clusterName, topicName]);
   }, [fetchTopicConfig, clusterName, topicName]);
 
 
   if (!isFetched) {
   if (!isFetched) {

+ 1 - 1
kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/SettingsContainer.ts

@@ -1,7 +1,7 @@
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
 import { RootState, ClusterName, TopicName } from 'redux/interfaces';
 import { RootState, ClusterName, TopicName } from 'redux/interfaces';
 import { withRouter, RouteComponentProps } from 'react-router-dom';
 import { withRouter, RouteComponentProps } from 'react-router-dom';
-import { fetchTopicConfig } from 'redux/actions';
+import { fetchTopicConfig } from 'redux/reducers/topics/topicsSlice';
 import {
 import {
   getTopicConfig,
   getTopicConfig,
   getTopicConfigFetched,
   getTopicConfigFetched,

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

@@ -101,10 +101,10 @@ describe('Details', () => {
       const submitButton = screen.getAllByText('Submit')[0];
       const submitButton = screen.getAllByText('Submit')[0];
       userEvent.click(submitButton);
       userEvent.click(submitButton);
 
 
-      expect(mockDelete).toHaveBeenCalledWith(
-        mockClusterName,
-        internalTopicPayload.name
-      );
+      expect(mockDelete).toHaveBeenCalledWith({
+        clusterName: mockClusterName,
+        topicName: internalTopicPayload.name,
+      });
     });
     });
 
 
     it('closes the modal when cancel button is clicked', () => {
     it('closes the modal when cancel button is clicked', () => {

+ 18 - 16
kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZone.tsx

@@ -6,6 +6,7 @@ import { FormError } from 'components/common/Input/Input.styled';
 import { InputLabel } from 'components/common/Input/InputLabel.styled';
 import { InputLabel } from 'components/common/Input/InputLabel.styled';
 import React from 'react';
 import React from 'react';
 import { FormProvider, useForm } from 'react-hook-form';
 import { FormProvider, useForm } from 'react-hook-form';
+import { ClusterName, TopicName } from 'redux/interfaces';
 
 
 import * as S from './DangerZone.styled';
 import * as S from './DangerZone.styled';
 
 
@@ -16,16 +17,16 @@ export interface Props {
   defaultReplicationFactor: number;
   defaultReplicationFactor: number;
   partitionsCountIncreased: boolean;
   partitionsCountIncreased: boolean;
   replicationFactorUpdated: boolean;
   replicationFactorUpdated: boolean;
-  updateTopicPartitionsCount: (
-    clusterName: string,
-    topicname: string,
-    partitions: number
-  ) => void;
-  updateTopicReplicationFactor: (
-    clusterName: string,
-    topicname: string,
-    replicationFactor: number
-  ) => void;
+  updateTopicPartitionsCount: (payload: {
+    clusterName: ClusterName;
+    topicName: TopicName;
+    partitions: number;
+  }) => void;
+  updateTopicReplicationFactor: (payload: {
+    clusterName: ClusterName;
+    topicName: TopicName;
+    replicationFactor: number;
+  }) => void;
 }
 }
 
 
 const DangerZone: React.FC<Props> = ({
 const DangerZone: React.FC<Props> = ({
@@ -91,18 +92,19 @@ const DangerZone: React.FC<Props> = ({
   }, [replicationFactorUpdated]);
   }, [replicationFactorUpdated]);
 
 
   const partitionsSubmit = () => {
   const partitionsSubmit = () => {
-    updateTopicPartitionsCount(
+    updateTopicPartitionsCount({
       clusterName,
       clusterName,
       topicName,
       topicName,
-      partitionsMethods.getValues('partitions')
-    );
+      partitions: partitionsMethods.getValues('partitions'),
+    });
   };
   };
   const replicationFactorSubmit = () => {
   const replicationFactorSubmit = () => {
-    updateTopicReplicationFactor(
+    updateTopicReplicationFactor({
       clusterName,
       clusterName,
       topicName,
       topicName,
-      replicationFactorMethods.getValues('replicationFactor')
-    );
+      replicationFactor:
+        replicationFactorMethods.getValues('replicationFactor'),
+    });
   };
   };
   return (
   return (
     <S.Wrapper>
     <S.Wrapper>

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

@@ -4,7 +4,7 @@ import { withRouter, RouteComponentProps } from 'react-router-dom';
 import {
 import {
   updateTopicPartitionsCount,
   updateTopicPartitionsCount,
   updateTopicReplicationFactor,
   updateTopicReplicationFactor,
-} from 'redux/actions';
+} from 'redux/reducers/topics/topicsSlice';
 import {
 import {
   getTopicPartitionsCountIncreased,
   getTopicPartitionsCountIncreased,
   getTopicReplicationFactorUpdated,
   getTopicReplicationFactorUpdated,

+ 16 - 13
kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx

@@ -25,17 +25,20 @@ export interface Props {
   topic?: TopicWithDetailedInfo;
   topic?: TopicWithDetailedInfo;
   isFetched: boolean;
   isFetched: boolean;
   isTopicUpdated: boolean;
   isTopicUpdated: boolean;
-  fetchTopicConfig: (clusterName: ClusterName, topicName: TopicName) => void;
-  updateTopic: (
-    clusterName: ClusterName,
-    topicName: TopicName,
-    form: TopicFormDataRaw
-  ) => void;
-  updateTopicPartitionsCount: (
-    clusterName: string,
-    topicname: string,
-    partitions: number
-  ) => void;
+  fetchTopicConfig: (payload: {
+    clusterName: ClusterName;
+    topicName: TopicName;
+  }) => void;
+  updateTopic: (payload: {
+    clusterName: ClusterName;
+    topicName: TopicName;
+    form: TopicFormDataRaw;
+  }) => void;
+  updateTopicPartitionsCount: (payload: {
+    clusterName: string;
+    topicname: string;
+    partitions: number;
+  }) => void;
 }
 }
 
 
 const EditWrapperStyled = styled.div`
 const EditWrapperStyled = styled.div`
@@ -98,7 +101,7 @@ const Edit: React.FC<Props> = ({
   const history = useHistory();
   const history = useHistory();
 
 
   React.useEffect(() => {
   React.useEffect(() => {
-    fetchTopicConfig(clusterName, topicName);
+    fetchTopicConfig({ clusterName, topicName });
   }, [fetchTopicConfig, clusterName, topicName]);
   }, [fetchTopicConfig, clusterName, topicName]);
 
 
   React.useEffect(() => {
   React.useEffect(() => {
@@ -126,7 +129,7 @@ const Edit: React.FC<Props> = ({
   });
   });
 
 
   const onSubmit = async (data: TopicFormDataRaw) => {
   const onSubmit = async (data: TopicFormDataRaw) => {
-    updateTopic(clusterName, topicName, data);
+    updateTopic({ clusterName, topicName, form: data });
     setIsSubmitting(true); // Keep this action after updateTopic to prevent redirect before update.
     setIsSubmitting(true); // Keep this action after updateTopic to prevent redirect before update.
   };
   };
 
 

+ 4 - 1
kafka-ui-react-app/src/components/Topics/Topic/Edit/EditContainer.tsx

@@ -1,7 +1,10 @@
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
 import { RootState, ClusterName, TopicName } from 'redux/interfaces';
 import { RootState, ClusterName, TopicName } from 'redux/interfaces';
 import { withRouter, RouteComponentProps } from 'react-router-dom';
 import { withRouter, RouteComponentProps } from 'react-router-dom';
-import { updateTopic, fetchTopicConfig } from 'redux/actions';
+import {
+  updateTopic,
+  fetchTopicConfig,
+} from 'redux/reducers/topics/topicsSlice';
 import {
 import {
   getTopicConfigFetched,
   getTopicConfigFetched,
   getTopicUpdated,
   getTopicUpdated,

+ 3 - 2
kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx

@@ -5,7 +5,8 @@ import { useForm, Controller } from 'react-hook-form';
 import { useHistory, useParams } from 'react-router-dom';
 import { useHistory, useParams } from 'react-router-dom';
 import { clusterTopicMessagesPath } from 'lib/paths';
 import { clusterTopicMessagesPath } from 'lib/paths';
 import jsf from 'json-schema-faker';
 import jsf from 'json-schema-faker';
-import { fetchTopicMessageSchema, messagesApiClient } from 'redux/actions';
+import { messagesApiClient } from 'redux/reducers/topicMessages/topicMessagesSlice';
+import { fetchTopicMessageSchema } from 'redux/reducers/topics/topicsSlice';
 import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
 import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
 import { alertAdded } from 'redux/reducers/alerts/alertsSlice';
 import { alertAdded } from 'redux/reducers/alerts/alertsSlice';
 import { now } from 'lodash';
 import { now } from 'lodash';
@@ -34,7 +35,7 @@ const SendMessage: React.FC = () => {
   jsf.option('alwaysFakeOptionals', true);
   jsf.option('alwaysFakeOptionals', true);
 
 
   React.useEffect(() => {
   React.useEffect(() => {
-    dispatch(fetchTopicMessageSchema(clusterName, topicName));
+    dispatch(fetchTopicMessageSchema({ clusterName, topicName }));
   }, [clusterName, dispatch, topicName]);
   }, [clusterName, dispatch, topicName]);
 
 
   const messageSchema = useAppSelector((state) =>
   const messageSchema = useAppSelector((state) =>

+ 11 - 7
kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/SendMessage.spec.tsx

@@ -11,8 +11,7 @@ import {
   clusterTopicSendMessagePath,
   clusterTopicSendMessagePath,
 } from 'lib/paths';
 } from 'lib/paths';
 import { store } from 'redux/store';
 import { store } from 'redux/store';
-import { fetchTopicDetailsAction } from 'redux/actions';
-import { initialState } from 'redux/reducers/topics/reducer';
+import { fetchTopicDetails } from 'redux/reducers/topics/topicsSlice';
 import { externalTopicPayload } from 'redux/reducers/topics/__test__/fixtures';
 import { externalTopicPayload } from 'redux/reducers/topics/__test__/fixtures';
 import validateMessage from 'components/Topics/Topic/SendMessage/validateMessage';
 import validateMessage from 'components/Topics/Topic/SendMessage/validateMessage';
 import Alerts from 'components/Alerts/Alerts';
 import Alerts from 'components/Alerts/Alerts';
@@ -73,12 +72,17 @@ const renderAndSubmitData = async (error: string[] = []) => {
 describe('SendMessage', () => {
 describe('SendMessage', () => {
   beforeAll(() => {
   beforeAll(() => {
     store.dispatch(
     store.dispatch(
-      fetchTopicDetailsAction.success({
-        ...initialState,
-        byName: {
-          [externalTopicPayload.name]: externalTopicPayload,
+      fetchTopicDetails.fulfilled(
+        {
+          topicDetails: externalTopicPayload,
+          topicName,
         },
         },
-      })
+        'topic',
+        {
+          clusterName,
+          topicName,
+        }
+      )
     );
     );
   });
   });
   afterEach(() => {
   afterEach(() => {

+ 5 - 2
kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx

@@ -15,7 +15,10 @@ interface RouterParams {
 interface TopicProps {
 interface TopicProps {
   isTopicFetching: boolean;
   isTopicFetching: boolean;
   resetTopicMessages: () => void;
   resetTopicMessages: () => void;
-  fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) => void;
+  fetchTopicDetails: (payload: {
+    clusterName: ClusterName;
+    topicName: TopicName;
+  }) => void;
 }
 }
 
 
 const Topic: React.FC<TopicProps> = ({
 const Topic: React.FC<TopicProps> = ({
@@ -26,7 +29,7 @@ const Topic: React.FC<TopicProps> = ({
   const { clusterName, topicName } = useParams<RouterParams>();
   const { clusterName, topicName } = useParams<RouterParams>();
 
 
   React.useEffect(() => {
   React.useEffect(() => {
-    fetchTopicDetails(clusterName, topicName);
+    fetchTopicDetails({ clusterName, topicName });
   }, [fetchTopicDetails, clusterName, topicName]);
   }, [fetchTopicDetails, clusterName, topicName]);
 
 
   React.useEffect(() => {
   React.useEffect(() => {

+ 1 - 1
kafka-ui-react-app/src/components/Topics/Topic/TopicContainer.tsx

@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
 import { RootState } from 'redux/interfaces';
 import { RootState } from 'redux/interfaces';
-import { fetchTopicDetails } from 'redux/actions';
+import { fetchTopicDetails } from 'redux/reducers/topics/topicsSlice';
 import { resetTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
 import { resetTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
 import { getIsTopicDetailsFetching } from 'redux/reducers/topics/selectors';
 import { getIsTopicDetailsFetching } from 'redux/reducers/topics/selectors';
 
 

+ 0 - 168
kafka-ui-react-app/src/redux/actions/__test__/actions.spec.ts

@@ -1,168 +0,0 @@
-import * as actions from 'redux/actions';
-import {
-  MessageSchemaSourceEnum,
-  TopicColumnsToSort,
-  TopicMessageSchema,
-} from 'generated-sources';
-import { FailurePayload } from 'redux/interfaces';
-
-import { mockTopicsState } from './fixtures';
-
-describe('Actions', () => {
-  describe('dismissAlert', () => {
-    it('creates a REQUEST action', () => {
-      const id = 'alert-id1';
-      expect(actions.dismissAlert(id)).toEqual({
-        type: 'DISMISS_ALERT',
-        payload: id,
-      });
-    });
-  });
-
-  describe('clearMessagesTopicAction', () => {
-    it('creates a REQUEST action', () => {
-      expect(actions.clearMessagesTopicAction.request()).toEqual({
-        type: 'CLEAR_TOPIC_MESSAGES__REQUEST',
-      });
-    });
-
-    it('creates a SUCCESS action', () => {
-      expect(actions.clearMessagesTopicAction.success()).toEqual({
-        type: 'CLEAR_TOPIC_MESSAGES__SUCCESS',
-      });
-    });
-
-    it('creates a FAILURE action', () => {
-      expect(actions.clearMessagesTopicAction.failure({})).toEqual({
-        type: 'CLEAR_TOPIC_MESSAGES__FAILURE',
-        payload: {},
-      });
-    });
-  });
-
-  describe('fetchTopicConsumerGroups', () => {
-    it('creates a REQUEST action', () => {
-      expect(actions.fetchTopicConsumerGroupsAction.request()).toEqual({
-        type: 'GET_TOPIC_CONSUMER_GROUPS__REQUEST',
-      });
-    });
-
-    it('creates a SUCCESS action', () => {
-      expect(
-        actions.fetchTopicConsumerGroupsAction.success(mockTopicsState)
-      ).toEqual({
-        type: 'GET_TOPIC_CONSUMER_GROUPS__SUCCESS',
-        payload: mockTopicsState,
-      });
-    });
-
-    it('creates a FAILURE action', () => {
-      expect(actions.fetchTopicConsumerGroupsAction.failure()).toEqual({
-        type: 'GET_TOPIC_CONSUMER_GROUPS__FAILURE',
-      });
-    });
-  });
-
-  describe('setTopicsSearchAction', () => {
-    it('creates SET_TOPICS_SEARCH', () => {
-      expect(actions.setTopicsSearchAction('test')).toEqual({
-        type: 'SET_TOPICS_SEARCH',
-        payload: 'test',
-      });
-    });
-  });
-
-  describe('setTopicsOrderByAction', () => {
-    it('creates SET_TOPICS_ORDER_BY', () => {
-      expect(actions.setTopicsOrderByAction(TopicColumnsToSort.NAME)).toEqual({
-        type: 'SET_TOPICS_ORDER_BY',
-        payload: TopicColumnsToSort.NAME,
-      });
-    });
-  });
-
-  describe('sending messages', () => {
-    describe('fetchTopicMessageSchemaAction', () => {
-      it('creates GET_TOPIC_SCHEMA__REQUEST', () => {
-        expect(actions.fetchTopicMessageSchemaAction.request()).toEqual({
-          type: 'GET_TOPIC_SCHEMA__REQUEST',
-        });
-      });
-      it('creates GET_TOPIC_SCHEMA__SUCCESS', () => {
-        const messageSchema: TopicMessageSchema = {
-          key: {
-            name: 'key',
-            source: MessageSchemaSourceEnum.SCHEMA_REGISTRY,
-            schema: `{
-        "$schema": "http://json-schema.org/draft-07/schema#",
-        "$id": "http://example.com/myURI.schema.json",
-        "title": "TestRecord",
-        "type": "object",
-        "additionalProperties": false,
-        "properties": {
-          "f1": {
-            "type": "integer"
-          },
-          "f2": {
-            "type": "string"
-          },
-          "schema": {
-            "type": "string"
-          }
-        }
-        }
-        `,
-          },
-          value: {
-            name: 'value',
-            source: MessageSchemaSourceEnum.SCHEMA_REGISTRY,
-            schema: `{
-        "$schema": "http://json-schema.org/draft-07/schema#",
-        "$id": "http://example.com/myURI1.schema.json",
-        "title": "TestRecord",
-        "type": "object",
-        "additionalProperties": false,
-        "properties": {
-          "f1": {
-            "type": "integer"
-          },
-          "f2": {
-            "type": "string"
-          },
-          "schema": {
-            "type": "string"
-          }
-        }
-        }
-        `,
-          },
-        };
-        expect(
-          actions.fetchTopicMessageSchemaAction.success({
-            topicName: 'test',
-            schema: messageSchema,
-          })
-        ).toEqual({
-          type: 'GET_TOPIC_SCHEMA__SUCCESS',
-          payload: {
-            topicName: 'test',
-            schema: messageSchema,
-          },
-        });
-      });
-
-      it('creates GET_TOPIC_SCHEMA__FAILURE', () => {
-        const alert: FailurePayload = {
-          subject: ['message-chema', 'test'].join('-'),
-          title: `Message Schema Test`,
-        };
-        expect(
-          actions.fetchTopicMessageSchemaAction.failure({ alert })
-        ).toEqual({
-          type: 'GET_TOPIC_SCHEMA__FAILURE',
-          payload: { alert },
-        });
-      });
-    });
-  });
-});

+ 0 - 295
kafka-ui-react-app/src/redux/actions/__test__/thunks/topics.spec.ts

@@ -1,295 +0,0 @@
-import fetchMock from 'fetch-mock-jest';
-import * as actions from 'redux/actions/actions';
-import * as thunks from 'redux/actions/thunks';
-import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator';
-import { mockTopicsState } from 'redux/actions/__test__/fixtures';
-import { MessageSchemaSourceEnum, TopicMessageSchema } from 'generated-sources';
-import { FailurePayload } from 'redux/interfaces';
-import { getResponse } from 'lib/errorHandling';
-import { internalTopicPayload } from 'redux/reducers/topics/__test__/fixtures';
-import { getAlertActions, getTypeAndPayload } from 'lib/testHelpers';
-
-const store = mockStoreCreator;
-
-const clusterName = 'local';
-const topicName = 'localTopic';
-
-describe('Thunks', () => {
-  afterEach(() => {
-    fetchMock.restore();
-    store.clearActions();
-  });
-
-  describe('deleteTopis', () => {
-    it('creates DELETE_TOPIC__SUCCESS when deleting existing topic', async () => {
-      fetchMock.deleteOnce(
-        `/api/clusters/${clusterName}/topics/${topicName}`,
-        200
-      );
-      await store.dispatch(thunks.deleteTopic(clusterName, topicName));
-      expect(store.getActions()).toEqual([
-        actions.deleteTopicAction.request(),
-        actions.deleteTopicAction.success(topicName),
-      ]);
-    });
-
-    it('creates DELETE_TOPIC__FAILURE when deleting existing topic', async () => {
-      fetchMock.deleteOnce(
-        `/api/clusters/${clusterName}/topics/${topicName}`,
-        404
-      );
-      try {
-        await store.dispatch(thunks.deleteTopic(clusterName, topicName));
-      } catch (error) {
-        const err = error as Response;
-        expect(err.status).toEqual(404);
-        expect(store.getActions()).toEqual([
-          actions.deleteTopicAction.request(),
-          actions.deleteTopicAction.failure(),
-        ]);
-      }
-    });
-  });
-
-  describe('recreateTopic', () => {
-    it('creates RECREATE_TOPIC__SUCCESS when recreating existing topic', async () => {
-      fetchMock.postOnce(
-        `/api/clusters/${clusterName}/topics/${topicName}`,
-        internalTopicPayload
-      );
-      await store.dispatch(thunks.recreateTopic(clusterName, topicName));
-      expect(getTypeAndPayload(store)).toEqual([
-        actions.recreateTopicAction.request(),
-        actions.recreateTopicAction.success(internalTopicPayload),
-        ...getAlertActions(store),
-      ]);
-    });
-
-    it('creates RECREATE_TOPIC__FAILURE when recreating existing topic', async () => {
-      fetchMock.postOnce(
-        `/api/clusters/${clusterName}/topics/${topicName}`,
-        404
-      );
-      try {
-        await store.dispatch(thunks.recreateTopic(clusterName, topicName));
-      } catch (error) {
-        const err = error as Response;
-        expect(err.status).toEqual(404);
-        expect(store.getActions()).toEqual([
-          actions.recreateTopicAction.request(),
-          actions.recreateTopicAction.failure(),
-        ]);
-      }
-    });
-  });
-
-  describe('fetchTopicConsumerGroups', () => {
-    it('GET_TOPIC_CONSUMER_GROUPS__FAILURE', async () => {
-      fetchMock.getOnce(
-        `api/clusters/${clusterName}/topics/${topicName}/consumer-groups`,
-        404
-      );
-      try {
-        await store.dispatch(
-          thunks.fetchTopicConsumerGroups(clusterName, topicName)
-        );
-      } catch (error) {
-        const err = error as Response;
-        expect(err.status).toEqual(404);
-        expect(store.getActions()).toEqual([
-          actions.fetchTopicConsumerGroupsAction.request(),
-          actions.fetchTopicConsumerGroupsAction.failure(),
-        ]);
-      }
-    });
-
-    it('GET_TOPIC_CONSUMER_GROUPS__SUCCESS', async () => {
-      fetchMock.getOnce(
-        `api/clusters/${clusterName}/topics/${topicName}/consumer-groups`,
-        200
-      );
-      try {
-        await store.dispatch(
-          thunks.fetchTopicConsumerGroups(clusterName, topicName)
-        );
-      } catch (error) {
-        const err = error as Response;
-        expect(err.status).toEqual(200);
-        expect(store.getActions()).toEqual([
-          actions.fetchTopicConsumerGroupsAction.request(),
-          actions.fetchTopicConsumerGroupsAction.success(mockTopicsState),
-        ]);
-      }
-    });
-  });
-
-  describe('fetchTopicMessageSchema', () => {
-    it('creates GET_TOPIC_SCHEMA__FAILURE', async () => {
-      fetchMock.getOnce(
-        `/api/clusters/${clusterName}/topics/${topicName}/messages/schema`,
-        404
-      );
-      try {
-        await store.dispatch(
-          thunks.fetchTopicMessageSchema(clusterName, topicName)
-        );
-      } catch (error) {
-        const err = error as Response;
-        expect(err.status).toEqual(404);
-        expect(store.getActions()).toEqual([
-          actions.fetchTopicMessageSchemaAction.request(),
-          actions.fetchTopicMessageSchemaAction.failure({
-            alert: {
-              subject: ['topic', topicName].join('-'),
-              title: `Topic Schema ${topicName}`,
-              response: err,
-            },
-          }),
-        ]);
-      }
-    });
-
-    it('creates GET_TOPIC_SCHEMA__SUCCESS', async () => {
-      const messageSchema: TopicMessageSchema = {
-        key: {
-          name: 'key',
-          source: MessageSchemaSourceEnum.SCHEMA_REGISTRY,
-          schema: `{
-      "$schema": "http://json-schema.org/draft-07/schema#",
-      "$id": "http://example.com/myURI.schema.json",
-      "title": "TestRecord",
-      "type": "object",
-      "additionalProperties": false,
-      "properties": {
-        "f1": {
-          "type": "integer"
-        },
-        "f2": {
-          "type": "string"
-        },
-        "schema": {
-          "type": "string"
-        }
-      }
-      }
-      `,
-        },
-        value: {
-          name: 'value',
-          source: MessageSchemaSourceEnum.SCHEMA_REGISTRY,
-          schema: `{
-      "$schema": "http://json-schema.org/draft-07/schema#",
-      "$id": "http://example.com/myURI1.schema.json",
-      "title": "TestRecord",
-      "type": "object",
-      "additionalProperties": false,
-      "properties": {
-        "f1": {
-          "type": "integer"
-        },
-        "f2": {
-          "type": "string"
-        },
-        "schema": {
-          "type": "string"
-        }
-      }
-      }
-      `,
-        },
-      };
-      fetchMock.getOnce(
-        `/api/clusters/${clusterName}/topics/${topicName}/messages/schema`,
-        messageSchema
-      );
-      await store.dispatch(
-        thunks.fetchTopicMessageSchema(clusterName, topicName)
-      );
-      expect(store.getActions()).toEqual([
-        actions.fetchTopicMessageSchemaAction.request(),
-        actions.fetchTopicMessageSchemaAction.success({
-          topicName,
-          schema: messageSchema,
-        }),
-      ]);
-    });
-  });
-  describe('increasing partitions count', () => {
-    it('calls updateTopicPartitionsCountAction.success on success', async () => {
-      fetchMock.patchOnce(
-        `/api/clusters/${clusterName}/topics/${topicName}/partitions`,
-        { totalPartitionsCount: 4, topicName }
-      );
-      await store.dispatch(
-        thunks.updateTopicPartitionsCount(clusterName, topicName, 4)
-      );
-      expect(store.getActions()).toEqual([
-        actions.updateTopicPartitionsCountAction.request(),
-        actions.updateTopicPartitionsCountAction.success(),
-      ]);
-    });
-
-    it('calls updateTopicPartitionsCountAction.failure on failure', async () => {
-      fetchMock.patchOnce(
-        `/api/clusters/${clusterName}/topics/${topicName}/partitions`,
-        404
-      );
-      try {
-        await store.dispatch(
-          thunks.updateTopicPartitionsCount(clusterName, topicName, 4)
-        );
-      } catch (error) {
-        const response = await getResponse(error as Response);
-        const alert: FailurePayload = {
-          subject: ['topic-partitions', topicName].join('-'),
-          title: `Topic ${topicName} partitions count increase failed`,
-          response,
-        };
-        expect(store.getActions()).toEqual([
-          actions.updateTopicPartitionsCountAction.request(),
-          actions.updateTopicPartitionsCountAction.failure({ alert }),
-        ]);
-      }
-    });
-  });
-
-  describe('updating replication factor', () => {
-    it('calls updateTopicReplicationFactorAction.success on success', async () => {
-      fetchMock.patchOnce(
-        `/api/clusters/${clusterName}/topics/${topicName}/replications`,
-        { totalReplicationFactor: 4, topicName }
-      );
-      await store.dispatch(
-        thunks.updateTopicReplicationFactor(clusterName, topicName, 4)
-      );
-      expect(store.getActions()).toEqual([
-        actions.updateTopicReplicationFactorAction.request(),
-        actions.updateTopicReplicationFactorAction.success(),
-      ]);
-    });
-
-    it('calls updateTopicReplicationFactorAction.failure on failure', async () => {
-      fetchMock.patchOnce(
-        `/api/clusters/${clusterName}/topics/${topicName}/replications`,
-        404
-      );
-      try {
-        await store.dispatch(
-          thunks.updateTopicReplicationFactor(clusterName, topicName, 4)
-        );
-      } catch (error) {
-        const err = error as Response;
-        const response = await getResponse(err);
-        const alert: FailurePayload = {
-          subject: ['topic-replication-factor', topicName].join('-'),
-          title: `Topic ${topicName} replication factor change failed`,
-          response,
-        };
-        expect(store.getActions()).toEqual([
-          actions.updateTopicReplicationFactorAction.request(),
-          actions.updateTopicReplicationFactorAction.failure({ alert }),
-        ]);
-      }
-    });
-  });
-});

+ 0 - 94
kafka-ui-react-app/src/redux/actions/actions.ts

@@ -1,94 +0,0 @@
-import { createAction, createAsyncAction } from 'typesafe-actions';
-import { FailurePayload, TopicName, TopicsState } from 'redux/interfaces';
-import {
-  TopicColumnsToSort,
-  Topic,
-  TopicMessageSchema,
-} from 'generated-sources';
-
-export const fetchTopicsListAction = createAsyncAction(
-  'GET_TOPICS__REQUEST',
-  'GET_TOPICS__SUCCESS',
-  'GET_TOPICS__FAILURE'
-)<undefined, TopicsState, undefined>();
-
-export const clearMessagesTopicAction = createAsyncAction(
-  'CLEAR_TOPIC_MESSAGES__REQUEST',
-  'CLEAR_TOPIC_MESSAGES__SUCCESS',
-  'CLEAR_TOPIC_MESSAGES__FAILURE'
-)<undefined, undefined, { alert?: FailurePayload }>();
-
-export const fetchTopicDetailsAction = createAsyncAction(
-  'GET_TOPIC_DETAILS__REQUEST',
-  'GET_TOPIC_DETAILS__SUCCESS',
-  'GET_TOPIC_DETAILS__FAILURE'
-)<undefined, TopicsState, undefined>();
-
-export const fetchTopicConfigAction = createAsyncAction(
-  'GET_TOPIC_CONFIG__REQUEST',
-  'GET_TOPIC_CONFIG__SUCCESS',
-  'GET_TOPIC_CONFIG__FAILURE'
-)<undefined, TopicsState, undefined>();
-
-export const createTopicAction = createAsyncAction(
-  'POST_TOPIC__REQUEST',
-  'POST_TOPIC__SUCCESS',
-  'POST_TOPIC__FAILURE'
-)<undefined, TopicsState, { alert?: FailurePayload }>();
-
-export const updateTopicAction = createAsyncAction(
-  'PATCH_TOPIC__REQUEST',
-  'PATCH_TOPIC__SUCCESS',
-  'PATCH_TOPIC__FAILURE'
-)<undefined, TopicsState, undefined>();
-
-export const deleteTopicAction = createAsyncAction(
-  'DELETE_TOPIC__REQUEST',
-  'DELETE_TOPIC__SUCCESS',
-  'DELETE_TOPIC__FAILURE',
-  'DELETE_TOPIC__CANCEL'
-)<undefined, TopicName, undefined, undefined>();
-
-export const recreateTopicAction = createAsyncAction(
-  'RECREATE_TOPIC__REQUEST',
-  'RECREATE_TOPIC__SUCCESS',
-  'RECREATE_TOPIC__FAILURE',
-  'RECREATE_TOPIC__CANCEL'
-)<undefined, Topic, undefined, undefined>();
-
-export const dismissAlert = createAction('DISMISS_ALERT')<string>();
-
-export const setTopicsSearchAction =
-  createAction('SET_TOPICS_SEARCH')<string>();
-
-export const setTopicsOrderByAction = createAction(
-  'SET_TOPICS_ORDER_BY'
-)<TopicColumnsToSort>();
-
-export const fetchTopicConsumerGroupsAction = createAsyncAction(
-  'GET_TOPIC_CONSUMER_GROUPS__REQUEST',
-  'GET_TOPIC_CONSUMER_GROUPS__SUCCESS',
-  'GET_TOPIC_CONSUMER_GROUPS__FAILURE'
-)<undefined, TopicsState, undefined>();
-
-export const fetchTopicMessageSchemaAction = createAsyncAction(
-  'GET_TOPIC_SCHEMA__REQUEST',
-  'GET_TOPIC_SCHEMA__SUCCESS',
-  'GET_TOPIC_SCHEMA__FAILURE'
-)<
-  undefined,
-  { topicName: string; schema: TopicMessageSchema },
-  { alert?: FailurePayload }
->();
-
-export const updateTopicPartitionsCountAction = createAsyncAction(
-  'UPDATE_PARTITIONS__REQUEST',
-  'UPDATE_PARTITIONS__SUCCESS',
-  'UPDATE_PARTITIONS__FAILURE'
-)<undefined, undefined, { alert?: FailurePayload }>();
-
-export const updateTopicReplicationFactorAction = createAsyncAction(
-  'UPDATE_REPLICATION_FACTOR__REQUEST',
-  'UPDATE_REPLICATION_FACTOR__SUCCESS',
-  'UPDATE_REPLICATION_FACTOR__FAILURE'
-)<undefined, undefined, { alert?: FailurePayload }>();

+ 0 - 2
kafka-ui-react-app/src/redux/actions/index.ts

@@ -1,2 +0,0 @@
-export * from './actions';
-export * from './thunks';

+ 0 - 1
kafka-ui-react-app/src/redux/actions/thunks/index.ts

@@ -1 +0,0 @@
-export * from './topics';

+ 0 - 370
kafka-ui-react-app/src/redux/actions/thunks/topics.ts

@@ -1,370 +0,0 @@
-import { v4 } from 'uuid';
-import {
-  TopicsApi,
-  MessagesApi,
-  Configuration,
-  Topic,
-  TopicCreation,
-  TopicUpdate,
-  TopicConfig,
-  ConsumerGroupsApi,
-  GetTopicsRequest,
-} from 'generated-sources';
-import {
-  PromiseThunkResult,
-  ClusterName,
-  TopicName,
-  TopicFormFormattedParams,
-  TopicFormDataRaw,
-  TopicsState,
-  FailurePayload,
-  TopicFormData,
-  AppDispatch,
-} from 'redux/interfaces';
-import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
-import { BASE_PARAMS } from 'lib/constants';
-import * as actions from 'redux/actions/actions';
-import { getResponse } from 'lib/errorHandling';
-import { showSuccessAlert } from 'redux/reducers/alerts/alertsSlice';
-
-const apiClientConf = new Configuration(BASE_PARAMS);
-export const topicsApiClient = new TopicsApi(apiClientConf);
-export const messagesApiClient = new MessagesApi(apiClientConf);
-export const topicConsumerGroupsApiClient = new ConsumerGroupsApi(
-  apiClientConf
-);
-
-export const fetchTopicsList =
-  (params: GetTopicsRequest): PromiseThunkResult =>
-  async (dispatch, getState) => {
-    dispatch(actions.fetchTopicsListAction.request());
-    try {
-      const { topics, pageCount } = await topicsApiClient.getTopics(params);
-      const newState = (topics || []).reduce(
-        (memo: TopicsState, topic) => ({
-          ...memo,
-          byName: {
-            ...memo.byName,
-            [topic.name]: {
-              ...memo.byName[topic.name],
-              ...topic,
-              id: v4(),
-            },
-          },
-          allNames: [...memo.allNames, topic.name],
-        }),
-        {
-          ...getState().topics,
-          allNames: [],
-          totalPages: pageCount || 1,
-        }
-      );
-      dispatch(actions.fetchTopicsListAction.success(newState));
-    } catch (e) {
-      dispatch(actions.fetchTopicsListAction.failure());
-    }
-  };
-
-export const clearTopicsMessages =
-  (clusterName: ClusterName, topicsName: TopicName[]): PromiseThunkResult =>
-  async (dispatch) => {
-    topicsName.forEach((topicName) => {
-      dispatch(clearTopicMessages({ clusterName, topicName }));
-    });
-  };
-
-export const fetchTopicDetails =
-  (clusterName: ClusterName, topicName: TopicName): PromiseThunkResult =>
-  async (dispatch, getState) => {
-    dispatch(actions.fetchTopicDetailsAction.request());
-    try {
-      const topicDetails = await topicsApiClient.getTopicDetails({
-        clusterName,
-        topicName,
-      });
-      const state = getState().topics;
-      const newState = {
-        ...state,
-        byName: {
-          ...state.byName,
-          [topicName]: {
-            ...state.byName[topicName],
-            ...topicDetails,
-          },
-        },
-      };
-      dispatch(actions.fetchTopicDetailsAction.success(newState));
-    } catch (e) {
-      dispatch(actions.fetchTopicDetailsAction.failure());
-    }
-  };
-
-export const fetchTopicConfig =
-  (clusterName: ClusterName, topicName: TopicName): PromiseThunkResult =>
-  async (dispatch, getState) => {
-    dispatch(actions.fetchTopicConfigAction.request());
-    try {
-      const config = await topicsApiClient.getTopicConfigs({
-        clusterName,
-        topicName,
-      });
-
-      const state = getState().topics;
-      const newState = {
-        ...state,
-        byName: {
-          ...state.byName,
-          [topicName]: {
-            ...state.byName[topicName],
-            config: config.map((inputConfig) => ({
-              ...inputConfig,
-            })),
-          },
-        },
-      };
-
-      dispatch(actions.fetchTopicConfigAction.success(newState));
-    } catch (e) {
-      dispatch(actions.fetchTopicConfigAction.failure());
-    }
-  };
-
-const topicReducer = (
-  result: TopicFormFormattedParams,
-  customParam: TopicConfig
-) => {
-  return {
-    ...result,
-    [customParam.name]: customParam.value,
-  };
-};
-
-export const formatTopicCreation = (form: TopicFormData): TopicCreation => {
-  const {
-    name,
-    partitions,
-    replicationFactor,
-    cleanupPolicy,
-    retentionBytes,
-    retentionMs,
-    maxMessageBytes,
-    minInsyncReplicas,
-    customParams,
-  } = form;
-
-  return {
-    name,
-    partitions,
-    replicationFactor,
-    configs: {
-      'cleanup.policy': cleanupPolicy,
-      'retention.ms': retentionMs.toString(),
-      'retention.bytes': retentionBytes.toString(),
-      'max.message.bytes': maxMessageBytes.toString(),
-      'min.insync.replicas': minInsyncReplicas.toString(),
-      ...Object.values(customParams || {}).reduce(topicReducer, {}),
-    },
-  };
-};
-
-const formatTopicUpdate = (form: TopicFormDataRaw): TopicUpdate => {
-  const {
-    cleanupPolicy,
-    retentionBytes,
-    retentionMs,
-    maxMessageBytes,
-    minInsyncReplicas,
-    customParams,
-  } = form;
-
-  return {
-    configs: {
-      'cleanup.policy': cleanupPolicy,
-      'retention.ms': retentionMs,
-      'retention.bytes': retentionBytes,
-      'max.message.bytes': maxMessageBytes,
-      'min.insync.replicas': minInsyncReplicas,
-      ...Object.values(customParams || {}).reduce(topicReducer, {}),
-    },
-  };
-};
-
-export const updateTopic =
-  (
-    clusterName: ClusterName,
-    topicName: TopicName,
-    form: TopicFormDataRaw
-  ): PromiseThunkResult =>
-  async (dispatch, getState) => {
-    dispatch(actions.updateTopicAction.request());
-    try {
-      const topic: Topic = await topicsApiClient.updateTopic({
-        clusterName,
-        topicName,
-        topicUpdate: formatTopicUpdate(form),
-      });
-
-      const state = getState().topics;
-      const newState = {
-        ...state,
-        byName: {
-          ...state.byName,
-          [topic.name]: {
-            ...state.byName[topic.name],
-            ...topic,
-          },
-        },
-      };
-
-      dispatch(actions.updateTopicAction.success(newState));
-    } catch (e) {
-      dispatch(actions.updateTopicAction.failure());
-    }
-  };
-
-export const deleteTopic =
-  (clusterName: ClusterName, topicName: TopicName): PromiseThunkResult =>
-  async (dispatch) => {
-    dispatch(actions.deleteTopicAction.request());
-    try {
-      await topicsApiClient.deleteTopic({
-        clusterName,
-        topicName,
-      });
-      dispatch(actions.deleteTopicAction.success(topicName));
-    } catch (e) {
-      dispatch(actions.deleteTopicAction.failure());
-    }
-  };
-
-export const recreateTopic =
-  (clusterName: ClusterName, topicName: TopicName): PromiseThunkResult =>
-  async (dispatch) => {
-    dispatch(actions.recreateTopicAction.request());
-    try {
-      const topic = await topicsApiClient.recreateTopic({
-        clusterName,
-        topicName,
-      });
-      dispatch(actions.recreateTopicAction.success(topic));
-
-      (dispatch as AppDispatch)(
-        showSuccessAlert({
-          id: topicName,
-          message: 'Topic successfully recreated!',
-        })
-      );
-    } catch (e) {
-      dispatch(actions.recreateTopicAction.failure());
-    }
-  };
-
-export const deleteTopics =
-  (clusterName: ClusterName, topicsName: TopicName[]): PromiseThunkResult =>
-  async (dispatch) => {
-    topicsName.forEach((topicName) => {
-      dispatch(deleteTopic(clusterName, topicName));
-    });
-  };
-
-export const fetchTopicConsumerGroups =
-  (clusterName: ClusterName, topicName: TopicName): PromiseThunkResult =>
-  async (dispatch, getState) => {
-    dispatch(actions.fetchTopicConsumerGroupsAction.request());
-    try {
-      const consumerGroups =
-        await topicConsumerGroupsApiClient.getTopicConsumerGroups({
-          clusterName,
-          topicName,
-        });
-      const state = getState().topics;
-      const newState = {
-        ...state,
-        byName: {
-          ...state.byName,
-          [topicName]: {
-            ...state.byName[topicName],
-            consumerGroups,
-          },
-        },
-      };
-      dispatch(actions.fetchTopicConsumerGroupsAction.success(newState));
-    } catch (e) {
-      dispatch(actions.fetchTopicConsumerGroupsAction.failure());
-    }
-  };
-
-export const fetchTopicMessageSchema =
-  (clusterName: ClusterName, topicName: TopicName): PromiseThunkResult =>
-  async (dispatch) => {
-    dispatch(actions.fetchTopicMessageSchemaAction.request());
-    try {
-      const schema = await messagesApiClient.getTopicSchema({
-        clusterName,
-        topicName,
-      });
-      dispatch(
-        actions.fetchTopicMessageSchemaAction.success({ topicName, schema })
-      );
-    } catch (e) {
-      const response = await getResponse(e);
-      const alert: FailurePayload = {
-        subject: ['topic', topicName].join('-'),
-        title: `Topic Schema ${topicName}`,
-        response,
-      };
-      dispatch(actions.fetchTopicMessageSchemaAction.failure({ alert }));
-    }
-  };
-
-export const updateTopicPartitionsCount =
-  (
-    clusterName: ClusterName,
-    topicName: TopicName,
-    partitions: number
-  ): PromiseThunkResult =>
-  async (dispatch) => {
-    dispatch(actions.updateTopicPartitionsCountAction.request());
-    try {
-      await topicsApiClient.increaseTopicPartitions({
-        clusterName,
-        topicName,
-        partitionsIncrease: { totalPartitionsCount: partitions },
-      });
-      dispatch(actions.updateTopicPartitionsCountAction.success());
-    } catch (error) {
-      const response = await getResponse(error);
-      const alert: FailurePayload = {
-        subject: ['topic-partitions', topicName].join('-'),
-        title: `Topic ${topicName} partitions count increase failed`,
-        response,
-      };
-      dispatch(actions.updateTopicPartitionsCountAction.failure({ alert }));
-    }
-  };
-
-export const updateTopicReplicationFactor =
-  (
-    clusterName: ClusterName,
-    topicName: TopicName,
-    replicationFactor: number
-  ): PromiseThunkResult =>
-  async (dispatch) => {
-    dispatch(actions.updateTopicReplicationFactorAction.request());
-    try {
-      await topicsApiClient.changeReplicationFactor({
-        clusterName,
-        topicName,
-        replicationFactorChange: { totalReplicationFactor: replicationFactor },
-      });
-      dispatch(actions.updateTopicReplicationFactorAction.success());
-    } catch (error) {
-      const response = await getResponse(error);
-      const alert: FailurePayload = {
-        subject: ['topic-replication-factor', topicName].join('-'),
-        title: `Topic ${topicName} replication factor change failed`,
-        response,
-      };
-      dispatch(actions.updateTopicReplicationFactorAction.failure({ alert }));
-    }
-  };

+ 0 - 7
kafka-ui-react-app/src/redux/interfaces/alerts.ts

@@ -7,13 +7,6 @@ export interface ServerResponse {
   message?: ErrorResponse['message'];
   message?: ErrorResponse['message'];
 }
 }
 
 
-export interface FailurePayload {
-  title: string;
-  message?: string;
-  subject: string;
-  response?: ServerResponse;
-}
-
 export type AlertType = 'error' | 'success' | 'warning' | 'info';
 export type AlertType = 'error' | 'success' | 'warning' | 'info';
 
 
 export interface Alert {
 export interface Alert {

+ 0 - 16
kafka-ui-react-app/src/redux/interfaces/index.ts

@@ -1,6 +1,3 @@
-import { ActionType } from 'typesafe-actions';
-import { ThunkAction } from 'redux-thunk';
-import * as actions from 'redux/actions/actions';
 import rootReducer from 'redux/reducers';
 import rootReducer from 'redux/reducers';
 import { store } from 'redux/store';
 import { store } from 'redux/store';
 
 
@@ -16,16 +13,3 @@ export * from './connect';
 export type RootState = ReturnType<typeof rootReducer>;
 export type RootState = ReturnType<typeof rootReducer>;
 export type AppStore = ReturnType<typeof store.getState>;
 export type AppStore = ReturnType<typeof store.getState>;
 export type AppDispatch = typeof store.dispatch;
 export type AppDispatch = typeof store.dispatch;
-
-export type Action = ActionType<typeof actions>;
-
-export type ThunkResult<ReturnType = void> = ThunkAction<
-  ReturnType,
-  RootState,
-  undefined,
-  Action
->;
-
-export type PromiseThunkResult<ReturnType = void> = ThunkResult<
-  Promise<ReturnType>
->;

+ 1 - 0
kafka-ui-react-app/src/redux/interfaces/topic.ts

@@ -43,6 +43,7 @@ export interface TopicFormCustomParams {
 }
 }
 
 
 export interface TopicWithDetailedInfo extends Topic, TopicDetails {
 export interface TopicWithDetailedInfo extends Topic, TopicDetails {
+  id?: string;
   config?: TopicConfig[];
   config?: TopicConfig[];
   consumerGroups?: ConsumerGroup[];
   consumerGroups?: ConsumerGroup[];
   messageSchema?: TopicMessageSchema;
   messageSchema?: TopicMessageSchema;

+ 0 - 10
kafka-ui-react-app/src/redux/reducers/alerts/__test__/fixtures.ts

@@ -1,10 +0,0 @@
-export const failurePayload1 = {
-  title: 'title',
-  message: 'message',
-  subject: 'topic-1',
-};
-
-export const failurePayload2 = {
-  ...failurePayload1,
-  subject: 'topic-2',
-};

+ 0 - 104
kafka-ui-react-app/src/redux/reducers/alerts/__test__/reducer.spec.ts

@@ -1,104 +0,0 @@
-import { dismissAlert, createTopicAction } from 'redux/actions';
-import reducer from 'redux/reducers/alerts/reducer';
-import { showSuccessAlert } from 'redux/reducers/alerts/alertsSlice';
-import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator';
-
-import { failurePayload1, failurePayload2 } from './fixtures';
-
-const store = mockStoreCreator;
-
-jest.mock('lodash', () => ({
-  ...jest.requireActual('lodash'),
-  now: () => 1234567890,
-}));
-
-describe('Alerts reducer', () => {
-  it('does not create error alert', () => {
-    expect(reducer(undefined, createTopicAction.failure({}))).toEqual({});
-  });
-
-  it('creates error alert', () => {
-    expect(
-      reducer(
-        undefined,
-        createTopicAction.failure({
-          alert: failurePayload2,
-        })
-      )
-    ).toEqual({
-      'POST_TOPIC__FAILURE-topic-2': {
-        createdAt: 1234567890,
-        id: 'POST_TOPIC__FAILURE-topic-2',
-        message: 'message',
-        response: undefined,
-        title: 'title',
-        type: 'error',
-      },
-    });
-  });
-
-  it('removes alert by ID', () => {
-    const state = reducer(
-      undefined,
-      createTopicAction.failure({
-        alert: failurePayload1,
-      })
-    );
-    expect(reducer(state, dismissAlert('POST_TOPIC__FAILURE-topic-1'))).toEqual(
-      {}
-    );
-  });
-
-  it('does not remove alert if id is wrong', () => {
-    const state = reducer(
-      undefined,
-      createTopicAction.failure({
-        alert: failurePayload1,
-      })
-    );
-    expect(reducer(state, dismissAlert('wrong-id'))).toEqual({
-      'POST_TOPIC__FAILURE-topic-1': {
-        createdAt: 1234567890,
-        id: 'POST_TOPIC__FAILURE-topic-1',
-        message: 'message',
-        response: undefined,
-        title: 'title',
-        type: 'error',
-      },
-    });
-  });
-
-  describe('Alert thunks', () => {
-    afterEach(() => {
-      store.clearActions();
-    });
-
-    it('dismisses alert after showing success alert', async () => {
-      const passedPayload = { id: 'some-id', message: 'Alert message.' };
-
-      const { payload: creationDate } = await store.dispatch(
-        showSuccessAlert(passedPayload)
-      );
-
-      const actionsData = store
-        .getActions()
-        .map(({ type, payload }) => ({ type, payload }));
-
-      const expectedActions = [
-        { type: 'alerts/showSuccessAlert/pending', payload: undefined },
-        {
-          type: 'alerts/alertAdded',
-          payload: {
-            ...passedPayload,
-            title: '',
-            type: 'success',
-            createdAt: creationDate,
-          },
-        },
-        { type: 'alerts/showSuccessAlert/fulfilled', payload: creationDate },
-      ];
-
-      expect(actionsData).toEqual(expectedActions);
-    });
-  });
-});

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

@@ -1,25 +0,0 @@
-import { store } from 'redux/store';
-import { createTopicAction } from 'redux/actions';
-import * as selectors from 'redux/reducers/alerts/selectors';
-
-import { failurePayload1, failurePayload2 } from './fixtures';
-
-describe('Alerts selectors', () => {
-  describe('Initial State', () => {
-    it('returns empty alert list', () => {
-      expect(selectors.getAlerts(store.getState())).toEqual([]);
-    });
-  });
-
-  describe('state', () => {
-    beforeAll(() => {
-      store.dispatch(createTopicAction.failure({ alert: failurePayload1 }));
-      store.dispatch(createTopicAction.failure({ alert: failurePayload2 }));
-    });
-
-    it('returns fetch status', () => {
-      const alerts = selectors.getAlerts(store.getState());
-      expect(alerts.length).toEqual(2);
-    });
-  });
-});

+ 0 - 22
kafka-ui-react-app/src/redux/reducers/alerts/reducer.ts

@@ -1,22 +0,0 @@
-import { getType } from 'typesafe-actions';
-import { dismissAlert } from 'redux/actions';
-import { Action, AlertsState } from 'redux/interfaces';
-
-import { addError, removeAlert } from './utils';
-
-export const initialState: AlertsState = {};
-
-// eslint-disable-next-line @typescript-eslint/default-param-last
-const reducer = (state = initialState, action: Action): AlertsState => {
-  const { type } = action;
-
-  if (type.endsWith('__FAILURE')) return addError(state, action);
-
-  if (type === getType(dismissAlert)) {
-    return removeAlert(state, action);
-  }
-
-  return state;
-};
-
-export default reducer;

+ 0 - 9
kafka-ui-react-app/src/redux/reducers/alerts/selectors.ts

@@ -1,9 +0,0 @@
-import { createSelector } from '@reduxjs/toolkit';
-import { RootState, AlertsState } from 'redux/interfaces';
-import { orderBy } from 'lodash';
-
-const alertsState = ({ legacyAlerts }: RootState): AlertsState => legacyAlerts;
-
-export const getAlerts = createSelector(alertsState, (alerts) =>
-  orderBy(Object.values(alerts), 'createdAt', 'desc')
-);

+ 0 - 37
kafka-ui-react-app/src/redux/reducers/alerts/utils.ts

@@ -1,37 +0,0 @@
-import { now, omit } from 'lodash';
-import { Action, AlertsState, Alert } from 'redux/interfaces';
-
-export const addError = (state: AlertsState, action: Action) => {
-  if (
-    'payload' in action &&
-    typeof action.payload === 'object' &&
-    'alert' in action.payload &&
-    action.payload.alert !== undefined
-  ) {
-    const { subject, title, message, response } = action.payload.alert;
-
-    const id = `${action.type}-${subject}`;
-
-    return {
-      ...state,
-      [id]: {
-        id,
-        type: 'error',
-        title,
-        message,
-        response,
-        createdAt: now(),
-      } as Alert,
-    };
-  }
-
-  return { ...state };
-};
-
-export const removeAlert = (state: AlertsState, action: Action) => {
-  if ('payload' in action && typeof action.payload === 'string') {
-    return omit(state, action.payload);
-  }
-
-  return { ...state };
-};

+ 4 - 9
kafka-ui-react-app/src/redux/reducers/index.ts

@@ -5,13 +5,10 @@ import brokers from 'redux/reducers/brokers/brokersSlice';
 import alerts from 'redux/reducers/alerts/alertsSlice';
 import alerts from 'redux/reducers/alerts/alertsSlice';
 import schemas from 'redux/reducers/schemas/schemasSlice';
 import schemas from 'redux/reducers/schemas/schemasSlice';
 import connect from 'redux/reducers/connect/connectSlice';
 import connect from 'redux/reducers/connect/connectSlice';
-
-import topics from './topics/reducer';
-import topicMessages from './topicMessages/topicMessagesSlice';
-import consumerGroups from './consumerGroups/consumerGroupsSlice';
-import ksqlDb from './ksqlDb/ksqlDbSlice';
-import legacyLoader from './loader/reducer';
-import legacyAlerts from './alerts/reducer';
+import topicMessages from 'redux/reducers/topicMessages/topicMessagesSlice';
+import topics from 'redux/reducers/topics/topicsSlice';
+import consumerGroups from 'redux/reducers/consumerGroups/consumerGroupsSlice';
+import ksqlDb from 'redux/reducers/ksqlDb/ksqlDbSlice';
 
 
 export default combineReducers({
 export default combineReducers({
   loader,
   loader,
@@ -24,6 +21,4 @@ export default combineReducers({
   schemas,
   schemas,
   connect,
   connect,
   ksqlDb,
   ksqlDb,
-  legacyLoader,
-  legacyAlerts,
 });
 });

+ 0 - 46
kafka-ui-react-app/src/redux/reducers/loader/reducer.ts

@@ -1,46 +0,0 @@
-import { Action, LoaderState } from 'redux/interfaces';
-
-export const initialState: LoaderState = {};
-
-// eslint-disable-next-line @typescript-eslint/default-param-last
-const reducer = (state = initialState, action: Action): LoaderState => {
-  const { type } = action;
-  const splitType = type.split('__');
-  const requestState = splitType.pop();
-  const requestName = splitType.join('__');
-
-  // not a *__REQUEST / *__SUCCESS /  *__FAILURE /  *__CANCEL actions, so we ignore them
-  if (
-    requestState &&
-    !['REQUEST', 'SUCCESS', 'FAILURE', 'CANCEL'].includes(requestState)
-  ) {
-    return state;
-  }
-
-  switch (requestState) {
-    case 'REQUEST':
-      return {
-        ...state,
-        [requestName]: 'fetching',
-      };
-    case 'SUCCESS':
-      return {
-        ...state,
-        [requestName]: 'fetched',
-      };
-    case 'FAILURE':
-      return {
-        ...state,
-        [requestName]: 'errorFetching',
-      };
-    case 'CANCEL':
-      return {
-        ...state,
-        [requestName]: 'notFetched',
-      };
-    default:
-      return state;
-  }
-};
-
-export default reducer;

+ 0 - 4
kafka-ui-react-app/src/redux/reducers/loader/selectors.ts

@@ -1,8 +1,4 @@
 import { RootState } from 'redux/interfaces';
 import { RootState } from 'redux/interfaces';
 
 
-export const createLeagcyFetchingSelector =
-  (action: string) => (state: RootState) =>
-    state.legacyLoader[action] || 'notFetched';
-
 export const createFetchingSelector = (action: string) => (state: RootState) =>
 export const createFetchingSelector = (action: string) => (state: RootState) =>
   state.loader[action] || 'initial';
   state.loader[action] || 'initial';

+ 800 - 67
kafka-ui-react-app/src/redux/reducers/topics/__test__/reducer.spec.ts

@@ -2,17 +2,33 @@ import {
   MessageSchemaSourceEnum,
   MessageSchemaSourceEnum,
   SortOrder,
   SortOrder,
   TopicColumnsToSort,
   TopicColumnsToSort,
+  ConfigSource,
 } from 'generated-sources';
 } from 'generated-sources';
+import reducer, {
+  clearTopicsMessages,
+  setTopicsSearch,
+  setTopicsOrderBy,
+  fetchTopicConsumerGroups,
+  fetchTopicMessageSchema,
+  recreateTopic,
+  createTopic,
+  deleteTopic,
+  fetchTopicsList,
+  fetchTopicDetails,
+  fetchTopicConfig,
+  updateTopic,
+  updateTopicPartitionsCount,
+  updateTopicReplicationFactor,
+  deleteTopics,
+} from 'redux/reducers/topics/topicsSlice';
 import {
 import {
-  deleteTopicAction,
-  clearMessagesTopicAction,
-  setTopicsSearchAction,
-  setTopicsOrderByAction,
-  fetchTopicConsumerGroupsAction,
-  fetchTopicMessageSchemaAction,
-  recreateTopicAction,
-} from 'redux/actions';
-import reducer from 'redux/reducers/topics/reducer';
+  createTopicPayload,
+  createTopicResponsePayload,
+} from 'components/Topics/New/__test__/fixtures';
+import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures';
+import fetchMock from 'fetch-mock-jest';
+import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator';
+import { getTypeAndPayload } from 'lib/testHelpers';
 
 
 const topic = {
 const topic = {
   name: 'topic',
   name: 'topic',
@@ -68,6 +84,51 @@ const messageSchema = {
   },
   },
 };
 };
 
 
+const config = [
+  {
+    name: 'compression.type',
+    value: 'producer',
+    defaultValue: 'producer',
+    source: ConfigSource.DYNAMIC_TOPIC_CONFIG,
+    isSensitive: false,
+    isReadOnly: false,
+    synonyms: [
+      {
+        name: 'compression.type',
+        value: 'producer',
+        source: ConfigSource.DYNAMIC_TOPIC_CONFIG,
+      },
+      {
+        name: 'compression.type',
+        value: 'producer',
+        source: ConfigSource.DEFAULT_CONFIG,
+      },
+    ],
+  },
+];
+const details = {
+  name: 'local',
+  internal: false,
+  partitionCount: 1,
+  replicationFactor: 1,
+  replicas: 1,
+  inSyncReplicas: 1,
+  segmentSize: 0,
+  segmentCount: 0,
+  cleanUpPolicy: 'DELETE',
+  partitions: [
+    {
+      partition: 0,
+      leader: 1,
+      replicas: [{ broker: 1, leader: false, inSync: true }],
+      offsetMax: 0,
+      offsetMin: 0,
+    },
+  ],
+  bytesInPerSec: 0.1,
+  bytesOutPerSec: 0.1,
+};
+
 let state = {
 let state = {
   byName: {
   byName: {
     [topic.name]: topic,
     [topic.name]: topic,
@@ -80,84 +141,756 @@ let state = {
   sortOrder: SortOrder.ASC,
   sortOrder: SortOrder.ASC,
   consumerGroups: [],
   consumerGroups: [],
 };
 };
+const clusterName = 'local';
 
 
-describe('topics reducer', () => {
-  describe('delete topic', () => {
-    it('deletes the topic from the list on DELETE_TOPIC__SUCCESS', () => {
-      expect(reducer(state, deleteTopicAction.success(topic.name))).toEqual({
-        ...state,
-        byName: {},
-        allNames: [],
-        consumerGroups: [],
+describe('topics Slice', () => {
+  describe('topics reducer', () => {
+    describe('fetch topic details', () => {
+      it('fetchTopicDetails/fulfilled', () => {
+        expect(
+          reducer(state, {
+            type: fetchTopicDetails.fulfilled,
+            payload: {
+              clusterName,
+              topicName: topic.name,
+              topicDetails: details,
+            },
+          })
+        ).toEqual({
+          ...state,
+          byName: {
+            [topic.name]: {
+              ...topic,
+              ...details,
+            },
+          },
+          allNames: [topic.name],
+        });
+      });
+    });
+    describe('fetch topics', () => {
+      it('fetchTopicsList/fulfilled', () => {
+        expect(
+          reducer(state, {
+            type: fetchTopicsList.fulfilled,
+            payload: { clusterName, topicName: topic.name },
+          })
+        ).toEqual({
+          ...state,
+          byName: { topic },
+          allNames: [topic.name],
+        });
+      });
+    });
+    describe('fetch topic config', () => {
+      it('fetchTopicConfig/fulfilled', () => {
+        expect(
+          reducer(state, {
+            type: fetchTopicConfig.fulfilled,
+            payload: {
+              clusterName,
+              topicName: topic.name,
+              topicConfig: config,
+            },
+          })
+        ).toEqual({
+          ...state,
+          byName: {
+            [topic.name]: {
+              ...topic,
+              config: config.map((conf) => ({ ...conf })),
+            },
+          },
+          allNames: [topic.name],
+        });
+      });
+    });
+    describe('update topic', () => {
+      it('updateTopic/fulfilled', () => {
+        const updatedTopic = {
+          name: 'topic',
+          id: 'id',
+          partitions: 1,
+        };
+        expect(
+          reducer(state, {
+            type: updateTopic.fulfilled,
+            payload: {
+              clusterName,
+              topicName: topic.name,
+              topic: updatedTopic,
+            },
+          })
+        ).toEqual({
+          ...state,
+          byName: {
+            [topic.name]: {
+              ...updatedTopic,
+            },
+          },
+        });
       });
       });
     });
     });
+    describe('delete topic', () => {
+      it('deleteTopic/fulfilled', () => {
+        expect(
+          reducer(state, {
+            type: deleteTopic.fulfilled,
+            payload: { clusterName, topicName: topic.name },
+          })
+        ).toEqual({
+          ...state,
+          byName: {},
+          allNames: [],
+        });
+      });
 
 
-    it('delete topic messages on CLEAR_TOPIC_MESSAGES__SUCCESS', () => {
-      expect(reducer(state, clearMessagesTopicAction.success())).toEqual(state);
+      it('clearTopicsMessages/fulfilled', () => {
+        expect(
+          reducer(state, {
+            type: clearTopicsMessages.fulfilled,
+            payload: { clusterName, topicName: topic.name },
+          })
+        ).toEqual({
+          ...state,
+          messages: [],
+        });
+      });
+
+      it('recreateTopic/fulfilled', () => {
+        expect(
+          reducer(state, {
+            type: recreateTopic.fulfilled,
+            payload: { topic, topicName: topic.name },
+          })
+        ).toEqual({
+          ...state,
+          byName: {
+            [topic.name]: topic,
+          },
+        });
+      });
     });
     });
 
 
-    it('recreate topic', () => {
-      expect(reducer(state, recreateTopicAction.success(topic))).toEqual({
-        ...state,
-        byName: {
-          [topic.name]: topic,
-        },
+    describe('create topics', () => {
+      it('createTopic/fulfilled', () => {
+        expect(
+          reducer(state, {
+            type: createTopic.fulfilled,
+            payload: { clusterName, data: createTopicPayload },
+          })
+        ).toEqual({
+          ...state,
+        });
       });
       });
     });
     });
-  });
 
 
-  describe('search topics', () => {
-    it('sets the search string', () => {
-      expect(reducer(state, setTopicsSearchAction('test'))).toEqual({
-        ...state,
-        search: 'test',
+    describe('search topics', () => {
+      it('setTopicsSearch', () => {
+        expect(
+          reducer(state, {
+            type: setTopicsSearch,
+            payload: 'test',
+          })
+        ).toEqual({
+          ...state,
+          search: 'test',
+        });
       });
       });
     });
     });
-  });
 
 
-  describe('order topics', () => {
-    it('sets the orderBy', () => {
-      expect(
-        reducer(state, setTopicsOrderByAction(TopicColumnsToSort.NAME))
-      ).toEqual({
-        ...state,
-        orderBy: TopicColumnsToSort.NAME,
+    describe('order topics', () => {
+      it('setTopicsOrderBy', () => {
+        expect(
+          reducer(state, {
+            type: setTopicsOrderBy,
+            payload: TopicColumnsToSort.NAME,
+          })
+        ).toEqual({
+          ...state,
+          orderBy: TopicColumnsToSort.NAME,
+        });
       });
       });
     });
     });
-  });
 
 
-  describe('topic consumer groups', () => {
-    it('GET_TOPIC_CONSUMER_GROUPS__SUCCESS', () => {
-      expect(
-        reducer(state, fetchTopicConsumerGroupsAction.success(state))
-      ).toEqual(state);
+    describe('topic consumer groups', () => {
+      it('fetchTopicConsumerGroups/fulfilled', () => {
+        expect(
+          reducer(state, {
+            type: fetchTopicConsumerGroups.fulfilled,
+            payload: {
+              clusterName,
+              topicName: topic.name,
+              consumerGroups: consumerGroupPayload,
+            },
+          })
+        ).toEqual({
+          ...state,
+          byName: {
+            [topic.name]: {
+              ...topic,
+              ...consumerGroupPayload,
+            },
+          },
+        });
+      });
+    });
+
+    describe('message sending', () => {
+      it('fetchTopicMessageSchema/fulfilled', () => {
+        state = {
+          byName: {
+            [topic.name]: topic,
+          },
+          allNames: [topic.name],
+          messages: [],
+          totalPages: 1,
+          search: '',
+          orderBy: null,
+          sortOrder: SortOrder.ASC,
+          consumerGroups: [],
+        };
+        expect(
+          reducer(state, {
+            type: fetchTopicMessageSchema.fulfilled,
+            payload: { topicName: topic.name, schema: messageSchema },
+          }).byName
+        ).toEqual({
+          [topic.name]: { ...topic, messageSchema },
+        });
+      });
     });
     });
   });
   });
+  describe('Thunks', () => {
+    const store = mockStoreCreator;
+    const topicName = topic.name;
 
 
-  describe('message sending', () => {
-    it('adds message shema after fetching it', () => {
-      state = {
-        byName: {
-          [topic.name]: topic,
+    afterEach(() => {
+      fetchMock.restore();
+      store.clearActions();
+    });
+    describe('fetchTopicsList', () => {
+      const topicResponse = {
+        pageCount: 1,
+        topics: [createTopicResponsePayload],
+      };
+      it('fetchTopicsList/fulfilled', async () => {
+        fetchMock.getOnce(`/api/clusters/${clusterName}/topics`, topicResponse);
+        await store.dispatch(fetchTopicsList({ clusterName }));
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: fetchTopicsList.pending.type },
+          {
+            type: fetchTopicsList.fulfilled.type,
+            payload: { ...topicResponse },
+          },
+        ]);
+      });
+      it('fetchTopicsList/rejected', async () => {
+        fetchMock.getOnce(`/api/clusters/${clusterName}/topics`, 404);
+        await store.dispatch(fetchTopicsList({ clusterName }));
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: fetchTopicsList.pending.type },
+          {
+            type: fetchTopicsList.rejected.type,
+            payload: {
+              status: 404,
+              statusText: 'Not Found',
+              url: `/api/clusters/${clusterName}/topics`,
+              message: undefined,
+            },
+          },
+        ]);
+      });
+    });
+    describe('fetchTopicDetails', () => {
+      it('fetchTopicDetails/fulfilled', async () => {
+        fetchMock.getOnce(
+          `/api/clusters/${clusterName}/topics/${topicName}`,
+          details
+        );
+        await store.dispatch(fetchTopicDetails({ clusterName, topicName }));
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: fetchTopicDetails.pending.type },
+          {
+            type: fetchTopicDetails.fulfilled.type,
+            payload: { topicDetails: { ...details }, topicName },
+          },
+        ]);
+      });
+      it('fetchTopicDetails/rejected', async () => {
+        fetchMock.getOnce(
+          `/api/clusters/${clusterName}/topics/${topicName}`,
+          404
+        );
+        await store.dispatch(fetchTopicDetails({ clusterName, topicName }));
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: fetchTopicDetails.pending.type },
+          {
+            type: fetchTopicDetails.rejected.type,
+            payload: {
+              status: 404,
+              statusText: 'Not Found',
+              url: `/api/clusters/${clusterName}/topics/${topicName}`,
+              message: undefined,
+            },
+          },
+        ]);
+      });
+    });
+    describe('fetchTopicConfig', () => {
+      it('fetchTopicConfig/fulfilled', async () => {
+        fetchMock.getOnce(
+          `/api/clusters/${clusterName}/topics/${topicName}/config`,
+          config
+        );
+        await store.dispatch(fetchTopicConfig({ clusterName, topicName }));
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: fetchTopicConfig.pending.type },
+          {
+            type: fetchTopicConfig.fulfilled.type,
+            payload: {
+              topicConfig: config,
+              topicName,
+            },
+          },
+        ]);
+      });
+      it('fetchTopicConfig/rejected', async () => {
+        fetchMock.getOnce(
+          `/api/clusters/${clusterName}/topics/${topicName}/config`,
+          404
+        );
+        await store.dispatch(fetchTopicConfig({ clusterName, topicName }));
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: fetchTopicConfig.pending.type },
+          {
+            type: fetchTopicConfig.rejected.type,
+            payload: {
+              status: 404,
+              statusText: 'Not Found',
+              url: `/api/clusters/${clusterName}/topics/${topicName}/config`,
+              message: undefined,
+            },
+          },
+        ]);
+      });
+    });
+    describe('deleteTopic', () => {
+      it('deleteTopic/fulfilled', async () => {
+        fetchMock.deleteOnce(
+          `/api/clusters/${clusterName}/topics/${topicName}`,
+          topicName
+        );
+        await store.dispatch(deleteTopic({ clusterName, topicName }));
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: deleteTopic.pending.type },
+          {
+            type: deleteTopic.fulfilled.type,
+            payload: { topicName },
+          },
+        ]);
+      });
+      it('deleteTopic/rejected', async () => {
+        fetchMock.deleteOnce(
+          `/api/clusters/${clusterName}/topics/${topicName}`,
+          404
+        );
+        await store.dispatch(deleteTopic({ clusterName, topicName }));
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: deleteTopic.pending.type },
+          {
+            type: deleteTopic.rejected.type,
+            payload: {
+              status: 404,
+              statusText: 'Not Found',
+              url: `/api/clusters/${clusterName}/topics/${topicName}`,
+              message: undefined,
+            },
+          },
+        ]);
+      });
+    });
+    describe('deleteTopics', () => {
+      it('deleteTopics/fulfilled', async () => {
+        fetchMock.delete(`/api/clusters/${clusterName}/topics/${topicName}`, [
+          topicName,
+          'topic2',
+        ]);
+        await store.dispatch(
+          deleteTopics({ clusterName, topicNames: [topicName, 'topic2'] })
+        );
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: deleteTopics.pending.type },
+          { type: deleteTopic.pending.type },
+          { type: deleteTopic.pending.type },
+          { type: deleteTopics.fulfilled.type },
+        ]);
+      });
+    });
+    describe('recreateTopic', () => {
+      const recreateResponse = {
+        cleanUpPolicy: 'DELETE',
+        inSyncReplicas: 1,
+        internal: false,
+        name: topicName,
+        partitionCount: 1,
+        partitions: undefined,
+        replicas: 1,
+        replicationFactor: 1,
+        segmentCount: 0,
+        segmentSize: 0,
+        underReplicatedPartitions: undefined,
+      };
+      it('recreateTopic/fulfilled', async () => {
+        fetchMock.postOnce(
+          `/api/clusters/${clusterName}/topics/${topicName}`,
+          recreateResponse
+        );
+        await store.dispatch(recreateTopic({ clusterName, topicName }));
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: recreateTopic.pending.type },
+          {
+            type: recreateTopic.fulfilled.type,
+            payload: { [topicName]: { ...recreateResponse } },
+          },
+        ]);
+      });
+      it('recreateTopic/rejected', async () => {
+        fetchMock.postOnce(
+          `/api/clusters/${clusterName}/topics/${topicName}`,
+          404
+        );
+        await store.dispatch(recreateTopic({ clusterName, topicName }));
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: recreateTopic.pending.type },
+          {
+            type: recreateTopic.rejected.type,
+            payload: {
+              status: 404,
+              statusText: 'Not Found',
+              url: `/api/clusters/${clusterName}/topics/${topicName}`,
+              message: undefined,
+            },
+          },
+        ]);
+      });
+    });
+    describe('fetchTopicConsumerGroups', () => {
+      const consumerGroups = [
+        {
+          groupId: 'groupId1',
+          members: 0,
+          topics: 1,
+          simple: false,
+          partitionAssignor: '',
+          coordinator: {
+            id: 1,
+            port: undefined,
+            host: 'host',
+          },
+          messagesBehind: undefined,
+          state: undefined,
+        },
+        {
+          groupId: 'groupId2',
+          members: 0,
+          topics: 1,
+          simple: false,
+          partitionAssignor: '',
+          coordinator: {
+            id: 1,
+            port: undefined,
+            host: 'host',
+          },
+          messagesBehind: undefined,
+          state: undefined,
+        },
+      ];
+      it('fetchTopicConsumerGroups/fulfilled', async () => {
+        fetchMock.getOnce(
+          `/api/clusters/${clusterName}/topics/${topicName}/consumer-groups`,
+          consumerGroups
+        );
+        await store.dispatch(
+          fetchTopicConsumerGroups({ clusterName, topicName })
+        );
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: fetchTopicConsumerGroups.pending.type },
+          {
+            type: fetchTopicConsumerGroups.fulfilled.type,
+            payload: { consumerGroups, topicName },
+          },
+        ]);
+      });
+      it('fetchTopicConsumerGroups/rejected', async () => {
+        fetchMock.getOnce(
+          `/api/clusters/${clusterName}/topics/${topicName}/consumer-groups`,
+          404
+        );
+        await store.dispatch(
+          fetchTopicConsumerGroups({ clusterName, topicName })
+        );
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: fetchTopicConsumerGroups.pending.type },
+          {
+            type: fetchTopicConsumerGroups.rejected.type,
+            payload: {
+              status: 404,
+              statusText: 'Not Found',
+              url: `/api/clusters/${clusterName}/topics/${topicName}/consumer-groups`,
+              message: undefined,
+            },
+          },
+        ]);
+      });
+    });
+    describe('updateTopicPartitionsCount', () => {
+      it('updateTopicPartitionsCount/fulfilled', async () => {
+        fetchMock.patchOnce(
+          `/api/clusters/${clusterName}/topics/${topicName}/partitions`,
+          { message: 'success' }
+        );
+        await store.dispatch(
+          updateTopicPartitionsCount({
+            clusterName,
+            topicName,
+            partitions: 1,
+          })
+        );
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: updateTopicPartitionsCount.pending.type },
+          {
+            type: updateTopicPartitionsCount.fulfilled.type,
+          },
+        ]);
+      });
+      it('updateTopicPartitionsCount/rejected', async () => {
+        fetchMock.patchOnce(
+          `/api/clusters/${clusterName}/topics/${topicName}/partitions`,
+          404
+        );
+        await store.dispatch(
+          updateTopicPartitionsCount({
+            clusterName,
+            topicName,
+            partitions: 1,
+          })
+        );
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: updateTopicPartitionsCount.pending.type },
+          {
+            type: updateTopicPartitionsCount.rejected.type,
+            payload: {
+              status: 404,
+              statusText: 'Not Found',
+              url: `/api/clusters/${clusterName}/topics/${topicName}/partitions`,
+              message: undefined,
+            },
+          },
+        ]);
+      });
+    });
+    describe('updateTopicReplicationFactor', () => {
+      it('updateTopicReplicationFactor/fulfilled', async () => {
+        fetchMock.patchOnce(
+          `/api/clusters/${clusterName}/topics/${topicName}/replications`,
+          { message: 'success' }
+        );
+        await store.dispatch(
+          updateTopicReplicationFactor({
+            clusterName,
+            topicName,
+            replicationFactor: 1,
+          })
+        );
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: updateTopicReplicationFactor.pending.type },
+          {
+            type: updateTopicReplicationFactor.fulfilled.type,
+          },
+        ]);
+      });
+      it('updateTopicReplicationFactor/rejected', async () => {
+        fetchMock.patchOnce(
+          `/api/clusters/${clusterName}/topics/${topicName}/replications`,
+          404
+        );
+        await store.dispatch(
+          updateTopicReplicationFactor({
+            clusterName,
+            topicName,
+            replicationFactor: 1,
+          })
+        );
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: updateTopicReplicationFactor.pending.type },
+          {
+            type: updateTopicReplicationFactor.rejected.type,
+            payload: {
+              status: 404,
+              statusText: 'Not Found',
+              url: `/api/clusters/${clusterName}/topics/${topicName}/replications`,
+              message: undefined,
+            },
+          },
+        ]);
+      });
+    });
+    describe('createTopic', () => {
+      const newTopic = {
+        name: 'newTopic',
+        partitions: 0,
+        replicationFactor: 0,
+        minInsyncReplicas: 0,
+        cleanupPolicy: 'DELETE',
+        retentionMs: 1,
+        retentionBytes: 1,
+        maxMessageBytes: 1,
+        customParams: [
+          {
+            name: '',
+            value: '',
+          },
+        ],
+      };
+      it('createTopic/fulfilled', async () => {
+        fetchMock.postOnce(`/api/clusters/${clusterName}/topics`, {
+          message: 'success',
+        });
+        await store.dispatch(
+          createTopic({
+            clusterName,
+            data: newTopic,
+          })
+        );
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: createTopic.pending.type },
+          {
+            type: createTopic.fulfilled.type,
+          },
+        ]);
+      });
+      it('createTopic/rejected', async () => {
+        fetchMock.postOnce(`/api/clusters/${clusterName}/topics`, 404);
+        await store.dispatch(
+          createTopic({
+            clusterName,
+            data: newTopic,
+          })
+        );
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: createTopic.pending.type },
+          {
+            type: createTopic.rejected.type,
+            payload: {
+              status: 404,
+              statusText: 'Not Found',
+              url: `/api/clusters/${clusterName}/topics`,
+              message: undefined,
+            },
+          },
+        ]);
+      });
+    });
+    describe('updateTopic', () => {
+      const updateTopicResponse = {
+        name: topicName,
+        partitions: 0,
+        replicationFactor: 0,
+        minInsyncReplicas: 0,
+        cleanupPolicy: 'DELETE',
+        retentionMs: 0,
+        retentionBytes: 0,
+        maxMessageBytes: 0,
+        customParams: {
+          byIndex: {},
+          allIndexes: [],
         },
         },
-        allNames: [topic.name],
-        messages: [],
-        totalPages: 1,
-        search: '',
-        orderBy: null,
-        sortOrder: SortOrder.ASC,
-        consumerGroups: [],
       };
       };
-      expect(
-        reducer(
-          state,
-          fetchTopicMessageSchemaAction.success({
-            topicName: 'topic',
-            schema: messageSchema,
-          })
-        ).byName
-      ).toEqual({
-        [topic.name]: { ...topic, messageSchema },
+      it('updateTopic/fulfilled', async () => {
+        fetchMock.patchOnce(
+          `/api/clusters/${clusterName}/topics/${topicName}`,
+          createTopicResponsePayload
+        );
+        await store.dispatch(
+          updateTopic({
+            clusterName,
+            topicName,
+            form: updateTopicResponse,
+          })
+        );
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: updateTopic.pending.type },
+          {
+            type: updateTopic.fulfilled.type,
+            payload: { [topicName]: { ...createTopicResponsePayload } },
+          },
+        ]);
+      });
+      it('updateTopic/rejected', async () => {
+        fetchMock.patchOnce(
+          `/api/clusters/${clusterName}/topics/${topicName}`,
+          404
+        );
+        await store.dispatch(
+          updateTopic({
+            clusterName,
+            topicName,
+            form: updateTopicResponse,
+          })
+        );
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: updateTopic.pending.type },
+          {
+            type: updateTopic.rejected.type,
+            payload: {
+              status: 404,
+              statusText: 'Not Found',
+              url: `/api/clusters/${clusterName}/topics/${topicName}`,
+              message: undefined,
+            },
+          },
+        ]);
+      });
+    });
+    describe('clearTopicsMessages', () => {
+      it('clearTopicsMessages/fulfilled', async () => {
+        fetchMock.deleteOnce(
+          `/api/clusters/${clusterName}/topics/${topicName}/messages`,
+          [topicName, 'topic2']
+        );
+        await store.dispatch(
+          clearTopicsMessages({
+            clusterName,
+            topicNames: [topicName, 'topic2'],
+          })
+        );
+
+        expect(getTypeAndPayload(store)).toEqual([
+          { type: clearTopicsMessages.pending.type },
+          { type: clearTopicsMessages.fulfilled.type },
+        ]);
       });
       });
     });
     });
   });
   });

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

@@ -0,0 +1,35 @@
+import { store } from 'redux/store';
+import * as selectors from 'redux/reducers/topics/selectors';
+
+describe('Topics selectors', () => {
+  describe('Initial State', () => {
+    it('returns initial values', () => {
+      expect(selectors.getTopicListTotalPages(store.getState())).toEqual(1);
+      expect(selectors.getIsTopicDeleted(store.getState())).toBeFalsy();
+      expect(selectors.getAreTopicsFetching(store.getState())).toEqual(false);
+      expect(selectors.getAreTopicsFetched(store.getState())).toEqual(false);
+      expect(selectors.getIsTopicDetailsFetching(store.getState())).toEqual(
+        false
+      );
+      expect(selectors.getIsTopicDetailsFetched(store.getState())).toEqual(
+        false
+      );
+      expect(selectors.getTopicConfigFetched(store.getState())).toEqual(false);
+      expect(selectors.getTopicCreated(store.getState())).toEqual(false);
+      expect(selectors.getTopicUpdated(store.getState())).toEqual(false);
+      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);
+      expect(selectors.getTopicList(store.getState())).toEqual([]);
+    });
+  });
+});

+ 0 - 73
kafka-ui-react-app/src/redux/reducers/topics/reducer.ts

@@ -1,73 +0,0 @@
-import { Action, TopicsState } from 'redux/interfaces';
-import { getType } from 'typesafe-actions';
-import * as actions from 'redux/actions';
-import * as _ from 'lodash';
-import { SortOrder, TopicColumnsToSort } from 'generated-sources';
-
-export const initialState: TopicsState = {
-  byName: {},
-  allNames: [],
-  totalPages: 1,
-  search: '',
-  orderBy: TopicColumnsToSort.NAME,
-  sortOrder: SortOrder.ASC,
-  consumerGroups: [],
-};
-
-// eslint-disable-next-line @typescript-eslint/default-param-last
-const reducer = (state = initialState, action: Action): TopicsState => {
-  switch (action.type) {
-    case getType(actions.fetchTopicsListAction.success):
-    case getType(actions.fetchTopicDetailsAction.success):
-    case getType(actions.fetchTopicConfigAction.success):
-    case getType(actions.createTopicAction.success):
-    case getType(actions.fetchTopicConsumerGroupsAction.success):
-    case getType(actions.updateTopicAction.success):
-      return action.payload;
-    case getType(actions.deleteTopicAction.success): {
-      const newState: TopicsState = { ...state };
-      delete newState.byName[action.payload];
-      newState.allNames = newState.allNames.filter(
-        (name) => name !== action.payload
-      );
-      return newState;
-    }
-    case getType(actions.recreateTopicAction.success):
-      return {
-        ...state,
-        byName: {
-          ...state.byName,
-          [action.payload.name]: { ...action.payload },
-        },
-      };
-    case getType(actions.setTopicsSearchAction): {
-      return {
-        ...state,
-        search: action.payload,
-      };
-    }
-    case getType(actions.setTopicsOrderByAction): {
-      return {
-        ...state,
-        orderBy: action.payload,
-        sortOrder:
-          state.orderBy === action.payload && state.sortOrder === SortOrder.ASC
-            ? SortOrder.DESC
-            : SortOrder.ASC,
-      };
-    }
-    case getType(actions.fetchTopicMessageSchemaAction.success): {
-      const { topicName, schema } = action.payload;
-      const newState = _.cloneDeep(state);
-      newState.byName[topicName] = {
-        ...newState.byName[topicName],
-        messageSchema: schema,
-      };
-      return newState;
-    }
-    default:
-      return state;
-  }
-};
-
-export default reducer;

+ 61 - 37
kafka-ui-react-app/src/redux/reducers/topics/selectors.ts

@@ -6,7 +6,19 @@ import {
   TopicConfigByName,
   TopicConfigByName,
 } from 'redux/interfaces';
 } from 'redux/interfaces';
 import { CleanUpPolicy } from 'generated-sources';
 import { CleanUpPolicy } from 'generated-sources';
-import { createLeagcyFetchingSelector } from 'redux/reducers/loader/selectors';
+import { createFetchingSelector } from 'redux/reducers/loader/selectors';
+import {
+  fetchTopicsList,
+  fetchTopicDetails,
+  fetchTopicConfig,
+  updateTopic,
+  fetchTopicMessageSchema,
+  fetchTopicConsumerGroups,
+  createTopic,
+  deleteTopic,
+  updateTopicPartitionsCount,
+  updateTopicReplicationFactor,
+} from 'redux/reducers/topics/topicsSlice';
 
 
 const topicsState = ({ topics }: RootState): TopicsState => topics;
 const topicsState = ({ topics }: RootState): TopicsState => topics;
 
 
@@ -16,85 +28,97 @@ const getTopicMap = (state: RootState) => topicsState(state).byName;
 export const getTopicListTotalPages = (state: RootState) =>
 export const getTopicListTotalPages = (state: RootState) =>
   topicsState(state).totalPages;
   topicsState(state).totalPages;
 
 
-const getTopicListFetchingStatus = createLeagcyFetchingSelector('GET_TOPICS');
-const getTopicDetailsFetchingStatus =
-  createLeagcyFetchingSelector('GET_TOPIC_DETAILS');
-
-const getTopicConfigFetchingStatus =
-  createLeagcyFetchingSelector('GET_TOPIC_CONFIG');
-const getTopicCreationStatus = createLeagcyFetchingSelector('POST_TOPIC');
-const getTopicUpdateStatus = createLeagcyFetchingSelector('PATCH_TOPIC');
-const getTopicMessageSchemaFetchingStatus =
-  createLeagcyFetchingSelector('GET_TOPIC_SCHEMA');
-const getPartitionsCountIncreaseStatus =
-  createLeagcyFetchingSelector('UPDATE_PARTITIONS');
-const getReplicationFactorUpdateStatus = createLeagcyFetchingSelector(
-  'UPDATE_REPLICATION_FACTOR'
-);
-const getTopicDeletingStatus = createLeagcyFetchingSelector('DELETE_TOPIC');
-
-const getTopicConsumerGroupsStatus = createLeagcyFetchingSelector(
-  'GET_TOPIC_CONSUMER_GROUPS'
-);
+const getTopicDeletingStatus = createFetchingSelector(deleteTopic.typePrefix);
 
 
 export const getIsTopicDeleted = createSelector(
 export const getIsTopicDeleted = createSelector(
   getTopicDeletingStatus,
   getTopicDeletingStatus,
-  (status) => status === 'fetched'
+  (status) => status === 'fulfilled'
 );
 );
 
 
-export const getAreTopicsFetching = createSelector(
-  getTopicListFetchingStatus,
-  (status) => status === 'fetching' || status === 'notFetched'
+const getAreTopicsFetchingStatus = createFetchingSelector(
+  fetchTopicsList.typePrefix
 );
 );
 
 
+export const getAreTopicsFetching = createSelector(
+  getAreTopicsFetchingStatus,
+  (status) => status === 'pending'
+);
 export const getAreTopicsFetched = createSelector(
 export const getAreTopicsFetched = createSelector(
-  getTopicListFetchingStatus,
-  (status) => status === 'fetched'
+  getAreTopicsFetchingStatus,
+  (status) => status === 'fulfilled'
+);
+
+const getTopicDetailsFetchingStatus = createFetchingSelector(
+  fetchTopicDetails.typePrefix
 );
 );
 
 
 export const getIsTopicDetailsFetching = createSelector(
 export const getIsTopicDetailsFetching = createSelector(
   getTopicDetailsFetchingStatus,
   getTopicDetailsFetchingStatus,
-  (status) => status === 'notFetched' || status === 'fetching'
+  (status) => status === 'pending'
 );
 );
 
 
 export const getIsTopicDetailsFetched = createSelector(
 export const getIsTopicDetailsFetched = createSelector(
   getTopicDetailsFetchingStatus,
   getTopicDetailsFetchingStatus,
-  (status) => status === 'fetched'
+  (status) => status === 'fulfilled'
+);
+
+const getTopicConfigFetchingStatus = createFetchingSelector(
+  fetchTopicConfig.typePrefix
 );
 );
 
 
 export const getTopicConfigFetched = createSelector(
 export const getTopicConfigFetched = createSelector(
   getTopicConfigFetchingStatus,
   getTopicConfigFetchingStatus,
-  (status) => status === 'fetched'
+  (status) => status === 'fulfilled'
 );
 );
 
 
+const getTopicCreationStatus = createFetchingSelector(createTopic.typePrefix);
+
 export const getTopicCreated = createSelector(
 export const getTopicCreated = createSelector(
   getTopicCreationStatus,
   getTopicCreationStatus,
-  (status) => status === 'fetched'
+  (status) => status === 'fulfilled'
 );
 );
 
 
+const getTopicUpdateStatus = createFetchingSelector(updateTopic.typePrefix);
+
 export const getTopicUpdated = createSelector(
 export const getTopicUpdated = createSelector(
   getTopicUpdateStatus,
   getTopicUpdateStatus,
-  (status) => status === 'fetched'
+  (status) => status === 'fulfilled'
+);
+
+const getTopicMessageSchemaFetchingStatus = createFetchingSelector(
+  fetchTopicMessageSchema.typePrefix
 );
 );
 
 
 export const getTopicMessageSchemaFetched = createSelector(
 export const getTopicMessageSchemaFetched = createSelector(
   getTopicMessageSchemaFetchingStatus,
   getTopicMessageSchemaFetchingStatus,
-  (status) => status === 'fetched'
+  (status) => status === 'fulfilled'
+);
+
+const getPartitionsCountIncreaseStatus = createFetchingSelector(
+  updateTopicPartitionsCount.typePrefix
 );
 );
 
 
 export const getTopicPartitionsCountIncreased = createSelector(
 export const getTopicPartitionsCountIncreased = createSelector(
   getPartitionsCountIncreaseStatus,
   getPartitionsCountIncreaseStatus,
-  (status) => status === 'fetched'
+  (status) => status === 'fulfilled'
+);
+
+const getReplicationFactorUpdateStatus = createFetchingSelector(
+  updateTopicReplicationFactor.typePrefix
 );
 );
 
 
 export const getTopicReplicationFactorUpdated = createSelector(
 export const getTopicReplicationFactorUpdated = createSelector(
   getReplicationFactorUpdateStatus,
   getReplicationFactorUpdateStatus,
-  (status) => status === 'fetched'
+  (status) => status === 'fulfilled'
+);
+
+const getTopicConsumerGroupsStatus = createFetchingSelector(
+  fetchTopicConsumerGroups.typePrefix
 );
 );
 
 
 export const getTopicsConsumerGroupsFetched = createSelector(
 export const getTopicsConsumerGroupsFetched = createSelector(
   getTopicConsumerGroupsStatus,
   getTopicConsumerGroupsStatus,
-  (status) => status === 'fetched'
+  (status) => status === 'fulfilled'
 );
 );
 
 
 export const getTopicList = createSelector(
 export const getTopicList = createSelector(
@@ -184,5 +208,5 @@ export const getTopicConsumerGroups = createSelector(
 
 
 export const getMessageSchemaByTopicName = createSelector(
 export const getMessageSchemaByTopicName = createSelector(
   getTopicByName,
   getTopicByName,
-  (topic) => topic.messageSchema
+  (topic) => topic?.messageSchema
 );
 );

+ 419 - 0
kafka-ui-react-app/src/redux/reducers/topics/topicsSlice.ts

@@ -0,0 +1,419 @@
+import { v4 } from 'uuid';
+import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
+import {
+  Configuration,
+  TopicsApi,
+  ConsumerGroupsApi,
+  TopicsResponse,
+  TopicDetails,
+  GetTopicsRequest,
+  GetTopicDetailsRequest,
+  GetTopicConfigsRequest,
+  TopicConfig,
+  TopicCreation,
+  ConsumerGroup,
+  Topic,
+  TopicUpdate,
+  DeleteTopicRequest,
+  RecreateTopicRequest,
+  SortOrder,
+  TopicColumnsToSort,
+  MessagesApi,
+  GetTopicSchemaRequest,
+  TopicMessageSchema,
+} from 'generated-sources';
+import {
+  TopicsState,
+  TopicName,
+  TopicFormData,
+  TopicFormFormattedParams,
+  TopicFormDataRaw,
+  ClusterName,
+} from 'redux/interfaces';
+import { BASE_PARAMS } from 'lib/constants';
+import { getResponse } from 'lib/errorHandling';
+import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
+
+const apiClientConf = new Configuration(BASE_PARAMS);
+const topicsApiClient = new TopicsApi(apiClientConf);
+const topicConsumerGroupsApiClient = new ConsumerGroupsApi(apiClientConf);
+const messagesApiClient = new MessagesApi(apiClientConf);
+
+export const fetchTopicsList = createAsyncThunk<
+  TopicsResponse,
+  GetTopicsRequest
+>('topic/fetchTopicsList', async (payload, { rejectWithValue }) => {
+  try {
+    return await topicsApiClient.getTopics(payload);
+  } catch (err) {
+    return rejectWithValue(await getResponse(err as Response));
+  }
+});
+
+export const fetchTopicDetails = createAsyncThunk<
+  { topicDetails: TopicDetails; topicName: TopicName },
+  GetTopicDetailsRequest
+>('topic/fetchTopicDetails', async (payload, { rejectWithValue }) => {
+  try {
+    const { topicName } = payload;
+    const topicDetails = await topicsApiClient.getTopicDetails(payload);
+
+    return { topicDetails, topicName };
+  } catch (err) {
+    return rejectWithValue(await getResponse(err as Response));
+  }
+});
+
+export const fetchTopicConfig = createAsyncThunk<
+  { topicConfig: TopicConfig[]; topicName: TopicName },
+  GetTopicConfigsRequest
+>('topic/fetchTopicConfig', async (payload, { rejectWithValue }) => {
+  try {
+    const { topicName } = payload;
+    const topicConfig = await topicsApiClient.getTopicConfigs(payload);
+
+    return { topicConfig, topicName };
+  } catch (err) {
+    return rejectWithValue(await getResponse(err as Response));
+  }
+});
+
+const topicReducer = (
+  result: TopicFormFormattedParams,
+  customParam: TopicConfig
+) => {
+  return {
+    ...result,
+    [customParam.name]: customParam.value,
+  };
+};
+
+export const formatTopicCreation = (form: TopicFormData): TopicCreation => {
+  const {
+    name,
+    partitions,
+    replicationFactor,
+    cleanupPolicy,
+    retentionBytes,
+    retentionMs,
+    maxMessageBytes,
+    minInsyncReplicas,
+    customParams,
+  } = form;
+
+  return {
+    name,
+    partitions,
+    replicationFactor,
+    configs: {
+      'cleanup.policy': cleanupPolicy,
+      'retention.ms': retentionMs.toString(),
+      'retention.bytes': retentionBytes.toString(),
+      'max.message.bytes': maxMessageBytes.toString(),
+      'min.insync.replicas': minInsyncReplicas.toString(),
+      ...Object.values(customParams || {}).reduce(topicReducer, {}),
+    },
+  };
+};
+
+export const createTopic = createAsyncThunk<
+  undefined,
+  {
+    clusterName: ClusterName;
+    data: TopicFormData;
+  }
+>('topic/createTopic', async (payload, { rejectWithValue }) => {
+  try {
+    const { data, clusterName } = payload;
+    await topicsApiClient.createTopic({
+      clusterName,
+      topicCreation: formatTopicCreation(data),
+    });
+
+    return undefined;
+  } catch (err) {
+    return rejectWithValue(await getResponse(err as Response));
+  }
+});
+
+export const fetchTopicConsumerGroups = createAsyncThunk<
+  { consumerGroups: ConsumerGroup[]; topicName: TopicName },
+  GetTopicConfigsRequest
+>('topic/fetchTopicConsumerGroups', async (payload, { rejectWithValue }) => {
+  try {
+    const { topicName } = payload;
+    const consumerGroups =
+      await topicConsumerGroupsApiClient.getTopicConsumerGroups(payload);
+
+    return { consumerGroups, topicName };
+  } catch (err) {
+    return rejectWithValue(await getResponse(err as Response));
+  }
+});
+
+const formatTopicUpdate = (form: TopicFormDataRaw): TopicUpdate => {
+  const {
+    cleanupPolicy,
+    retentionBytes,
+    retentionMs,
+    maxMessageBytes,
+    minInsyncReplicas,
+    customParams,
+  } = form;
+
+  return {
+    configs: {
+      'cleanup.policy': cleanupPolicy,
+      'retention.ms': retentionMs,
+      'retention.bytes': retentionBytes,
+      'max.message.bytes': maxMessageBytes,
+      'min.insync.replicas': minInsyncReplicas,
+      ...Object.values(customParams || {}).reduce(topicReducer, {}),
+    },
+  };
+};
+
+export const updateTopic = createAsyncThunk<
+  { topic: Topic },
+  {
+    clusterName: ClusterName;
+    topicName: TopicName;
+    form: TopicFormDataRaw;
+  }
+>(
+  'topic/updateTopic',
+  async ({ clusterName, topicName, form }, { rejectWithValue }) => {
+    try {
+      const topic = await topicsApiClient.updateTopic({
+        clusterName,
+        topicName,
+        topicUpdate: formatTopicUpdate(form),
+      });
+
+      return { topic };
+    } catch (err) {
+      return rejectWithValue(await getResponse(err as Response));
+    }
+  }
+);
+
+export const deleteTopic = createAsyncThunk<
+  { topicName: TopicName },
+  DeleteTopicRequest
+>('topic/deleteTopic', async (payload, { rejectWithValue }) => {
+  try {
+    const { topicName } = payload;
+    await topicsApiClient.deleteTopic(payload);
+
+    return { topicName };
+  } catch (err) {
+    return rejectWithValue(await getResponse(err as Response));
+  }
+});
+
+export const recreateTopic = createAsyncThunk<
+  { topic: Topic },
+  RecreateTopicRequest
+>('topic/recreateTopic', async (payload, { rejectWithValue }) => {
+  try {
+    const topic = await topicsApiClient.recreateTopic(payload);
+    return { topic };
+  } catch (err) {
+    return rejectWithValue(await getResponse(err as Response));
+  }
+});
+
+export const fetchTopicMessageSchema = createAsyncThunk<
+  { schema: TopicMessageSchema; topicName: TopicName },
+  GetTopicSchemaRequest
+>('topic/fetchTopicMessageSchema', async (payload, { rejectWithValue }) => {
+  try {
+    const { topicName } = payload;
+    const schema = await messagesApiClient.getTopicSchema(payload);
+    return { schema, topicName };
+  } catch (err) {
+    return rejectWithValue(await getResponse(err as Response));
+  }
+});
+
+export const updateTopicPartitionsCount = createAsyncThunk<
+  undefined,
+  {
+    clusterName: ClusterName;
+    topicName: TopicName;
+    partitions: number;
+  }
+>('topic/updateTopicPartitionsCount', async (payload, { rejectWithValue }) => {
+  try {
+    const { clusterName, topicName, partitions } = payload;
+
+    await topicsApiClient.increaseTopicPartitions({
+      clusterName,
+      topicName,
+      partitionsIncrease: { totalPartitionsCount: partitions },
+    });
+
+    return undefined;
+  } catch (err) {
+    return rejectWithValue(await getResponse(err as Response));
+  }
+});
+
+export const updateTopicReplicationFactor = createAsyncThunk<
+  undefined,
+  {
+    clusterName: ClusterName;
+    topicName: TopicName;
+    replicationFactor: number;
+  }
+>(
+  'topic/updateTopicReplicationFactor',
+  async (payload, { rejectWithValue }) => {
+    try {
+      const { clusterName, topicName, replicationFactor } = payload;
+
+      await topicsApiClient.changeReplicationFactor({
+        clusterName,
+        topicName,
+        replicationFactorChange: { totalReplicationFactor: replicationFactor },
+      });
+
+      return undefined;
+    } catch (err) {
+      return rejectWithValue(await getResponse(err as Response));
+    }
+  }
+);
+
+export const deleteTopics = createAsyncThunk<
+  undefined,
+  {
+    clusterName: ClusterName;
+    topicNames: TopicName[];
+  }
+>('topic/deleteTopics', async (payload, { rejectWithValue, dispatch }) => {
+  try {
+    const { clusterName, topicNames } = payload;
+
+    topicNames.forEach((topicName) => {
+      dispatch(deleteTopic({ clusterName, topicName }));
+    });
+
+    return undefined;
+  } catch (err) {
+    return rejectWithValue(await getResponse(err as Response));
+  }
+});
+
+export const clearTopicsMessages = createAsyncThunk<
+  undefined,
+  {
+    clusterName: ClusterName;
+    topicNames: TopicName[];
+  }
+>('topic/clearTopicsMessages', async (payload, { rejectWithValue }) => {
+  try {
+    const { clusterName, topicNames } = payload;
+
+    topicNames.forEach((topicName) => {
+      clearTopicMessages({ clusterName, topicName });
+    });
+
+    return undefined;
+  } catch (err) {
+    return rejectWithValue(await getResponse(err as Response));
+  }
+});
+
+export const initialState: TopicsState = {
+  byName: {},
+  allNames: [],
+  totalPages: 1,
+  search: '',
+  orderBy: TopicColumnsToSort.NAME,
+  sortOrder: SortOrder.ASC,
+  consumerGroups: [],
+};
+
+const topicsSlice = createSlice({
+  name: 'topics',
+  initialState,
+  reducers: {
+    setTopicsSearch: (state, action) => {
+      state.search = action.payload;
+    },
+    setTopicsOrderBy: (state, action) => {
+      state.sortOrder =
+        state.orderBy === action.payload && state.sortOrder === SortOrder.ASC
+          ? SortOrder.DESC
+          : SortOrder.ASC;
+      state.orderBy = action.payload;
+    },
+  },
+  extraReducers: (builder) => {
+    builder.addCase(fetchTopicsList.fulfilled, (state, { payload }) => {
+      if (payload.topics) {
+        state.allNames = [];
+        state.totalPages = payload.pageCount || 1;
+
+        payload.topics.forEach((topic) => {
+          state.allNames.push(topic.name);
+          state.byName[topic.name] = {
+            ...state.byName[topic.name],
+            ...topic,
+            id: v4(),
+          };
+        });
+      }
+    });
+    builder.addCase(fetchTopicDetails.fulfilled, (state, { payload }) => {
+      state.byName[payload.topicName] = {
+        ...state.byName[payload.topicName],
+        ...payload.topicDetails,
+      };
+    });
+    builder.addCase(fetchTopicConfig.fulfilled, (state, { payload }) => {
+      state.byName[payload.topicName] = {
+        ...state.byName[payload.topicName],
+        config: payload.topicConfig,
+      };
+    });
+    builder.addCase(
+      fetchTopicConsumerGroups.fulfilled,
+      (state, { payload }) => {
+        state.byName[payload.topicName] = {
+          ...state.byName[payload.topicName],
+          ...payload.consumerGroups,
+        };
+      }
+    );
+    builder.addCase(updateTopic.fulfilled, (state, { payload }) => {
+      state.byName[payload.topic.name] = {
+        ...state.byName[payload.topic.name],
+        ...payload.topic,
+      };
+    });
+    builder.addCase(deleteTopic.fulfilled, (state, { payload }) => {
+      delete state.byName[payload.topicName];
+      state.allNames = state.allNames.filter(
+        (name) => name !== payload.topicName
+      );
+    });
+    builder.addCase(recreateTopic.fulfilled, (state, { payload }) => {
+      state.byName = {
+        ...state.byName,
+        [payload.topic.name]: { ...payload.topic },
+      };
+    });
+    builder.addCase(fetchTopicMessageSchema.fulfilled, (state, { payload }) => {
+      state.byName[payload.topicName] = {
+        ...state.byName[payload.topicName],
+        messageSchema: payload.schema,
+      };
+    });
+  },
+});
+
+export const { setTopicsSearch, setTopicsOrderBy } = topicsSlice.actions;
+
+export default topicsSlice.reducer;

+ 3 - 3
kafka-ui-react-app/src/redux/store/configureStore/mockStoreCreator.ts

@@ -1,10 +1,10 @@
 import configureMockStore, { MockStoreCreator } from 'redux-mock-store';
 import configureMockStore, { MockStoreCreator } from 'redux-mock-store';
 import thunk, { ThunkDispatch } from 'redux-thunk';
 import thunk, { ThunkDispatch } from 'redux-thunk';
-import { Middleware } from 'redux';
-import { RootState, Action } from 'redux/interfaces';
+import { AnyAction, Middleware } from 'redux';
+import { RootState } from 'redux/interfaces';
 
 
 const middlewares: Array<Middleware> = [thunk];
 const middlewares: Array<Middleware> = [thunk];
-type DispatchExts = ThunkDispatch<RootState, undefined, Action>;
+type DispatchExts = ThunkDispatch<RootState, undefined, AnyAction>;
 
 
 const mockStoreCreator: MockStoreCreator<RootState, DispatchExts> =
 const mockStoreCreator: MockStoreCreator<RootState, DispatchExts> =
   configureMockStore<RootState, DispatchExts>(middlewares);
   configureMockStore<RootState, DispatchExts>(middlewares);