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 096a714c4ee..73b1ae3ae87 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 @@ -11,7 +11,7 @@ import { TopicMessageEvent, TopicMessageEventTypeEnum, } from 'generated-sources'; -import React, { useContext } from 'react'; +import React, { useContext, useRef } from 'react'; import omitBy from 'lodash/omitBy'; import { useNavigate, useLocation, useSearchParams } from 'react-router-dom'; import MultiSelect from 'components/common/MultiSelect/MultiSelect.styled'; @@ -38,6 +38,8 @@ import { useTopicDetails } from 'lib/hooks/api/topics'; import { InputLabel } from 'components/common/Input/InputLabel.styled'; import { getSerdeOptions } from 'components/Topics/Topic/SendMessage/utils'; import { useSerdes } from 'lib/hooks/api/topicMessages'; +import { useAppSelector } from 'lib/hooks/redux'; +import { getTopicMessges } from 'redux/reducers/topicMessages/selectors'; import * as S from './Filters.styled'; import { @@ -78,6 +80,39 @@ export const SeekTypeOptions = [ { value: SeekType.TIMESTAMP, label: 'Timestamp' }, ]; +const getNextSeekTo = ( + messages: TopicMessage[], + searchParams: URLSearchParams, + defaultCalculatedSeekTo: string +) => { + const seekTo = searchParams.get('seekTo') || defaultCalculatedSeekTo; + + // parse current seekTo query param to array of [partition, offset] tuples + const configTuple = seekTo?.split(',').map((item) => { + const [partition, offset] = item.split('::'); + return { partition: Number(partition), offset: Number(offset) }; + }); + + // Reverse messages array for faster last displayed message search. + const reversedMessages = [...messages].reverse(); + + if (!configTuple) return ''; + + const newConfigTuple = configTuple.map(({ partition, offset }) => { + const message = reversedMessages.find((m) => partition === m.partition); + if (!message) { + return { partition, offset }; + } + + return { + partition, + offset: message.offset, + }; + }); + + return newConfigTuple.map((t) => `${t.partition}::${t.offset}`).join(','); +}; + const Filters: React.FC = ({ phaseMessage, meta: { elapsedMs, bytesConsumed, messagesConsumed }, @@ -93,14 +128,22 @@ const Filters: React.FC = ({ const navigate = useNavigate(); const [searchParams] = useSearchParams(); - const page = searchParams.get('page'); - const { data: topic } = useTopicDetails({ clusterName, topicName }); const partitions = topic?.partitions || []; - const { seekDirection, isLive, changeSeekDirection } = - useContext(TopicMessagesContext); + const { + seekDirection, + isLive, + changeSeekDirection, + page, + setPageNumber, + paginated, + } = useContext(TopicMessagesContext); + + const paginationCache = useRef(page); + + const messages = useAppSelector(getTopicMessges); const { value: isOpen, toggle } = useBoolean(); @@ -156,15 +199,18 @@ const Filters: React.FC = ({ [selectedPartitions] ); + const isFiltersDisabled = isTailing || paginated; + const isSubmitDisabled = React.useMemo(() => { if (isSeekTypeControlVisible) { return ( - (currentSeekType === SeekType.TIMESTAMP && !timestamp) || isTailing + (currentSeekType === SeekType.TIMESTAMP && !timestamp) || + isFiltersDisabled ); } return false; - }, [isSeekTypeControlVisible, currentSeekType, timestamp, isTailing]); + }, [isSeekTypeControlVisible, currentSeekType, timestamp, isFiltersDisabled]); const partitionMap = React.useMemo( () => @@ -183,6 +229,8 @@ const Filters: React.FC = ({ setOffset(''); setQuery(''); changeSeekDirection(SeekDirection.FORWARD); + setPageNumber(0); + paginationCache.current = 0; getSelectedPartitionsFromSeekToParam(searchParams, partitions); setSelectedPartitions( partitions.map((partition: Partition) => { @@ -195,6 +243,14 @@ const Filters: React.FC = ({ }; const handleFiltersSubmit = (currentOffset: string) => { + // so it will not work during back navigations + + if (paginationCache.current > page) { + // update for the next + paginationCache.current = page; + return; + } + const nextAttempt = Number(searchParams.get('attempt') || 0) + 1; const props: Query = { q: @@ -231,11 +287,13 @@ const Filters: React.FC = ({ props.seekType = SeekType.TIMESTAMP; } - props.seekTo = selectedPartitions.map(({ value }) => { + const defaultPartitionedSeekTo = selectedPartitions.map(({ value }) => { const offsetProperty = seekDirection === SeekDirection.FORWARD ? 'offsetMin' : 'offsetMax'; + const offsetBasedSeekTo = currentOffset || partitionMap[value][offsetProperty]; + const seekToOffset = currentSeekType === SeekType.OFFSET ? offsetBasedSeekTo @@ -243,12 +301,27 @@ const Filters: React.FC = ({ return `${value}::${seekToOffset || '0'}`; }); + + if (page && page !== 0) { + // during pagination, it should always be offset + props.seekType = SeekType.OFFSET; + props.seekTo = getNextSeekTo( + messages, + searchParams, + defaultPartitionedSeekTo.join(',') + ); + } else { + props.seekTo = defaultPartitionedSeekTo; + } } const newProps = omitBy(props, (v) => v === undefined || v === ''); const qs = Object.keys(newProps) .map((key) => `${key}=${encodeURIComponent(newProps[key] as string)}`) .join('&'); + + paginationCache.current = Number(page); + navigate({ search: `?${qs}`, }); @@ -314,6 +387,7 @@ const Filters: React.FC = ({ localStorage.setItem('savedFilters', JSON.stringify(filters)); setSavedFilters(filters); }; + // eslint-disable-next-line consistent-return React.useEffect(() => { if (location.search?.length !== 0) { @@ -364,7 +438,6 @@ const Filters: React.FC = ({ }, [ clusterName, topicName, - seekDirection, location, addMessage, resetMessages, @@ -372,19 +445,7 @@ const Filters: React.FC = ({ updateMeta, updatePhase, ]); - React.useEffect(() => { - if (location.search?.length === 0) { - handleFiltersSubmit(offset); - } - }, [ - seekDirection, - queryType, - activeFilter, - currentSeekType, - timestamp, - query, - location, - ]); + React.useEffect(() => { handleFiltersSubmit(offset); }, [ @@ -422,7 +483,7 @@ const Filters: React.FC = ({ selectSize="M" minWidth="100px" options={SeekTypeOptions} - disabled={isTailing} + disabled={isFiltersDisabled} /> {currentSeekType === SeekType.OFFSET ? ( @@ -433,7 +494,7 @@ const Filters: React.FC = ({ value={offset} placeholder="Offset" onChange={({ target: { value } }) => setOffset(value)} - disabled={isTailing} + disabled={isFiltersDisabled} /> ) : ( = ({ timeInputLabel="Time:" dateFormat="MMM d, yyyy HH:mm" placeholderText="Select timestamp" - disabled={isTailing} + disabled={isFiltersDisabled} /> )} @@ -459,7 +520,7 @@ const Filters: React.FC = ({ value={selectedPartitions} onChange={setSelectedPartitions} labelledBy="Select partitions" - disabled={isTailing} + disabled={isFiltersDisabled} />
@@ -472,7 +533,7 @@ const Filters: React.FC = ({ options={getSerdeOptions(serdes.key || [])} value={searchParams.get('keySerde') as string} selectSize="M" - disabled={isTailing} + disabled={isFiltersDisabled} />
@@ -485,7 +546,7 @@ const Filters: React.FC = ({ value={searchParams.get('valueSerde') as string} minWidth="170px" selectSize="M" - disabled={isTailing} + disabled={isFiltersDisabled} />
Clear all @@ -509,12 +570,18 @@ const Filters: React.FC = ({ minWidth="120px" options={SeekDirectionOptions} isLive={isLive} + disabled={paginated} /> - - - diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/__tests__/Filters.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/__tests__/Filters.spec.tsx index e248fdeaeb8..f713a258c3b 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/__tests__/Filters.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Filters/__tests__/Filters.spec.tsx @@ -29,6 +29,9 @@ const defaultContextValue: ContextProps = { isLive: false, seekDirection: SeekDirection.FORWARD, changeSeekDirection: jest.fn(), + page: 0, + setPageNumber: jest.fn(), + paginated: false, }; jest.mock('components/common/Icons/CloseIcon', () => () => 'mock-CloseIcon'); 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 35490c627e3..198cc3eac43 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 @@ -65,6 +65,10 @@ const Messages: React.FC = () => { SeekDirectionOptionsObj[seekDirection].isLive ); + const [page, setPage] = useState( + Number(searchParams.get('page') || '0') + ); + const changeSeekDirection = useCallback((val: string) => { switch (val) { case SeekDirection.FORWARD: @@ -88,8 +92,11 @@ const Messages: React.FC = () => { seekDirection, changeSeekDirection, isLive, + page, + paginated: !!page, + setPageNumber: setPage, }), - [seekDirection, changeSeekDirection] + [seekDirection, page, changeSeekDirection] ); return ( 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 96e9f40d73d..7dbf102a626 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 @@ -3,6 +3,7 @@ import { Table } from 'components/common/table/Table/Table.styled'; import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; import { TopicMessage } from 'generated-sources'; import React, { useContext, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { getTopicMessges, getIsTopicMessagesFetching, @@ -10,7 +11,6 @@ import { 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'; @@ -19,13 +19,12 @@ import Message, { PreviewFilter } from './Message'; const MessagesTable: React.FC = () => { const [previewFor, setPreviewFor] = useState(null); + const navigate = useNavigate(); const [keyFilters, setKeyFilters] = useState([]); const [contentFilters, setContentFilters] = useState([]); - const [searchParams, setSearchParams] = useSearchParams(); - const page = searchParams.get('page'); - const { isLive } = useContext(TopicMessagesContext); + const { isLive, setPageNumber, page } = useContext(TopicMessagesContext); const messages = useAppSelector(getTopicMessges); const isFetching = useAppSelector(getIsTopicMessagesFetching); @@ -38,17 +37,12 @@ const MessagesTable: React.FC = () => { 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 isPrevPageButtonDisabled = isPaginationDisabled || !page; - const handlePrevPage = () => { - searchParams.set('page', String(Number(page || 0) - 1)); - setSearchParams(searchParams); + const handlePrevClick = () => { + setPageNumber((prevState) => prevState - 1); + navigate(-1); }; return ( @@ -125,7 +119,7 @@ const MessagesTable: React.FC = () => { buttonType="secondary" buttonSize="L" disabled={isPrevPageButtonDisabled} - onClick={handlePrevPage} + onClick={handlePrevClick} > ← Back @@ -133,7 +127,7 @@ const MessagesTable: React.FC = () => { buttonType="secondary" buttonSize="L" disabled={isNextPageButtonDisabled} - onClick={handleNextPage} + onClick={() => setPageNumber((prev) => prev + 1)} > Next → 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 7b1e80f8c27..600f028960f 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 @@ -33,6 +33,9 @@ describe('MessagesTable', () => { isLive: false, seekDirection: SeekDirection.FORWARD, changeSeekDirection: jest.fn(), + page: 0, + paginated: false, + setPageNumber: jest.fn(), }; const renderComponent = ( diff --git a/kafka-ui-react-app/src/components/contexts/TopicMessagesContext.ts b/kafka-ui-react-app/src/components/contexts/TopicMessagesContext.ts index 3ca2ca65521..751c32916eb 100644 --- a/kafka-ui-react-app/src/components/contexts/TopicMessagesContext.ts +++ b/kafka-ui-react-app/src/components/contexts/TopicMessagesContext.ts @@ -5,6 +5,9 @@ export interface ContextProps { seekDirection: SeekDirection; changeSeekDirection(val: string): void; isLive: boolean; + page: number; + setPageNumber: React.Dispatch>; + paginated: boolean; } const TopicMessagesContext = React.createContext( diff --git a/kafka-ui-react-app/src/lib/hooks/useBoolean.ts b/kafka-ui-react-app/src/lib/hooks/useBoolean.ts index ac016ef1a05..e757ad3b156 100644 --- a/kafka-ui-react-app/src/lib/hooks/useBoolean.ts +++ b/kafka-ui-react-app/src/lib/hooks/useBoolean.ts @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import React, { useCallback, useState } from 'react'; interface ReturnType { value: boolean;