浏览代码

Add topic messages UI (#61)

maxim_tereshin 5 年之前
父节点
当前提交
f974febb5f

+ 129 - 36
kafka-ui-react-app/package-lock.json

@@ -3369,8 +3369,7 @@
         },
         "kind-of": {
           "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
-          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "resolved": "",
           "dev": true
         }
       }
@@ -3415,7 +3414,8 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
       "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==",
-      "dev": true
+      "dev": true,
+      "optional": true
     },
     "bindings": {
       "version": "1.5.0",
@@ -4008,6 +4008,7 @@
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz",
       "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==",
       "dev": true,
+      "optional": true,
       "requires": {
         "anymatch": "~3.1.1",
         "braces": "~3.0.2",
@@ -4024,6 +4025,7 @@
           "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
           "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
           "dev": true,
+          "optional": true,
           "requires": {
             "normalize-path": "^3.0.0",
             "picomatch": "^2.0.4"
@@ -4034,6 +4036,7 @@
           "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
           "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
           "dev": true,
+          "optional": true,
           "requires": {
             "fill-range": "^7.0.1"
           }
@@ -4043,6 +4046,7 @@
           "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
           "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
           "dev": true,
+          "optional": true,
           "requires": {
             "to-regex-range": "^5.0.1"
           }
@@ -4051,19 +4055,22 @@
           "version": "7.0.0",
           "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
           "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "normalize-path": {
           "version": "3.0.0",
           "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
           "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "to-regex-range": {
           "version": "5.0.1",
           "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
           "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
           "dev": true,
+          "optional": true,
           "requires": {
             "is-number": "^7.0.0"
           }
@@ -5100,6 +5107,11 @@
         }
       }
     },
+    "date-fns": {
+      "version": "2.14.0",
+      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.14.0.tgz",
+      "integrity": "sha512-1zD+68jhFgDIM0rF05rcwYO8cExdNqxjq4xP1QKM60Q45mnO6zaMWB4tOzrIr4M4GSLntsKeE4c9Bdl2jhL/yw=="
+    },
     "debug": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@@ -5236,9 +5248,9 @@
           }
         },
         "kind-of": {
-          "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
-          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "version": "6.0.3",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+          "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
           "dev": true
         }
       }
@@ -6905,8 +6917,7 @@
         },
         "kind-of": {
           "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
-          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "resolved": "",
           "dev": true
         }
       }
@@ -7243,11 +7254,100 @@
         "worker-rpc": "^0.1.0"
       },
       "dependencies": {
+        "anymatch": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
+          "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
+          "dev": true,
+          "requires": {
+            "normalize-path": "^3.0.0",
+            "picomatch": "^2.0.4"
+          }
+        },
+        "binary-extensions": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
+          "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==",
+          "dev": true
+        },
+        "braces": {
+          "version": "3.0.2",
+          "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+          "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+          "dev": true,
+          "requires": {
+            "fill-range": "^7.0.1"
+          }
+        },
+        "chokidar": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz",
+          "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==",
+          "dev": true,
+          "requires": {
+            "anymatch": "~3.1.1",
+            "braces": "~3.0.2",
+            "fsevents": "~2.1.2",
+            "glob-parent": "~5.1.0",
+            "is-binary-path": "~2.1.0",
+            "is-glob": "~4.0.1",
+            "normalize-path": "~3.0.0",
+            "readdirp": "~3.4.0"
+          }
+        },
+        "fill-range": {
+          "version": "7.0.1",
+          "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+          "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+          "dev": true,
+          "requires": {
+            "to-regex-range": "^5.0.1"
+          }
+        },
+        "is-binary-path": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+          "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+          "dev": true,
+          "requires": {
+            "binary-extensions": "^2.0.0"
+          }
+        },
+        "is-number": {
+          "version": "7.0.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+          "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+          "dev": true
+        },
+        "normalize-path": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+          "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+          "dev": true
+        },
+        "readdirp": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
+          "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
+          "dev": true,
+          "requires": {
+            "picomatch": "^2.2.1"
+          }
+        },
         "semver": {
           "version": "5.7.1",
           "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
           "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
           "dev": true
+        },
+        "to-regex-range": {
+          "version": "5.0.1",
+          "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+          "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+          "dev": true,
+          "requires": {
+            "is-number": "^7.0.0"
+          }
         }
       }
     },
@@ -8623,6 +8723,7 @@
       "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
       "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
       "dev": true,
+      "optional": true,
       "requires": {
         "binary-extensions": "^2.0.0"
       }
