Browse Source

Merge pull request #746 from provectus/#710/internal-topics-repagination

[#710] feat(react-app): call fetchTopics on toggle show internal topics
Ilnur Farukhshin 3 years ago
parent
commit
ec70e28bff

+ 35 - 9
kafka-ui-react-app/package-lock.json

@@ -9710,16 +9710,12 @@
       "dev": true
       "dev": true
     },
     },
     "history": {
     "history": {
-      "version": "4.10.1",
-      "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
-      "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/history/-/history-5.0.0.tgz",
+      "integrity": "sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg==",
+      "dev": true,
       "requires": {
       "requires": {
-        "@babel/runtime": "^7.1.2",
-        "loose-envify": "^1.2.0",
-        "resolve-pathname": "^3.0.0",
-        "tiny-invariant": "^1.0.2",
-        "tiny-warning": "^1.0.0",
-        "value-equal": "^1.0.1"
+        "@babel/runtime": "^7.7.6"
       }
       }
     },
     },
     "hmac-drbg": {
     "hmac-drbg": {
@@ -16643,6 +16639,21 @@
         "react-is": "^16.6.0",
         "react-is": "^16.6.0",
         "tiny-invariant": "^1.0.2",
         "tiny-invariant": "^1.0.2",
         "tiny-warning": "^1.0.0"
         "tiny-warning": "^1.0.0"
+      },
+      "dependencies": {
+        "history": {
+          "version": "4.10.1",
+          "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
+          "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
+          "requires": {
+            "@babel/runtime": "^7.1.2",
+            "loose-envify": "^1.2.0",
+            "resolve-pathname": "^3.0.0",
+            "tiny-invariant": "^1.0.2",
+            "tiny-warning": "^1.0.0",
+            "value-equal": "^1.0.1"
+          }
+        }
       }
       }
     },
     },
     "react-router-dom": {
     "react-router-dom": {
@@ -16657,6 +16668,21 @@
         "react-router": "5.2.0",
         "react-router": "5.2.0",
         "tiny-invariant": "^1.0.2",
         "tiny-invariant": "^1.0.2",
         "tiny-warning": "^1.0.0"
         "tiny-warning": "^1.0.0"
+      },
+      "dependencies": {
+        "history": {
+          "version": "4.10.1",
+          "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
+          "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
+          "requires": {
+            "@babel/runtime": "^7.1.2",
+            "loose-envify": "^1.2.0",
+            "resolve-pathname": "^3.0.0",
+            "tiny-invariant": "^1.0.2",
+            "tiny-warning": "^1.0.0",
+            "value-equal": "^1.0.1"
+          }
+        }
       }
       }
     },
     },
     "react-scripts": {
     "react-scripts": {

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

@@ -109,6 +109,7 @@
     "eslint-plugin-react-hooks": "^4.2.0",
     "eslint-plugin-react-hooks": "^4.2.0",
     "esprint": "^3.1.0",
     "esprint": "^3.1.0",
     "fetch-mock-jest": "^1.5.1",
     "fetch-mock-jest": "^1.5.1",
+    "history": "^5.0.0",
     "husky": "^7.0.0",
     "husky": "^7.0.0",
     "jest-sonar-reporter": "^2.0.0",
     "jest-sonar-reporter": "^2.0.0",
     "lint-staged": "^11.0.0",
     "lint-staged": "^11.0.0",

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

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import { useHistory } from 'react-router';
 import {
 import {
   TopicWithDetailedInfo,
   TopicWithDetailedInfo,
   ClusterName,
   ClusterName,
@@ -8,22 +9,21 @@ import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
 import { Link, useParams } from 'react-router-dom';
 import { Link, useParams } from 'react-router-dom';
 import { clusterTopicNewPath } from 'lib/paths';
 import { clusterTopicNewPath } from 'lib/paths';
 import usePagination from 'lib/hooks/usePagination';
 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 PageLoader from 'components/common/PageLoader/PageLoader';
 import Pagination from 'components/common/Pagination/Pagination';
 import Pagination from 'components/common/Pagination/Pagination';
-import { TopicColumnsToSort } from 'generated-sources';
+import { GetTopicsRequest, TopicColumnsToSort } from 'generated-sources';
 import SortableColumnHeader from 'components/common/table/SortableCulumnHeader/SortableColumnHeader';
 import SortableColumnHeader from 'components/common/table/SortableCulumnHeader/SortableColumnHeader';
 import Search from 'components/common/Search/Search';
 import Search from 'components/common/Search/Search';
+import { PER_PAGE } from 'lib/constants';
 
 
 import ListItem from './ListItem';
 import ListItem from './ListItem';
 
 
-interface Props {
+export interface TopicsListProps {
   areTopicsFetching: boolean;
   areTopicsFetching: boolean;
   topics: TopicWithDetailedInfo[];
   topics: TopicWithDetailedInfo[];
-  externalTopics: TopicWithDetailedInfo[];
   totalPages: number;
   totalPages: number;
-  fetchTopicsList(props: FetchTopicsListParams): void;
+  fetchTopicsList(props: GetTopicsRequest): void;
   deleteTopic(topicName: TopicName, clusterName: ClusterName): void;
   deleteTopic(topicName: TopicName, clusterName: ClusterName): void;
   clearTopicMessages(
   clearTopicMessages(
     topicName: TopicName,
     topicName: TopicName,
@@ -36,10 +36,9 @@ interface Props {
   setTopicsOrderBy(orderBy: TopicColumnsToSort | null): void;
   setTopicsOrderBy(orderBy: TopicColumnsToSort | null): void;
 }
 }
 
 
-const List: React.FC<Props> = ({
+const List: React.FC<TopicsListProps> = ({
   areTopicsFetching,
   areTopicsFetching,
   topics,
   topics,
-  externalTopics,
   totalPages,
   totalPages,
   fetchTopicsList,
   fetchTopicsList,
   deleteTopic,
   deleteTopic,
@@ -51,7 +50,9 @@ const List: React.FC<Props> = ({
 }) => {
 }) => {
   const { isReadOnly } = React.useContext(ClusterContext);
   const { isReadOnly } = React.useContext(ClusterContext);
   const { clusterName } = useParams<{ clusterName: ClusterName }>();
   const { clusterName } = useParams<{ clusterName: ClusterName }>();
-  const { page, perPage } = usePagination();
+  const { page, perPage, pathname } = usePagination();
+  const [showInternal, setShowInternal] = React.useState<boolean>(true);
+  const history = useHistory();
 
 
   React.useEffect(() => {
   React.useEffect(() => {
     fetchTopicsList({
     fetchTopicsList({
@@ -60,19 +61,23 @@ const List: React.FC<Props> = ({
       perPage,
       perPage,
       orderBy: orderBy || undefined,
       orderBy: orderBy || undefined,
       search,
       search,
+      showInternal,
     });
     });
-  }, [fetchTopicsList, clusterName, page, perPage, orderBy, search]);
-
-  const [showInternal, setShowInternal] = React.useState<boolean>(true);
+  }, [
+    fetchTopicsList,
+    clusterName,
+    page,
+    perPage,
+    orderBy,
+    search,
+    showInternal,
+  ]);
 
 
   const handleSwitch = React.useCallback(() => {
   const handleSwitch = React.useCallback(() => {
     setShowInternal(!showInternal);
     setShowInternal(!showInternal);
+    history.push(`${pathname}?page=1&perPage=${perPage || PER_PAGE}`);
   }, [showInternal]);
   }, [showInternal]);
 
 
-  const handleSearch = (value: string) => setTopicsSearch(value);
-
-  const items = showInternal ? topics : externalTopics;
-
   return (
   return (
     <div className="section">
     <div className="section">
       <Breadcrumb>{showInternal ? `All Topics` : `External Topics`}</Breadcrumb>
       <Breadcrumb>{showInternal ? `All Topics` : `External Topics`}</Breadcrumb>
@@ -93,7 +98,7 @@ const List: React.FC<Props> = ({
           </div>
           </div>
           <div className="column">
           <div className="column">
             <Search
             <Search
-              handleSearch={handleSearch}
+              handleSearch={setTopicsSearch}
               placeholder="Search by Topic Name"
               placeholder="Search by Topic Name"
               value={search}
               value={search}
             />
             />
@@ -144,7 +149,7 @@ const List: React.FC<Props> = ({
               </tr>
               </tr>
             </thead>
             </thead>
             <tbody>
             <tbody>
-              {items.map((topic) => (
+              {topics.map((topic) => (
                 <ListItem
                 <ListItem
                   clusterName={clusterName}
                   clusterName={clusterName}
                   key={topic.name}
                   key={topic.name}
@@ -153,7 +158,7 @@ const List: React.FC<Props> = ({
                   clearTopicMessages={clearTopicMessages}
                   clearTopicMessages={clearTopicMessages}
                 />
                 />
               ))}
               ))}
-              {items.length === 0 && (
+              {topics.length === 0 && (
                 <tr>
                 <tr>
                   <td colSpan={10}>No topics found</td>
                   <td colSpan={10}>No topics found</td>
                 </tr>
                 </tr>

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

@@ -9,7 +9,6 @@ import {
 } from 'redux/actions';
 } from 'redux/actions';
 import {
 import {
   getTopicList,
   getTopicList,
-  getExternalTopicList,
   getAreTopicsFetching,
   getAreTopicsFetching,
   getTopicListTotalPages,
   getTopicListTotalPages,
   getTopicsSearch,
   getTopicsSearch,
@@ -21,7 +20,6 @@ import List from './List';
 const mapStateToProps = (state: RootState) => ({
 const mapStateToProps = (state: RootState) => ({
   areTopicsFetching: getAreTopicsFetching(state),
   areTopicsFetching: getAreTopicsFetching(state),
   topics: getTopicList(state),
   topics: getTopicList(state),
-  externalTopics: getExternalTopicList(state),
   totalPages: getTopicListTotalPages(state),
   totalPages: getTopicListTotalPages(state),
   search: getTopicsSearch(state),
   search: getTopicsSearch(state),
   orderBy: getTopicsOrderBy(state),
   orderBy: getTopicsOrderBy(state),

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

@@ -1,84 +1,126 @@
 import React from 'react';
 import React from 'react';
-import { mount } from 'enzyme';
-import { StaticRouter } from 'react-router-dom';
-import ClusterContext from 'components/contexts/ClusterContext';
-import List from 'components/Topics/List/List';
+import { mount, ReactWrapper } from 'enzyme';
+import { Router } from 'react-router-dom';
+import ClusterContext, {
+  ContextProps,
+} from 'components/contexts/ClusterContext';
+import List, { TopicsListProps } from 'components/Topics/List/List';
+import { createMemoryHistory } from 'history';
+import { StaticRouter } from 'react-router';
+import Search from 'components/common/Search/Search';
 
 
 describe('List', () => {
 describe('List', () => {
+  const setupComponent = (props: Partial<TopicsListProps> = {}) => (
+    <List
+      areTopicsFetching={false}
+      topics={[]}
+      totalPages={1}
+      fetchTopicsList={jest.fn()}
+      deleteTopic={jest.fn()}
+      clearTopicMessages={jest.fn()}
+      search=""
+      orderBy={null}
+      setTopicsSearch={jest.fn()}
+      setTopicsOrderBy={jest.fn()}
+      {...props}
+    />
+  );
+
+  const historyMock = createMemoryHistory();
+
+  const mountComponentWithProviders = (
+    contextProps: Partial<ContextProps> = {},
+    props: Partial<TopicsListProps> = {},
+    history = historyMock
+  ) =>
+    mount(
+      <Router history={history}>
+        <ClusterContext.Provider
+          value={{
+            isReadOnly: true,
+            hasKafkaConnectConfigured: true,
+            hasSchemaRegistryConfigured: true,
+            ...contextProps,
+          }}
+        >
+          {setupComponent(props)}
+        </ClusterContext.Provider>
+      </Router>
+    );
+
   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 component = mount(
+      const component = mountComponentWithProviders();
+      expect(component.exists('Link')).toBeFalsy();
+    });
+  });
+
+  describe('when it does not have readonly flag', () => {
+    let fetchTopicsList = jest.fn();
+    let component: ReactWrapper;
+
+    jest.useFakeTimers();
+
+    beforeEach(() => {
+      fetchTopicsList = jest.fn();
+      component = mountComponentWithProviders(
+        { isReadOnly: false },
+        { fetchTopicsList }
+      );
+    });
+
+    it('renders the Add a Topic button', () => {
+      expect(component.exists('Link')).toBeTruthy();
+    });
+    it('matches the snapshot', () => {
+      component = mount(
         <StaticRouter>
         <StaticRouter>
           <ClusterContext.Provider
           <ClusterContext.Provider
             value={{
             value={{
-              isReadOnly: true,
+              isReadOnly: false,
               hasKafkaConnectConfigured: true,
               hasKafkaConnectConfigured: true,
               hasSchemaRegistryConfigured: true,
               hasSchemaRegistryConfigured: true,
             }}
             }}
           >
           >
-            <List
-              areTopicsFetching={false}
-              topics={[]}
-              externalTopics={[]}
-              totalPages={1}
-              fetchTopicsList={jest.fn()}
-              deleteTopic={jest.fn()}
-              clearTopicMessages={jest.fn()}
-              search=""
-              orderBy={null}
-              setTopicsSearch={jest.fn()}
-              setTopicsOrderBy={jest.fn()}
-            />
+            {setupComponent()}
           </ClusterContext.Provider>
           </ClusterContext.Provider>
         </StaticRouter>
         </StaticRouter>
       );
       );
-      expect(component.exists('Link')).toBeFalsy();
+      expect(component).toMatchSnapshot();
     });
     });
-  });
 
 
-  describe('when it does not have readonly flag', () => {
-    const mockFetch = jest.fn();
-    jest.useFakeTimers();
-    const component = mount(
-      <StaticRouter>
-        <ClusterContext.Provider
-          value={{
-            isReadOnly: false,
-            hasKafkaConnectConfigured: true,
-            hasSchemaRegistryConfigured: true,
-          }}
-        >
-          <List
-            areTopicsFetching={false}
-            topics={[]}
-            externalTopics={[]}
-            totalPages={1}
-            fetchTopicsList={mockFetch}
-            deleteTopic={jest.fn()}
-            clearTopicMessages={jest.fn()}
-            search=""
-            orderBy={null}
-            setTopicsSearch={jest.fn()}
-            setTopicsOrderBy={jest.fn()}
-          />
-        </ClusterContext.Provider>
-      </StaticRouter>
-    );
-    it('renders the Add a Topic button', () => {
-      expect(component.exists('Link')).toBeTruthy();
+    it('calls setTopicsSearch on input', () => {
+      const setTopicsSearch = jest.fn();
+      component = mountComponentWithProviders({}, { setTopicsSearch });
+      const query = 'topic';
+      const input = component.find(Search);
+      input.props().handleSearch(query);
+      expect(setTopicsSearch).toHaveBeenCalledWith(query);
     });
     });
-    it('matches the snapshot', () => {
-      expect(component).toMatchSnapshot();
+
+    it('should refetch topics on show internal toggle change', () => {
+      jest.clearAllMocks();
+      const toggle = component.find('input#switchRoundedDefault');
+      toggle.simulate('change');
+      expect(fetchTopicsList).toHaveBeenLastCalledWith({
+        search: '',
+        showInternal: false,
+      });
     });
     });
 
 
-    it('calls fetchTopicsList on input', () => {
-      const input = component.find('input').at(1);
-      input.simulate('change', { target: { value: 't' } });
-      expect(setTimeout).toHaveBeenCalledTimes(1);
-      expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 300);
-      setTimeout(() => {
-        expect(mockFetch).toHaveBeenCalledTimes(1);
-      }, 301);
+    it('should reset page query param on show internal toggle change', () => {
+      const mockedHistory = createMemoryHistory();
+      jest.spyOn(mockedHistory, 'push');
+      component = mountComponentWithProviders(
+        { isReadOnly: false },
+        { fetchTopicsList },
+        mockedHistory
+      );
+
+      const toggle = component.find('input#switchRoundedDefault');
+      toggle.simulate('change');
+
+      expect(mockedHistory.push).toHaveBeenCalledWith('/?page=1&perPage=25');
     });
     });
   });
   });
 });
 });

+ 23 - 3
kafka-ui-react-app/src/components/Topics/List/__tests__/__snapshots__/List.spec.tsx.snap

@@ -28,8 +28,28 @@ exports[`List when it does not have readonly flag matches the snapshot 1`] = `
       areTopicsFetching={false}
       areTopicsFetching={false}
       clearTopicMessages={[MockFunction]}
       clearTopicMessages={[MockFunction]}
       deleteTopic={[MockFunction]}
       deleteTopic={[MockFunction]}
-      externalTopics={Array []}
-      fetchTopicsList={[MockFunction]}
+      fetchTopicsList={
+        [MockFunction] {
+          "calls": Array [
+            Array [
+              Object {
+                "clusterName": undefined,
+                "orderBy": undefined,
+                "page": undefined,
+                "perPage": undefined,
+                "search": "",
+                "showInternal": true,
+              },
+            ],
+          ],
+          "results": Array [
+            Object {
+              "type": "return",
+              "value": undefined,
+            },
+          ],
+        }
+      }
       orderBy={null}
       orderBy={null}
       search=""
       search=""
       setTopicsOrderBy={[MockFunction]}
       setTopicsOrderBy={[MockFunction]}
@@ -89,7 +109,7 @@ exports[`List when it does not have readonly flag matches the snapshot 1`] = `
               className="column"
               className="column"
             >
             >
               <Search
               <Search
-                handleSearch={[Function]}
+                handleSearch={[MockFunction]}
                 placeholder="Search by Topic Name"
                 placeholder="Search by Topic Name"
                 value=""
                 value=""
               >
               >

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

@@ -7,9 +7,9 @@ import {
   TopicCreation,
   TopicCreation,
   TopicUpdate,
   TopicUpdate,
   TopicConfig,
   TopicConfig,
-  TopicColumnsToSort,
   ConsumerGroupsApi,
   ConsumerGroupsApi,
   CreateTopicMessage,
   CreateTopicMessage,
+  GetTopicsRequest,
 } from 'generated-sources';
 } from 'generated-sources';
 import {
 import {
   PromiseThunkResult,
   PromiseThunkResult,
@@ -32,17 +32,8 @@ export const topicConsumerGroupsApiClient = new ConsumerGroupsApi(
   apiClientConf
   apiClientConf
 );
 );
 
 
-export interface FetchTopicsListParams {
-  clusterName: ClusterName;
-  page?: number;
-  perPage?: number;
-  showInternal?: boolean;
-  search?: string;
-  orderBy?: TopicColumnsToSort;
-}
-
 export const fetchTopicsList =
 export const fetchTopicsList =
-  (params: FetchTopicsListParams): PromiseThunkResult =>
+  (params: GetTopicsRequest): PromiseThunkResult =>
   async (dispatch, getState) => {
   async (dispatch, getState) => {
     dispatch(actions.fetchTopicsListAction.request());
     dispatch(actions.fetchTopicsListAction.request());
     try {
     try {

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

@@ -112,10 +112,6 @@ export const getTopicList = createSelector(
   }
   }
 );
 );
 
 
-export const getExternalTopicList = createSelector(getTopicList, (topics) =>
-  topics.filter(({ internal }) => !internal)
-);
-
 const getTopicName = (_: RootState, topicName: TopicName) => topicName;
 const getTopicName = (_: RootState, topicName: TopicName) => topicName;
 
 
 export const getTopicByName = createSelector(
 export const getTopicByName = createSelector(