瀏覽代碼

Topics page refactoring (#251)

* Split thunks on files. Refactoring

* [CHORE] Refactor Topics section
Oleg Shur 4 年之前
父節點
當前提交
bbdd60b7a5
共有 42 個文件被更改,包括 789 次插入756 次删除
  1. 2 5
      kafka-ui-react-app/src/components/Cluster/Cluster.tsx
  2. 0 91
      kafka-ui-react-app/src/components/Topics/Details/Overview/Overview.tsx
  3. 0 16
      kafka-ui-react-app/src/components/Topics/Details/Settings/SettingsEditButton.tsx
  4. 0 64
      kafka-ui-react-app/src/components/Topics/Edit/EditContainer.tsx
  5. 52 24
      kafka-ui-react-app/src/components/Topics/List/List.tsx
  6. 10 18
      kafka-ui-react-app/src/components/Topics/List/ListContainer.ts
  7. 29 8
      kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx
  8. 35 44
      kafka-ui-react-app/src/components/Topics/Topic/Details/Details.tsx
  9. 0 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/DetailsContainer.ts
  10. 0 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessageItem.tsx
  11. 0 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Messages.tsx
  12. 0 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessagesContainer.ts
  13. 0 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessagesTable.tsx
  14. 1 1
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessageItem.spec.tsx
  15. 4 2
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/Messages.spec.tsx
  16. 1 1
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessagesTable.spec.tsx
  17. 0 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/__snapshots__/MessageItem.spec.tsx.snap
  18. 0 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/__snapshots__/MessagesTable.spec.tsx.snap
  19. 0 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/fixtures.ts
  20. 70 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx
  21. 25 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/OverviewContainer.ts
  22. 3 2
      kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/Settings.tsx
  23. 0 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/SettingsContainer.ts
  24. 17 34
      kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx
  25. 14 13
      kafka-ui-react-app/src/components/Topics/Topic/Edit/EditContainer.tsx
  26. 80 0
      kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx
  27. 15 0
      kafka-ui-react-app/src/components/Topics/Topic/TopicContainer.tsx
  28. 19 47
      kafka-ui-react-app/src/components/Topics/Topics.tsx
  29. 0 30
      kafka-ui-react-app/src/components/Topics/TopicsContainer.ts
  30. 1 1
      kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.tsx
  31. 15 0
      kafka-ui-react-app/src/lib/hooks/usePagination.ts
  32. 2 2
      kafka-ui-react-app/src/redux/actions/actions.ts
  33. 0 318
      kafka-ui-react-app/src/redux/actions/thunks.ts
  34. 36 0
      kafka-ui-react-app/src/redux/actions/thunks/brokers.ts
  35. 42 0
      kafka-ui-react-app/src/redux/actions/thunks/clusters.ts
  36. 49 0
      kafka-ui-react-app/src/redux/actions/thunks/consumerGroups.ts
  37. 5 0
      kafka-ui-react-app/src/redux/actions/thunks/index.ts
  38. 59 0
      kafka-ui-react-app/src/redux/actions/thunks/schemas.ts
  39. 186 0
      kafka-ui-react-app/src/redux/actions/thunks/topics.ts
  40. 2 6
      kafka-ui-react-app/src/redux/interfaces/topic.ts
  41. 3 27
      kafka-ui-react-app/src/redux/reducers/topics/reducer.ts
  42. 12 2
      kafka-ui-react-app/src/redux/reducers/topics/selectors.ts

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

@@ -2,7 +2,7 @@ import React from 'react';
 import { useSelector } from 'react-redux';
 import { useSelector } from 'react-redux';
 import { Switch, Route, Redirect, useParams } from 'react-router-dom';
 import { Switch, Route, Redirect, useParams } from 'react-router-dom';
 import BrokersContainer from 'components/Brokers/BrokersContainer';
 import BrokersContainer from 'components/Brokers/BrokersContainer';
-import TopicsContainer from 'components/Topics/TopicsContainer';
+import Topics from 'components/Topics/Topics';
 import ConsumersGroupsContainer from 'components/ConsumerGroups/ConsumersGroupsContainer';
 import ConsumersGroupsContainer from 'components/ConsumerGroups/ConsumersGroupsContainer';
 import Schemas from 'components/Schemas/Schemas';
 import Schemas from 'components/Schemas/Schemas';
 import { getClustersReadonlyStatus } from 'redux/reducers/clusters/selectors';
 import { getClustersReadonlyStatus } from 'redux/reducers/clusters/selectors';
@@ -18,10 +18,7 @@ const Cluster: React.FC = () => {
           path="/ui/clusters/:clusterName/brokers"
           path="/ui/clusters/:clusterName/brokers"
           component={BrokersContainer}
           component={BrokersContainer}
         />
         />
-        <Route
-          path="/ui/clusters/:clusterName/topics"
-          component={TopicsContainer}
-        />
+        <Route path="/ui/clusters/:clusterName/topics" component={Topics} />
         <Route
         <Route
           path="/ui/clusters/:clusterName/consumer-groups"
           path="/ui/clusters/:clusterName/consumer-groups"
           component={ConsumersGroupsContainer}
           component={ConsumersGroupsContainer}

+ 0 - 91
kafka-ui-react-app/src/components/Topics/Details/Overview/Overview.tsx

@@ -1,91 +0,0 @@
-import React from 'react';
-import { ClusterName, TopicName } from 'redux/interfaces';
-import { Topic, TopicDetails } from 'generated-sources';
-import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
-import Indicator from 'components/common/Dashboard/Indicator';
-import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
-
-interface Props extends Topic, TopicDetails {
-  isFetched: boolean;
-  clusterName: ClusterName;
-  topicName: TopicName;
-  fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) => void;
-}
-
-const Overview: React.FC<Props> = ({
-  isFetched,
-  clusterName,
-  topicName,
-  partitions,
-  underReplicatedPartitions,
-  inSyncReplicas,
-  replicas,
-  partitionCount,
-  internal,
-  replicationFactor,
-  segmentSize,
-  segmentCount,
-  fetchTopicDetails,
-}) => {
-  React.useEffect(() => {
-    fetchTopicDetails(clusterName, topicName);
-  }, [fetchTopicDetails, clusterName, topicName]);
-
-  if (!isFetched) {
-    return null;
-  }
-
-  return (
-    <>
-      <MetricsWrapper>
-        <Indicator label="Partitions">{partitionCount}</Indicator>
-        <Indicator label="Replication Factor">{replicationFactor}</Indicator>
-        <Indicator label="URP" title="Under replicated partitions">
-          {underReplicatedPartitions}
-        </Indicator>
-        <Indicator label="In sync replicas">
-          {inSyncReplicas}
-          <span className="subtitle has-text-weight-light">
-            {' '}
-            of
-            {replicas}
-          </span>
-        </Indicator>
-        <Indicator label="Type">
-          <span className="tag is-primary">
-            {internal ? 'Internal' : 'External'}
-          </span>
-        </Indicator>
-        <Indicator label="Segment Size" title="">
-          <BytesFormatted value={segmentSize} />
-        </Indicator>
-        <Indicator label="Segment count">{segmentCount}</Indicator>
-      </MetricsWrapper>
-      <div className="box">
-        <table className="table is-striped is-fullwidth">
-          <thead>
-            <tr>
-              <th>Partition ID</th>
-              <th>Broker leader</th>
-              <th>Min offset</th>
-              <th>Max offset</th>
-            </tr>
-          </thead>
-          <tbody>
-            {partitions &&
-              partitions.map(({ partition, leader, offsetMin, offsetMax }) => (
-                <tr key={`partition-list-item-key-${partition}`}>
-                  <td>{partition}</td>
-                  <td>{leader}</td>
-                  <td>{offsetMin}</td>
-                  <td>{offsetMax}</td>
-                </tr>
-              ))}
-          </tbody>
-        </table>
-      </div>
-    </>
-  );
-};
-
-export default Overview;

+ 0 - 16
kafka-ui-react-app/src/components/Topics/Details/Settings/SettingsEditButton.tsx

@@ -1,16 +0,0 @@
-import React from 'react';
-import { Link } from 'react-router-dom';
-
-interface Props {
-  to: string;
-}
-
-const SettingsEditButton: React.FC<Props> = ({ to }) => (
-  <Link to={to}>
-    <button type="button" className="button is-small is-warning">
-      Edit settings
-    </button>
-  </Link>
-);
-
-export default SettingsEditButton;

+ 0 - 64
kafka-ui-react-app/src/components/Topics/Edit/EditContainer.tsx

@@ -1,64 +0,0 @@
-import { connect } from 'react-redux';
-import {
-  RootState,
-  ClusterName,
-  TopicName,
-  Action,
-  TopicFormDataRaw,
-} from 'redux/interfaces';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
-import {
-  updateTopic,
-  fetchTopicConfig,
-  fetchTopicDetails,
-} from 'redux/actions';
-import {
-  getTopicConfigFetched,
-  getTopicUpdated,
-  getIsTopicDetailsFetched,
-  getFullTopic,
-} from 'redux/reducers/topics/selectors';
-import { clusterTopicPath } from 'lib/paths';
-import { ThunkDispatch } from 'redux-thunk';
-
-import Edit from './Edit';
-
-interface RouteProps {
-  clusterName: ClusterName;
-  topicName: TopicName;
-}
-
-type OwnProps = RouteComponentProps<RouteProps>;
-
-const mapStateToProps = (
-  state: RootState,
-  {
-    match: {
-      params: { topicName, clusterName },
-    },
-  }: OwnProps
-) => ({
-  clusterName,
-  topicName,
-  topic: getFullTopic(state, topicName),
-  isFetched: getTopicConfigFetched(state),
-  isTopicDetailsFetched: getIsTopicDetailsFetched(state),
-  isTopicUpdated: getTopicUpdated(state),
-});
-
-const mapDispatchToProps = (
-  dispatch: ThunkDispatch<RootState, undefined, Action>,
-  { history }: OwnProps
-) => ({
-  fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) =>
-    dispatch(fetchTopicDetails(clusterName, topicName)),
-  fetchTopicConfig: (clusterName: ClusterName, topicName: TopicName) =>
-    dispatch(fetchTopicConfig(clusterName, topicName)),
-  updateTopic: (clusterName: ClusterName, form: TopicFormDataRaw) =>
-    dispatch(updateTopic(clusterName, form)),
-  redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => {
-    history.push(clusterTopicPath(clusterName, topicName));
-  },
-});
-
-export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Edit));