@@ -9762,9 +9863,9 @@
           }
         },
         "yargs-parser": {
-          "version": "15.0.0",
-          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.0.tgz",
-          "integrity": "sha512-xLTUnCMc4JhxrPEPUYD5IBR1mWCK/aT6+RJ/K29JY2y1vD+FhtgKK0AXRWvI262q3QSffAQuTouFIKUuHX89wQ==",
+          "version": "15.0.1",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz",
+          "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==",
           "dev": true,
           "requires": {
             "camelcase": "^5.0.0",
@@ -10983,9 +11084,9 @@
       },
       "dependencies": {
         "kind-of": {
-          "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
-          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "version": "6.0.3",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+          "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
           "dev": true
         }
       }
@@ -11104,9 +11205,9 @@
       }
     },
     "minimist": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
-      "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
       "dev": true
     },
     "minipass": {
@@ -11203,20 +11304,12 @@
       }
     },
     "mkdirp": {
-      "version": "0.5.1",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
-      "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+      "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
       "dev": true,
       "requires": {
-        "minimist": "0.0.8"
-      },
-      "dependencies": {
-        "minimist": {
-          "version": "0.0.8",
-          "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
-          "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
-          "dev": true
-        }
+        "minimist": "^1.2.5"
       }
     },
     "morgan": {
@@ -11323,9 +11416,9 @@
       },
       "dependencies": {
         "kind-of": {
-          "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
-          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "version": "6.0.3",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+          "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
           "dev": true
         }
       }
@@ -14610,6 +14703,7 @@
       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
       "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
       "dev": true,
+      "optional": true,
       "requires": {
         "picomatch": "^2.2.1"
       }
@@ -16062,8 +16156,7 @@
         },
         "kind-of": {
           "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
-          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "resolved": "",
           "dev": true
         }
       }

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

@@ -7,6 +7,7 @@
     "bulma-switch": "^2.0.0",
     "classnames": "^2.2.6",
     "immer": "^6.0.5",
+    "date-fns": "^2.14.0",
     "lodash": "^4.17.15",
     "pretty-ms": "^6.0.1",
     "react": "^16.12.0",

+ 1 - 1
kafka-ui-react-app/src/components/App.scss

@@ -25,6 +25,6 @@ $navbar-width: 250px;
     left: 0;
     bottom: 0;
     padding: 20px 20px;
-
+    overflow-y: scroll;
   }
 }

+ 90 - 4
kafka-ui-react-app/src/components/Topics/Details/Messages/Messages.tsx

@@ -1,19 +1,105 @@
 import React from 'react';
-import { ClusterName, TopicName } from 'redux/interfaces';
+import { ClusterName, TopicMessage, TopicName } from 'redux/interfaces';
+import PageLoader from 'components/common/PageLoader/PageLoader';
+import { format } from 'date-fns';
 
 interface Props {
   clusterName: ClusterName;
   topicName: TopicName;
+  isFetched: boolean;
+  fetchTopicMessages: (clusterName: ClusterName, topicName: TopicName) => void;
+  messages: TopicMessage[];
 }
 
 const Messages: React.FC<Props> = ({
+  isFetched,
   clusterName,
   topicName,
+  messages,
+  fetchTopicMessages,
 }) => {
+  React.useEffect(() => {
+    fetchTopicMessages(clusterName, topicName);
+  }, [fetchTopicMessages, clusterName, topicName]);
+
+  const [searchText, setSearchText] = React.useState<string>('');
+
+  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    setSearchText(event.target.value);
+  };
+
+  const getTimestampDate = (timestamp: number) => {
+    return format(new Date(timestamp * 1000), 'MM.dd.yyyy HH:mm:ss');
+  };
+
+  const getMessageContentHeaders = () => {
+    const message = messages[0];
+    const headers: JSX.Element[] = [];
+    const content = JSON.parse(message.content);
+    Object.keys(content).forEach((k) =>
+      headers.push(<th>{`content.${k}`}</th>)
+    );
+
+    return headers;
+  };
+
+  const getMessageContentBody = (content: string) => {
+    const c = JSON.parse(content);
+    const columns: JSX.Element[] = [];
+    Object.values(c).map((v) => columns.push(<td>{JSON.stringify(v)}</td>));
+    return columns;
+  };
+
   return (
-    <h1>
-      Messages from {clusterName}{topicName}
-    </h1>
+    // eslint-disable-next-line no-nested-ternary
+    isFetched ? (
+      messages.length > 0 ? (
+        <div>
+          <div className="columns">
+            <div className="column is-half is-offset-half">
+              <input
+                id="searchText"
+                type="text"
+                name="searchText"
+                className="input"
+                placeholder="Search"
+                value={searchText}
+                onChange={handleInputChange}
+              />
+            </div>
+          </div>
+          <table className="table is-striped is-fullwidth">
+            <thead>
+              <tr>
+                <th>Timestamp</th>
+                <th>Offset</th>
+                <th>Partition</th>
+                {getMessageContentHeaders()}
+              </tr>
+            </thead>
+            <tbody>
+              {messages
+                .filter(
+                  (message) =>
+                    !searchText || message?.content?.indexOf(searchText) >= 0
+                )
+                .map((message) => (
+                  <tr key={message.timestamp}>
+                    <td>{getTimestampDate(message.timestamp)}</td>
+                    <td>{message.offset}</td>
+                    <td>{message.partition}</td>
+                    {getMessageContentBody(message.content)}
+                  </tr>
+                ))}
+            </tbody>
+          </table>
+        </div>
+      ) : (
+        <div>No messages at selected topic</div>
+      )
+    ) : (
+      <PageLoader isFullHeight={false} />
+    )
   );
 };
 

