Repository: provectus/kafka-ui
Issue: Live tailing improvements
Labels: type/enhancement
good first issue
scope/frontend
status/accepted

1. New messages should appear on top
2. Pagination buttons (e.g. "Next") shouldn't be visible.
3.
wrong parameters passed for live tailing: 
`https://www.kafka-ui.provectus.io/api/clusters/local/topics/7890/messages?filterQueryType=STRING_CONTAINS&attempt=1&limit=100&seekDirection=TAILING&seekType=OFFSET&seekTo=0::0`

this should be `https://www.kafka-ui.provectus.io/api/clusters/local/topics/7890/messages&seekDirection=TAILING&seekType=LATEST&seekTo=0::0`

`seekType` should be `LATEST`
`attempt` has no effect (can be removed from all endpoints)
`limit` has no effect in TAILING mode
(not critical) `filterQueryType` make sense only when `q` param passed
`seekTo` should be 0::0,1::0,... where first number is partition

#943

Issue comments:
1. While live mode is on (active loading) all the controls (search, offset, partitions select) should be disabled. 
2. Once "stop loading" is pressed, fetched messages should NOT disappear. And controls (p. 1) should be enabled back again.

Pull request review comments:
As I can see, the above 7 lines are the exact same thing that appear in topicMessagePayload declared above. Can we reduce code duplication by storing the repeated information in a separate variable and using it by spreading at the end?
Sure.

