From 3488a0bd7e9ffe668ff24111d423f5bd34251634 Mon Sep 17 00:00:00 2001 From: Xaroz Date: Wed, 11 Dec 2024 13:40:59 -0600 Subject: [PATCH 1/6] feat: set up chain url filtering --- src/components/search/SearchFilterBar.tsx | 2 +- src/features/messages/MessageSearch.tsx | 51 ++++++++++++++----- .../messages/queries/useMessageQuery.ts | 18 +++++-- src/utils/queryParams.ts | 39 +++++++++++--- 4 files changed, 85 insertions(+), 25 deletions(-) diff --git a/src/components/search/SearchFilterBar.tsx b/src/components/search/SearchFilterBar.tsx index b6387262..cf577d80 100644 --- a/src/components/search/SearchFilterBar.tsx +++ b/src/components/search/SearchFilterBar.tsx @@ -71,7 +71,7 @@ function ChainSelector({ const multiProvider = useMultiProvider(); - const chainName = value ? multiProvider.getChainName(value) : undefined; + const chainName = value ? multiProvider.tryGetChainName(value) : undefined; const chainDisplayName = chainName ? trimToLength(getChainDisplayName(multiProvider, chainName, true), 12) : undefined; diff --git a/src/features/messages/MessageSearch.tsx b/src/features/messages/MessageSearch.tsx index 4b47353d..33fc50e4 100644 --- a/src/features/messages/MessageSearch.tsx +++ b/src/features/messages/MessageSearch.tsx @@ -12,7 +12,7 @@ import { SearchUnknownError, } from '../../components/search/SearchStates'; import { useReadyMultiProvider } from '../../store'; -import { useQueryParam, useSyncQueryParam } from '../../utils/queryParams'; +import { useMultipleQueryParams, useSyncQueryParam } from '../../utils/queryParams'; import { sanitizeString } from '../../utils/string'; import { MessageTable } from './MessageTable'; @@ -20,33 +20,54 @@ import { usePiChainMessageSearchQuery } from './pi-queries/usePiChainMessageQuer import { useMessageSearchQuery } from './queries/useMessageQuery'; const QUERY_SEARCH_PARAM = 'search'; +const QUERY_ORIGIN_PARAM = 'origin'; +const QUERY_DESTINATION_PARAM = 'destination'; +// const QUERY_ORIGIN_PARAM = 'origin' export function MessageSearch() { // Chain metadata const multiProvider = useReadyMultiProvider(); + // query params + const [defaultSearchQuery, defaultOriginQuery, defaultDestinationQuery] = useMultipleQueryParams([ + QUERY_SEARCH_PARAM, + QUERY_ORIGIN_PARAM, + QUERY_DESTINATION_PARAM, + ]); + // Search text input - const defaultSearchQuery = useQueryParam(QUERY_SEARCH_PARAM); const [searchInput, setSearchInput] = useState(defaultSearchQuery); const debouncedSearchInput = useDebounce(searchInput, 750); const hasInput = !!debouncedSearchInput; const sanitizedInput = sanitizeString(debouncedSearchInput); // Filter state - const [originChainFilter, setOriginChainFilter] = useState(null); - const [destinationChainFilter, setDestinationChainFilter] = useState(null); + const [originChainFilter, setOriginChainFilter] = useState( + defaultOriginQuery || null, + ); + const [destinationChainFilter, setDestinationChainFilter] = useState( + defaultDestinationQuery || null, + ); const [startTimeFilter, setStartTimeFilter] = useState(null); const [endTimeFilter, setEndTimeFilter] = useState(null); // GraphQL query and results - const { isValidInput, isError, isFetching, hasRun, messageList, isMessagesFound } = - useMessageSearchQuery( - sanitizedInput, - originChainFilter, - destinationChainFilter, - startTimeFilter, - endTimeFilter, - ); + const { + isValidInput, + isValidOrigin, + isValidDestination, + isError, + isFetching, + hasRun, + messageList, + isMessagesFound, + } = useMessageSearchQuery( + sanitizedInput, + originChainFilter, + destinationChainFilter, + startTimeFilter, + endTimeFilter, + ); // Run permissionless interop chains query if needed const { @@ -70,7 +91,11 @@ export function MessageSearch() { const messageListResult = isMessagesFound ? messageList : piMessageList; // Keep url in sync - useSyncQueryParam(QUERY_SEARCH_PARAM, isValidInput ? sanitizedInput : ''); + useSyncQueryParam({ + [QUERY_SEARCH_PARAM]: isValidInput ? sanitizedInput : '', + [QUERY_ORIGIN_PARAM]: (isValidOrigin && originChainFilter) || '', + [QUERY_DESTINATION_PARAM]: (isValidDestination && destinationChainFilter) || '', + }); return ( <> diff --git a/src/features/messages/queries/useMessageQuery.ts b/src/features/messages/queries/useMessageQuery.ts index 62be9d65..376b78b0 100644 --- a/src/features/messages/queries/useMessageQuery.ts +++ b/src/features/messages/queries/useMessageQuery.ts @@ -5,6 +5,7 @@ import { useMultiProvider } from '../../../store'; import { MessageStatus } from '../../../types'; import { useScrapedDomains } from '../../chains/queries/useScrapedChains'; +import { MultiProvider } from '@hyperlane-xyz/sdk'; import { useInterval } from '@hyperlane-xyz/widgets'; import { MessageIdentifierType, buildMessageQuery, buildMessageSearchQuery } from './build'; import { searchValueToPostgresBytea } from './encoding'; @@ -21,6 +22,11 @@ export function isValidSearchQuery(input: string) { return !!searchValueToPostgresBytea(input); } +export function isValidDomainId(domainId: string | null, multiProvider: MultiProvider) { + if (!domainId) return false; + return !!multiProvider.tryGetDomainId(domainId); +} + export function useMessageSearchQuery( sanitizedInput: string, originChainFilter: string | null, @@ -29,15 +35,20 @@ export function useMessageSearchQuery( endTimeFilter: number | null, ) { const { scrapedDomains: scrapedChains } = useScrapedDomains(); + const multiProvider = useMultiProvider(); const hasInput = !!sanitizedInput; const isValidInput = !hasInput || isValidSearchQuery(sanitizedInput); + // validating filters + const isValidOrigin = isValidDomainId(originChainFilter, multiProvider); + const isValidDestination = isValidDomainId(destinationChainFilter, multiProvider); + // Assemble GraphQL query const { query, variables } = buildMessageSearchQuery( sanitizedInput, - originChainFilter, - destinationChainFilter, + isValidOrigin ? originChainFilter : null, + isValidDestination ? destinationChainFilter : null, startTimeFilter, endTimeFilter, hasInput ? SEARCH_QUERY_LIMIT : LATEST_QUERY_LIMIT, @@ -53,7 +64,6 @@ export function useMessageSearchQuery( const { data, fetching: isFetching, error } = result; // Parse results - const multiProvider = useMultiProvider(); const unfilteredMessageList = useMemo( () => parseMessageStubResult(multiProvider, scrapedChains, data), [multiProvider, scrapedChains, data], @@ -90,6 +100,8 @@ export function useMessageSearchQuery( hasRun: !!data, isMessagesFound, messageList, + isValidOrigin, + isValidDestination, }; } diff --git a/src/utils/queryParams.ts b/src/utils/queryParams.ts index aee9ee15..1a879c37 100644 --- a/src/utils/queryParams.ts +++ b/src/utils/queryParams.ts @@ -15,28 +15,51 @@ export function getQueryParamString(query: ParsedUrlQuery, key: string, defaultV // Use query param form URL export function useQueryParam(key: string, defaultVal = '') { const router = useRouter(); + return getQueryParamString(router.query, key, defaultVal); } +export function useMultipleQueryParams(keys: string[]) { + const router = useRouter(); + + return keys.map((key) => { + return getQueryParamString(router.query, key); + }); +} + // Keep value in sync with query param in URL -export function useSyncQueryParam(key: string, value = '') { +export function useSyncQueryParam(params: Record) { const router = useRouter(); const { pathname, query } = router; useEffect(() => { + let hasChanged = false; const newQuery = new URLSearchParams( Object.fromEntries( Object.entries(query).filter((kv): kv is [string, string] => typeof kv[0] === 'string'), ), ); - if (value) newQuery.set(key, value); - else newQuery.delete(key); - const path = `${pathname}?${newQuery.toString()}`; - router - .replace(path, undefined, { shallow: true }) - .catch((e) => logger.error('Error shallow updating url', e)); + Object.entries(params).forEach(([key, value]) => { + if (value) { + if (newQuery.get(key) !== value) { + newQuery.set(key, value); + hasChanged = true; + } + } else { + if (newQuery.has(key)) { + newQuery.delete(key); + hasChanged = true; + } + } + }); + if (hasChanged) { + const path = `${pathname}?${newQuery.toString()}`; + router + .replace(path, undefined, { shallow: true }) + .catch((e) => logger.error('Error shallow updating URL', e)); + } // Must exclude router for next.js shallow routing, otherwise links break: // eslint-disable-next-line react-hooks/exhaustive-deps - }, [key, value]); + }, [params]); } // Circumventing Next's router.replace method here because From a8c78cb82264e8870844302b5c5d62764c3014c2 Mon Sep 17 00:00:00 2001 From: Xaroz Date: Thu, 12 Dec 2024 08:53:52 -0600 Subject: [PATCH 2/6] feat: time filters sync --- src/features/messages/MessageSearch.tsx | 37 ++++++++++++++++++++----- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/features/messages/MessageSearch.tsx b/src/features/messages/MessageSearch.tsx index 33fc50e4..a7bfd24c 100644 --- a/src/features/messages/MessageSearch.tsx +++ b/src/features/messages/MessageSearch.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Fade, useDebounce } from '@hyperlane-xyz/widgets'; @@ -15,6 +15,7 @@ import { useReadyMultiProvider } from '../../store'; import { useMultipleQueryParams, useSyncQueryParam } from '../../utils/queryParams'; import { sanitizeString } from '../../utils/string'; +import { tryToDecimalNumber } from '../../utils/number'; import { MessageTable } from './MessageTable'; import { usePiChainMessageSearchQuery } from './pi-queries/usePiChainMessageQuery'; import { useMessageSearchQuery } from './queries/useMessageQuery'; @@ -22,17 +23,26 @@ import { useMessageSearchQuery } from './queries/useMessageQuery'; const QUERY_SEARCH_PARAM = 'search'; const QUERY_ORIGIN_PARAM = 'origin'; const QUERY_DESTINATION_PARAM = 'destination'; -// const QUERY_ORIGIN_PARAM = 'origin' +const QUERY_START_TIME_PARAM = 'startTime'; +const QUERY_END_TIME_PARAM = 'endTime'; export function MessageSearch() { // Chain metadata const multiProvider = useReadyMultiProvider(); // query params - const [defaultSearchQuery, defaultOriginQuery, defaultDestinationQuery] = useMultipleQueryParams([ + const [ + defaultSearchQuery, + defaultOriginQuery, + defaultDestinationQuery, + defaultStartTime, + defaultEndTime, + ] = useMultipleQueryParams([ QUERY_SEARCH_PARAM, QUERY_ORIGIN_PARAM, QUERY_DESTINATION_PARAM, + QUERY_START_TIME_PARAM, + QUERY_END_TIME_PARAM, ]); // Search text input @@ -48,8 +58,12 @@ export function MessageSearch() { const [destinationChainFilter, setDestinationChainFilter] = useState( defaultDestinationQuery || null, ); - const [startTimeFilter, setStartTimeFilter] = useState(null); - const [endTimeFilter, setEndTimeFilter] = useState(null); + const [startTimeFilter, setStartTimeFilter] = useState( + tryToDecimalNumber(defaultStartTime), + ); + const [endTimeFilter, setEndTimeFilter] = useState( + tryToDecimalNumber(defaultEndTime), + ); // GraphQL query and results const { @@ -93,10 +107,19 @@ export function MessageSearch() { // Keep url in sync useSyncQueryParam({ [QUERY_SEARCH_PARAM]: isValidInput ? sanitizedInput : '', - [QUERY_ORIGIN_PARAM]: (isValidOrigin && originChainFilter) || '', - [QUERY_DESTINATION_PARAM]: (isValidDestination && destinationChainFilter) || '', + [QUERY_ORIGIN_PARAM]: originChainFilter || '', + [QUERY_DESTINATION_PARAM]: destinationChainFilter || '', + [QUERY_START_TIME_PARAM]: startTimeFilter !== null ? String(startTimeFilter) : '', + [QUERY_END_TIME_PARAM]: endTimeFilter !== null ? String(endTimeFilter) : '', }); + // For the cases where the URL param for origin and destination chain is not valid + // This will reset the chain picker to null instead of having a empty selected chain + useEffect(() => { + if (!isValidOrigin) setOriginChainFilter(null); + if (!isValidDestination) setDestinationChainFilter(null); + }, [isValidOrigin, isValidDestination]); + return ( <> Date: Thu, 12 Dec 2024 10:10:35 -0600 Subject: [PATCH 3/6] feat: chain error messages components, clean up invalid --- src/components/search/SearchStates.tsx | 11 ++++ src/features/messages/MessageSearch.tsx | 54 ++++++++++--------- .../messages/queries/useMessageQuery.ts | 5 +- 3 files changed, 44 insertions(+), 26 deletions(-) diff --git a/src/components/search/SearchStates.tsx b/src/components/search/SearchStates.tsx index 2da819d5..e790b374 100644 --- a/src/components/search/SearchStates.tsx +++ b/src/components/search/SearchStates.tsx @@ -118,3 +118,14 @@ export function SearchUnknownError({ show }: { show: boolean }) { /> ); } + +export function SearchChainError({ show }: { show: boolean }) { + return ( + + ); +} diff --git a/src/features/messages/MessageSearch.tsx b/src/features/messages/MessageSearch.tsx index a7bfd24c..dd38f636 100644 --- a/src/features/messages/MessageSearch.tsx +++ b/src/features/messages/MessageSearch.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { Fade, useDebounce } from '@hyperlane-xyz/widgets'; @@ -6,6 +6,7 @@ import { Card } from '../../components/layout/Card'; import { SearchBar } from '../../components/search/SearchBar'; import { SearchFilterBar } from '../../components/search/SearchFilterBar'; import { + SearchChainError, SearchEmptyError, SearchFetching, SearchInvalidError, @@ -20,11 +21,13 @@ import { MessageTable } from './MessageTable'; import { usePiChainMessageSearchQuery } from './pi-queries/usePiChainMessageQuery'; import { useMessageSearchQuery } from './queries/useMessageQuery'; -const QUERY_SEARCH_PARAM = 'search'; -const QUERY_ORIGIN_PARAM = 'origin'; -const QUERY_DESTINATION_PARAM = 'destination'; -const QUERY_START_TIME_PARAM = 'startTime'; -const QUERY_END_TIME_PARAM = 'endTime'; +enum MESSAGE_QUERY_PARAMS { + SEARCH = 'search', + ORIGIN = 'origin', + DESTINATION = 'destination', + START_TIME = 'startTime', + END_TIME = 'endTime', +} export function MessageSearch() { // Chain metadata @@ -38,11 +41,11 @@ export function MessageSearch() { defaultStartTime, defaultEndTime, ] = useMultipleQueryParams([ - QUERY_SEARCH_PARAM, - QUERY_ORIGIN_PARAM, - QUERY_DESTINATION_PARAM, - QUERY_START_TIME_PARAM, - QUERY_END_TIME_PARAM, + MESSAGE_QUERY_PARAMS.SEARCH, + MESSAGE_QUERY_PARAMS.ORIGIN, + MESSAGE_QUERY_PARAMS.DESTINATION, + MESSAGE_QUERY_PARAMS.START_TIME, + MESSAGE_QUERY_PARAMS.END_TIME, ]); // Search text input @@ -106,20 +109,13 @@ export function MessageSearch() { // Keep url in sync useSyncQueryParam({ - [QUERY_SEARCH_PARAM]: isValidInput ? sanitizedInput : '', - [QUERY_ORIGIN_PARAM]: originChainFilter || '', - [QUERY_DESTINATION_PARAM]: destinationChainFilter || '', - [QUERY_START_TIME_PARAM]: startTimeFilter !== null ? String(startTimeFilter) : '', - [QUERY_END_TIME_PARAM]: endTimeFilter !== null ? String(endTimeFilter) : '', + [MESSAGE_QUERY_PARAMS.SEARCH]: isValidInput ? sanitizedInput : '', + [MESSAGE_QUERY_PARAMS.ORIGIN]: (isValidOrigin && originChainFilter) || '', + [MESSAGE_QUERY_PARAMS.DESTINATION]: (isValidDestination && destinationChainFilter) || '', + [MESSAGE_QUERY_PARAMS.START_TIME]: startTimeFilter !== null ? String(startTimeFilter) : '', + [MESSAGE_QUERY_PARAMS.END_TIME]: endTimeFilter !== null ? String(endTimeFilter) : '', }); - // For the cases where the URL param for origin and destination chain is not valid - // This will reset the chain picker to null instead of having a empty selected chain - useEffect(() => { - if (!isValidOrigin) setOriginChainFilter(null); - if (!isValidDestination) setDestinationChainFilter(null); - }, [isValidOrigin, isValidDestination]); - return ( <> - + + ); diff --git a/src/features/messages/queries/useMessageQuery.ts b/src/features/messages/queries/useMessageQuery.ts index 376b78b0..9b5a7768 100644 --- a/src/features/messages/queries/useMessageQuery.ts +++ b/src/features/messages/queries/useMessageQuery.ts @@ -41,8 +41,9 @@ export function useMessageSearchQuery( const isValidInput = !hasInput || isValidSearchQuery(sanitizedInput); // validating filters - const isValidOrigin = isValidDomainId(originChainFilter, multiProvider); - const isValidDestination = isValidDomainId(destinationChainFilter, multiProvider); + const isValidOrigin = !originChainFilter || isValidDomainId(originChainFilter, multiProvider); + const isValidDestination = + !destinationChainFilter || isValidDomainId(destinationChainFilter, multiProvider); // Assemble GraphQL query const { query, variables } = buildMessageSearchQuery( From 32da31d5e909ba64b4dfbfb586012ee027a8210f Mon Sep 17 00:00:00 2001 From: Xaroz Date: Thu, 12 Dec 2024 10:22:04 -0600 Subject: [PATCH 4/6] chore: clean up validation for search chain error --- src/components/search/SearchStates.tsx | 2 +- src/features/messages/MessageSearch.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/search/SearchStates.tsx b/src/components/search/SearchStates.tsx index e790b374..1558a545 100644 --- a/src/components/search/SearchStates.tsx +++ b/src/components/search/SearchStates.tsx @@ -124,7 +124,7 @@ export function SearchChainError({ show }: { show: boolean }) { ); diff --git a/src/features/messages/MessageSearch.tsx b/src/features/messages/MessageSearch.tsx index dd38f636..bfaefe34 100644 --- a/src/features/messages/MessageSearch.tsx +++ b/src/features/messages/MessageSearch.tsx @@ -163,7 +163,7 @@ export function MessageSearch() { /> - + ); From 4bd59227ec20ace11beaf849732acd2f71411c2a Mon Sep 17 00:00:00 2001 From: Xaroz Date: Thu, 12 Dec 2024 11:48:24 -0600 Subject: [PATCH 5/6] chore: clean up message query and search state --- src/components/search/SearchStates.tsx | 2 +- src/features/messages/MessageSearch.tsx | 20 +++++++++---------- .../messages/queries/useMessageQuery.ts | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/search/SearchStates.tsx b/src/components/search/SearchStates.tsx index 1558a545..765de27d 100644 --- a/src/components/search/SearchStates.tsx +++ b/src/components/search/SearchStates.tsx @@ -124,7 +124,7 @@ export function SearchChainError({ show }: { show: boolean }) { ); diff --git a/src/features/messages/MessageSearch.tsx b/src/features/messages/MessageSearch.tsx index bfaefe34..2ff4ec0a 100644 --- a/src/features/messages/MessageSearch.tsx +++ b/src/features/messages/MessageSearch.tsx @@ -107,6 +107,15 @@ export function MessageSearch() { const isAnyMessageFound = isMessagesFound || isPiMessagesFound; const messageListResult = isMessagesFound ? messageList : piMessageList; + // Show message list if there are no errors and filters are valid + const showMessageTable = + !isAnyError && + isValidInput && + isValidOrigin && + isValidDestination && + isAnyMessageFound && + !!multiProvider; + // Keep url in sync useSyncQueryParam({ [MESSAGE_QUERY_PARAMS.SEARCH]: isValidInput ? sanitizedInput : '', @@ -140,16 +149,7 @@ export function MessageSearch() { onChangeEndTimestamp={setEndTimeFilter} /> - + Date: Thu, 12 Dec 2024 11:56:16 -0600 Subject: [PATCH 6/6] chore: combine query params conditions check --- src/utils/queryParams.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/utils/queryParams.ts b/src/utils/queryParams.ts index 1a879c37..190edd2f 100644 --- a/src/utils/queryParams.ts +++ b/src/utils/queryParams.ts @@ -39,16 +39,12 @@ export function useSyncQueryParam(params: Record) { ), ); Object.entries(params).forEach(([key, value]) => { - if (value) { - if (newQuery.get(key) !== value) { - newQuery.set(key, value); - hasChanged = true; - } - } else { - if (newQuery.has(key)) { - newQuery.delete(key); - hasChanged = true; - } + if (value && newQuery.get(key) !== value) { + newQuery.set(key, value); + hasChanged = true; + } else if (!value && newQuery.has(key)) { + newQuery.delete(key); + hasChanged = true; } }); if (hasChanged) {