+ 25 - 5
kafka-ui-react-app/src/components/Topics/Details/Messages/MessagesContainer.ts

@@ -1,20 +1,40 @@
 import { connect } from 'react-redux';
+import { ClusterName, RootState, TopicName } from 'redux/interfaces';
+import { RouteComponentProps, withRouter } from 'react-router-dom';
+import { fetchTopicMessages } from 'redux/actions';
+import {
+  getIsTopicMessagesFetched,
+  getTopicMessages,
+} from 'redux/reducers/topics/selectors';
+
 import Messages from './Messages';
-import {ClusterName, RootState, TopicName} from 'redux/interfaces';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
 
 interface RouteProps {
   clusterName: ClusterName;
   topicName: TopicName;
 }
 
-interface OwnProps extends RouteComponentProps<RouteProps> { }
+type OwnProps = RouteComponentProps<RouteProps>;
 
-const mapStateToProps = (state: RootState, { match: { params: { topicName, clusterName } } }: OwnProps) => ({
+const mapStateToProps = (
+  state: RootState,
+  {
+    match: {
+      params: { topicName, clusterName },
+    },
+  }: OwnProps
+) => ({
   clusterName,
   topicName,
+  isFetched: getIsTopicMessagesFetched(state),
+  messages: getTopicMessages(state),
 });
 
+const mapDispatchToProps = {
+  fetchTopicMessages: (clusterName: ClusterName, topicName: TopicName) =>
+    fetchTopicMessages(clusterName, topicName),
+};
+
 export default withRouter(
-  connect(mapStateToProps)(Messages)
+  connect(mapStateToProps, mapDispatchToProps)(Messages)
 );

+ 16 - 3
kafka-ui-react-app/src/components/common/PageLoader/PageLoader.tsx

@@ -1,8 +1,21 @@
 import React from 'react';
+import cx from 'classnames';
 
-const PageLoader: React.FC = () => (
-  <section className="hero is-fullheight-with-navbar">
-    <div className="hero-body has-text-centered" style={{ justifyContent: 'center' }}>
+interface Props {
+  isFullHeight: boolean;
+}
+
+const PageLoader: React.FC<Partial<Props>> = ({ isFullHeight = true }) => (
+  <section
+    className={cx(
+      'hero',
+      isFullHeight ? 'is-fullheight-with-navbar' : 'is-halfheight'
+    )}
+  >
+    <div
+      className="hero-body has-text-centered"
+      style={{ justifyContent: 'center' }}
+    >
       <div style={{ width: 300 }}>
         <div className="subtitle">Loading...</div>
         <progress

+ 4 - 0
kafka-ui-react-app/src/redux/actionType.ts

@@ -15,6 +15,10 @@ enum ActionType {
   GET_TOPICS__SUCCESS = 'GET_TOPICS__SUCCESS',
   GET_TOPICS__FAILURE = 'GET_TOPICS__FAILURE',
 
+  GET_TOPIC_MESSAGES__REQUEST = 'GET_TOPIC_MESSAGES__REQUEST',
+  GET_TOPIC_MESSAGES__SUCCESS = 'GET_TOPIC_MESSAGES__SUCCESS',
+  GET_TOPIC_MESSAGES__FAILURE = 'GET_TOPIC_MESSAGES__FAILURE',
+
   GET_TOPIC_DETAILS__REQUEST = 'GET_TOPIC_DETAILS__REQUEST',
   GET_TOPIC_DETAILS__SUCCESS = 'GET_TOPIC_DETAILS__SUCCESS',
   GET_TOPIC_DETAILS__FAILURE = 'GET_TOPIC_DETAILS__FAILURE',

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

@@ -7,6 +7,7 @@ import {
   Topic,
   TopicConfig,
   TopicDetails,
+  TopicMessage,
   TopicName,
   ConsumerGroup,
   ConsumerGroupDetails,
@@ -37,6 +38,12 @@ export const fetchTopicListAction = createAsyncAction(
   ActionType.GET_TOPICS__FAILURE
 )<undefined, Topic[], undefined>();
 
+export const fetchTopicMessagesAction = createAsyncAction(
+  ActionType.GET_TOPIC_MESSAGES__REQUEST,
+  ActionType.GET_TOPIC_MESSAGES__SUCCESS,
+  ActionType.GET_TOPIC_MESSAGES__FAILURE
+)<undefined, TopicMessage[], undefined>();
+
 export const fetchTopicDetailsAction = createAsyncAction(
   ActionType.GET_TOPIC_DETAILS__REQUEST,
   ActionType.GET_TOPIC_DETAILS__SUCCESS,

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

@@ -57,6 +57,19 @@ export const fetchTopicList = (
   }
 };
 
+export const fetchTopicMessages = (
+  clusterName: ClusterName,
+  topicName: TopicName
+): PromiseThunk<void> => async (dispatch) => {
+  dispatch(actions.fetchTopicMessagesAction.request());
+  try {
+    const messages = await api.getTopicMessages(clusterName, topicName);
+    dispatch(actions.fetchTopicMessagesAction.success(messages));
+  } catch (e) {
+    dispatch(actions.fetchTopicMessagesAction.failure());
+  }
+};
+
 export const fetchTopicDetails = (
   clusterName: ClusterName,
   topicName: TopicName

+ 9 - 0
kafka-ui-react-app/src/redux/api/topics.ts

@@ -1,5 +1,6 @@
 import {
   TopicName,
+  TopicMessage,
   Topic,
   ClusterName,
   TopicDetails,
@@ -46,6 +47,14 @@ export const getTopics = (clusterName: ClusterName): Promise<Topic[]> =>
     ...BASE_PARAMS,
   }).then((res) => res.json());
 
+export const getTopicMessages = (
+  clusterName: ClusterName,
+  topicName: TopicName
+): Promise<TopicMessage[]> =>
+  fetch(`${BASE_URL}/clusters/${clusterName}/topics/${topicName}/messages`, {
+    ...BASE_PARAMS,
+  }).then((res) => res.json());
+
 export const postTopic = (
   clusterName: ClusterName,
   form: TopicFormData

+ 11 - 0
kafka-ui-react-app/src/redux/interfaces/topic.ts

@@ -50,6 +50,16 @@ export interface Topic {
   partitions: TopicPartition[];
 }
 
+export interface TopicMessage {
+  partition: number;
+  offset: number;
+  timestamp: number;
+  timestampType: string;
+  key: string;
+  headers: Record<string, string>;
+  content: string;
+}
+
 export interface TopicFormCustomParam {
   name: string;
   value: string;
@@ -67,6 +77,7 @@ export interface TopicWithDetailedInfo extends Topic, TopicDetails {
 export interface TopicsState {
   byName: { [topicName: string]: TopicWithDetailedInfo };
   allNames: TopicName[];
+  messages: TopicMessage[];
 }
 
 export interface TopicFormFormattedParams {

+ 6 - 0
kafka-ui-react-app/src/redux/reducers/topics/reducer.ts

@@ -4,6 +4,7 @@ import ActionType from 'redux/actionType';
 export const initialState: TopicsState = {
   byName: {},
   allNames: [],
+  messages: [],
 };
 
 const updateTopicList = (state: TopicsState, payload: Topic[]): TopicsState => {
@@ -48,6 +49,11 @@ const reducer = (state = initialState, action: Action): TopicsState => {
           },
         },
       };
+    case ActionType.GET_TOPIC_MESSAGES__SUCCESS:
+      return {
+        ...state,
+        messages: action.payload,
+      };
     case ActionType.GET_TOPIC_CONFIG__SUCCESS:
       return {
         ...state,

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

@@ -12,11 +12,16 @@ const topicsState = ({ topics }: RootState): TopicsState => topics;
 
 const getAllNames = (state: RootState) => topicsState(state).allNames;
 const getTopicMap = (state: RootState) => topicsState(state).byName;
+export const getTopicMessages = (state: RootState) =>
+  topicsState(state).messages;
 
 const getTopicListFetchingStatus = createFetchingSelector('GET_TOPICS');
 const getTopicDetailsFetchingStatus = createFetchingSelector(
   'GET_TOPIC_DETAILS'
 );
+const getTopicMessagesFetchingStatus = createFetchingSelector(
+  'GET_TOPIC_MESSAGES'
+);
 const getTopicConfigFetchingStatus = createFetchingSelector('GET_TOPIC_CONFIG');
 const getTopicCreationStatus = createFetchingSelector('POST_TOPIC');
 const getTopicUpdateStatus = createFetchingSelector('PATCH_TOPIC');
@@ -31,6 +36,11 @@ export const getIsTopicDetailsFetched = createSelector(
   (status) => status === FetchStatus.fetched
 );
 
+export const getIsTopicMessagesFetched = createSelector(
+  getTopicMessagesFetchingStatus,
+  (status) => status === FetchStatus.fetched
+);
+
 export const getTopicConfigFetched = createSelector(
   getTopicConfigFetchingStatus,
   (status) => status === FetchStatus.fetched