diff --git a/kafka-ui-react-app/package-lock.json b/kafka-ui-react-app/package-lock.json index c4c0c86b407..c4db16d76c1 100644 --- a/kafka-ui-react-app/package-lock.json +++ b/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 } } diff --git a/kafka-ui-react-app/package.json b/kafka-ui-react-app/package.json index 6ef6b6e9cd5..b145959d691 100644 --- a/kafka-ui-react-app/package.json +++ b/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", diff --git a/kafka-ui-react-app/src/components/App.scss b/kafka-ui-react-app/src/components/App.scss index 2508994b1ba..a3b857b53c8 100644 --- a/kafka-ui-react-app/src/components/App.scss +++ b/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; } } diff --git a/kafka-ui-react-app/src/components/Topics/Details/Messages/Messages.tsx b/kafka-ui-react-app/src/components/Topics/Details/Messages/Messages.tsx index 429456465b6..012952b15b6 100644 --- a/kafka-ui-react-app/src/components/Topics/Details/Messages/Messages.tsx +++ b/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 = ({ + isFetched, clusterName, topicName, + messages, + fetchTopicMessages, }) => { + React.useEffect(() => { + fetchTopicMessages(clusterName, topicName); + }, [fetchTopicMessages, clusterName, topicName]); + + const [searchText, setSearchText] = React.useState(''); + + const handleInputChange = (event: React.ChangeEvent) => { + 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({`content.${k}`}) + ); + + return headers; + }; + + const getMessageContentBody = (content: string) => { + const c = JSON.parse(content); + const columns: JSX.Element[] = []; + Object.values(c).map((v) => columns.push({JSON.stringify(v)})); + return columns; + }; + return ( -

- Messages from {clusterName}{topicName} -

+ // eslint-disable-next-line no-nested-ternary + isFetched ? ( + messages.length > 0 ? ( +
+
+
+ +
+
+ + + + + + + {getMessageContentHeaders()} + + + + {messages + .filter( + (message) => + !searchText || message?.content?.indexOf(searchText) >= 0 + ) + .map((message) => ( + + + + + {getMessageContentBody(message.content)} + + ))} + +
TimestampOffsetPartition
{getTimestampDate(message.timestamp)}{message.offset}{message.partition}
+
+ ) : ( +
No messages at selected topic
+ ) + ) : ( + + ) ); }; diff --git a/kafka-ui-react-app/src/components/Topics/Details/Messages/MessagesContainer.ts b/kafka-ui-react-app/src/components/Topics/Details/Messages/MessagesContainer.ts index 2a26dd181eb..336389054aa 100644 --- a/kafka-ui-react-app/src/components/Topics/Details/Messages/MessagesContainer.ts +++ b/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 { } +type OwnProps = RouteComponentProps; -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) ); diff --git a/kafka-ui-react-app/src/components/common/PageLoader/PageLoader.tsx b/kafka-ui-react-app/src/components/common/PageLoader/PageLoader.tsx index 66868cf0dff..2f5932a4f87 100644 --- a/kafka-ui-react-app/src/components/common/PageLoader/PageLoader.tsx +++ b/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 = () => ( -
-
+interface Props { + isFullHeight: boolean; +} + +const PageLoader: React.FC> = ({ isFullHeight = true }) => ( +
+
Loading...
(); +export const fetchTopicMessagesAction = createAsyncAction( + ActionType.GET_TOPIC_MESSAGES__REQUEST, + ActionType.GET_TOPIC_MESSAGES__SUCCESS, + ActionType.GET_TOPIC_MESSAGES__FAILURE +)(); + export const fetchTopicDetailsAction = createAsyncAction( ActionType.GET_TOPIC_DETAILS__REQUEST, ActionType.GET_TOPIC_DETAILS__SUCCESS, diff --git a/kafka-ui-react-app/src/redux/actions/thunks.ts b/kafka-ui-react-app/src/redux/actions/thunks.ts index 67e9588bff9..d3134727280 100644 --- a/kafka-ui-react-app/src/redux/actions/thunks.ts +++ b/kafka-ui-react-app/src/redux/actions/thunks.ts @@ -57,6 +57,19 @@ export const fetchTopicList = ( } }; +export const fetchTopicMessages = ( + clusterName: ClusterName, + topicName: TopicName +): PromiseThunk => 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 diff --git a/kafka-ui-react-app/src/redux/api/topics.ts b/kafka-ui-react-app/src/redux/api/topics.ts index 037802adb25..e12473f1d58 100644 --- a/kafka-ui-react-app/src/redux/api/topics.ts +++ b/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 => ...BASE_PARAMS, }).then((res) => res.json()); +export const getTopicMessages = ( + clusterName: ClusterName, + topicName: TopicName +): Promise => + fetch(`${BASE_URL}/clusters/${clusterName}/topics/${topicName}/messages`, { + ...BASE_PARAMS, + }).then((res) => res.json()); + export const postTopic = ( clusterName: ClusterName, form: TopicFormData diff --git a/kafka-ui-react-app/src/redux/interfaces/topic.ts b/kafka-ui-react-app/src/redux/interfaces/topic.ts index 49656ba2cfb..1d56ebec550 100644 --- a/kafka-ui-react-app/src/redux/interfaces/topic.ts +++ b/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; + 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 { diff --git a/kafka-ui-react-app/src/redux/reducers/topics/reducer.ts b/kafka-ui-react-app/src/redux/reducers/topics/reducer.ts index 6a8510c07e2..a76d8b7a3b6 100644 --- a/kafka-ui-react-app/src/redux/reducers/topics/reducer.ts +++ b/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, diff --git a/kafka-ui-react-app/src/redux/reducers/topics/selectors.ts b/kafka-ui-react-app/src/redux/reducers/topics/selectors.ts index 69de07eb3e5..7db2724fa85 100644 --- a/kafka-ui-react-app/src/redux/reducers/topics/selectors.ts +++ b/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