Kaynağa Gözat

[CHORE] Refactor Topics section

Oleg Shuralev 4 yıl önce
ebeveyn
işleme
0595f23434
35 değiştirilmiş dosya ile 418 ekleme ve 433 silme
  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. 11 21
      kafka-ui-react-app/src/redux/actions/thunks/topics.ts
  33. 1 6
      kafka-ui-react-app/src/redux/interfaces/topic.ts
  34. 1 3
      kafka-ui-react-app/src/redux/reducers/topics/reducer.ts
  35. 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 { Switch, Route, Redirect, useParams } from 'react-router-dom';
 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 Schemas from 'components/Schemas/Schemas';
 import { getClustersReadonlyStatus } from 'redux/reducers/clusters/selectors';
@@ -18,10 +18,7 @@ const Cluster: React.FC = () => {
           path="/ui/clusters/:clusterName/brokers"
           component={BrokersContainer}
         />
-        <Route
-          path="/ui/clusters/:clusterName/topics"
-          component={TopicsContainer}
-        />
+        <Route path="/ui/clusters/:clusterName/topics" component={Topics} />
         <Route
           path="/ui/clusters/:clusterName/consumer-groups"
           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 { TopicWithDetailedInfo, ClusterName } from 'redux/interfaces';
 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 usePagination from 'lib/hooks/usePagination';
+import { FetchTopicsListParams } from 'redux/actions';
 import ClusterContext from 'components/contexts/ClusterContext';
+import PageLoader from 'components/common/PageLoader/PageLoader';
 import ListItem from './ListItem';
 
 interface Props {
-  clusterName: ClusterName;
+  areTopicsFetching: boolean;
   topics: 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 handleSwitch = () => setShowInternal(!showInternal);
-  const { isReadOnly } = React.useContext(ClusterContext);
+  const handleSwitch = React.useCallback(() => {
+    setShowInternal(!showInternal);
+  }, [showInternal]);
+
   const items = showInternal ? topics : externalTopics;
 
   return (
     <div className="section">
-      <Breadcrumb>All Topics</Breadcrumb>
-
+      <Breadcrumb>{showInternal ? `All Topics` : `External Topics`}</Breadcrumb>
       <div className="box">
         <div className="level">
           <div className="level-item level-left">
@@ -50,23 +68,33 @@ const List: React.FC<Props> = ({ clusterName, topics, externalTopics }) => {
           </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>
   );
 };

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

@@ -1,29 +1,21 @@
 import { connect } from 'react-redux';
-import { ClusterName, RootState } from 'redux/interfaces';
+import { RootState } from 'redux/interfaces';
+import { fetchTopicsList } from 'redux/actions';
 import {
   getTopicList,
   getExternalTopicList,
+  getAreTopicsFetching,
 } from 'redux/reducers/topics/selectors';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
 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),
   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 React from 'react';
+import { StaticRouter } from 'react-router-dom';
 import ClusterContext from 'components/contexts/ClusterContext';
 import List from '../List';
 
 describe('List', () => {
   describe('when it has readonly flag', () => {
     it('does not render the Add a Topic button', () => {
-      const props = {
-        clusterName: 'Cluster',
-        topics: [],
-        externalTopics: [],
-      };
       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();
     });
   });
+
+  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 { ClusterName, TopicName } from 'redux/interfaces';
 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 {
-  clusterTopicsPath,
   clusterTopicSettingsPath,
   clusterTopicPath,
   clusterTopicMessagesPath,
@@ -14,7 +12,6 @@ import ClusterContext from 'components/contexts/ClusterContext';
 import OverviewContainer from './Overview/OverviewContainer';
 import MessagesContainer from './Messages/MessagesContainer';
 import SettingsContainer from './Settings/SettingsContainer';
-import SettingsEditButton from './Settings/SettingsEditButton';
 
 interface Props extends Topic, TopicDetails {
   clusterName: ClusterName;
@@ -23,27 +20,11 @@ interface Props extends Topic, TopicDetails {
 
 const Details: React.FC<Props> = ({ clusterName, topicName }) => {
   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
             exact
             to={clusterTopicPath(clusterName, topicName)}
@@ -68,26 +49,36 @@ const Details: React.FC<Props> = ({ clusterName, topicName }) => {
           >
             Settings
           </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>
   );
 };

+ 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 { shallow } from 'enzyme';
-import MessageItem from 'components/Topics/Details/Messages/MessageItem';
+import MessageItem from 'components/Topics/Topic/Details/Messages/MessageItem';
 import { messages } from './fixtures';
 
 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 * as useDebounce from 'use-debounce';
 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 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 MessagesTable, {
   MessagesTableProp,
-} from 'components/Topics/Details/Messages/MessagesTable';
+} from 'components/Topics/Topic/Details/Messages/MessagesTable';
 import { messages } from './fixtures';
 
 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 { ClusterName, TopicName, TopicConfig } from 'redux/interfaces';
+import { ClusterName, TopicName } from 'redux/interfaces';
 
 interface Props {
   clusterName: ClusterName;
@@ -56,7 +57,7 @@ const Sertings: React.FC<Props> = ({
         </thead>
         <tbody>
           {config.map((item) => (
-            <ConfigListItem key={item.id} config={item} />
+            <ConfigListItem key={item.name} config={item} />
           ))}
         </tbody>
       </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 { useForm, FormProvider } from 'react-hook-form';
 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 {
   clusterName: ClusterName;
   topicName: TopicName;
   topic?: TopicWithDetailedInfo;
   isFetched: boolean;
-  isTopicDetailsFetched: boolean;
   isTopicUpdated: boolean;
-  fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) => void;
   fetchTopicConfig: (clusterName: ClusterName, topicName: TopicName) => void;
   updateTopic: (clusterName: ClusterName, form: TopicFormDataRaw) => void;
-  redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => void;
-  resetUploadedState: () => void;
 }
 
 const DEFAULTS = {
@@ -68,32 +64,29 @@ const Edit: React.FC<Props> = ({
   topicName,
   topic,
   isFetched,
-  isTopicDetailsFetched,
   isTopicUpdated,
-  fetchTopicDetails,
   fetchTopicConfig,
   updateTopic,
-  redirectToTopicPath,
 }) => {
   const defaultValues = topicParams(topic);
 
   const methods = useForm<TopicFormDataRaw>({ defaultValues });
 
   const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);
+  const history = useHistory();
 
   React.useEffect(() => {
     fetchTopicConfig(clusterName, topicName);
-    fetchTopicDetails(clusterName, topicName);
-  }, [fetchTopicConfig, fetchTopicDetails, clusterName, topicName]);
+  }, [fetchTopicConfig, clusterName, topicName]);
 
   React.useEffect(() => {
     if (isSubmitting && isTopicUpdated) {
       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;
   }
 
@@ -116,27 +109,17 @@ const Edit: React.FC<Props> = ({
   };
 
   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}
-          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>
   );
 };

+ 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 { 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 {
-  getTopicByName,
-  getIsTopicDetailsFetched,
+  getTopicConfigFetched,
+  getTopicUpdated,
+  getFullTopic,
 } from 'redux/reducers/topics/selectors';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
-import Overview from './Overview';
+
+import Edit from './Edit';
 
 interface RouteProps {
   clusterName: ClusterName;
@@ -25,15 +27,14 @@ const mapStateToProps = (
 ) => ({
   clusterName,
   topicName,
-  isFetched: getIsTopicDetailsFetched(state),
-  ...getTopicByName(state, topicName),
+  topic: getFullTopic(state, topicName),
+  isFetched: getTopicConfigFetched(state),
+  isTopicUpdated: getTopicUpdated(state),
 });
 
 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 { ClusterName } from 'redux/interfaces';
 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 DetailsContainer from './Details/DetailsContainer';
+import TopicContainer from './Topic/TopicContainer';
 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;

+ 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,
       byIndex: {
         ...formCustomParams.byIndex,
-        [newIndex]: { name: '', value: '', id: v4() },
+        [newIndex]: { name: '', value: '' },
       },
       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;

+ 11 - 21
kafka-ui-react-app/src/redux/actions/thunks/topics.ts

@@ -30,26 +30,13 @@ export interface FetchTopicsListParams {
   perPage?: number;
 }
 
-export const fetchTopicsList = ({
-  clusterName,
-  page,
-  perPage,
-}: FetchTopicsListParams): PromiseThunkResult => async (dispatch, getState) => {
+export const fetchTopicsList = (
+  params: FetchTopicsListParams
+): PromiseThunkResult => async (dispatch, getState) => {
   dispatch(actions.fetchTopicsListAction.request());
   try {
-    const { topics, pageCount } = await topicsApiClient.getTopics({
-      clusterName,
-      page,
-      perPage,
-    });
-
-    const initialMemo: TopicsState = {
-      ...getState().topics,
-      allNames: [],
-      totalPages: pageCount || 1,
-    };
-
-    const state = (topics || []).reduce(
+    const { topics, pageCount } = await topicsApiClient.getTopics(params);
+    const newState = (topics || []).reduce(
       (memo: TopicsState, topic) => ({
         ...memo,
         byName: {
@@ -62,10 +49,13 @@ export const fetchTopicsList = ({
         },
         allNames: [...memo.allNames, topic.name],
       }),
-      initialMemo
+      {
+        ...getState().topics,
+        allNames: [],
+        totalPages: pageCount || 1,
+      }
     );
-
-    dispatch(actions.fetchTopicsListAction.success(state));
+    dispatch(actions.fetchTopicsListAction.success(newState));
   } catch (e) {
     dispatch(actions.fetchTopicsListAction.failure());
   }

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

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

+ 1 - 3
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 { Action, TopicsState } from 'redux/interfaces';
 import { getType } from 'typesafe-actions';
@@ -16,7 +15,7 @@ const addToTopicList = (state: TopicsState, payload: Topic): TopicsState => {
     ...state,
   };
   newState.allNames.push(payload.name);
-  newState.byName[payload.name] = { ...payload, id: v4() };
+  newState.byName[payload.name] = { ...payload };
   return newState;
 };
 
@@ -71,7 +70,6 @@ const reducer = (state = initialState, action: Action): TopicsState => {
             ...state.byName[action.payload.topicName],
             config: action.payload.config.map((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 getTopicUpdateStatus = createFetchingSelector('PATCH_TOPIC');
 
-export const getIsTopicListFetched = createSelector(
+export const getAreTopicsFetching = createSelector(
+  getTopicListFetchingStatus,
+  (status) => status === 'fetching' || status === 'notFetched'
+);
+
+export const getAreTopicsFetched = createSelector(
   getTopicListFetchingStatus,
   (status) => status === 'fetched'
 );
 
+export const getIsTopicDetailsFetching = createSelector(
+  getTopicDetailsFetchingStatus,
+  (status) => status === 'notFetched' || status === 'fetching'
+);
+
 export const getIsTopicDetailsFetched = createSelector(
   getTopicDetailsFetchingStatus,
   (status) => status === 'fetched'
@@ -57,7 +67,7 @@ export const getTopicUpdated = createSelector(
 );
 
 export const getTopicList = createSelector(
-  getIsTopicListFetched,
+  getAreTopicsFetched,
   getAllNames,
   getTopicMap,
   (isFetched, allNames, byName) => {