+ 52 - 24
kafka-ui-react-app/src/components/Topics/List/List.tsx

@@ -1,28 +1,46 @@
 import React from 'react';
 import React from 'react';
 import { TopicWithDetailedInfo, ClusterName } from 'redux/interfaces';
 import { TopicWithDetailedInfo, ClusterName } from 'redux/interfaces';
 import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
 import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
-import { NavLink } from 'react-router-dom';
+import { NavLink, useParams } from 'react-router-dom';
 import { clusterTopicNewPath } from 'lib/paths';
 import { clusterTopicNewPath } from 'lib/paths';
+import usePagination from 'lib/hooks/usePagination';
+import { FetchTopicsListParams } from 'redux/actions';
 import ClusterContext from 'components/contexts/ClusterContext';
 import ClusterContext from 'components/contexts/ClusterContext';
+import PageLoader from 'components/common/PageLoader/PageLoader';
 import ListItem from './ListItem';
 import ListItem from './ListItem';
 
 
 interface Props {
 interface Props {
-  clusterName: ClusterName;
+  areTopicsFetching: boolean;
   topics: TopicWithDetailedInfo[];
   topics: TopicWithDetailedInfo[];
   externalTopics: TopicWithDetailedInfo[];
   externalTopics: TopicWithDetailedInfo[];
+  fetchTopicsList(props: FetchTopicsListParams): void;
 }
 }
 
 