Patch that resolved the issue:
diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx
index d840fc80c3d..e5dad2dafca 100644
--- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx
+++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx
@@ -1,6 +1,7 @@
 import 'react-datepicker/dist/react-datepicker.css';
 
 import {
+  MessageFilterType,
   Partition,
   SeekDirection,
   SeekType,
@@ -8,7 +9,6 @@ import {
   TopicMessageConsuming,
   TopicMessageEvent,
   TopicMessageEventTypeEnum,
-  MessageFilterType,
 } from 'generated-sources';
 import React, { useContext } from 'react';
 import { omitBy } from 'lodash';
@@ -17,7 +17,7 @@ import DatePicker from 'react-datepicker';
 import MultiSelect from 'components/common/MultiSelect/MultiSelect.styled';
 import { Option } from 'react-multi-select-component/dist/lib/interfaces';
 import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
-import { TopicName, ClusterName } from 'redux/interfaces';
+import { ClusterName, TopicName } from 'redux/interfaces';
 import { BASE_PARAMS } from 'lib/constants';
 import Input from 'components/common/Input/Input';
 import Select from 'components/common/Select/Select';
@@ -45,7 +45,7 @@ export interface FiltersProps {
   partitions: Partition[];
   meta: TopicMessageConsuming;
   isFetching: boolean;
-  addMessage(message: TopicMessage): void;
+  addMessage(content: { message: TopicMessage; prepend: boolean }): void;
   resetMessages(): void;
   updatePhase(phase: string): void;
   updateMeta(meta: TopicMessageConsuming): void;
@@ -304,7 +304,12 @@ const Filters: React.FC<FiltersProps> = ({
 
         switch (type) {
           case TopicMessageEventTypeEnum.MESSAGE:
-            if (message) addMessage(message);
+            if (message) {
+              addMessage({
+                message,
+                prepend: isLive,
+              });
+            }
             break;
           case TopicMessageEventTypeEnum.PHASE:
             if (phase?.name) updatePhase(phase.name);
diff --git a/kafka-ui-react-app/src/redux/actions/__test__/actions.spec.ts b/kafka-ui-react-app/src/redux/actions/__test__/actions.spec.ts
index 5058d5c6e14..6cd6550f8c6 100644
--- a/kafka-ui-react-app/src/redux/actions/__test__/actions.spec.ts
+++ b/kafka-ui-react-app/src/redux/actions/__test__/actions.spec.ts
@@ -68,7 +68,7 @@ describe('Actions', () => {
   });
 
   describe('setTopicsSearchAction', () => {
-    it('creartes SET_TOPICS_SEARCH', () => {
+    it('creates SET_TOPICS_SEARCH', () => {
       expect(actions.setTopicsSearchAction('test')).toEqual({
         type: 'SET_TOPICS_SEARCH',
         payload: 'test',
@@ -77,7 +77,7 @@ describe('Actions', () => {
   });
 
   describe('setTopicsOrderByAction', () => {
-    it('creartes SET_TOPICS_ORDER_BY', () => {
+    it('creates SET_TOPICS_ORDER_BY', () => {
       expect(actions.setTopicsOrderByAction(TopicColumnsToSort.NAME)).toEqual({
         type: 'SET_TOPICS_ORDER_BY',
         payload: TopicColumnsToSort.NAME,
@@ -87,10 +87,12 @@ describe('Actions', () => {
 
   describe('topic messages', () => {
     it('creates ADD_TOPIC_MESSAGE', () => {
-      expect(actions.addTopicMessage(topicMessagePayload)).toEqual({
-        type: 'ADD_TOPIC_MESSAGE',
-        payload: topicMessagePayload,
-      });
+      expect(actions.addTopicMessage({ message: topicMessagePayload })).toEqual(
+        {
+          type: 'ADD_TOPIC_MESSAGE',
+          payload: { message: topicMessagePayload },
+        }
+      );
     });
 
     it('creates RESET_TOPIC_MESSAGES', () => {
diff --git a/kafka-ui-react-app/src/redux/actions/actions.ts b/kafka-ui-react-app/src/redux/actions/actions.ts
index dd72f87a8a4..849941b2369 100644
--- a/kafka-ui-react-app/src/redux/actions/actions.ts
+++ b/kafka-ui-react-app/src/redux/actions/actions.ts
@@ -161,8 +161,10 @@ export const fetchTopicConsumerGroupsAction = createAsyncAction(
   'GET_TOPIC_CONSUMER_GROUPS__FAILURE'
 )<undefined, TopicsState, undefined>();
 
-export const addTopicMessage =
-  createAction('ADD_TOPIC_MESSAGE')<TopicMessage>();
+export const addTopicMessage = createAction('ADD_TOPIC_MESSAGE')<{
+  message: TopicMessage;
+  prepend?: boolean;
+}>();
 
 export const resetTopicMessages = createAction('RESET_TOPIC_MESSAGES')();
 
diff --git a/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/fixtures.ts b/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/fixtures.ts
index 05c6959c63e..8f8bce2c281 100644
--- a/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/fixtures.ts
+++ b/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/fixtures.ts
@@ -15,6 +15,12 @@ export const topicMessagePayload: TopicMessage = {
     '{"host":"schemaregistry1","port":8085,"master_eligibility":true,"scheme":"http","version":1}',
 };
 
+export const topicMessagePayloadV2: TopicMessage = {
+  ...topicMessagePayload,
+  partition: 28,
+  offset: 88,
+};
+
 export const topicMessagesMetaPayload: TopicMessageConsuming = {
   bytesConsumed: 1830,
   elapsedMs: 440,
diff --git a/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/reducer.spec.ts b/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/reducer.spec.ts
index 7d18e3284c8..c0e2ab283cd 100644
--- a/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/reducer.spec.ts
+++ b/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/reducer.spec.ts
@@ -6,16 +6,59 @@ import {
 } from 'redux/actions';
 import reducer from 'redux/reducers/topicMessages/reducer';
 
-import { topicMessagePayload, topicMessagesMetaPayload } from './fixtures';
+import {
+  topicMessagePayload,
+  topicMessagePayloadV2,
+  topicMessagesMetaPayload,
+} from './fixtures';
 
 describe('TopicMessages reducer', () => {
   it('Adds new message', () => {
-    const state = reducer(undefined, addTopicMessage(topicMessagePayload));
+    const state = reducer(
+      undefined,
+      addTopicMessage({ message: topicMessagePayload })
+    );
     expect(state.messages.length).toEqual(1);
     expect(state).toMatchSnapshot();
   });
+
+  it('Adds new message with live tailing one', () => {
+    const state = reducer(
+      undefined,
+      addTopicMessage({ message: topicMessagePayload })
+    );
+    const modifiedState = reducer(
+      state,
+      addTopicMessage({ message: topicMessagePayloadV2, prepend: true })
+    );
+    expect(modifiedState.messages.length).toEqual(2);
+    expect(modifiedState.messages).toEqual([
+      topicMessagePayloadV2,
+      topicMessagePayload,
+    ]);
+  });
+
+  it('Adds new message with live tailing off', () => {
+    const state = reducer(
+      undefined,
+      addTopicMessage({ message: topicMessagePayload })
+    );
+    const modifiedState = reducer(
+      state,
+      addTopicMessage({ message: topicMessagePayloadV2 })
+    );
+    expect(modifiedState.messages.length).toEqual(2);
+    expect(modifiedState.messages).toEqual([
+      topicMessagePayload,
+      topicMessagePayloadV2,
+    ]);
+  });
+
   it('Clears messages', () => {
-    const state = reducer(undefined, addTopicMessage(topicMessagePayload));
+    const state = reducer(
+      undefined,
+      addTopicMessage({ message: topicMessagePayload })
+    );
     expect(state.messages.length).toEqual(1);
 
     const newState = reducer(state, resetTopicMessages());
diff --git a/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/selectors.spec.ts b/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/selectors.spec.ts
index 86fdc186946..f42e03d4c1a 100644
--- a/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/selectors.spec.ts
+++ b/kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/selectors.spec.ts
@@ -28,7 +28,7 @@ describe('TopicMessages selectors', () => {
 
   describe('state', () => {
     beforeAll(() => {
-      store.dispatch(addTopicMessage(topicMessagePayload));
+      store.dispatch(addTopicMessage({ message: topicMessagePayload }));
       store.dispatch(updateTopicMessagesPhase('consuming'));
       store.dispatch(updateTopicMessagesMeta(topicMessagesMetaPayload));
     });
diff --git a/kafka-ui-react-app/src/redux/reducers/topicMessages/reducer.ts b/kafka-ui-react-app/src/redux/reducers/topicMessages/reducer.ts
index ecab598063d..1d4ee836a15 100644
--- a/kafka-ui-react-app/src/redux/reducers/topicMessages/reducer.ts
+++ b/kafka-ui-react-app/src/redux/reducers/topicMessages/reducer.ts
@@ -1,6 +1,7 @@
 import { Action, TopicMessagesState } from 'redux/interfaces';
 import { getType } from 'typesafe-actions';
 import * as actions from 'redux/actions';
+import { TopicMessage } from 'generated-sources';
 
 export const initialState: TopicMessagesState = {
   messages: [],
@@ -17,9 +18,13 @@ export const initialState: TopicMessagesState = {
 const reducer = (state = initialState, action: Action): TopicMessagesState => {
   switch (action.type) {
     case getType(actions.addTopicMessage): {
+      const messages: TopicMessage[] = action.payload.prepend
+        ? [action.payload.message, ...state.messages]
+        : [...state.messages, action.payload.message];
+
       return {
         ...state,
-        messages: [action.payload, ...state.messages],
+        messages,
       };
     }
     case getType(actions.resetTopicMessages):


Test patch: