From 19ff0a682c4670274870d466fe8e616ec7bcffa6 Mon Sep 17 00:00:00 2001 From: Renat Kalimulin Date: Mon, 24 Jun 2024 00:59:46 +0300 Subject: [PATCH] FE: Topics: Remember user polling options Resolves: #440 --- .../Topics/Topic/Messages/Filters/Filters.tsx | 2 +- .../Filters/__tests__/Filters.spec.tsx | 6 +- .../src/components/common/Search/Search.tsx | 8 +- .../src/components/common/Select/Select.tsx | 6 +- frontend/src/lib/constants.ts | 20 +++ frontend/src/lib/hooks/api/topicMessages.tsx | 11 +- frontend/src/lib/hooks/useLocalStorage.ts | 9 +- frontend/src/lib/hooks/useMessagesFilters.ts | 53 +++--- .../src/lib/hooks/useMessagesFiltersFields.ts | 160 ++++++++++++++++++ frontend/src/lib/types.ts | 4 + 10 files changed, 239 insertions(+), 40 deletions(-) create mode 100644 frontend/src/lib/hooks/useMessagesFiltersFields.ts diff --git a/frontend/src/components/Topics/Topic/Messages/Filters/Filters.tsx b/frontend/src/components/Topics/Topic/Messages/Filters/Filters.tsx index 926a38e35..5f7dc659f 100644 --- a/frontend/src/components/Topics/Topic/Messages/Filters/Filters.tsx +++ b/frontend/src/components/Topics/Topic/Messages/Filters/Filters.tsx @@ -63,7 +63,7 @@ const Filters: React.FC = ({ smartFilter, setSmartFilter, refreshData, - } = useMessagesFilters(); + } = useMessagesFilters(topicName); const { data: topic } = useTopicDetails({ clusterName, topicName }); const [createdEditedSmartId, setCreatedEditedSmartId] = useState(); diff --git a/frontend/src/components/Topics/Topic/Messages/Filters/__tests__/Filters.spec.tsx b/frontend/src/components/Topics/Topic/Messages/Filters/__tests__/Filters.spec.tsx index f24ddd325..016494559 100644 --- a/frontend/src/components/Topics/Topic/Messages/Filters/__tests__/Filters.spec.tsx +++ b/frontend/src/components/Topics/Topic/Messages/Filters/__tests__/Filters.spec.tsx @@ -10,12 +10,10 @@ import { useTopicDetails } from 'lib/hooks/api/topics'; import { externalTopicPayload } from 'lib/fixtures/topics'; import { useSerdes } from 'lib/hooks/api/topicMessages'; import { serdesPayload } from 'lib/fixtures/topicMessages'; -import { - MessagesFilterKeys, - MessagesFilterKeysTypes, -} from 'lib/hooks/useMessagesFilters'; import { PollingMode } from 'generated-sources'; import { ModeOptions } from 'lib/hooks/filterUtils'; +import { MessagesFilterKeysTypes } from 'lib/types'; +import { MessagesFilterKeys } from 'lib/constants'; const closeIconMock = 'closeIconMock'; const filtersSideBarMock = 'filtersSideBarMock'; diff --git a/frontend/src/components/common/Search/Search.tsx b/frontend/src/components/common/Search/Search.tsx index d62a1d0e6..83e6ae5a4 100644 --- a/frontend/src/components/common/Search/Search.tsx +++ b/frontend/src/components/common/Search/Search.tsx @@ -1,4 +1,4 @@ -import React, { ComponentRef, useRef } from 'react'; +import React, { ComponentRef, useEffect, useRef } from 'react'; import { useDebouncedCallback } from 'use-debounce'; import Input from 'components/common/Input/Input'; import { useSearchParams } from 'react-router-dom'; @@ -31,6 +31,12 @@ const Search: React.FC = ({ const [searchParams, setSearchParams] = useSearchParams(); const ref = useRef>(null); + useEffect(() => { + if (ref.current != null && value) { + ref.current.value = value; + } + }, [value]); + const handleChange = useDebouncedCallback((e) => { if (ref.current != null) { ref.current.value = e.target.value; diff --git a/frontend/src/components/common/Select/Select.tsx b/frontend/src/components/common/Select/Select.tsx index 72bf358ba..dfbca08af 100644 --- a/frontend/src/components/common/Select/Select.tsx +++ b/frontend/src/components/common/Select/Select.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import useClickOutside from 'lib/hooks/useClickOutside'; import DropdownArrowIcon from 'components/common/Icons/DropdownArrowIcon'; @@ -42,6 +42,10 @@ const Select = ( const [selectedOption, setSelectedOption] = useState(value); const [showOptions, setShowOptions] = useState(false); + useEffect(() => { + setSelectedOption(value); + }, [value]); + const showOptionsHandler = () => { if (!disabled) setShowOptions(!showOptions); }; diff --git a/frontend/src/lib/constants.ts b/frontend/src/lib/constants.ts index 102b79faa..9cf3fe7c1 100644 --- a/frontend/src/lib/constants.ts +++ b/frontend/src/lib/constants.ts @@ -106,3 +106,23 @@ export const CONSUMER_GROUP_STATE_TOOLTIPS: Record = DEAD: 'The group is going to be removed. It might be due to the inactivity, or the group is being migrated to different group coordinator.', UNKNOWN: '', } as const; + +/** + * @description !! Note !! + * Key value should match + * */ +export const MessagesFilterKeys = { + mode: 'mode', + timestamp: 'timestamp', + keySerde: 'keySerde', + valueSerde: 'valueSerde', + limit: 'limit', + offset: 'offset', + stringFilter: 'stringFilter', + partitions: 'partitions', + smartFilterId: 'smartFilterId', + activeFilterId: 'activeFilterId', + activeFilterNPId: 'activeFilterNPId', // not persisted filter name to indicate the refresh + cursor: 'cursor', + r: 'r', // used tp force refresh of the data +} as const; diff --git a/frontend/src/lib/hooks/api/topicMessages.tsx b/frontend/src/lib/hooks/api/topicMessages.tsx index ec407c901..c21684d17 100644 --- a/frontend/src/lib/hooks/api/topicMessages.tsx +++ b/frontend/src/lib/hooks/api/topicMessages.tsx @@ -1,6 +1,10 @@ import React, { useCallback, useRef } from 'react'; import { fetchEventSource } from '@microsoft/fetch-event-source'; -import { BASE_PARAMS, MESSAGES_PER_PAGE } from 'lib/constants'; +import { + BASE_PARAMS, + MESSAGES_PER_PAGE, + MessagesFilterKeys, +} from 'lib/constants'; import { GetSerdesRequest, PollingMode, @@ -13,10 +17,7 @@ import { showServerError } from 'lib/errorHandling'; import { useMutation, useQuery } from '@tanstack/react-query'; import { messagesApiClient } from 'lib/api'; import { useSearchParams } from 'react-router-dom'; -import { - getCursorValue, - MessagesFilterKeys, -} from 'lib/hooks/useMessagesFilters'; +import { getCursorValue } from 'lib/hooks/useMessagesFilters'; import { convertStrToPollingMode } from 'lib/hooks/filterUtils'; import { useMessageFiltersStore } from 'lib/hooks/useMessageFiltersStore'; import { TopicName } from 'lib/interfaces/topic'; diff --git a/frontend/src/lib/hooks/useLocalStorage.ts b/frontend/src/lib/hooks/useLocalStorage.ts index d8945620d..65215fd2c 100644 --- a/frontend/src/lib/hooks/useLocalStorage.ts +++ b/frontend/src/lib/hooks/useLocalStorage.ts @@ -1,9 +1,12 @@ import { LOCAL_STORAGE_KEY_PREFIX } from 'lib/constants'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, Dispatch, SetStateAction } from 'react'; -export const useLocalStorage = (featureKey: string, defaultValue: string) => { +export const useLocalStorage = ( + featureKey: string, + defaultValue: T +): [T, Dispatch>] => { const key = `${LOCAL_STORAGE_KEY_PREFIX}-${featureKey}`; - const [value, setValue] = useState(() => { + const [value, setValue] = useState(() => { const saved = localStorage.getItem(key); if (saved !== null) { diff --git a/frontend/src/lib/hooks/useMessagesFilters.ts b/frontend/src/lib/hooks/useMessagesFilters.ts index c72a91b49..c430d89e7 100644 --- a/frontend/src/lib/hooks/useMessagesFilters.ts +++ b/frontend/src/lib/hooks/useMessagesFilters.ts @@ -2,7 +2,7 @@ import { useSearchParams } from 'react-router-dom'; import { PollingMode } from 'generated-sources'; import { useEffect } from 'react'; import { Option } from 'react-multi-select-component'; -import { ObjectValues } from 'lib/types'; +import { MessagesFilterKeys } from 'lib/constants'; import { convertStrToPollingMode, ModeOptions } from './filterUtils'; import { @@ -10,28 +10,7 @@ import { selectFilter, useMessageFiltersStore, } from './useMessageFiltersStore'; - -/** - * @description !! Note !! - * Key value should match - * */ -export const MessagesFilterKeys = { - mode: 'mode', - timestamp: 'timestamp', - keySerde: 'keySerde', - valueSerde: 'valueSerde', - limit: 'limit', - offset: 'offset', - stringFilter: 'stringFilter', - partitions: 'partitions', - smartFilterId: 'smartFilterId', - activeFilterId: 'activeFilterId', - activeFilterNPId: 'activeFilterNPId', // not persisted filter name to indicate the refresh - cursor: 'cursor', - r: 'r', // used tp force refresh of the data -} as const; - -export type MessagesFilterKeysTypes = ObjectValues; +import { useMessagesFiltersFields } from './useMessagesFiltersFields'; const PER_PAGE = 100; @@ -80,12 +59,18 @@ export function usePaginateTopics(initSearchParams?: URLSearchParams) { }; } -export function useMessagesFilters() { +export function useMessagesFilters(topicName: string) { const [searchParams, setSearchParams] = useSearchParams(); const refreshData = useRefreshData(searchParams); + const { + initMessagesFiltersFields, + setMessagesFiltersField, + removeMessagesFiltersField, + } = useMessagesFiltersFields(topicName); useEffect(() => { setSearchParams((params) => { + initMessagesFiltersFields(params); params.set(MessagesFilterKeys.limit, PER_PAGE.toString()); if (!params.get(MessagesFilterKeys.mode)) { @@ -140,8 +125,10 @@ export function useMessagesFilters() { * */ const setMode = (newMode: PollingMode) => { setSearchParams((params) => { + removeMessagesFiltersField(MessagesFilterKeys.offset); + removeMessagesFiltersField(MessagesFilterKeys.timestamp); + setMessagesFiltersField(MessagesFilterKeys.mode, newMode); params.set(MessagesFilterKeys.mode, newMode); - params.delete(MessagesFilterKeys.offset); params.delete(MessagesFilterKeys.timestamp); return params; @@ -151,6 +138,7 @@ export function useMessagesFilters() { const setTimeStamp = (newDate: Date | null) => { if (newDate === null) { setSearchParams((params) => { + removeMessagesFiltersField(MessagesFilterKeys.timestamp); params.delete(MessagesFilterKeys.timestamp); return params; }); @@ -158,6 +146,10 @@ export function useMessagesFilters() { } setSearchParams((params) => { + setMessagesFiltersField( + MessagesFilterKeys.timestamp, + newDate.getTime().toString() + ); params.set(MessagesFilterKeys.timestamp, newDate.getTime().toString()); return params; }); @@ -166,12 +158,14 @@ export function useMessagesFilters() { const setKeySerde = (newKeySerde: string) => { setSearchParams((params) => { params.set(MessagesFilterKeys.keySerde, newKeySerde); + setMessagesFiltersField(MessagesFilterKeys.keySerde, newKeySerde); return params; }); }; const setValueSerde = (newValueSerde: string) => { setSearchParams((params) => { + setMessagesFiltersField(MessagesFilterKeys.valueSerde, newValueSerde); params.set(MessagesFilterKeys.valueSerde, newValueSerde); return params; }); @@ -179,6 +173,7 @@ export function useMessagesFilters() { const setOffsetValue = (newOffsetValue: string) => { setSearchParams((params) => { + setMessagesFiltersField(MessagesFilterKeys.offset, newOffsetValue); params.set(MessagesFilterKeys.offset, newOffsetValue); return params; }); @@ -187,8 +182,10 @@ export function useMessagesFilters() { const setSearch = (value: string) => { setSearchParams((params) => { if (value) { + setMessagesFiltersField(MessagesFilterKeys.stringFilter, value); params.set(MessagesFilterKeys.stringFilter, value); } else { + removeMessagesFiltersField(MessagesFilterKeys.stringFilter); params.delete(MessagesFilterKeys.stringFilter); } return params; @@ -200,10 +197,16 @@ export function useMessagesFilters() { params.delete(MessagesFilterKeys.partitions); if (values.length) { + setMessagesFiltersField( + MessagesFilterKeys.partitions, + values.map((v) => v.value).join(',') + ); params.append( MessagesFilterKeys.partitions, values.map((v) => v.value).join(',') ); + } else { + removeMessagesFiltersField(MessagesFilterKeys.partitions); } return params; diff --git a/frontend/src/lib/hooks/useMessagesFiltersFields.ts b/frontend/src/lib/hooks/useMessagesFiltersFields.ts new file mode 100644 index 000000000..680a315bc --- /dev/null +++ b/frontend/src/lib/hooks/useMessagesFiltersFields.ts @@ -0,0 +1,160 @@ +import { PollingMode } from 'generated-sources'; +import { MessagesFilterKeysTypes } from 'lib/types'; +import { MessagesFilterKeys } from 'lib/constants'; +import { useLocalStorage } from 'lib/hooks/useLocalStorage'; + +type MessagesFilterFieldsType = Pick< + Partial<{ + [key in MessagesFilterKeysTypes]: string; + }>, + | 'mode' + | 'offset' + | 'timestamp' + | 'partitions' + | 'keySerde' + | 'valueSerde' + | 'stringFilter' +>; + +export function useMessagesFiltersFields(topicName: string) { + const [messageFilters, setMessageFilters] = useLocalStorage<{ + [topicName: string]: MessagesFilterFieldsType; + }>('message-filters-fields', {}); + + const removeMessagesFilterFields = () => { + setMessageFilters((prev) => { + const { [topicName]: topicFilters, ...rest } = prev || {}; + return rest; + }); + }; + + const removeMessagesFiltersField = (key: keyof MessagesFilterFieldsType) => { + setMessageFilters((prev) => { + const { [key]: value, ...rest } = prev[topicName] || {}; + return { ...prev, [topicName]: rest }; + }); + }; + + const setMessagesFiltersField = ( + key: keyof MessagesFilterFieldsType, + value: string + ) => { + setMessageFilters((prev) => ({ + ...prev, + [topicName]: { ...prev[topicName], [key]: value }, + })); + }; + + const initMessagesFiltersFields = (params: URLSearchParams) => { + const topicMessagesFilters = messageFilters[topicName]; + if (params.size === 0 && !!topicMessagesFilters) { + if (topicMessagesFilters.mode) { + params.set(MessagesFilterKeys.mode, topicMessagesFilters.mode); + if ( + topicMessagesFilters.mode === PollingMode.FROM_OFFSET || + topicMessagesFilters.mode === PollingMode.TO_OFFSET + ) { + if (topicMessagesFilters.offset) { + params.set(MessagesFilterKeys.offset, topicMessagesFilters.offset); + } + } + + if ( + topicMessagesFilters.mode === PollingMode.FROM_TIMESTAMP || + topicMessagesFilters.mode === PollingMode.TO_TIMESTAMP + ) { + if (topicMessagesFilters.timestamp) { + params.set( + MessagesFilterKeys.timestamp, + topicMessagesFilters.timestamp + ); + } + } + } + if (topicMessagesFilters.partitions) { + params.set( + MessagesFilterKeys.partitions, + topicMessagesFilters.partitions + ); + } + if (topicMessagesFilters.keySerde) { + params.set(MessagesFilterKeys.keySerde, topicMessagesFilters.keySerde); + } + if (topicMessagesFilters.valueSerde) { + params.set( + MessagesFilterKeys.valueSerde, + topicMessagesFilters.valueSerde + ); + } + if (topicMessagesFilters.stringFilter) { + params.set( + MessagesFilterKeys.stringFilter, + topicMessagesFilters.stringFilter + ); + } + } else { + const MessagesFiltersMode = params.get(MessagesFilterKeys.mode); + if (MessagesFiltersMode) { + removeMessagesFilterFields(); + setMessagesFiltersField(MessagesFilterKeys.mode, MessagesFiltersMode); + const MessagesFiltersOffset = params.get(MessagesFilterKeys.offset); + if (MessagesFiltersOffset) { + setMessagesFiltersField( + MessagesFilterKeys.offset, + MessagesFiltersOffset + ); + } + const MessagesFiltersTimestamp = params.get( + MessagesFilterKeys.timestamp + ); + if (MessagesFiltersTimestamp) { + setMessagesFiltersField( + MessagesFilterKeys.timestamp, + MessagesFiltersTimestamp + ); + } + } + + const MessageFiltersPartitions = params.get( + MessagesFilterKeys.partitions + ); + if (MessageFiltersPartitions) { + setMessagesFiltersField( + MessagesFilterKeys.partitions, + MessageFiltersPartitions + ); + } + const MessagesFiltersKeySerde = params.get(MessagesFilterKeys.keySerde); + if (MessagesFiltersKeySerde) { + setMessagesFiltersField( + MessagesFilterKeys.keySerde, + MessagesFiltersKeySerde + ); + } + const MessagesFiltersValueSerde = params.get( + MessagesFilterKeys.valueSerde + ); + if (MessagesFiltersValueSerde) { + setMessagesFiltersField( + MessagesFilterKeys.valueSerde, + MessagesFiltersValueSerde + ); + } + const MessagesFiltersStringFilter = params.get( + MessagesFilterKeys.stringFilter + ); + if (MessagesFiltersStringFilter) { + setMessagesFiltersField( + MessagesFilterKeys.stringFilter, + MessagesFiltersStringFilter + ); + } + } + }; + + return { + initMessagesFiltersFields, + removeMessagesFiltersField, + setMessagesFiltersField, + }; +} diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 7bc715840..b322abf56 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -1,4 +1,8 @@ +import { MessagesFilterKeys } from './constants'; + export type ObjectValues> = T[keyof T]; export type WithPartialKey = Omit & Partial>>; + +export type MessagesFilterKeysTypes = ObjectValues;