-const List: React.FC<Props> = ({ clusterName, topics, externalTopics }) => {
+const List: React.FC<Props> = ({
+  areTopicsFetching,
+  topics,
+  externalTopics,
+  fetchTopicsList,
+}) => {
+  const { isReadOnly } = React.useContext(ClusterContext);
+  const { clusterName } = useParams<{ clusterName: ClusterName }>();
+  const { page, perPage } = usePagination();
+
+  React.useEffect(() => {
+    fetchTopicsList({ clusterName, page, perPage });
+  }, [fetchTopicsList, clusterName, page, perPage]);
+
   const [showInternal, setShowInternal] = React.useState<boolean>(true);
   const [showInternal, setShowInternal] = React.useState<boolean>(true);
 
 
-  const handleSwitch = () => setShowInternal(!showInternal);
-  const { isReadOnly } = React.useContext(ClusterContext);
+  const handleSwitch = React.useCallback(() => {
+    setShowInternal(!showInternal);
+  }, [showInternal]);
+
   const items = showInternal ? topics : externalTopics;
   const items = showInternal ? topics : externalTopics;
 
 
   return (
   return (
     <div className="section">
     <div className="section">
-      <Breadcrumb>All Topics</Breadcrumb>
-
+      <Breadcrumb>{showInternal ? `All Topics` : `External Topics`}</Breadcrumb>
       <div className="box">
       <div className="box">
         <div className="level">
         <div className="level">
           <div className="level-item level-left">
           <div className="level-item level-left">
@@ -50,23 +68,33 @@ const List: React.FC<Props> = ({ clusterName, topics, externalTopics }) => {
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
-      <div className="box">
-        <table className="table is-striped is-fullwidth">
-          <thead>
-            <tr>
-              <th>Topic Name</th>
-              <th>Total Partitions</th>
-              <th>Out of sync replicas</th>
-              <th>Type</th>
-            </tr>
-          </thead>
-          <tbody>
-            {items.map((topic) => (
-              <ListItem key={topic.id} topic={topic} />
-            ))}
-          </tbody>
-        </table>
-      </div>
+      {areTopicsFetching ? (
+        <PageLoader />
+      ) : (
+        <div className="box">
+          <table className="table is-striped is-fullwidth">
+            <thead>
+              <tr>
+                <th>Topic Name</th>
+                <th>Total Partitions</th>
+                <th>Out of sync replicas</th>
+                <th>Type</th>
+              </tr>
+            </thead>
+            <tbody>
+              {items.length > 0 ? (
+                items.map((topic) => (
+                  <ListItem key={topic.name} topic={topic} />
+                ))
+              ) : (
+                <tr>
+                  <td colSpan={10}>No topics found</td>
+                </tr>
+              )}
+            </tbody>
+          </table>
+        </div>
+      )}
     </div>
     </div>
   );
   );
 };
 };

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

@@ -1,29 +1,21 @@
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
-import { ClusterName, RootState } from 'redux/interfaces';
+import { RootState } from 'redux/interfaces';
+import { fetchTopicsList } from 'redux/actions';
 import {
 import {
   getTopicList,
   getTopicList,
   getExternalTopicList,
   getExternalTopicList,
+  getAreTopicsFetching,
 } from 'redux/reducers/topics/selectors';
 } from 'redux/reducers/topics/selectors';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
 import List from './List';
 import List from './List';
 
 
-interface RouteProps {
-  clusterName: ClusterName;
-}
-
-type OwnProps = RouteComponentProps<RouteProps>;
-
-const mapStateToProps = (
-  state: RootState,
-  {
-    match: {
-      params: { clusterName },
-    },
-  }: OwnProps
-) => ({
-  clusterName,
+const mapStateToProps = (state: RootState) => ({
+  areTopicsFetching: getAreTopicsFetching(state),
   topics: getTopicList(state),
   topics: getTopicList(state),
   externalTopics: getExternalTopicList(state),
   externalTopics: getExternalTopicList(state),
 });
 });
 
 
-export default withRouter(connect(mapStateToProps)(List));
+const mapDispatchToProps = {
+  fetchTopicsList,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(List);

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

@@ -1,22 +1,43 @@
 import { mount } from 'enzyme';
 import { mount } from 'enzyme';
 import React from 'react';
 import React from 'react';
+import { StaticRouter } from 'react-router-dom';
 import ClusterContext from 'components/contexts/ClusterContext';
 import ClusterContext from 'components/contexts/ClusterContext';
 import List from '../List';
 import List from '../List';
 
 
 describe('List', () => {
 describe('List', () => {
   describe('when it has readonly flag', () => {
   describe('when it has readonly flag', () => {
     it('does not render the Add a Topic button', () => {
     it('does not render the Add a Topic button', () => {
-      const props = {
-        clusterName: 'Cluster',
-        topics: [],
-        externalTopics: [],
-      };
       const component = mount(
       const component = mount(
-        <ClusterContext.Provider value={{ isReadOnly: true }}>
-          <List {...props} />
-        </ClusterContext.Provider>
+        <StaticRouter>
+          <ClusterContext.Provider value={{ isReadOnly: true }}>
+            <List
+              areTopicsFetching={false}
+              topics={[]}
+              externalTopics={[]}
+              fetchTopicsList={jest.fn()}
+            />
+          </ClusterContext.Provider>
+        </StaticRouter>
       );
       );
       expect(component.exists('NavLink')).toBeFalsy();
       expect(component.exists('NavLink')).toBeFalsy();
     });
     });
   });
   });
+
+  describe('when it does not have readonly flag', () => {
+    it('renders the Add a Topic button', () => {
+      const component = mount(
+        <StaticRouter>
+          <ClusterContext.Provider value={{ isReadOnly: false }}>
+            <List
+              areTopicsFetching={false}
+              topics={[]}
+              externalTopics={[]}
+              fetchTopicsList={jest.fn()}
+            />
+          </ClusterContext.Provider>
+        </StaticRouter>
+      );
+      expect(component.exists('NavLink')).toBeTruthy();
+    });
+  });
 });
 });

+ 35 - 44
kafka-ui-react-app/src/components/Topics/Details/Details.tsx → kafka-ui-react-app/src/components/Topics/Topic/Details/Details.tsx

@@ -1,10 +1,8 @@
 import React from 'react';
 import React from 'react';
 import { ClusterName, TopicName } from 'redux/interfaces';
 import { ClusterName, TopicName } from 'redux/interfaces';
 import { Topic, TopicDetails } from 'generated-sources';
 import { Topic, TopicDetails } from 'generated-sources';
-import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
-import { NavLink, Switch, Route } from 'react-router-dom';
+import { NavLink, Switch, Route, Link } from 'react-router-dom';
 import {
 import {
-  clusterTopicsPath,
   clusterTopicSettingsPath,
   clusterTopicSettingsPath,
   clusterTopicPath,
   clusterTopicPath,
   clusterTopicMessagesPath,
   clusterTopicMessagesPath,
@@ -14,7 +12,6 @@ import ClusterContext from 'components/contexts/ClusterContext';
 import OverviewContainer from './Overview/OverviewContainer';
 import OverviewContainer from './Overview/OverviewContainer';
 import MessagesContainer from './Messages/MessagesContainer';
 import MessagesContainer from './Messages/MessagesContainer';
 import SettingsContainer from './Settings/SettingsContainer';
 import SettingsContainer from './Settings/SettingsContainer';
-import SettingsEditButton from './Settings/SettingsEditButton';
 
 
 interface Props extends Topic, TopicDetails {
 interface Props extends Topic, TopicDetails {
   clusterName: ClusterName;
   clusterName: ClusterName;
@@ -23,27 +20,11 @@ interface Props extends Topic, TopicDetails {
 
 
 const Details: React.FC<Props> = ({ clusterName, topicName }) => {
 const Details: React.FC<Props> = ({ clusterName, topicName }) => {
   const { isReadOnly } = React.useContext(ClusterContext);
   const { isReadOnly } = React.useContext(ClusterContext);
-  return (
-    <div className="section">
-      <div className="level">
-        <div className="level-item level-left">
-          <Breadcrumb
-            links={[
-              { href: clusterTopicsPath(clusterName), label: 'All Topics' },
-            ]}
-          >
-            {topicName}
-          </Breadcrumb>
-        </div>
-        {!isReadOnly && (
-          <SettingsEditButton
-            to={clusterTopicsTopicEditPath(clusterName, topicName)}
-          />
-        )}
-      </div>
 
 
-      <div className="box">
-        <nav className="navbar" role="navigation">
+  return (
+    <div className="box">
+      <nav className="navbar" role="navigation">
+        <div className="navbar-start">
           <NavLink
           <NavLink
             exact
             exact
             to={clusterTopicPath(clusterName, topicName)}
             to={clusterTopicPath(clusterName, topicName)}
@@ -68,26 +49,36 @@ const Details: React.FC<Props> = ({ clusterName, topicName }) => {
           >
           >
             Settings
             Settings
           </NavLink>
           </NavLink>
-        </nav>
-        <br />
-        <Switch>
-          <Route
-            exact
-            path="/ui/clusters/:clusterName/topics/:topicName/messages"
-            component={MessagesContainer}
-          />
-          <Route
-            exact
-            path="/ui/clusters/:clusterName/topics/:topicName/settings"
-            component={SettingsContainer}
-          />
-          <Route
-            exact
-            path="/ui/clusters/:clusterName/topics/:topicName"
-            component={OverviewContainer}
-          />
-        </Switch>
-      </div>
+        </div>
+        <div className="navbar-end">
+          {!isReadOnly && (
+            <Link
+              to={clusterTopicsTopicEditPath(clusterName, topicName)}
+              className="button"
+            >
+              Edit settings
+            </Link>
+          )}
+        </div>
+      </nav>
+      <br />
+      <Switch>
+        <Route
+          exact
+          path="/ui/clusters/:clusterName/topics/:topicName/messages"
+          component={MessagesContainer}
+        />
+        <Route
+          exact
+          path="/ui/clusters/:clusterName/topics/:topicName/settings"
+          component={SettingsContainer}
+        />
+        <Route
+          exact
+          path="/ui/clusters/:clusterName/topics/:topicName"
+          component={OverviewContainer}
+        />
+      </Switch>
     </div>
     </div>
   );
   );
 };
 };

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


+ 0 - 0
kafka-ui-react-app/src/components/Topics/Details/Messages/MessageItem.tsx → kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessageItem.tsx


+ 0 - 0
kafka-ui-react-app/src/components/Topics/Details/Messages/Messages.tsx → kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Messages.tsx


+ 0 - 0
kafka-ui-react-app/src/components/Topics/Details/Messages/MessagesContainer.ts → kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessagesContainer.ts


+ 0 - 0
kafka-ui-react-app/src/components/Topics/Details/Messages/MessagesTable.tsx → kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessagesTable.tsx


+ 1 - 1
kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/MessageItem.spec.tsx → kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessageItem.spec.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 import React from 'react';
 import { shallow } from 'enzyme';
 import { shallow } from 'enzyme';
-import MessageItem from 'components/Topics/Details/Messages/MessageItem';
+import MessageItem from 'components/Topics/Topic/Details/Messages/MessageItem';
 import { messages } from './fixtures';
 import { messages } from './fixtures';
 
 
 jest.mock('date-fns', () => ({
 jest.mock('date-fns', () => ({

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

@@ -3,8 +3,10 @@ import { Provider } from 'react-redux';
 import { mount, shallow } from 'enzyme';
 import { mount, shallow } from 'enzyme';
 import * as useDebounce from 'use-debounce';
 import * as useDebounce from 'use-debounce';
 import DatePicker from 'react-datepicker';
 import DatePicker from 'react-datepicker';
-import Messages, { Props } from 'components/Topics/Details/Messages/Messages';
-import MessagesContainer from 'components/Topics/Details/Messages/MessagesContainer';
+import Messages, {
+  Props,
+} from 'components/Topics/Topic/Details/Messages/Messages';
+import MessagesContainer from 'components/Topics/Topic/Details/Messages/MessagesContainer';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import configureStore from 'redux/store/configureStore';
 import configureStore from 'redux/store/configureStore';
 
 

+ 1 - 1
kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/MessagesTable.spec.tsx → kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessagesTable.spec.tsx

@@ -2,7 +2,7 @@ import React from 'react';
 import { shallow } from 'enzyme';
 import { shallow } from 'enzyme';
 import MessagesTable, {
 import MessagesTable, {
   MessagesTableProp,
   MessagesTableProp,
-} from 'components/Topics/Details/Messages/MessagesTable';
+} from 'components/Topics/Topic/Details/Messages/MessagesTable';
 import { messages } from './fixtures';
 import { messages } from './fixtures';
 
 
 jest.mock('date-fns', () => ({
 jest.mock('date-fns', () => ({

+ 0 - 0
kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/__snapshots__/MessageItem.spec.tsx.snap → kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/__snapshots__/MessageItem.spec.tsx.snap


+ 0 - 0
kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/__snapshots__/MessagesTable.spec.tsx.snap → kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/__snapshots__/MessagesTable.spec.tsx.snap


+ 0 - 0
kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/fixtures.ts → kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/fixtures.ts


+ 70 - 0
kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx

@@ -0,0 +1,70 @@
+import React from 'react';
+import { Topic, TopicDetails } from 'generated-sources';
+import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
+import Indicator from 'components/common/Dashboard/Indicator';
+import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
+
+interface Props extends Topic, TopicDetails {}
+
+const Overview: React.FC<Props> = ({
+  partitions,
+  underReplicatedPartitions,
+  inSyncReplicas,
+  replicas,
+  partitionCount,
+  internal,
+  replicationFactor,
+  segmentSize,
+  segmentCount,
+}) => (
+  <>
+    <MetricsWrapper>
+      <Indicator label="Partitions">{partitionCount}</Indicator>
+      <Indicator label="Replication Factor">{replicationFactor}</Indicator>
+      <Indicator label="URP" title="Under replicated partitions">
+        {underReplicatedPartitions}
+      </Indicator>
+      <Indicator label="In sync replicas">
+        {inSyncReplicas}
+        <span className="subtitle has-text-weight-light">
+          {' '}
+          of
+          {replicas}
+        </span>
+      </Indicator>
+      <Indicator label="Type">
+        <span className="tag is-primary">
+          {internal ? 'Internal' : 'External'}
+        </span>
+      </Indicator>
+      <Indicator label="Segment Size" title="">
+        <BytesFormatted value={segmentSize} />
+      </Indicator>
+      <Indicator label="Segment count">{segmentCount}</Indicator>
+    </MetricsWrapper>
+    <div className="box">
+      <table className="table is-striped is-fullwidth">
+        <thead>
+          <tr>
+            <th>Partition ID</th>
+            <th>Broker leader</th>
+            <th>Min offset</th>
+            <th>Max offset</th>
+          </tr>
+        </thead>
+        <tbody>
+          {partitions?.map(({ partition, leader, offsetMin, offsetMax }) => (
+            <tr key={`partition-list-item-key-${partition}`}>
+              <td>{partition}</td>
+              <td>{leader}</td>
+              <td>{offsetMin}</td>
+              <td>{offsetMax}</td>
+            </tr>
+          ))}
+        </tbody>
+      </table>
+    </div>
+  </>
+);
+
+export default Overview;

+ 25 - 0
kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/OverviewContainer.ts

@@ -0,0 +1,25 @@
+import { connect } from 'react-redux';
+import { RootState, TopicName, ClusterName } from 'redux/interfaces';
+import { getTopicByName } from 'redux/reducers/topics/selectors';
+import { withRouter, RouteComponentProps } from 'react-router-dom';
+import Overview from './Overview';
+
+interface RouteProps {
+  clusterName: ClusterName;
+  topicName: TopicName;
+}
+
+type OwnProps = RouteComponentProps<RouteProps>;
+
+const mapStateToProps = (
+  state: RootState,
+  {
+    match: {
+      params: { topicName },
+    },
+  }: OwnProps
+) => ({
+  ...getTopicByName(state, topicName),
+});
+
+export default withRouter(connect(mapStateToProps)(Overview));

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

@@ -1,5 +1,6 @@
+import { TopicConfig } from 'generated-sources';
 import React from 'react';
 import React from 'react';
-import { ClusterName, TopicName, TopicConfig } from 'redux/interfaces';
+import { ClusterName, TopicName } from 'redux/interfaces';
 
 
 interface Props {
 interface Props {
   clusterName: ClusterName;
   clusterName: ClusterName;
@@ -56,7 +57,7 @@ const Sertings: React.FC<Props> = ({
         </thead>
         </thead>
         <tbody>
         <tbody>
           {config.map((item) => (
           {config.map((item) => (
-            <ConfigListItem key={item.id} config={item} />
+            <ConfigListItem key={item.name} config={item} />
           ))}
           ))}
         </tbody>
         </tbody>
       </table>
       </table>

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


+ 17 - 34
kafka-ui-react-app/src/components/Topics/Edit/Edit.tsx → kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx

@@ -10,22 +10,18 @@ import {
 import { TopicConfig } from 'generated-sources';
 import { TopicConfig } from 'generated-sources';
 import { useForm, FormProvider } from 'react-hook-form';
 import { useForm, FormProvider } from 'react-hook-form';
 import { camelCase } from 'lodash';
 import { camelCase } from 'lodash';
-
-import TopicForm from '../shared/Form/TopicForm';
-import FormBreadcrumbs from '../shared/Form/FormBreadcrumbs';
+import TopicForm from 'components/Topics/shared/Form/TopicForm';
+import { clusterTopicPath } from 'lib/paths';
+import { useHistory } from 'react-router';
 
 
 interface Props {
 interface Props {
   clusterName: ClusterName;
   clusterName: ClusterName;
   topicName: TopicName;
   topicName: TopicName;
   topic?: TopicWithDetailedInfo;
   topic?: TopicWithDetailedInfo;
   isFetched: boolean;
   isFetched: boolean;
-  isTopicDetailsFetched: boolean;
   isTopicUpdated: boolean;
   isTopicUpdated: boolean;
-  fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) => void;
   fetchTopicConfig: (clusterName: ClusterName, topicName: TopicName) => void;
   fetchTopicConfig: (clusterName: ClusterName, topicName: TopicName) => void;
   updateTopic: (clusterName: ClusterName, form: TopicFormDataRaw) => void;
   updateTopic: (clusterName: ClusterName, form: TopicFormDataRaw) => void;
-  redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => void;
-  resetUploadedState: () => void;
 }
 }
 
 
 const DEFAULTS = {
 const DEFAULTS = {
@@ -68,32 +64,29 @@ const Edit: React.FC<Props> = ({
   topicName,
   topicName,
   topic,
   topic,
   isFetched,
   isFetched,
-  isTopicDetailsFetched,
   isTopicUpdated,
   isTopicUpdated,
-  fetchTopicDetails,
   fetchTopicConfig,
   fetchTopicConfig,
   updateTopic,
   updateTopic,
-  redirectToTopicPath,
 }) => {
 }) => {
   const defaultValues = topicParams(topic);
   const defaultValues = topicParams(topic);
 
 
   const methods = useForm<TopicFormDataRaw>({ defaultValues });
   const methods = useForm<TopicFormDataRaw>({ defaultValues });
 
 
   const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);
   const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);
+  const history = useHistory();
 
 
   React.useEffect(() => {
   React.useEffect(() => {
     fetchTopicConfig(clusterName, topicName);
     fetchTopicConfig(clusterName, topicName);
-    fetchTopicDetails(clusterName, topicName);
-  }, [fetchTopicConfig, fetchTopicDetails, clusterName, topicName]);
+  }, [fetchTopicConfig, clusterName, topicName]);
 
 
   React.useEffect(() => {
   React.useEffect(() => {
     if (isSubmitting && isTopicUpdated) {
     if (isSubmitting && isTopicUpdated) {
       const { name } = methods.getValues();
       const { name } = methods.getValues();
-      redirectToTopicPath(clusterName, name);
+      history.push(clusterTopicPath(clusterName, name));
     }
     }
-  }, [isSubmitting, isTopicUpdated, redirectToTopicPath, clusterName, methods]);
+  }, [isSubmitting, isTopicUpdated, clusterTopicPath, clusterName, methods]);
 
 
-  if (!isFetched || !isTopicDetailsFetched || !topic || !topic.config) {
+  if (!isFetched || !topic || !topic.config) {
     return null;
     return null;
   }
   }
 
 
@@ -116,27 +109,17 @@ const Edit: React.FC<Props> = ({
   };
   };
 
 
   return (
   return (
-    <div className="section">
-      <div className="level">
-        <FormBreadcrumbs
-          clusterName={clusterName}
+    <div className="box">
+      {/* eslint-disable-next-line react/jsx-props-no-spreading */}
+      <FormProvider {...methods}>
+        <TopicForm
           topicName={topicName}
           topicName={topicName}
-          current="Edit Topic"
+          config={config}
+          isSubmitting={isSubmitting}
+          isEditing
+          onSubmit={methods.handleSubmit(onSubmit)}
         />
         />
-      </div>
-
-      <div className="box">
-        {/* eslint-disable-next-line react/jsx-props-no-spreading */}
-        <FormProvider {...methods}>
-          <TopicForm
-            topicName={topicName}
-            config={config}
-            isSubmitting={isSubmitting}
-            isEditing
-            onSubmit={methods.handleSubmit(onSubmit)}
-          />
-        </FormProvider>
-      </div>
+      </FormProvider>
     </div>
     </div>
   );
   );
 };
 };

+ 14 - 13
kafka-ui-react-app/src/components/Topics/Details/Overview/OverviewContainer.ts → kafka-ui-react-app/src/components/Topics/Topic/Edit/EditContainer.tsx

@@ -1,12 +1,14 @@
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
-import { fetchTopicDetails } from 'redux/actions';
-import { RootState, TopicName, ClusterName } from 'redux/interfaces';
+import { RootState, ClusterName, TopicName } from 'redux/interfaces';
+import { withRouter, RouteComponentProps } from 'react-router-dom';
+import { updateTopic, fetchTopicConfig } from 'redux/actions';
 import {
 import {
-  getTopicByName,
-  getIsTopicDetailsFetched,
+  getTopicConfigFetched,
+  getTopicUpdated,
+  getFullTopic,
 } from 'redux/reducers/topics/selectors';
 } from 'redux/reducers/topics/selectors';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
-import Overview from './Overview';
+
+import Edit from './Edit';
 
 
 interface RouteProps {
 interface RouteProps {
   clusterName: ClusterName;
   clusterName: ClusterName;
@@ -25,15 +27,14 @@ const mapStateToProps = (
 ) => ({
 ) => ({
   clusterName,
   clusterName,
   topicName,
   topicName,
-  isFetched: getIsTopicDetailsFetched(state),
-  ...getTopicByName(state, topicName),
+  topic: getFullTopic(state, topicName),
+  isFetched: getTopicConfigFetched(state),
+  isTopicUpdated: getTopicUpdated(state),
 });
 });
 
 
 const mapDispatchToProps = {
 const mapDispatchToProps = {
-  fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) =>
-    fetchTopicDetails(clusterName, topicName),
+  fetchTopicConfig,
+  updateTopic,
 };
 };
 
 
-export default withRouter(
-  connect(mapStateToProps, mapDispatchToProps)(Overview)
-);
+export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Edit));

+ 80 - 0
kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx

@@ -0,0 +1,80 @@
+import React from 'react';
+import { Switch, Route, useParams } from 'react-router-dom';
+import { clusterTopicPath, clusterTopicsPath } from 'lib/paths';
+import { ClusterName, TopicName } from 'redux/interfaces';
+import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
+import EditContainer from 'components/Topics/Topic/Edit/EditContainer';
+import DetailsContainer from 'components/Topics/Topic/Details/DetailsContainer';
+import PageLoader from 'components/common/PageLoader/PageLoader';
+
+interface RouterParams {
+  clusterName: ClusterName;
+  topicName: TopicName;
+}
+
+interface TopicProps {
+  isTopicFetching: boolean;
+  fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) => void;
+}
+
+const Topic: React.FC<TopicProps> = ({
+  isTopicFetching,
+  fetchTopicDetails,
+}) => {
+  const { clusterName, topicName } = useParams<RouterParams>();
+
+  React.useEffect(() => {
+    fetchTopicDetails(clusterName, topicName);
+  }, [fetchTopicDetails, clusterName, topicName]);
+
+  const rootBreadcrumbLinks = [
+    {
+      href: clusterTopicsPath(clusterName),
+      label: 'All Topics',
+    },
+  ];
+
+  const childBreadcrumbLinks = [
+    ...rootBreadcrumbLinks,
+    {
+      href: clusterTopicPath(clusterName, topicName),
+      label: topicName,
+    },
+  ];
+
+  const topicPageUrl = '/ui/clusters/:clusterName/topics/:topicName';
+
+  return (
+    <div className="section">
+      <div className="level">
+        <div className="level-item level-left">
+          <Switch>
+            <Route exact path={`${topicPageUrl}/edit`}>
+              <Breadcrumb links={childBreadcrumbLinks}>Edit</Breadcrumb>
+            </Route>
+            <Route path={topicPageUrl}>
+              <Breadcrumb links={rootBreadcrumbLinks}>{topicName}</Breadcrumb>
+            </Route>
+          </Switch>
+        </div>
+      </div>
+      {isTopicFetching ? (
+        <PageLoader />
+      ) : (
+        <Switch>
+          <Route
+            exact
+            path="/ui/clusters/:clusterName/topics/:topicName/edit"
+            component={EditContainer}
+          />
+          <Route
+            path="/ui/clusters/:clusterName/topics/:topicName"
+            component={DetailsContainer}
+          />
+        </Switch>
+      )}
+    </div>
+  );
+};
+
+export default Topic;

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

@@ -0,0 +1,15 @@
+import { connect } from 'react-redux';
+import { RootState } from 'redux/interfaces';
+import { fetchTopicDetails } from 'redux/actions';
+import { getIsTopicDetailsFetching } from 'redux/reducers/topics/selectors';
+import Topic from './Topic';
+
+const mapStateToProps = (state: RootState) => ({
+  isTopicFetching: getIsTopicDetailsFetching(state),
+});
+
+const mapDispatchToProps = {
+  fetchTopicDetails,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(Topic);

+ 19 - 47
kafka-ui-react-app/src/components/Topics/Topics.tsx

@@ -1,54 +1,26 @@
 import React from 'react';
 import React from 'react';
-import { ClusterName } from 'redux/interfaces';
 import { Switch, Route } from 'react-router-dom';
 import { Switch, Route } from 'react-router-dom';
-import PageLoader from 'components/common/PageLoader/PageLoader';
-import EditContainer from 'components/Topics/Edit/EditContainer';
 import ListContainer from './List/ListContainer';
 import ListContainer from './List/ListContainer';
-import DetailsContainer from './Details/DetailsContainer';
+import TopicContainer from './Topic/TopicContainer';
 import NewContainer from './New/NewContainer';
 import NewContainer from './New/NewContainer';
 
 
-interface Props {
-  clusterName: ClusterName;
-  isFetched: boolean;
-  fetchTopicsList: (clusterName: ClusterName) => void;
-}
-
-const Topics: React.FC<Props> = ({
-  clusterName,
-  isFetched,
-  fetchTopicsList,
-}) => {
-  React.useEffect(() => {
-    fetchTopicsList(clusterName);
-  }, [fetchTopicsList, clusterName]);
-
-  if (isFetched) {
-    return (
-      <Switch>
-        <Route
-          exact
-          path="/ui/clusters/:clusterName/topics"
-          component={ListContainer}
-        />
-        <Route
-          exact
-          path="/ui/clusters/:clusterName/topics/new"
-          component={NewContainer}
-        />
-        <Route
-          exact
-          path="/ui/clusters/:clusterName/topics/:topicName/edit"
-          component={EditContainer}
-        />
-        <Route
-          path="/ui/clusters/:clusterName/topics/:topicName"
-          component={DetailsContainer}
-        />
-      </Switch>
-    );
-  }
-
-  return <PageLoader />;
-};
+const Topics: React.FC = () => (
+  <Switch>
+    <Route
+      exact
+      path="/ui/clusters/:clusterName/topics"
+      component={ListContainer}
+    />
+    <Route
+      exact
+      path="/ui/clusters/:clusterName/topics/new"
+      component={NewContainer}
+    />
+    <Route
+      path="/ui/clusters/:clusterName/topics/:topicName"
+      component={TopicContainer}
+    />
+  </Switch>
+);
 
 
 export default Topics;
 export default Topics;

+ 0 - 30
kafka-ui-react-app/src/components/Topics/TopicsContainer.ts

@@ -1,30 +0,0 @@
-import { connect } from 'react-redux';
-import { fetchTopicsList } from 'redux/actions';
-import { getIsTopicListFetched } from 'redux/reducers/topics/selectors';
-import { RootState, ClusterName } from 'redux/interfaces';
-import { RouteComponentProps } from 'react-router-dom';
-import Topics from './Topics';
-
-interface RouteProps {
-  clusterName: ClusterName;
-}
-
-type OwnProps = RouteComponentProps<RouteProps>;
-
-const mapStateToProps = (
-  state: RootState,
-  {
-    match: {
-      params: { clusterName },
-    },
-  }: OwnProps
-) => ({
-  isFetched: getIsTopicListFetched(state),
-  clusterName,
-});
-
-const mapDispatchToProps = {
-  fetchTopicsList: (clusterName: ClusterName) => fetchTopicsList(clusterName),
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(Topics);

+ 1 - 1
kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.tsx

@@ -52,7 +52,7 @@ const CustomParams: React.FC<Props> = ({ isSubmitting, config }) => {
       ...formCustomParams,
       ...formCustomParams,
       byIndex: {
       byIndex: {
         ...formCustomParams.byIndex,
         ...formCustomParams.byIndex,
-        [newIndex]: { name: '', value: '', id: v4() },
+        [newIndex]: { name: '', value: '' },
       },
       },
       allIndexes: [newIndex, ...formCustomParams.allIndexes],
       allIndexes: [newIndex, ...formCustomParams.allIndexes],
     });
     });

+ 15 - 0
kafka-ui-react-app/src/lib/hooks/usePagination.ts

@@ -0,0 +1,15 @@
+import { useLocation } from 'react-router';
+
+const usePagination = () => {
+  const params = new URLSearchParams(useLocation().search);
+
+  const page = params.get('page');
+  const perPage = params.get('perPage');
+
+  return {
+    page: page ? Number(page) : undefined,
+    perPage: perPage ? Number(perPage) : undefined,
+  };
+};
+
+export default usePagination;

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

@@ -1,5 +1,5 @@
 import { createAsyncAction } from 'typesafe-actions';
 import { createAsyncAction } from 'typesafe-actions';
-import { TopicName, ConsumerGroupID } from 'redux/interfaces';
+import { TopicName, ConsumerGroupID, TopicsState } from 'redux/interfaces';
 
 
 import {
 import {
   Cluster,
   Cluster,
@@ -50,7 +50,7 @@ export const fetchTopicsListAction = createAsyncAction(
   'GET_TOPICS__REQUEST',
   'GET_TOPICS__REQUEST',
   'GET_TOPICS__SUCCESS',
   'GET_TOPICS__SUCCESS',
   'GET_TOPICS__FAILURE'
   'GET_TOPICS__FAILURE'
-)<undefined, Topic[], undefined>();
+)<undefined, TopicsState, undefined>();
 
 
 export const fetchTopicMessagesAction = createAsyncAction(
 export const fetchTopicMessagesAction = createAsyncAction(
   'GET_TOPIC_MESSAGES__REQUEST',
   'GET_TOPIC_MESSAGES__REQUEST',

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

@@ -1,318 +0,0 @@
-import {
-  ClustersApi,
-  BrokersApi,
-  TopicsApi,
-  ConsumerGroupsApi,
-  SchemasApi,
-  MessagesApi,
-  Configuration,
-  Cluster,
-  Topic,
-  TopicFormData,
-  TopicConfig,
-  NewSchemaSubject,
-  SchemaSubject,
-} from 'generated-sources';
-import {
-  ConsumerGroupID,
-  PromiseThunkResult,
-  ClusterName,
-  BrokerId,
-  TopicName,
-  TopicMessageQueryParams,
-  TopicFormFormattedParams,
-  TopicFormDataRaw,
-  SchemaName,
-} from 'redux/interfaces';
-
-import { BASE_PARAMS } from 'lib/constants';
-import * as actions from './actions';
-
-const apiClientConf = new Configuration(BASE_PARAMS);
-export const clustersApiClient = new ClustersApi(apiClientConf);
-export const brokersApiClient = new BrokersApi(apiClientConf);
-export const topicsApiClient = new TopicsApi(apiClientConf);
-export const consumerGroupsApiClient = new ConsumerGroupsApi(apiClientConf);
-export const schemasApiClient = new SchemasApi(apiClientConf);
-export const messagesApiClient = new MessagesApi(apiClientConf);
-
-export const fetchClustersList = (): PromiseThunkResult => async (dispatch) => {
-  dispatch(actions.fetchClusterListAction.request());
-  try {
-    const clusters: Cluster[] = await clustersApiClient.getClusters();
-    dispatch(actions.fetchClusterListAction.success(clusters));
-  } catch (e) {
-    dispatch(actions.fetchClusterListAction.failure());
-  }
-};
-
-export const fetchClusterStats = (
-  clusterName: ClusterName
-): PromiseThunkResult => async (dispatch) => {
-  dispatch(actions.fetchClusterStatsAction.request());
-  try {
-    const payload = await clustersApiClient.getClusterStats({ clusterName });
-    dispatch(actions.fetchClusterStatsAction.success(payload));
-  } catch (e) {
-    dispatch(actions.fetchClusterStatsAction.failure());
-  }
-};
-
-export const fetchClusterMetrics = (
-  clusterName: ClusterName
-): PromiseThunkResult => async (dispatch) => {
-  dispatch(actions.fetchClusterMetricsAction.request());
-  try {
-    const payload = await clustersApiClient.getClusterMetrics({ clusterName });
-    dispatch(actions.fetchClusterMetricsAction.success(payload));
-  } catch (e) {
-    dispatch(actions.fetchClusterMetricsAction.failure());
-  }
-};
-
-export const fetchBrokers = (
-  clusterName: ClusterName
-): PromiseThunkResult => async (dispatch) => {
-  dispatch(actions.fetchBrokersAction.request());
-  try {
-    const payload = await brokersApiClient.getBrokers({ clusterName });
-    dispatch(actions.fetchBrokersAction.success(payload));
-  } catch (e) {
-    dispatch(actions.fetchBrokersAction.failure());
-  }
-};
-
-export const fetchBrokerMetrics = (
-  clusterName: ClusterName,
-  brokerId: BrokerId
-): PromiseThunkResult => async (dispatch) => {
-  dispatch(actions.fetchBrokerMetricsAction.request());
-  try {
-    const payload = await brokersApiClient.getBrokersMetrics({
-      clusterName,
-      id: brokerId,
-    });
-    dispatch(actions.fetchBrokerMetricsAction.success(payload));
-  } catch (e) {
-    dispatch(actions.fetchBrokerMetricsAction.failure());
-  }
-};
-
-export const fetchTopicsList = (
-  clusterName: ClusterName
-): PromiseThunkResult => async (dispatch) => {
-  dispatch(actions.fetchTopicsListAction.request());
-  try {
-    const topics = await topicsApiClient.getTopics({ clusterName });
-    dispatch(actions.fetchTopicsListAction.success(topics.topics || []));
-  } catch (e) {
-    dispatch(actions.fetchTopicsListAction.failure());
-  }
-};
-
-export const fetchTopicMessages = (
-  clusterName: ClusterName,
-  topicName: TopicName,
-  queryParams: Partial<TopicMessageQueryParams>
-): PromiseThunkResult => async (dispatch) => {
-  dispatch(actions.fetchTopicMessagesAction.request());
-  try {
-    const messages = await messagesApiClient.getTopicMessages({
-      clusterName,
-      topicName,
-      ...queryParams,
-    });
-    dispatch(actions.fetchTopicMessagesAction.success(messages));
-  } catch (e) {
-    dispatch(actions.fetchTopicMessagesAction.failure());
-  }
-};
-
-export const fetchTopicDetails = (
-  clusterName: ClusterName,
-  topicName: TopicName
-): PromiseThunkResult => async (dispatch) => {
-  dispatch(actions.fetchTopicDetailsAction.request());
-  try {
-    const topicDetails = await topicsApiClient.getTopicDetails({
-      clusterName,
-      topicName,
-    });
-    dispatch(
-      actions.fetchTopicDetailsAction.success({
-        topicName,
-        details: topicDetails,
-      })
-    );
-  } catch (e) {
-    dispatch(actions.fetchTopicDetailsAction.failure());
-  }
-};
-
-export const fetchTopicConfig = (
-  clusterName: ClusterName,
-  topicName: TopicName
-): PromiseThunkResult => async (dispatch) => {
-  dispatch(actions.fetchTopicConfigAction.request());
-  try {
-    const config = await topicsApiClient.getTopicConfigs({
-      clusterName,
-      topicName,
-    });
-    dispatch(actions.fetchTopicConfigAction.success({ topicName, config }));
-  } catch (e) {
-    dispatch(actions.fetchTopicConfigAction.failure());
-  }
-};
-
-const formatTopicFormData = (form: TopicFormDataRaw): TopicFormData => {
-  const {
-    name,
-    partitions,
-    replicationFactor,
-    cleanupPolicy,
-    retentionBytes,
-    retentionMs,
-    maxMessageBytes,
-    minInSyncReplicas,
-    customParams,
-  } = form;
-
-  return {
-    name,
-    partitions,
-    replicationFactor,
-    configs: {
-      'cleanup.policy': cleanupPolicy,
-      'retention.ms': retentionMs,
-      'retention.bytes': retentionBytes,
-      'max.message.bytes': maxMessageBytes,
-      'min.insync.replicas': minInSyncReplicas,
-      ...Object.values(customParams || {}).reduce(
-        (result: TopicFormFormattedParams, customParam: TopicConfig) => {
-          return {
-            ...result,
-            [customParam.name]: customParam.value,
-          };
-        },
-        {}
-      ),
-    },
-  };
-};
-
-export const createTopic = (
-  clusterName: ClusterName,
-  form: TopicFormDataRaw
-): PromiseThunkResult => async (dispatch) => {
-  dispatch(actions.createTopicAction.request());
-  try {
-    const topic: Topic = await topicsApiClient.createTopic({
-      clusterName,
-      topicFormData: formatTopicFormData(form),
-    });
-    dispatch(actions.createTopicAction.success(topic));
-  } catch (e) {
-    dispatch(actions.createTopicAction.failure());
-  }
-};
-
-export const updateTopic = (
-  clusterName: ClusterName,
-  form: TopicFormDataRaw
-): PromiseThunkResult => async (dispatch) => {
-  dispatch(actions.updateTopicAction.request());
-  try {
-    const topic: Topic = await topicsApiClient.updateTopic({
-      clusterName,
-      topicName: form.name,
-      topicFormData: formatTopicFormData(form),
-    });
-    dispatch(actions.updateTopicAction.success(topic));
-  } catch (e) {
-    dispatch(actions.updateTopicAction.failure());
-  }
-};
-
-export const fetchConsumerGroupsList = (
-  clusterName: ClusterName
-): PromiseThunkResult => async (dispatch) => {
-  dispatch(actions.fetchConsumerGroupsAction.request());
-  try {
-    const consumerGroups = await consumerGroupsApiClient.getConsumerGroups({
-      clusterName,
-    });
-    dispatch(actions.fetchConsumerGroupsAction.success(consumerGroups));
-  } catch (e) {
-    dispatch(actions.fetchConsumerGroupsAction.failure());
-  }
-};
-
-export const fetchConsumerGroupDetails = (
-  clusterName: ClusterName,
-  consumerGroupID: ConsumerGroupID
-): PromiseThunkResult => async (dispatch) => {
-  dispatch(actions.fetchConsumerGroupDetailsAction.request());
-  try {
-    const consumerGroupDetails = await consumerGroupsApiClient.getConsumerGroup(
-      {
-        clusterName,
-        id: consumerGroupID,
-      }
-    );
-    dispatch(
-      actions.fetchConsumerGroupDetailsAction.success({
-        consumerGroupID,
-        details: consumerGroupDetails,
-      })
-    );
-  } catch (e) {
-    dispatch(actions.fetchConsumerGroupDetailsAction.failure());
-  }
-};
-
-export const fetchSchemasByClusterName = (
-  clusterName: ClusterName
-): PromiseThunkResult<void> => async (dispatch) => {
-  dispatch(actions.fetchSchemasByClusterNameAction.request());
-  try {
-    const schemas = await schemasApiClient.getSchemas({ clusterName });
-    dispatch(actions.fetchSchemasByClusterNameAction.success(schemas));
-  } catch (e) {
-    dispatch(actions.fetchSchemasByClusterNameAction.failure());
-  }
-};
-
-export const fetchSchemaVersions = (
-  clusterName: ClusterName,
-  subject: SchemaName
-): PromiseThunkResult<void> => async (dispatch) => {
-  if (!subject) return;
-  dispatch(actions.fetchSchemaVersionsAction.request());
-  try {
-    const versions = await schemasApiClient.getAllVersionsBySubject({
-      clusterName,
-      subject,
-    });
-    dispatch(actions.fetchSchemaVersionsAction.success(versions));
-  } catch (e) {
-    dispatch(actions.fetchSchemaVersionsAction.failure());
-  }
-};
-
-export const createSchema = (
-  clusterName: ClusterName,
-  newSchemaSubject: NewSchemaSubject
-): PromiseThunkResult => async (dispatch) => {
-  dispatch(actions.createSchemaAction.request());
-  try {
-    const schema: SchemaSubject = await schemasApiClient.createNewSchema({
-      clusterName,
-      newSchemaSubject,
-    });
-    dispatch(actions.createSchemaAction.success(schema));
-  } catch (e) {
-    dispatch(actions.createSchemaAction.failure());
-    throw e;
-  }
-};

+ 36 - 0
kafka-ui-react-app/src/redux/actions/thunks/brokers.ts

@@ -0,0 +1,36 @@
+import { BrokersApi, Configuration } from 'generated-sources';
+import { PromiseThunkResult, ClusterName, BrokerId } from 'redux/interfaces';
+
+import { BASE_PARAMS } from 'lib/constants';
+import * as actions from '../actions';
+
+const apiClientConf = new Configuration(BASE_PARAMS);
+export const brokersApiClient = new BrokersApi(apiClientConf);
+
+export const fetchBrokers = (
+  clusterName: ClusterName
+): PromiseThunkResult => async (dispatch) => {
+  dispatch(actions.fetchBrokersAction.request());
+  try {
+    const payload = await brokersApiClient.getBrokers({ clusterName });
+    dispatch(actions.fetchBrokersAction.success(payload));
+  } catch (e) {
+    dispatch(actions.fetchBrokersAction.failure());
+  }
+};
+
+export const fetchBrokerMetrics = (
+  clusterName: ClusterName,
+  brokerId: BrokerId
+): PromiseThunkResult => async (dispatch) => {
+  dispatch(actions.fetchBrokerMetricsAction.request());
+  try {
+    const payload = await brokersApiClient.getBrokersMetrics({
+      clusterName,
+      id: brokerId,
+    });
+    dispatch(actions.fetchBrokerMetricsAction.success(payload));
+  } catch (e) {
+    dispatch(actions.fetchBrokerMetricsAction.failure());
+  }
+};

+ 42 - 0
kafka-ui-react-app/src/redux/actions/thunks/clusters.ts

@@ -0,0 +1,42 @@
+import { ClustersApi, Configuration, Cluster } from 'generated-sources';
+import { PromiseThunkResult, ClusterName } from 'redux/interfaces';
+
+import { BASE_PARAMS } from 'lib/constants';
+import * as actions from '../actions';
+
+const apiClientConf = new Configuration(BASE_PARAMS);
+export const clustersApiClient = new ClustersApi(apiClientConf);
+
+export const fetchClustersList = (): PromiseThunkResult => async (dispatch) => {
+  dispatch(actions.fetchClusterListAction.request());
+  try {
+    const clusters: Cluster[] = await clustersApiClient.getClusters();
+    dispatch(actions.fetchClusterListAction.success(clusters));
+  } catch (e) {
+    dispatch(actions.fetchClusterListAction.failure());
+  }
+};
+
+export const fetchClusterStats = (
+  clusterName: ClusterName
+): PromiseThunkResult => async (dispatch) => {
+  dispatch(actions.fetchClusterStatsAction.request());
+  try {
+    const payload = await clustersApiClient.getClusterStats({ clusterName });
+    dispatch(actions.fetchClusterStatsAction.success(payload));
+  } catch (e) {
+    dispatch(actions.fetchClusterStatsAction.failure());
+  }
+};
+
+export const fetchClusterMetrics = (
+  clusterName: ClusterName
+): PromiseThunkResult => async (dispatch) => {
+  dispatch(actions.fetchClusterMetricsAction.request());
+  try {
+    const payload = await clustersApiClient.getClusterMetrics({ clusterName });
+    dispatch(actions.fetchClusterMetricsAction.success(payload));
+  } catch (e) {
+    dispatch(actions.fetchClusterMetricsAction.failure());
+  }
+};

+ 49 - 0
kafka-ui-react-app/src/redux/actions/thunks/consumerGroups.ts

@@ -0,0 +1,49 @@
+import { ConsumerGroupsApi, Configuration } from 'generated-sources';
+import {
+  ConsumerGroupID,
+  PromiseThunkResult,
+  ClusterName,
+} from 'redux/interfaces';
+
+import { BASE_PARAMS } from 'lib/constants';
+import * as actions from '../actions';
+
+const apiClientConf = new Configuration(BASE_PARAMS);
+export const consumerGroupsApiClient = new ConsumerGroupsApi(apiClientConf);
+
+export const fetchConsumerGroupsList = (
+  clusterName: ClusterName
+): PromiseThunkResult => async (dispatch) => {
+  dispatch(actions.fetchConsumerGroupsAction.request());
+  try {
+    const consumerGroups = await consumerGroupsApiClient.getConsumerGroups({
+      clusterName,
+    });
+    dispatch(actions.fetchConsumerGroupsAction.success(consumerGroups));
+  } catch (e) {
+    dispatch(actions.fetchConsumerGroupsAction.failure());
+  }
+};
+
+export const fetchConsumerGroupDetails = (
+  clusterName: ClusterName,
+  consumerGroupID: ConsumerGroupID
+): PromiseThunkResult => async (dispatch) => {
+  dispatch(actions.fetchConsumerGroupDetailsAction.request());
+  try {
+    const consumerGroupDetails = await consumerGroupsApiClient.getConsumerGroup(
+      {
+        clusterName,
+        id: consumerGroupID,
+      }
+    );
+    dispatch(
+      actions.fetchConsumerGroupDetailsAction.success({
+        consumerGroupID,
+        details: consumerGroupDetails,
+      })
+    );
+  } catch (e) {
+    dispatch(actions.fetchConsumerGroupDetailsAction.failure());
+  }
+};

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

@@ -0,0 +1,5 @@
+export * from './brokers';
+export * from './clusters';
+export * from './consumerGroups';
+export * from './schemas';
+export * from './topics';

+ 59 - 0
kafka-ui-react-app/src/redux/actions/thunks/schemas.ts

@@ -0,0 +1,59 @@
+import {
+  SchemasApi,
+  Configuration,
+  NewSchemaSubject,
+  SchemaSubject,
+} from 'generated-sources';
+import { PromiseThunkResult, ClusterName, SchemaName } from 'redux/interfaces';
+
+import { BASE_PARAMS } from 'lib/constants';
+import * as actions from '../actions';
+
+const apiClientConf = new Configuration(BASE_PARAMS);
+export const schemasApiClient = new SchemasApi(apiClientConf);
+
+export const fetchSchemasByClusterName = (
+  clusterName: ClusterName
+): PromiseThunkResult<void> => async (dispatch) => {
+  dispatch(actions.fetchSchemasByClusterNameAction.request());
+  try {
+    const schemas = await schemasApiClient.getSchemas({ clusterName });
+    dispatch(actions.fetchSchemasByClusterNameAction.success(schemas));
+  } catch (e) {
+    dispatch(actions.fetchSchemasByClusterNameAction.failure());
+  }
+};
+
+export const fetchSchemaVersions = (
+  clusterName: ClusterName,
+  subject: SchemaName
+): PromiseThunkResult<void> => async (dispatch) => {
+  if (!subject) return;
+  dispatch(actions.fetchSchemaVersionsAction.request());
+  try {
+    const versions = await schemasApiClient.getAllVersionsBySubject({
+      clusterName,
+      subject,
+    });
+    dispatch(actions.fetchSchemaVersionsAction.success(versions));
+  } catch (e) {
+    dispatch(actions.fetchSchemaVersionsAction.failure());
+  }
+};
+
+export const createSchema = (
+  clusterName: ClusterName,
+  newSchemaSubject: NewSchemaSubject
+): PromiseThunkResult => async (dispatch) => {
+  dispatch(actions.createSchemaAction.request());
+  try {
+    const schema: SchemaSubject = await schemasApiClient.createNewSchema({
+      clusterName,
+      newSchemaSubject,
+    });
+    dispatch(actions.createSchemaAction.success(schema));
+  } catch (e) {
+    dispatch(actions.createSchemaAction.failure());
+    throw e;
+  }
+};

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

@@ -0,0 +1,186 @@
+import { v4 } from 'uuid';
+import {
+  TopicsApi,
+  MessagesApi,
+  Configuration,
+  Topic,
+  TopicFormData,
+  TopicConfig,
+} from 'generated-sources';
+import {
+  PromiseThunkResult,
+  ClusterName,
+  TopicName,
+  TopicMessageQueryParams,
+  TopicFormFormattedParams,
+  TopicFormDataRaw,
+  TopicsState,
+} from 'redux/interfaces';
+
+import { BASE_PARAMS } from 'lib/constants';
+import * as actions from '../actions';
+
+const apiClientConf = new Configuration(BASE_PARAMS);
+export const topicsApiClient = new TopicsApi(apiClientConf);
+export const messagesApiClient = new MessagesApi(apiClientConf);
+
+export interface FetchTopicsListParams {
+  clusterName: ClusterName;
+  page?: number;
+  perPage?: number;
+}
+
+export const fetchTopicsList = (
+  params: FetchTopicsListParams
+): 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 fetchTopicMessages = (
+  clusterName: ClusterName,
+  topicName: TopicName,
+  queryParams: Partial<TopicMessageQueryParams>
+): PromiseThunkResult => async (dispatch) => {
+  dispatch(actions.fetchTopicMessagesAction.request());
+  try {
+    const messages = await messagesApiClient.getTopicMessages({
+      clusterName,
+      topicName,
+      ...queryParams,
+    });
+    dispatch(actions.fetchTopicMessagesAction.success(messages));
+  } catch (e) {
+    dispatch(actions.fetchTopicMessagesAction.failure());
+  }
+};
+
+export const fetchTopicDetails = (
+  clusterName: ClusterName,
+  topicName: TopicName
+): PromiseThunkResult => async (dispatch) => {
+  dispatch(actions.fetchTopicDetailsAction.request());
+  try {
+    const topicDetails = await topicsApiClient.getTopicDetails({
+      clusterName,
+      topicName,
+    });
+    dispatch(
+      actions.fetchTopicDetailsAction.success({
+        topicName,
+        details: topicDetails,
+      })
+    );
+  } catch (e) {
+    dispatch(actions.fetchTopicDetailsAction.failure());
+  }
+};
+
+export const fetchTopicConfig = (
+  clusterName: ClusterName,
+  topicName: TopicName
+): PromiseThunkResult => async (dispatch) => {
+  dispatch(actions.fetchTopicConfigAction.request());
+  try {
+    const config = await topicsApiClient.getTopicConfigs({
+      clusterName,
+      topicName,
+    });
+    dispatch(actions.fetchTopicConfigAction.success({ topicName, config }));
+  } catch (e) {
+    dispatch(actions.fetchTopicConfigAction.failure());
+  }
+};
+
+const formatTopicFormData = (form: TopicFormDataRaw): TopicFormData => {
+  const {
+    name,
+    partitions,
+    replicationFactor,
+    cleanupPolicy,
+    retentionBytes,
+    retentionMs,
+    maxMessageBytes,
+    minInSyncReplicas,
+    customParams,
+  } = form;
+
+  return {
+    name,
+    partitions,
+    replicationFactor,
+    configs: {
+      'cleanup.policy': cleanupPolicy,
+      'retention.ms': retentionMs,
+      'retention.bytes': retentionBytes,
+      'max.message.bytes': maxMessageBytes,
+      'min.insync.replicas': minInSyncReplicas,
+      ...Object.values(customParams || {}).reduce(
+        (result: TopicFormFormattedParams, customParam: TopicConfig) => {
+          return {
+            ...result,
+            [customParam.name]: customParam.value,
+          };
+        },
+        {}
+      ),
+    },
+  };
+};
+
+export const createTopic = (
+  clusterName: ClusterName,
+  form: TopicFormDataRaw
+): PromiseThunkResult => async (dispatch) => {
+  dispatch(actions.createTopicAction.request());
+  try {
+    const topic: Topic = await topicsApiClient.createTopic({
+      clusterName,
+      topicFormData: formatTopicFormData(form),
+    });
+    dispatch(actions.createTopicAction.success(topic));
+  } catch (e) {
+    dispatch(actions.createTopicAction.failure());
+  }
+};
+
+export const updateTopic = (
+  clusterName: ClusterName,
+  form: TopicFormDataRaw
+): PromiseThunkResult => async (dispatch) => {
+  dispatch(actions.updateTopicAction.request());
+  try {
+    const topic: Topic = await topicsApiClient.updateTopic({
+      clusterName,
+      topicName: form.name,
+      topicFormData: formatTopicFormData(form),
+    });
+    dispatch(actions.updateTopicAction.success(topic));
+  } catch (e) {
+    dispatch(actions.updateTopicAction.failure());
+  }
+};

+ 2 - 6
kafka-ui-react-app/src/redux/interfaces/topic.ts

@@ -2,7 +2,7 @@ import {
   Topic,
   Topic,
   TopicDetails,
   TopicDetails,
   TopicMessage,
   TopicMessage,
-  TopicConfig as InputTopicConfig,
+  TopicConfig,
   TopicFormData,
   TopicFormData,
   GetTopicMessagesRequest,
   GetTopicMessagesRequest,
 } from 'generated-sources';
 } from 'generated-sources';
@@ -14,10 +14,6 @@ export enum CleanupPolicy {
   Compact = 'compact',
   Compact = 'compact',
 }
 }
 
 
-export interface TopicConfig extends InputTopicConfig {
-  id: string;
-}
-
 export interface TopicConfigByName {
 export interface TopicConfigByName {
   byName: TopicConfigParams;
   byName: TopicConfigParams;
 }
 }
@@ -50,12 +46,12 @@ export interface TopicFormCustomParams {
 
 
 export interface TopicWithDetailedInfo extends Topic, TopicDetails {
 export interface TopicWithDetailedInfo extends Topic, TopicDetails {
   config?: TopicConfig[];
   config?: TopicConfig[];
-  id: string;
 }
 }
 
 
 export interface TopicsState {
 export interface TopicsState {
   byName: { [topicName: string]: TopicWithDetailedInfo };
   byName: { [topicName: string]: TopicWithDetailedInfo };
   allNames: TopicName[];
   allNames: TopicName[];
+  totalPages: number;
   messages: TopicMessage[];
   messages: TopicMessage[];
 }
 }
 
 

+ 3 - 27
kafka-ui-react-app/src/redux/reducers/topics/reducer.ts

@@ -1,4 +1,3 @@
-import { v4 } from 'uuid';
 import { Topic, TopicMessage } from 'generated-sources';
 import { Topic, TopicMessage } from 'generated-sources';
 import { Action, TopicsState } from 'redux/interfaces';
 import { Action, TopicsState } from 'redux/interfaces';
 import { getType } from 'typesafe-actions';
 import { getType } from 'typesafe-actions';
@@ -7,38 +6,16 @@ import * as actions from 'redux/actions';
 export const initialState: TopicsState = {
 export const initialState: TopicsState = {
   byName: {},
   byName: {},
   allNames: [],
   allNames: [],
+  totalPages: 1,
   messages: [],
   messages: [],
 };
 };
 
 
-const updateTopicList = (state: TopicsState, payload: Topic[]): TopicsState => {
-  const initialMemo: TopicsState = {
-    ...state,
-    allNames: [],
-  };
-
-  return payload.reduce(
-    (memo: TopicsState, topic) => ({
-      ...memo,
-      byName: {
-        ...memo.byName,
-        [topic.name]: {
-          ...memo.byName[topic.name],
-          ...topic,
-          id: v4(),
-        },
-      },
-      allNames: [...memo.allNames, topic.name],
-    }),
-    initialMemo
-  );
-};
-
 const addToTopicList = (state: TopicsState, payload: Topic): TopicsState => {
 const addToTopicList = (state: TopicsState, payload: Topic): TopicsState => {
   const newState: TopicsState = {
   const newState: TopicsState = {
     ...state,
     ...state,
   };
   };
   newState.allNames.push(payload.name);
   newState.allNames.push(payload.name);
-  newState.byName[payload.name] = { ...payload, id: v4() };
+  newState.byName[payload.name] = { ...payload };
   return newState;
   return newState;
 };
 };
 
 
@@ -70,7 +47,7 @@ const transformTopicMessages = (
 const reducer = (state = initialState, action: Action): TopicsState => {
 const reducer = (state = initialState, action: Action): TopicsState => {
   switch (action.type) {
   switch (action.type) {
     case getType(actions.fetchTopicsListAction.success):
     case getType(actions.fetchTopicsListAction.success):
-      return updateTopicList(state, action.payload);
+      return action.payload;
     case getType(actions.fetchTopicDetailsAction.success):
     case getType(actions.fetchTopicDetailsAction.success):
       return {
       return {
         ...state,
         ...state,
@@ -93,7 +70,6 @@ const reducer = (state = initialState, action: Action): TopicsState => {
             ...state.byName[action.payload.topicName],
             ...state.byName[action.payload.topicName],
             config: action.payload.config.map((inputConfig) => ({
             config: action.payload.config.map((inputConfig) => ({
               ...inputConfig,
               ...inputConfig,
-              id: v4(),
             })),
             })),
           },
           },
         },
         },

+ 12 - 2
kafka-ui-react-app/src/redux/reducers/topics/selectors.ts

@@ -26,11 +26,21 @@ const getTopicConfigFetchingStatus = createFetchingSelector('GET_TOPIC_CONFIG');
 const getTopicCreationStatus = createFetchingSelector('POST_TOPIC');
 const getTopicCreationStatus = createFetchingSelector('POST_TOPIC');
 const getTopicUpdateStatus = createFetchingSelector('PATCH_TOPIC');
 const getTopicUpdateStatus = createFetchingSelector('PATCH_TOPIC');
 
 
-export const getIsTopicListFetched = createSelector(
+export const getAreTopicsFetching = createSelector(
+  getTopicListFetchingStatus,
+  (status) => status === 'fetching' || status === 'notFetched'
+);
+
+export const getAreTopicsFetched = createSelector(
   getTopicListFetchingStatus,
   getTopicListFetchingStatus,
   (status) => status === 'fetched'
   (status) => status === 'fetched'
 );
 );
 
 
+export const getIsTopicDetailsFetching = createSelector(
+  getTopicDetailsFetchingStatus,
+  (status) => status === 'notFetched' || status === 'fetching'
+);
+
 export const getIsTopicDetailsFetched = createSelector(
 export const getIsTopicDetailsFetched = createSelector(
   getTopicDetailsFetchingStatus,
   getTopicDetailsFetchingStatus,
   (status) => status === 'fetched'
   (status) => status === 'fetched'
@@ -57,7 +67,7 @@ export const getTopicUpdated = createSelector(
 );
 );
 
 
 export const getTopicList = createSelector(
 export const getTopicList = createSelector(
-  getIsTopicListFetched,
+  getAreTopicsFetched,
   getAllNames,
   getAllNames,
   getTopicMap,
   getTopicMap,
   (isFetched, allNames, byName) => {
   (isFetched, allNames, byName) => {