-
Notifications
You must be signed in to change notification settings - Fork 67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: sync url with filters #148
Changes from 4 commits
3488a0b
a8c78cb
37057cd
32da31d
4bd5922
c04a0d5
69c0e28
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<string | null>(null); | ||
const [destinationChainFilter, setDestinationChainFilter] = useState<string | null>(null); | ||
const [startTimeFilter, setStartTimeFilter] = useState<number | null>(null); | ||
const [endTimeFilter, setEndTimeFilter] = useState<number | null>(null); | ||
const [originChainFilter, setOriginChainFilter] = useState<string | null>( | ||
defaultOriginQuery || null, | ||
); | ||
const [destinationChainFilter, setDestinationChainFilter] = useState<string | null>( | ||
defaultDestinationQuery || null, | ||
); | ||
const [startTimeFilter, setStartTimeFilter] = useState<number | null>( | ||
tryToDecimalNumber(defaultStartTime), | ||
); | ||
const [endTimeFilter, setEndTimeFilter] = useState<number | null>( | ||
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 { | ||
|
@@ -70,7 +108,13 @@ export function MessageSearch() { | |
const messageListResult = isMessagesFound ? messageList : piMessageList; | ||
|
||
// 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 +140,16 @@ export function MessageSearch() { | |
onChangeEndTimestamp={setEndTimeFilter} | ||
/> | ||
</div> | ||
<Fade show={!isAnyError && isValidInput && isAnyMessageFound && !!multiProvider}> | ||
<Fade | ||
show={ | ||
!isAnyError && | ||
isValidInput && | ||
isValidOrigin && | ||
isValidDestination && | ||
isAnyMessageFound && | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Long list, should we make a var above to combine these? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. created a |
||
!!multiProvider | ||
} | ||
> | ||
<MessageTable messageList={messageListResult} isFetching={isAnyFetching} /> | ||
</Fade> | ||
<SearchFetching | ||
|
@@ -110,6 +163,7 @@ export function MessageSearch() { | |
/> | ||
<SearchUnknownError show={isAnyError && isValidInput} /> | ||
<SearchInvalidError show={!isValidInput} allowAddress={true} /> | ||
<SearchChainError show={(!isValidOrigin || !isValidDestination) && isValidInput} /> | ||
</Card> | ||
</> | ||
); | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Didn't know there was a function for this, thanks! |
||||||
} | ||||||
|
||||||
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, | ||||||
}; | ||||||
} | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<string, string>) { | ||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Awkward structure, combine these There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
newQuery.set(key, value); | ||
hasChanged = true; | ||
} | ||
} else { | ||
if (newQuery.has(key)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto |
||
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 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done