diff --git a/src/components/search/SearchFilterBar.tsx b/src/components/search/SearchFilterBar.tsx
index b638726..cf577d8 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/components/search/SearchStates.tsx b/src/components/search/SearchStates.tsx
index 2da819d..765de27 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 4b47353..2ff4ec0 100644
--- a/src/features/messages/MessageSearch.tsx
+++ b/src/features/messages/MessageSearch.tsx
@@ -6,47 +6,85 @@ import { Card } from '../../components/layout/Card';
import { SearchBar } from '../../components/search/SearchBar';
import { SearchFilterBar } from '../../components/search/SearchFilterBar';
import {
+ SearchChainError,
SearchEmptyError,
SearchFetching,
SearchInvalidError,
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 { tryToDecimalNumber } from '../../utils/number';
import { MessageTable } from './MessageTable';
import { usePiChainMessageSearchQuery } from './pi-queries/usePiChainMessageQuery';
import { useMessageSearchQuery } from './queries/useMessageQuery';
-const QUERY_SEARCH_PARAM = 'search';
+enum MESSAGE_QUERY_PARAMS {
+ SEARCH = 'search',
+ ORIGIN = 'origin',
+ DESTINATION = 'destination',
+ START_TIME = 'startTime',
+ END_TIME = 'endTime',
+}
export function MessageSearch() {
// Chain metadata
const multiProvider = useReadyMultiProvider();
+ // query params
+ const [
+ defaultSearchQuery,
+ defaultOriginQuery,
+ defaultDestinationQuery,
+ defaultStartTime,
+ defaultEndTime,
+ ] = useMultipleQueryParams([
+ 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
- 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 [startTimeFilter, setStartTimeFilter] = useState(null);
- const [endTimeFilter, setEndTimeFilter] = useState(null);
+ const [originChainFilter, setOriginChainFilter] = useState(
+ defaultOriginQuery || null,
+ );
+ const [destinationChainFilter, setDestinationChainFilter] = useState(
+ defaultDestinationQuery || null,
+ );
+ const [startTimeFilter, setStartTimeFilter] = useState(
+ tryToDecimalNumber(defaultStartTime),
+ );
+ const [endTimeFilter, setEndTimeFilter] = useState(
+ tryToDecimalNumber(defaultEndTime),
+ );
// 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 {
@@ -69,8 +107,23 @@ 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(QUERY_SEARCH_PARAM, isValidInput ? sanitizedInput : '');
+ useSyncQueryParam({
+ [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) : '',
+ });
return (
<>
@@ -96,7 +149,7 @@ export function MessageSearch() {
onChangeEndTimestamp={setEndTimeFilter}
/>
-
+
+
>
);
diff --git a/src/features/messages/queries/useMessageQuery.ts b/src/features/messages/queries/useMessageQuery.ts
index 62be9d6..e0d4431 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.hasChain(domainId);
+}
+
export function useMessageSearchQuery(
sanitizedInput: string,
originChainFilter: string | null,
@@ -29,15 +35,21 @@ 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 = !originChainFilter || isValidDomainId(originChainFilter, multiProvider);
+ const isValidDestination =
+ !destinationChainFilter || 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 +65,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 +101,8 @@ export function useMessageSearchQuery(
hasRun: !!data,
isMessagesFound,
messageList,
+ isValidOrigin,
+ isValidDestination,
};
}
diff --git a/src/utils/queryParams.ts b/src/utils/queryParams.ts
index aee9ee1..190edd2 100644
--- a/src/utils/queryParams.ts
+++ b/src/utils/queryParams.ts
@@ -15,28 +15,47 @@ 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 && newQuery.get(key) !== value) {
+ newQuery.set(key, value);
+ hasChanged = true;
+ } else if (!value && 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