From be2d38133d5f9001d83611cbfc5d9c65a97d1184 Mon Sep 17 00:00:00 2001 From: maxim_tereshin Date: Tue, 7 Jul 2020 14:45:34 +0500 Subject: [PATCH] Topic messages filtering paginating (#72) * Add filtering and pagination for topic messages * Add delay to search query, momoize some functions * Add partition selection --- kafka-ui-react-app/.eslintrc.json | 3 +- kafka-ui-react-app/package-lock.json | 99 +++++++-- kafka-ui-react-app/package.json | 5 +- .../Topics/Details/Messages/Messages.tsx | 196 ++++++++++++++---- .../Details/Messages/MessagesContainer.ts | 2 + kafka-ui-react-app/src/redux/api/topics.ts | 4 +- .../src/redux/reducers/topics/selectors.ts | 6 + 7 files changed, 258 insertions(+), 57 deletions(-) diff --git a/kafka-ui-react-app/.eslintrc.json b/kafka-ui-react-app/.eslintrc.json index a28472dca9..8e195bfad7 100644 --- a/kafka-ui-react-app/.eslintrc.json +++ b/kafka-ui-react-app/.eslintrc.json @@ -57,7 +57,8 @@ "node": { "extensions": [".js", ".jsx", ".ts", ".tsx"], "paths": ["src"] - } + }, + "typescript": {} } } } diff --git a/kafka-ui-react-app/package-lock.json b/kafka-ui-react-app/package-lock.json index 19d0a8eee3..80d728a361 100644 --- a/kafka-ui-react-app/package-lock.json +++ b/kafka-ui-react-app/package-lock.json @@ -1556,6 +1556,11 @@ "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", "dev": true }, + "@rooks/use-outside-click": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@rooks/use-outside-click/-/use-outside-click-3.6.0.tgz", + "integrity": "sha512-DDxdcD9bDDArV2tBmh5okaJNee/7EWaC5DsPrjTxIhhvXPpUatizcn2qYLcvX7y1vYpd64Wyqvkb87E6fsIfEQ==" + }, "@samverschueren/stream-to-observable": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz", @@ -1889,6 +1894,11 @@ "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", "dev": true }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" + }, "@types/lodash": { "version": "4.14.149", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz", @@ -5133,7 +5143,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, "requires": { "ms": "^2.1.1" } @@ -6054,6 +6063,18 @@ } } }, + "eslint-import-resolver-typescript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.0.0.tgz", + "integrity": "sha512-bT5Frpl8UWoHBtY25vKUOMoVIMlJQOMefHLyQ4Tz3MQpIZ2N6yYKEEIHMo38bszBNUuMBW6M3+5JNYxeiGFH4w==", + "requires": { + "debug": "^4.1.1", + "is-glob": "^4.0.1", + "resolve": "^1.12.0", + "tiny-glob": "^0.2.6", + "tsconfig-paths": "^3.9.0" + } + }, "eslint-loader": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/eslint-loader/-/eslint-loader-3.0.3.tgz", @@ -7726,6 +7747,11 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, + "globalyzer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.4.tgz", + "integrity": "sha512-LeguVWaxgHN0MNbWC6YljNMzHkrCny9fzjmEUdnF1kQ7wATFD1RHFRqA1qxaX2tgxGENlcxjOflopBwj3YZiXA==" + }, "globby": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.2.tgz", @@ -7755,6 +7781,11 @@ } } }, + "globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" + }, "globule": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.0.tgz", @@ -7766,6 +7797,11 @@ "minimatch": "~3.0.2" } }, + "goober": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/goober/-/goober-1.8.0.tgz", + "integrity": "sha512-9ZFoOkBccexjqIgcwlhq7C/eCSkgTZX0BdNUkOnBFLedrJgo3R8lp9ckd/qqtngtF/JDyXSxJzwMU98kNjZ4Mw==" + }, "got": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", @@ -8826,8 +8862,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-finite": { "version": "1.0.2", @@ -8854,7 +8889,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, "requires": { "is-extglob": "^2.1.1" } @@ -11213,8 +11247,7 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "minipass": { "version": "3.1.3", @@ -11365,8 +11398,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "multicast-dns": { "version": "6.2.3", @@ -12427,8 +12459,7 @@ "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, "path-to-regexp": { "version": "0.1.7", @@ -14313,6 +14344,15 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==" }, + "react-multi-select-component": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/react-multi-select-component/-/react-multi-select-component-2.0.12.tgz", + "integrity": "sha512-QcOc8zQgz9AQQkX51EuDokqPi8BIRGBQdvnn1im3d1gsSIIY2W09jkvd9+/ByVk6NiL4XjygJtwCGJSGQcr3+A==", + "requires": { + "@rooks/use-outside-click": "^3.6.0", + "goober": "^1.8.0" + } + }, "react-onclickoutside": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.9.0.tgz", @@ -15102,7 +15142,6 @@ "version": "1.12.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.2.tgz", "integrity": "sha512-cAVTI2VLHWYsGOirfeYVVQ7ZDejtQ9fp4YhYckWDEkFfqbVjaT11iM8k6xSAfGFMM+gDpZjMnFssPu8we+mqFw==", - "dev": true, "requires": { "path-parse": "^1.0.6" } @@ -16875,8 +16914,7 @@ "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" }, "strip-comments": { "version": "1.0.2", @@ -17351,6 +17389,15 @@ "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", "dev": true }, + "tiny-glob": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.6.tgz", + "integrity": "sha512-A7ewMqPu1B5PWwC3m7KVgAu96Ch5LA0w4SnEN/LbDREj/gAD0nPWboRbn8YoP9ISZXqeNAlMvKSKoEuhcfK3Pw==", + "requires": { + "globalyzer": "^0.1.0", + "globrex": "^0.1.1" + } + }, "tiny-invariant": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz", @@ -17477,6 +17524,27 @@ "integrity": "sha512-ti7OGMOUOzo66wLF3liskw6YQIaSsBgc4GOAlWRnIEj8htCxJUxskanMUoJOD6MDCRAXo36goXJZch+nOS0VMA==", "dev": true }, + "tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + } + } + }, "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", @@ -17806,6 +17874,11 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, + "use-debounce": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-3.4.3.tgz", + "integrity": "sha512-nxy+opOxDccWfhMl36J5BSCTpvcj89iaQk2OZWLAtBJQj7ISCtx1gh+rFbdjGfMl6vtCZf6gke/kYvrkVfHMoA==" + }, "util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", diff --git a/kafka-ui-react-app/package.json b/kafka-ui-react-app/package.json index 7c7c7aeb49..092e9a8e0a 100644 --- a/kafka-ui-react-app/package.json +++ b/kafka-ui-react-app/package.json @@ -8,6 +8,7 @@ "bulma-switch": "^2.0.0", "classnames": "^2.2.6", "date-fns": "^2.14.0", + "eslint-import-resolver-typescript": "^2.0.0", "immer": "^6.0.5", "lodash": "^4.17.15", "pretty-ms": "^6.0.1", @@ -15,12 +16,14 @@ "react-datepicker": "^3.0.0", "react-dom": "^16.12.0", "react-hook-form": "^4.5.5", + "react-multi-select-component": "^2.0.12", "react-redux": "^7.1.3", "react-router-dom": "^5.1.2", "redux": "^4.0.5", "redux-thunk": "^2.3.0", "reselect": "^4.0.0", - "typesafe-actions": "^5.1.0" + "typesafe-actions": "^5.1.0", + "use-debounce": "^3.4.3" }, "lint-staged": { "*.{js,ts,jsx,tsx}": [ 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 f6603d9f44..32d88dedaf 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,10 +1,12 @@ -import React, { useCallback, useEffect, useRef } from 'react'; +import React, { useEffect, useRef } from 'react'; import { ClusterName, + SeekType, SeekTypes, TopicMessage, TopicMessageQueryParams, TopicName, + TopicPartition, } from 'redux/interfaces'; import PageLoader from 'components/common/PageLoader/PageLoader'; import { format } from 'date-fns'; @@ -15,7 +17,11 @@ import CustomParamButton, { CustomParamButtonType, } from 'components/Topics/shared/Form/CustomParams/CustomParamButton'; -import { debounce } from 'lodash'; +import MultiSelect from 'react-multi-select-component'; + +import * as _ from 'lodash'; +import { useDebouncedCallback } from 'use-debounce'; +import { Option } from 'react-multi-select-component/dist/lib/interfaces'; interface Props { clusterName: ClusterName; @@ -27,6 +33,7 @@ interface Props { queryParams: Partial ) => void; messages: TopicMessage[]; + partitions: TopicPartition[]; } interface FilterProps { @@ -47,6 +54,7 @@ const Messages: React.FC = ({ clusterName, topicName, messages, + partitions, fetchTopicMessages, }) => { const [searchQuery, setSearchQuery] = React.useState(''); @@ -54,23 +62,51 @@ const Messages: React.FC = ({ null ); const [filterProps, setFilterProps] = React.useState([]); + const [selectedSeekType, setSelectedSeekType] = React.useState( + SeekTypes.OFFSET + ); + const [searchOffset, setSearchOffset] = React.useState('0'); + const [selectedPartitions, setSelectedPartitions] = React.useState( + [] + ); const [queryParams, setQueryParams] = React.useState< Partial >({ limit: 100 }); + const [debouncedCallback] = useDebouncedCallback( + (query: any) => setQueryParams({ ...queryParams, ...query }), + 1000 + ); const prevSearchTimestamp = usePrevious(searchTimestamp); const getUniqueDataForEachPartition: FilterProps[] = React.useMemo(() => { - const map = messages.map((message) => [ - message.partition, - { - partition: message.partition, - offset: message.offset, - }, - ]); - // @ts-ignore - return [...new Map(map).values()]; - }, [messages]); + const partitionUniqs: FilterProps[] = partitions.map((p) => ({ + offset: 0, + partition: p.partition, + })); + const messageUniqs: FilterProps[] = _.map( + _.groupBy(messages, 'partition'), + (v) => _.maxBy(v, 'offset') + ).map((v) => ({ + offset: v ? v.offset : 0, + partition: v ? v.partition : 0, + })); + + return _.map( + _.groupBy(_.concat(partitionUniqs, messageUniqs), 'partition'), + (v) => _.maxBy(v, 'offset') as FilterProps + ); + }, [messages, partitions]); + + const getSeekToValuesForPartitions = (partition: any) => { + const foundedValues = filterProps.find( + (prop) => prop.partition === partition.value + ); + if (selectedSeekType === SeekTypes.OFFSET) { + return foundedValues ? foundedValues.offset : 0; + } + return searchTimestamp ? searchTimestamp.getTime() : null; + }; React.useEffect(() => { fetchTopicMessages(clusterName, topicName, queryParams); @@ -78,20 +114,13 @@ const Messages: React.FC = ({ React.useEffect(() => { setFilterProps(getUniqueDataForEachPartition); - }, [messages]); + }, [messages, partitions]); - const handleDelayedQuery = useCallback( - debounce( - (query: string) => setQueryParams({ ...queryParams, q: query }), - 1000 - ), - [] - ); const handleQueryChange = (event: React.ChangeEvent) => { const query = event.target.value; setSearchQuery(query); - handleDelayedQuery(query); + debouncedCallback({ q: query }); }; const handleDateTimeChange = () => { @@ -103,7 +132,7 @@ const Messages: React.FC = ({ setQueryParams({ ...queryParams, seekType: SeekTypes.TIMESTAMP, - seekTo: filterProps.map((p) => `${p.partition}::${timestamp}`), + seekTo: selectedPartitions.map((p) => `${p.value}::${timestamp}`), }); } else { setSearchTimestamp(null); @@ -113,6 +142,33 @@ const Messages: React.FC = ({ } }; + const handleSeekTypeChange = ( + event: React.ChangeEvent + ) => { + setSelectedSeekType(event.target.value as SeekType); + }; + + const handleOffsetChange = (event: React.ChangeEvent) => { + const offset = event.target.value || '0'; + setSearchOffset(offset); + debouncedCallback({ + seekType: SeekTypes.OFFSET, + seekTo: selectedPartitions.map((p) => `${p.value}::${offset}`), + }); + }; + + const handlePartitionsChange = (options: Option[]) => { + setSelectedPartitions(options); + + debouncedCallback({ + seekType: options.length > 0 ? selectedSeekType : undefined, + seekTo: + options.length > 0 + ? options.map((p) => `${p.value}::${getSeekToValuesForPartitions(p)}`) + : undefined, + }); + }; + const getTimestampDate = (timestamp: number) => { return format(new Date(timestamp * 1000), 'MM.dd.yyyy HH:mm:ss'); }; @@ -150,9 +206,13 @@ const Messages: React.FC = ({ const onNext = (event: React.MouseEvent) => { event.preventDefault(); - const seekTo: string[] = filterProps.map( - (p) => `${p.partition}::${p.offset}` - ); + const seekTo: string[] = filterProps + .filter( + (value) => + selectedPartitions.findIndex((p) => p.value === value.partition) > -1 + ) + .map((p) => `${p.partition}::${p.offset}`); + setQueryParams({ ...queryParams, seekType: SeekTypes.OFFSET, @@ -160,6 +220,15 @@ const Messages: React.FC = ({ }); }; + const filterOptions = (options: Option[], filter: any) => { + if (!filter) { + return options; + } + return options.filter( + ({ value }) => value.toString() && value.toString() === filter + ); + }; + const getTopicMessagesTable = () => { return messages.length > 0 ? (
@@ -199,23 +268,70 @@ const Messages: React.FC = ({ ); }; - return isFetched ? ( + if (!isFetched) { + return ; + } + + return (
-
- - setSearchTimestamp(date)} - onCalendarClose={handleDateTimeChange} - isClearable - showTimeInput - timeInputLabel="Time:" - dateFormat="MMMM d, yyyy h:mm aa" - className="input" +
+ + ({ + label: `Partition #${p.partition.toString()}`, + value: p.partition, + }))} + filterOptions={filterOptions} + value={selectedPartitions} + onChange={handlePartitionsChange} + labelledBy="Select partitions" />
-
+
+ +
+ +
+
+
+ {selectedSeekType === SeekTypes.OFFSET ? ( + <> + + + + ) : ( + <> + + setSearchTimestamp(date)} + onCalendarClose={handleDateTimeChange} + showTimeInput + timeInputLabel="Time:" + dateFormat="MMMM d, yyyy h:mm aa" + className="input" + /> + + )} +
+
= ({ />
-
{getTopicMessagesTable()}
+ {getTopicMessagesTable()}
- ) : ( - ); }; 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 23dae9d4a7..209802d1fb 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 @@ -9,6 +9,7 @@ import { RouteComponentProps, withRouter } from 'react-router-dom'; import { fetchTopicMessages } from 'redux/actions'; import { getIsTopicMessagesFetched, + getPartitionsByTopicName, getTopicMessages, } from 'redux/reducers/topics/selectors'; @@ -33,6 +34,7 @@ const mapStateToProps = ( topicName, isFetched: getIsTopicMessagesFetched(state), messages: getTopicMessages(state), + partitions: getPartitionsByTopicName(state, topicName), }); const mapDispatchToProps = { diff --git a/kafka-ui-react-app/src/redux/api/topics.ts b/kafka-ui-react-app/src/redux/api/topics.ts index 55d3cc2dbb..988bd88be8 100644 --- a/kafka-ui-react-app/src/redux/api/topics.ts +++ b/kafka-ui-react-app/src/redux/api/topics.ts @@ -59,7 +59,9 @@ export const getTopicMessages = ( const value = entry[1]; if (value) { if (Array.isArray(value)) { - searchParams += value.map((v) => `${key}=${v}&`); + value.forEach((v) => { + searchParams += `${key}=${v}&`; + }); } else { searchParams += `${key}=${value}&`; } 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 7db2724fa8..003cb7f192 100644 --- a/kafka-ui-react-app/src/redux/reducers/topics/selectors.ts +++ b/kafka-ui-react-app/src/redux/reducers/topics/selectors.ts @@ -80,6 +80,12 @@ export const getTopicByName = createSelector( (topics, topicName) => topics[topicName] ); +export const getPartitionsByTopicName = createSelector( + getTopicMap, + getTopicName, + (topics, topicName) => topics[topicName].partitions +); + export const getFullTopic = createSelector(getTopicByName, (topic) => topic && topic.config && !!topic.partitionCount ? topic : undefined );