Repository: provectus/kafka-ui
Issue: Messages pagination is missing
Labels: type/bug
scope/frontend
status/accepted
status/confirmed
type/regression

backward button -- use as browser's "back" function
forward -- as it was before

use /v2 as reference

Issue comments:

Pull request review comments:

Patch that resolved the issue:
diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.tsx
index c010ca65cb5..87d59f3b8e3 100644
--- a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.tsx
+++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/Filters.tsx
@@ -93,6 +93,8 @@ const Filters: React.FC<FiltersProps> = ({
   const navigate = useNavigate();
   const [searchParams] = useSearchParams();
 
+  const page = searchParams.get('page');
+
   const { data: topic } = useTopicDetails({ clusterName, topicName });
 
   const partitions = topic?.partitions || [];
@@ -198,6 +200,7 @@ const Filters: React.FC<FiltersProps> = ({
       filterQueryType: queryType,
       attempt: nextAttempt,
       limit: PER_PAGE,
+      page: page || 0,
       seekDirection,
       keySerde: keySerde || (searchParams.get('keySerde') as string),
       valueSerde: valueSerde || (searchParams.get('valueSerde') as string),
@@ -379,6 +382,7 @@ const Filters: React.FC<FiltersProps> = ({
     timestamp,
     query,
     seekDirection,
+    page,
   ]);
 
   React.useEffect(() => {
diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Messages.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Messages.tsx
index b71e4eb8fa2..35490c627e3 100644
--- a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Messages.tsx
+++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Messages.tsx
@@ -6,6 +6,7 @@ import { useSerdes } from 'lib/hooks/api/topicMessages';
 import useAppParams from 'lib/hooks/useAppParams';
 import { RouteParamsClusterTopic } from 'lib/paths';
 import { getDefaultSerdeName } from 'components/Topics/Topic/Messages/getDefaultSerdeName';
+import { MESSAGES_PER_PAGE } from 'lib/constants';
 
 import MessagesTable from './MessagesTable';
 import FiltersContainer from './Filters/FiltersContainer';
@@ -47,6 +48,9 @@ const Messages: React.FC = () => {
     if (!searchParams.get('valueSerde')) {
       searchParams.set('valueSerde', getDefaultSerdeName(serdes.value || []));
     }
+    if (!searchParams.get('limit')) {
+      searchParams.set('limit', MESSAGES_PER_PAGE);
+    }
     setSearchParams(searchParams);
   }, [serdes]);
 
diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/MessagesTable.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/MessagesTable.tsx
index 3c5901214b0..96e9f40d73d 100644
--- a/kafka-ui-react-app/src/components/Topics/Topic/Messages/MessagesTable.tsx
+++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/MessagesTable.tsx
@@ -9,6 +9,10 @@ import {
 } from 'redux/reducers/topicMessages/selectors';
 import TopicMessagesContext from 'components/contexts/TopicMessagesContext';
 import { useAppSelector } from 'lib/hooks/redux';
+import { Button } from 'components/common/Button/Button';
+import { useSearchParams } from 'react-router-dom';
+import { MESSAGES_PER_PAGE } from 'lib/constants';
+import * as S from 'components/common/NewTable/Table.styled';
 
 import PreviewModal from './PreviewModal';
 import Message, { PreviewFilter } from './Message';
@@ -19,75 +23,123 @@ const MessagesTable: React.FC = () => {
   const [keyFilters, setKeyFilters] = useState<PreviewFilter[]>([]);
   const [contentFilters, setContentFilters] = useState<PreviewFilter[]>([]);
 
+  const [searchParams, setSearchParams] = useSearchParams();
+  const page = searchParams.get('page');
   const { isLive } = useContext(TopicMessagesContext);
 
   const messages = useAppSelector(getTopicMessges);
   const isFetching = useAppSelector(getIsTopicMessagesFetching);
+
+  const isTailing = isLive && isFetching;
+
+  // Pagination is disabled in live mode, also we don't want to show the button
+  // if we are fetching the messages or if we are at the end of the topic
+  const isPaginationDisabled = isTailing || isFetching;
+
+  const isNextPageButtonDisabled =
+    isPaginationDisabled || messages.length < Number(MESSAGES_PER_PAGE);
+  const isPrevPageButtonDisabled =
+    isPaginationDisabled || !Number(searchParams.get('page'));
+
+  const handleNextPage = () => {
+    searchParams.set('page', String(Number(page || 0) + 1));
+    setSearchParams(searchParams);
+  };
+
+  const handlePrevPage = () => {
+    searchParams.set('page', String(Number(page || 0) - 1));
+    setSearchParams(searchParams);
+  };
+
   return (
-    <Table isFullwidth>
-      <thead>
-        <tr>
-          <TableHeaderCell> </TableHeaderCell>
-          <TableHeaderCell title="Offset" />
-          <TableHeaderCell title="Partition" />
-          <TableHeaderCell title="Timestamp" />
-          <TableHeaderCell
-            title="Key"
-            previewText={`Preview ${
-              keyFilters.length ? `(${keyFilters.length} selected)` : ''
-            }`}
-            onPreview={() => setPreviewFor('key')}
-          />
-          <TableHeaderCell
-            title="Value"
-            previewText={`Preview ${
-              contentFilters.length ? `(${contentFilters.length} selected)` : ''
-            }`}
-            onPreview={() => setPreviewFor('content')}
-          />
-          <TableHeaderCell> </TableHeaderCell>
+    <>
+      <Table isFullwidth>
+        <thead>
+          <tr>
+            <TableHeaderCell> </TableHeaderCell>
+            <TableHeaderCell title="Offset" />
+            <TableHeaderCell title="Partition" />
+            <TableHeaderCell title="Timestamp" />
+            <TableHeaderCell
+              title="Key"
+              previewText={`Preview ${
+                keyFilters.length ? `(${keyFilters.length} selected)` : ''
+              }`}
+              onPreview={() => setPreviewFor('key')}
+            />
+            <TableHeaderCell
+              title="Value"
+              previewText={`Preview ${
+                contentFilters.length
+                  ? `(${contentFilters.length} selected)`
+                  : ''
+              }`}
+              onPreview={() => setPreviewFor('content')}
+            />
+            <TableHeaderCell> </TableHeaderCell>
 
-          {previewFor !== null && (
-            <PreviewModal
-              values={previewFor === 'key' ? keyFilters : contentFilters}
-              toggleIsOpen={() => setPreviewFor(null)}
-              setFilters={(payload: PreviewFilter[]) =>
-                previewFor === 'key'
-                  ? setKeyFilters(payload)
-                  : setContentFilters(payload)
-              }
+            {previewFor !== null && (
+              <PreviewModal
+                values={previewFor === 'key' ? keyFilters : contentFilters}
+                toggleIsOpen={() => setPreviewFor(null)}
+                setFilters={(payload: PreviewFilter[]) =>
+                  previewFor === 'key'
+                    ? setKeyFilters(payload)
+                    : setContentFilters(payload)
+                }
+              />
+            )}
+          </tr>
+        </thead>
+        <tbody>
+          {messages.map((message: TopicMessage) => (
+            <Message
+              key={[
+                message.offset,
+                message.timestamp,
+                message.key,
+                message.partition,
+              ].join('-')}
+              message={message}
+              keyFilters={keyFilters}
+              contentFilters={contentFilters}
             />
+          ))}
+          {isFetching && isLive && !messages.length && (
+            <tr>
+              <td colSpan={10}>
+                <PageLoader />
+              </td>
+            </tr>
           )}
-        </tr>
-      </thead>
-      <tbody>
-        {messages.map((message: TopicMessage) => (
-          <Message
-            key={[
-              message.offset,
-              message.timestamp,
-              message.key,
-              message.partition,
-            ].join('-')}
-            message={message}
-            keyFilters={keyFilters}
-            contentFilters={contentFilters}
-          />
-        ))}
-        {isFetching && isLive && !messages.length && (
-          <tr>
-            <td colSpan={10}>
-              <PageLoader />
-            </td>
-          </tr>
-        )}
-        {messages.length === 0 && !isFetching && (
-          <tr>
-            <td colSpan={10}>No messages found</td>
-          </tr>
-        )}
-      </tbody>
-    </Table>
+          {messages.length === 0 && !isFetching && (
+            <tr>
+              <td colSpan={10}>No messages found</td>
+            </tr>
+          )}
+        </tbody>
+      </Table>
+      <S.Pagination>
+        <S.Pages>
+          <Button
+            buttonType="secondary"
+            buttonSize="L"
+            disabled={isPrevPageButtonDisabled}
+            onClick={handlePrevPage}
+          >
+            ← Back
+          </Button>
+          <Button
+            buttonType="secondary"
+            buttonSize="L"
+            disabled={isNextPageButtonDisabled}
+            onClick={handleNextPage}
+          >
+            Next →
+          </Button>
+        </S.Pages>
+      </S.Pagination>
+    </>
   );
 };
 
diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/MessagesTable.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/MessagesTable.spec.tsx
index 37ad28b202b..e2aeb68bbba 100644
--- a/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/MessagesTable.spec.tsx
+++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/__test__/MessagesTable.spec.tsx
@@ -85,9 +85,10 @@ describe('MessagesTable', () => {
   });
 
   describe('Custom Setup with different props value', () => {
-    it('should check if next click is gone during isLive Param', () => {
+    it('should check if next button and previous is disabled isLive Param', () => {
       setUpComponent(searchParams, { ...contextValue, isLive: true });
-      expect(screen.queryByText(/next/i)).not.toBeInTheDocument();
+      expect(screen.queryByText(/next/i)).toBeDisabled();
+      expect(screen.queryByText(/back/i)).toBeDisabled();
     });
 
     it('should check the display of the loader element', () => {


Test patch: