From f0c7e0559a5608334a9a9668f59016782db1a301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Jedli=C4=8Dka?= Date: Mon, 30 Oct 2023 16:34:23 +0100 Subject: [PATCH] search pagination refactoring --- src/components/AccountAddress.tsx | 6 +- src/components/ItemsTable.tsx | 18 +- src/components/Loading.tsx | 11 +- src/components/TabbedContent.tsx | 4 +- src/components/Time.tsx | 10 +- .../account/AccountBalancesTable.tsx | 6 +- .../search/AccountSearchResultsTable.tsx | 21 ++- .../search/BlockSearchResultsTable.tsx | 22 +-- .../search/EventSearchResultsTable.tsx | 21 ++- .../search/ExtrinsicSearchResultsTable.tsx | 21 ++- src/components/search/SearchResultsTable.tsx | 21 +-- src/hooks/useParam.ts | 41 +++++ src/hooks/useResource.ts | 2 +- src/hooks/useSearch.ts | 57 ++++++- src/hooks/useTab.ts | 33 ++-- src/model/pageInfo.ts | 1 + src/model/searchResult.ts | 12 +- src/model/searchResultItem.ts | 1 + src/router.tsx | 8 +- src/screens/network.tsx | 68 +++++--- src/screens/search.tsx | 93 +++++++--- src/services/balancesService.ts | 12 +- src/services/eventsService.ts | 9 +- src/services/searchService.ts | 161 ++++++++++-------- src/services/transfersService.ts | 11 +- src/utils/equal.ts | 3 + src/utils/itemsConnection.ts | 16 +- src/utils/itemsResponse.ts | 7 +- src/utils/router.ts | 24 +++ 29 files changed, 456 insertions(+), 264 deletions(-) create mode 100644 src/hooks/useParam.ts create mode 100644 src/utils/equal.ts create mode 100644 src/utils/router.ts diff --git a/src/components/AccountAddress.tsx b/src/components/AccountAddress.tsx index 3b648168..620f1991 100644 --- a/src/components/AccountAddress.tsx +++ b/src/components/AccountAddress.tsx @@ -12,7 +12,7 @@ import { AccountAvatar } from "./AccountAvatar"; import CopyToClipboardButton, { CopyToClipboardButtonProps } from "./CopyToClipboardButton"; const accountAddressStyle = css` - display: inline-flex; + display: flex; align-items: center; `; @@ -72,10 +72,6 @@ export const AccountAddress = (props: AccountLinkProps) => { return content; }, [network, address, encodeAddress, link]); - /*if (!icon) { - return <>{content}; - }*/ - return (
{icon && ( diff --git a/src/components/ItemsTable.tsx b/src/components/ItemsTable.tsx index bc38803d..dd2c1c85 100644 --- a/src/components/ItemsTable.tsx +++ b/src/components/ItemsTable.tsx @@ -14,6 +14,10 @@ import { TablePagination } from "./TablePagination"; import { TableSortOptions, TableSortOptionsProps } from "./TableSortOptions"; import { TableSortToggle } from "./TableSortToggle"; +const containerStyle = css` + position: relative; +`; + const tableStyle = css` table-layout: fixed; min-width: 860px; @@ -35,6 +39,15 @@ const cellStyle = css` } `; +const loadingOverlayStyle = css` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, .5); +`; + type ItemsTableItem = { id: string; } @@ -106,7 +119,7 @@ export const ItemsTable = ; } @@ -126,7 +139,7 @@ export const ItemsTable = - + {Children.map(children, (child) => child && )} @@ -172,6 +185,7 @@ export const ItemsTable =
+ {loading && }
{pageInfo && ( { +export interface LoadingProps extends HTMLAttributes {} + +const Loading = (props: LoadingProps) => { return ( -
+
); diff --git a/src/components/TabbedContent.tsx b/src/components/TabbedContent.tsx index 1f6f6e67..2b114dfd 100644 --- a/src/components/TabbedContent.tsx +++ b/src/components/TabbedContent.tsx @@ -103,7 +103,7 @@ export const TabbedContent = (props: TabbedContentProps) => { <> {label} {count !== undefined && ({formatNumber(count)})} - {(loading) && } + {(count === undefined && loading) && } {!!error && } } @@ -125,7 +125,7 @@ export const TabbedContent = (props: TabbedContentProps) => { if (!currentTabPane) { tabHandles[0] && onTabChange?.(tabHandles[0].props.value); } - }, [currentTabPane, tabPanes, onTabChange]); + }, [currentTabPane, tabHandles, onTabChange]); return ( <> diff --git a/src/components/Time.tsx b/src/components/Time.tsx index c8dbfd2f..e49f3a1d 100644 --- a/src/components/Time.tsx +++ b/src/components/Time.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { formatDistanceToNowStrict } from "date-fns"; import enGB from "date-fns/locale/en-GB"; import { format as formatTime, formatInTimeZone as formatTimeInTimeZone } from "date-fns-tz"; @@ -23,7 +23,11 @@ export const Time = (props: TimeProps) => { tooltip = false } = props; - const [fromNowFormatted, setFromNowFormatted] = useState(); + const formatFromNow = useCallback((time: string|Date|number) => { + return formatDistanceToNowStrict(new Date(time), {addSuffix: true, locale: enGB}); + }, []); + + const [fromNowFormatted, setFromNowFormatted] = useState(formatFromNow(time)); const formatted = useMemo(() => { let format = formatProp; @@ -42,7 +46,7 @@ export const Time = (props: TimeProps) => { useEffect(() => { if (fromNow) { const interval = setInterval(() => - setFromNowFormatted(formatDistanceToNowStrict(new Date(time), {addSuffix: true, locale: enGB})) + setFromNowFormatted(formatFromNow(time)) ); return () => clearInterval(interval); diff --git a/src/components/account/AccountBalancesTable.tsx b/src/components/account/AccountBalancesTable.tsx index cd6c2e11..cbcf6d96 100644 --- a/src/components/account/AccountBalancesTable.tsx +++ b/src/components/account/AccountBalancesTable.tsx @@ -5,6 +5,7 @@ import { css } from "@emotion/react"; import Decimal from "decimal.js"; import { AccountBalance } from "../../model/accountBalance"; +import { PageInfo } from "../../model/pageInfo"; import { PaginationOptions } from "../../model/paginationOptions"; import { Resource } from "../../model/resource"; import { SortDirection } from "../../model/sortDirection"; @@ -96,10 +97,11 @@ export const AccountBalancesTable = (props: AccountBalancesTableProps) => { return data?.slice((pagination.page - 1) * pagination.pageSize, pagination.page * pagination.pageSize); }, [data, pagination.page, pagination.pageSize]); - const pageInfo = useMemo(() => ({ + const pageInfo = useMemo(() => ({ page: pagination.page, pageSize: pagination.pageSize, - hasNextPage: pagination.page * pagination.pageSize < data.length + hasNextPage: pagination.page * pagination.pageSize < data.length, + totalPageCount: Math.ceil(data.length / pagination.pageSize) }), [data, pagination]); const handleSortSelected = useCallback((value: SortOrder>) => { diff --git a/src/components/search/AccountSearchResultsTable.tsx b/src/components/search/AccountSearchResultsTable.tsx index 42a7446e..2215232a 100644 --- a/src/components/search/AccountSearchResultsTable.tsx +++ b/src/components/search/AccountSearchResultsTable.tsx @@ -1,27 +1,30 @@ import { Account } from "../../model/account"; import { ItemsResponse } from "../../model/itemsResponse"; +import { PaginatedResource } from "../../model/paginatedResource"; import { SearchResultItem } from "../../model/searchResultItem"; import { encodeAddress } from "../../utils/address"; import { AccountAddress } from "../AccountAddress"; -import { SearchResultsTable, SearchResultsTableItemAttribute } from "./SearchResultsTable"; +import { SearchResultsTable, SearchResultsTableItemAttribute, SearchResultsTableProps } from "./SearchResultsTable"; -export interface AccountSearchResultsTable { - query: string; - items: ItemsResponse, true>; - onPageChange?: (page: number) => void; +export interface AccountSearchResultsTable + extends Pick, "query" | "onPageChange"> { + accounts: PaginatedResource>; } export const AccountSearchResultsTable = (props: AccountSearchResultsTable) => { - const {query, items, onPageChange} = props; + const {accounts, ...tableProps} = props; return ( - query={query} - items={items} + data={accounts.data} + loading={accounts.loading} + pageInfo={accounts.pageInfo} + notFound={accounts.notFound} + error={accounts.error} itemsPlural="accounts" - onPageChange={onPageChange} + {...tableProps} > label="Account" diff --git a/src/components/search/BlockSearchResultsTable.tsx b/src/components/search/BlockSearchResultsTable.tsx index 4e1fd100..7cfe57fd 100644 --- a/src/components/search/BlockSearchResultsTable.tsx +++ b/src/components/search/BlockSearchResultsTable.tsx @@ -1,28 +1,30 @@ import { Block } from "../../model/block"; -import { ItemsResponse } from "../../model/itemsResponse"; +import { PaginatedResource } from "../../model/paginatedResource"; import { SearchResultItem } from "../../model/searchResultItem"; import { AccountAddress } from "../AccountAddress"; import { Link } from "../Link"; import { Time } from "../Time"; -import { SearchResultsTable, SearchResultsTableItemAttribute } from "./SearchResultsTable"; +import { SearchResultsTable, SearchResultsTableItemAttribute, SearchResultsTableProps } from "./SearchResultsTable"; -export interface BlockSearchResultsTable { - query: string; - items: ItemsResponse, true>; - onPageChange?: (page: number) => void; +export interface BlockSearchResultsTable + extends Pick, "query" | "onPageChange"> { + blocks: PaginatedResource>; } export const BlockSearchResultsTable = (props: BlockSearchResultsTable) => { - const {query, items, onPageChange} = props; + const {blocks, ...tableProps} = props; return ( - query={query} - items={items} + data={blocks.data} + loading={blocks.loading} + pageInfo={blocks.pageInfo} + notFound={blocks.notFound} + error={blocks.error} itemsPlural="blocks" - onPageChange={onPageChange} + {...tableProps} > label="Block (Height)" diff --git a/src/components/search/EventSearchResultsTable.tsx b/src/components/search/EventSearchResultsTable.tsx index 3d891dcf..17a3338b 100644 --- a/src/components/search/EventSearchResultsTable.tsx +++ b/src/components/search/EventSearchResultsTable.tsx @@ -8,27 +8,30 @@ import { ButtonLink } from "../ButtonLink"; import { DataViewer } from "../DataViewer"; import { Link } from "../Link"; -import { SearchResultsTable, SearchResultsTableItemAttribute } from "./SearchResultsTable"; +import { SearchResultsTable, SearchResultsTableItemAttribute, SearchResultsTableProps } from "./SearchResultsTable"; +import { PaginatedResource } from "../../model/paginatedResource"; const eventArgsColCss = css` width: 35%; `; -export interface EventSearchResultsTable { - query: string; - items: ItemsResponse, true>; - onPageChange?: (page: number) => void; +export interface EventSearchResultsTable + extends Pick, "query" | "onPageChange"> { + events: PaginatedResource>; } export const EventSearchResultsTable = (props: EventSearchResultsTable) => { - const {query, items, onPageChange} = props; + const {events, ...tableProps} = props; return ( - query={query} - items={items} + data={events.data} + loading={events.loading} + pageInfo={events.pageInfo} + notFound={events.notFound} + error={events.error} itemsPlural="events" - onPageChange={onPageChange} + {...tableProps} > label="Event (ID)" diff --git a/src/components/search/ExtrinsicSearchResultsTable.tsx b/src/components/search/ExtrinsicSearchResultsTable.tsx index 3b6faf7a..2ded2567 100644 --- a/src/components/search/ExtrinsicSearchResultsTable.tsx +++ b/src/components/search/ExtrinsicSearchResultsTable.tsx @@ -1,5 +1,6 @@ import { Extrinsic } from "../../model/extrinsic"; import { ItemsResponse } from "../../model/itemsResponse"; +import { PaginatedResource } from "../../model/paginatedResource"; import { SearchResultItem } from "../../model/searchResultItem"; import { AccountAddress } from "../AccountAddress"; @@ -7,23 +8,25 @@ import { ButtonLink } from "../ButtonLink"; import { Link } from "../Link"; import { Time } from "../Time"; -import { SearchResultsTable, SearchResultsTableItemAttribute } from "./SearchResultsTable"; +import { SearchResultsTable, SearchResultsTableItemAttribute, SearchResultsTableProps } from "./SearchResultsTable"; -export interface ExtrinsicSearchResultsTable { - query: string; - items: ItemsResponse, true>; - onPageChange?: (page: number) => void; +export interface ExtrinsicSearchResultsTable + extends Pick, "query" | "onPageChange"> { + extrinsics: PaginatedResource>; } export const ExtrinsicSearchResultsTable = (props: ExtrinsicSearchResultsTable) => { - const {query, items, onPageChange} = props; + const {extrinsics, ...tableProps} = props; return ( - query={query} - items={items} + data={extrinsics.data} + loading={extrinsics.loading} + pageInfo={extrinsics.pageInfo} + notFound={extrinsics.notFound} + error={extrinsics.error} itemsPlural="extrinsics" - onPageChange={onPageChange} + {...tableProps} > label="Extrinsic (ID)" diff --git a/src/components/search/SearchResultsTable.tsx b/src/components/search/SearchResultsTable.tsx index daad499d..c9abfa3f 100644 --- a/src/components/search/SearchResultsTable.tsx +++ b/src/components/search/SearchResultsTable.tsx @@ -7,7 +7,7 @@ import { Network } from "../../model/network"; import { ItemsResponse } from "../../model/itemsResponse"; import { formatNumber } from "../../utils/number"; -import { ItemsTable, ItemsTableAttribute, ItemsTableAttributeProps } from "../ItemsTable"; +import { ItemsTable, ItemsTableAttribute, ItemsTableAttributeProps, ItemsTableProps } from "../ItemsTable"; import { Link } from "../Link"; import { SearchResultItem } from "../../model/searchResultItem"; @@ -31,7 +31,7 @@ const networkIconStyle = css` height: 20px; object-fit: contain; margin-right: 16px; - float: left; + flex: 0 0 auto; `; type SearchResultsTableChild = ReactElement>; @@ -39,21 +39,14 @@ type SearchResultsTableChild = ReactElement(props: ItemsTableAttributeProps) => ; -export interface SearchResultsTableProps { +export interface SearchResultsTableProps extends Omit>, "children"> { children: SearchResultsTableChild|(SearchResultsTableChild|false|undefined|null)[]; query: string; - items: ItemsResponse, true>; itemsPlural: string - onPageChange?: (page: number) => void; } export const SearchResultsTable = (props: SearchResultsTableProps) => { - const { children, query, items, itemsPlural, onPageChange } = props; - - const data = useMemo(() => items.data.map(it => ({ - ...it, - id: `${it.network.name}-${it.data?.id || "grouped"}` - })), [items]); + const { children, query, itemsPlural, ...itemsTableProps } = props; const itemAttributes = useMemo(() => Children.map(children, (child, index) => { if (!child) { @@ -114,12 +107,8 @@ export const SearchResultsTable = (pro return ( > label="Network" diff --git a/src/hooks/useParam.ts b/src/hooks/useParam.ts new file mode 100644 index 00000000..25f870ac --- /dev/null +++ b/src/hooks/useParam.ts @@ -0,0 +1,41 @@ +import { useCallback } from "react"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; + +import { setLocationParams } from "../utils/router"; + +import { routes } from "../router"; + +export type UseSetParamValueOptions = { + preserveQueryParams?: string[]; + replace?: boolean; +} + +export function useParam(name: string) { + const location = useLocation(); + const params = useParams(); + const navigate = useNavigate(); + + const value = params[name] as T; + + const setValue = useCallback((newValue: T, options: UseSetParamValueOptions) => { + const newPath = setLocationParams(location, { + [name]: newValue + }, routes); + + const qs = new URLSearchParams(location.search); + const newQs = new URLSearchParams(); + + for (const param of options.preserveQueryParams || []) { + for (const values of qs.getAll(param)) { + newQs.append(param, values); + } + } + + navigate({ + pathname: newPath, + search: newQs.toString() + }, {replace: options.replace}); + }, [location, params, navigate]); + + return [value, setValue] as const; +} diff --git a/src/hooks/useResource.ts b/src/hooks/useResource.ts index b3eb2963..78b5f92f 100644 --- a/src/hooks/useResource.ts +++ b/src/hooks/useResource.ts @@ -30,7 +30,7 @@ export function useResource( const {skip, refresh, refreshInterval = 3000, ...swrOptions} = options; const swrKey = !skip - ? [fetchItem, args] + ? [fetchItem, args] as const : null; const {data, isLoading, error, mutate} = useSwr(swrKey, swrFetcher, { diff --git a/src/hooks/useSearch.ts b/src/hooks/useSearch.ts index c867b2d9..094c3b61 100644 --- a/src/hooks/useSearch.ts +++ b/src/hooks/useSearch.ts @@ -1,10 +1,22 @@ +import { Account } from "../model/account"; +import { Block } from "../model/block"; +import { Event } from "../model/event"; +import { Extrinsic } from "../model/extrinsic"; +import { ItemsResponse } from "../model/itemsResponse"; import { Network } from "../model/network"; -import { search } from "../services/searchService"; +import { PaginatedResource } from "../model/paginatedResource"; +import { Resource } from "../model/resource"; +import { SearchResultItem } from "../model/searchResultItem"; +import { SearchResult } from "../model/searchResult"; + +import { SearchPaginationOptions, search } from "../services/searchService"; + +import { PickByType } from "../utils/types"; import { UseResourceOptions, useResource } from "./useResource"; interface UseSearchOptions extends UseResourceOptions { - page?: number; + pagination?: SearchPaginationOptions; } export function useSearch( @@ -12,10 +24,45 @@ export function useSearch( networks: Network[], options: UseSearchOptions = {} ) { - const { page = 1 } = options; + const { pagination } = options; - return useResource(search, [query, networks, { - page, + console.log("search pagination", pagination); + + const defaultPagination = { + page: 1, pageSize: 10 // TODO constant + }; + + const resource = useResource(search, [query, networks, { + accounts: pagination?.accounts || defaultPagination, + blocks: pagination?.blocks || defaultPagination, + extrinsics: pagination?.extrinsics || defaultPagination, + events: pagination?.events || defaultPagination }], options); + + return { + ...resource, + accounts: searchResultItemsToPaginatedResource(resource, "accounts"), + blocks: searchResultItemsToPaginatedResource(resource, "blocks"), + extrinsics: searchResultItemsToPaginatedResource(resource, "extrinsics"), + events: searchResultItemsToPaginatedResource(resource, "events"), + totalCount: resource.data?.totalCount + }; +} + +function searchResultItemsToPaginatedResource( + resource: Resource, + itemsType: keyof PickByType, true>>, +): PaginatedResource> { + const items = resource.data?.[itemsType]; + + return { + data: items?.data as SearchResultItem[] | undefined, + pageInfo: items?.pageInfo, + totalCount: items?.totalCount, + loading: resource.loading, + notFound: resource.notFound || items?.data.length === 0, + error: undefined, // TODO + refetch: resource.refetch + }; } diff --git a/src/hooks/useTab.ts b/src/hooks/useTab.ts index 08fd5d9b..f6aeaff6 100644 --- a/src/hooks/useTab.ts +++ b/src/hooks/useTab.ts @@ -1,5 +1,6 @@ import { useCallback } from "react"; -import { useNavigate, useParams, useSearchParams } from "react-router-dom"; + +import { useParam } from "./useParam"; export type UseTabParamOptions = { paramName?: string; @@ -7,28 +8,14 @@ export type UseTabParamOptions = { } export function useTab(options: UseTabParamOptions = {}) { - const [qs] = useSearchParams(); - const params = useParams(); - - const navigate = useNavigate(); - - const currentTab = params[options.paramName || "tab"] as T; - - const setTab = useCallback((newTab: T) => { - const path = currentTab ? `./../${newTab}` : newTab; - const newQs = new URLSearchParams(); - - for (const param of options.preserveQueryParams || []) { - for (const values of qs.getAll(param)) { - newQs.append(param, values); - } - } + const [value, setValue] = useParam(options.paramName || "tab"); - navigate({ - pathname: path, - search: newQs.toString() - }, {replace: !currentTab}); - }, [currentTab, navigate]); + const setTab = useCallback((newValue: T) => { + setValue(newValue, { + preserveQueryParams: options.preserveQueryParams, + replace: !value + }); + }, [value, options.preserveQueryParams, setValue]); - return [currentTab, setTab] as const; + return [value, setTab] as const; } diff --git a/src/model/pageInfo.ts b/src/model/pageInfo.ts index eff3ee32..00d147a7 100644 --- a/src/model/pageInfo.ts +++ b/src/model/pageInfo.ts @@ -2,4 +2,5 @@ export type PageInfo = { page: number; pageSize: number; hasNextPage: boolean; + totalPageCount: number|undefined; } diff --git a/src/model/searchResult.ts b/src/model/searchResult.ts index 26864ba4..62777053 100644 --- a/src/model/searchResult.ts +++ b/src/model/searchResult.ts @@ -7,13 +7,9 @@ import { Event } from "./event"; import { Extrinsic } from "./extrinsic"; export type SearchResult = { - accountItems: ItemsResponse, true> - blockItems: ItemsResponse, true> - extrinsicItems: ItemsResponse, true> - eventItems: ItemsResponse, true> - accountsTotalCount: number; - blocksTotalCount: number; - extrinsicsTotalCount: number; - eventsTotalCount: number; + accounts: ItemsResponse, true> + blocks: ItemsResponse, true> + extrinsics: ItemsResponse, true> + events: ItemsResponse, true> totalCount: number; } diff --git a/src/model/searchResultItem.ts b/src/model/searchResultItem.ts index 4377ebb4..e5d6093f 100644 --- a/src/model/searchResultItem.ts +++ b/src/model/searchResultItem.ts @@ -1,6 +1,7 @@ import { Network } from "./network"; export type SearchResultItem = { + id: string; network: Network; data?: T; groupedCount?: number; diff --git a/src/router.tsx b/src/router.tsx index ac8b7ad3..aca04ddb 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,4 +1,4 @@ -import { Navigate, createBrowserRouter, redirect } from "react-router-dom"; +import { Navigate, RouteObject, createBrowserRouter, redirect } from "react-router-dom"; import { ResultLayout } from "./components/ResultLayout"; import { getNetwork } from "./services/networksService"; @@ -18,7 +18,7 @@ import { RuntimePage } from "./screens/runtime"; import { config } from "./config"; -export const router = createBrowserRouter([ +export const routes: RouteObject[] = [ { path: "/", element: , @@ -104,7 +104,9 @@ export const router = createBrowserRouter([ }, ] } -], { +]; + +export const router = createBrowserRouter(routes, { basename: window.location.hostname === "localhost" ? undefined : process.env.PUBLIC_URL diff --git a/src/screens/network.tsx b/src/screens/network.tsx index 1116ccd4..08987992 100644 --- a/src/screens/network.tsx +++ b/src/screens/network.tsx @@ -44,6 +44,8 @@ export const NetworkPage = () => { const [tab, setTab] = useTab(); const [page, setPage] = usePage(); + console.log("tab", tab); + const extrinsics = useExtrinsicsWithoutTotalCount(network.name, undefined, { page: tab === "extrinsics" ? page : 1, refreshFirstPage: true @@ -103,8 +105,6 @@ export const NetworkPage = () => { } - - { error={extrinsics.error} value="extrinsics" > - + { error={blocks.error} value="blocks" > - + - - {hasSupport(network.name, "main-squid") && - - - + + + } {hasSupport(network.name, "stats-squid") && - - - + + + } diff --git a/src/screens/search.tsx b/src/screens/search.tsx index 3aa77ef1..fda0e786 100644 --- a/src/screens/search.tsx +++ b/src/screens/search.tsx @@ -1,5 +1,5 @@ /** @jsxImportSource @emotion/react */ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Navigate, useSearchParams } from "react-router-dom"; import { css } from "@emotion/react"; @@ -7,6 +7,7 @@ import { Card, CardHeader } from "../components/Card"; import { ErrorMessage } from "../components/ErrorMessage"; import Loading from "../components/Loading"; import NotFound from "../components/NotFound"; + import { BlockSearchResultsTable } from "../components/search/BlockSearchResultsTable"; import { AccountSearchResultsTable } from "../components/search/AccountSearchResultsTable"; import { ExtrinsicSearchResultsTable } from "../components/search/ExtrinsicSearchResultsTable"; @@ -20,6 +21,8 @@ import { useTab } from "../hooks/useTab"; import { getNetworks } from "../services/networksService"; +import { isDeepEqual } from "../utils/equal"; + const queryStyle = css` font-weight: normal; word-break: break-all; @@ -48,13 +51,38 @@ export const SearchPage = () => { preserveQueryParams: ["query", "network"] }); + const previousQueryRef = useRef(); + const previousNetworkNamesRef = useRef(); + const [page, setPage] = usePage(); console.log("query", query, networkNames); const [forceLoading, setForceLoading] = useState(true); - const searchResult = useSearch(query, getNetworks(networkNames), { page }); + const searchResult = useSearch(query, getNetworks(networkNames), { + pagination: { + accounts: { + page: tab === "accounts" ? page : 1, + pageSize: 10 + }, + blocks: { + page: tab === "blocks" ? page : 1, + pageSize: 10 + }, + extrinsics: { + page: tab === "extrinsics" ? page : 1, + pageSize: 10 + }, + events: { + page: tab === "events" ? page : 1, + pageSize: 10 + } + }, + keepPreviousData: + query === previousQueryRef.current + && isDeepEqual(networkNames, previousNetworkNamesRef.current) + }); console.log("results", searchResult); @@ -64,32 +92,37 @@ export const SearchPage = () => { setTimeout(() => setForceLoading(false), 1000); }, [query]); + useEffect(() => { + if (!searchResult.loading) { + previousQueryRef.current = query; + previousNetworkNamesRef.current = networkNames; + } + }, [searchResult.loading]); + useDOMEventTrigger("data-loaded", !searchResult.loading); if (!query) { return ; } - if (!forceLoading && searchResult.data?.totalCount === 1) { - const result = searchResult.data; - - const extrinsicItem = result.extrinsicItems.data[0]; + if (!forceLoading && searchResult.totalCount === 1) { + const extrinsicItem = searchResult.extrinsics.data?.[0]; if (extrinsicItem?.data) { return ; } - const blockItem = result.blockItems.data[0]; + const blockItem = searchResult.blocks.data?.[0]; if (blockItem?.data) { return ; } - const accountItem = result.accountItems.data[0]; + const accountItem = searchResult.accounts.data?.[0]; if (accountItem?.data) { return ; } } - if (searchResult.loading || forceLoading) { + if ((!searchResult.data && searchResult.loading) || forceLoading) { return ( @@ -100,7 +133,7 @@ export const SearchPage = () => { ); } - if (searchResult.notFound || !searchResult.data) { + if (searchResult.notFound) { return ( Nothing was found for query {query} @@ -127,49 +160,57 @@ export const SearchPage = () => { diff --git a/src/services/balancesService.ts b/src/services/balancesService.ts index 21ddfdf0..cd43ccf1 100644 --- a/src/services/balancesService.ts +++ b/src/services/balancesService.ts @@ -8,12 +8,12 @@ import { PaginationOptions } from "../model/paginationOptions"; import { encodeAddress } from "../utils/address"; import { extractConnectionItems, paginationToConnectionCursor } from "../utils/itemsConnection"; +import { emptyItemsResponse } from "../utils/itemsResponse"; import { rawAmountToDecimal } from "../utils/number"; import { fetchStatsSquid } from "./fetchService"; import { getNetwork, getNetworks, hasSupport } from "./networksService"; - export type BalancesFilter = { id_eq: string; } @@ -63,15 +63,7 @@ export async function getBalances( return balances; } - return { - data: [], - pageInfo: { - page: 1, - pageSize: 10, // TODO constant - hasNextPage: false, - }, - totalCount: 0, - }; + return emptyItemsResponse(); } export async function getAccountBalances(address: string) { diff --git a/src/services/eventsService.ts b/src/services/eventsService.ts index a72660a5..f48b0e0f 100644 --- a/src/services/eventsService.ts +++ b/src/services/eventsService.ts @@ -8,6 +8,7 @@ import { ItemsResponse } from "../model/itemsResponse"; import { PaginationOptions } from "../model/paginationOptions"; import { extractConnectionItems, paginationToConnectionCursor } from "../utils/itemsConnection"; +import { emptyItemsResponse } from "../utils/itemsResponse"; import { upperFirst } from "../utils/string"; import { fetchArchive, fetchExplorerSquid } from "./fetchService"; @@ -65,13 +66,7 @@ export async function getEventsByName( ); if (countResponse.itemsCounterById === null || countResponse.itemsCounterById.total === 0) { - return { - data: [], - pageInfo: { - ...pagination, - hasNextPage: false - } - }; + return emptyItemsResponse(); } const events = await getExplorerSquidEvents(network, filter, order, pagination, false); diff --git a/src/services/searchService.ts b/src/services/searchService.ts index dc0b37eb..3430ce02 100644 --- a/src/services/searchService.ts +++ b/src/services/searchService.ts @@ -15,7 +15,7 @@ import { Network } from "../model/network"; import { decodeAddress, encodeAddress, isAccountPublicKey, isEncodedAddress } from "../utils/address"; import { warningAssert } from "../utils/assert"; import { extractConnectionItems, paginationToConnectionCursor } from "../utils/itemsConnection"; -import { emptyResponse } from "../utils/itemsResponse"; +import { emptyItemsResponse } from "../utils/itemsResponse"; import { BlocksFilter, blocksFilterToExplorerSquidFilter, unifyExplorerSquidBlock } from "./blocksService"; import { EventsFilter, addEventsArgs, eventsFilterToExplorerSquidFilter, normalizeEventName, unifyExplorerSquidEvent } from "./eventsService"; @@ -27,7 +27,10 @@ import { PickByType } from "../utils/types"; import { SearchResult } from "../model/searchResult"; import { SearchResultItem } from "../model/searchResultItem"; -export async function search(query: string, networks: Network[], pagination: PaginationOptions): Promise { + +export type SearchPaginationOptions = Record>, PaginationOptions>; + +export async function search(query: string, networks: Network[], pagination: SearchPaginationOptions): Promise { if (networks.length === 0) { networks = getNetworks(); } @@ -60,6 +63,7 @@ export async function search(query: string, networks: Network[], pagination: Pag page: 1, pageSize: 10, // TODO constant hasNextPage: false, + totalPageCount: 1 }, totalCount: 1 }, @@ -83,43 +87,31 @@ export async function search(query: string, networks: Network[], pagination: Pag } = nonEmptyNetworkResults[0]; return { - accountItems: itemsToSearchResultItems(accounts), - blockItems: itemsToSearchResultItems(blocks), - extrinsicItems: itemsToSearchResultItems(extrinsics), - eventItems: itemsToSearchResultItems(events), - accountsTotalCount: accounts.totalCount, - blocksTotalCount: blocks.totalCount, - extrinsicsTotalCount: extrinsics.totalCount, - eventsTotalCount: events.totalCount, + accounts: itemsToSearchResultItems(accounts), + blocks: itemsToSearchResultItems(blocks), + extrinsics: itemsToSearchResultItems(extrinsics), + events: itemsToSearchResultItems(events), totalCount }; } - const accountItems = networkResultsToSearchResultItems(nonEmptyNetworkResults, "accounts", pagination); - const blockItems = networkResultsToSearchResultItems(nonEmptyNetworkResults, "blocks", pagination); - const extrinsicItems = networkResultsToSearchResultItems(nonEmptyNetworkResults, "extrinsics", pagination); - const eventItems = networkResultsToSearchResultItems(nonEmptyNetworkResults, "events", pagination); + const accounts = networkResultsToSearchResultItems(nonEmptyNetworkResults, "accounts", pagination.accounts); + const blocks = networkResultsToSearchResultItems(nonEmptyNetworkResults, "blocks", pagination.blocks); + const extrinsics = networkResultsToSearchResultItems(nonEmptyNetworkResults, "extrinsics", pagination.extrinsics); + const events = networkResultsToSearchResultItems(nonEmptyNetworkResults, "events", pagination.events); - const accountsTotalCount = nonEmptyNetworkResults.reduce((total, result) => total + result.accounts.totalCount, 0); - const blocksTotalCount = nonEmptyNetworkResults.reduce((total, result) => total + result.blocks.totalCount, 0); - const extrinsicsTotalCount = nonEmptyNetworkResults.reduce((total, result) => total + result.extrinsics.totalCount, 0); - const eventsTotalCount = nonEmptyNetworkResults.reduce((total, result) => total + result.events.totalCount, 0); - const totalCount = nonEmptyNetworkResults.reduce((total, result) => total + result.totalCount, 0); + const totalCount = accounts.totalCount + blocks.totalCount + extrinsics.totalCount + events.totalCount; - warningAssert(!isHex(query) || blocksTotalCount <= 1, { + warningAssert(!isHex(query) || blocks.totalCount <= 1, { message: "Block hashes should be unique", query }); return { - accountItems, - blockItems, - extrinsicItems, - eventItems, - accountsTotalCount, - blocksTotalCount, - extrinsicsTotalCount, - eventsTotalCount, + accounts, + blocks, + extrinsics, + events, totalCount }; } @@ -138,7 +130,7 @@ type NetworkSearchResult = { async function searchNetwork( network: Network, query: string, - pagination: PaginationOptions, + pagination: SearchPaginationOptions, fetchAll = false ) { if (!hasSupport(network.name, "explorer-squid")) { @@ -160,12 +152,13 @@ async function searchNetwork( async function searchNetworkByHash( network: Network, hash: string, - pagination: PaginationOptions, + pagination: SearchPaginationOptions, ) { const blocksFilter: BlocksFilter = {hash_eq: hash}; const extrinsicsFilter: ExtrinsicsFilter = {hash_eq: hash}; - const {first, after} = paginationToConnectionCursor(pagination); + const blocksCursor = paginationToConnectionCursor(pagination.blocks); + const extrinsicsCursor = paginationToConnectionCursor(pagination.extrinsics); const response = await fetchExplorerSquid<{ blocks: ItemsConnection, @@ -173,12 +166,14 @@ async function searchNetworkByHash( }>( network.name, `query ( - $first: Int!, - $after: String + $blocksFirst: Int!, + $blocksAfter: String, $blocksFilter: BlockWhereInput, + $extrinsicsFirst: Int!, + $extrinsicsAfter: String, $extrinsicsFilter: ExtrinsicWhereInput, ) { - blocks: blocksConnection(first: $first, after: $after, where: $blocksFilter, orderBy: id_ASC) { + blocks: blocksConnection(first: $blocksFirst, after: $blocksAfter, where: $blocksFilter, orderBy: id_ASC) { edges { node { id @@ -198,7 +193,7 @@ async function searchNetworkByHash( } totalCount }, - extrinsics: extrinsicsConnection(first: $first, after: $after, where: $extrinsicsFilter, orderBy: id_ASC) { + extrinsics: extrinsicsConnection(first: $extrinsicsFirst, after: $extrinsicsAfter, where: $extrinsicsFilter, orderBy: id_ASC) { edges { node { id @@ -233,9 +228,11 @@ async function searchNetworkByHash( } }`, { - first, - after, + blocksFirst: blocksCursor.first, + blocksAfter: blocksCursor.after, blocksFilter: blocksFilterToExplorerSquidFilter(blocksFilter), + extrinsicsFirst: extrinsicsCursor.first, + extrinsicsAfter: extrinsicsCursor.after, extrinsicsFilter: extrinsicFilterToExplorerSquidFilter(extrinsicsFilter), } ); @@ -245,10 +242,10 @@ async function searchNetworkByHash( const result: NetworkSearchResult = { network, - accounts: emptyResponse(), + accounts: emptyItemsResponse(0), blocks, extrinsics, - events: emptyResponse(), + events: emptyItemsResponse(0), totalCount: blocks.totalCount + extrinsics.totalCount }; @@ -293,10 +290,10 @@ async function searchNetworkByBlockHeight(network: Network, height: number) { const result: NetworkSearchResult = { network, - accounts: emptyResponse(), + accounts: emptyItemsResponse(0), blocks, - extrinsics: emptyResponse(), - events: emptyResponse(), + extrinsics: emptyItemsResponse(0), + events: emptyItemsResponse(0), totalCount: blocks.totalCount }; @@ -316,13 +313,14 @@ async function searchNetworkByEncodedAddress(network: Network, encodedAddress: s pageInfo: { page: 1, pageSize: 10, - hasNextPage: false + hasNextPage: false, + totalPageCount: 1 }, totalCount: 1 }, - blocks: emptyResponse(), - extrinsics: emptyResponse(), - events: emptyResponse(), + blocks: emptyItemsResponse(0), + extrinsics: emptyItemsResponse(0), + events: emptyItemsResponse(0), totalCount: 1 }; @@ -332,7 +330,7 @@ async function searchNetworkByEncodedAddress(network: Network, encodedAddress: s async function searchNetworkByName( network: Network, query: string, - pagination: PaginationOptions, + pagination: SearchPaginationOptions, fetchAll: boolean, ) { const extrinsicName = await normalizeExtrinsicName(network.name, query); @@ -385,7 +383,8 @@ async function searchNetworkByName( } } - const {first, after} = paginationToConnectionCursor(pagination); + const extrinsicsCursor = paginationToConnectionCursor(pagination.extrinsics); + const eventsCursor = paginationToConnectionCursor(pagination.events); const response = await fetchExplorerSquid<{ extrinsics: ItemsConnection, @@ -393,12 +392,14 @@ async function searchNetworkByName( }>( network.name, `query ( - $first: Int!, - $after: String - $eventsFilter: EventWhereInput, + $extrinsicsFirst: Int!, + $extrinsicsAfter: String, $extrinsicsFilter: ExtrinsicWhereInput, + $eventsFirst: Int!, + $eventsAfter: String, + $eventsFilter: EventWhereInput, ) { - extrinsics: extrinsicsConnection(first: $first, after: $after, where: $extrinsicsFilter, orderBy: id_DESC) { + extrinsics: extrinsicsConnection(first: $extrinsicsFirst, after: $extrinsicsAfter, where: $extrinsicsFilter, orderBy: id_DESC) { edges { node { id @@ -430,7 +431,7 @@ async function searchNetworkByName( startCursor } } - events: eventsConnection(first: $first, after: $after, where: $eventsFilter, orderBy: id_DESC) { + events: eventsConnection(first: $eventsFirst, after: $eventsAfter, where: $eventsFilter, orderBy: id_DESC) { edges { node { id @@ -459,10 +460,13 @@ async function searchNetworkByName( } }`, { - first, - after, + extrinsicsFirst: extrinsicsCursor.first, + extrinsicsAfter: extrinsicsCursor.after, extrinsicsFilter: extrinsicFilterToExplorerSquidFilter(extrinsicsFilter), + eventsFirst: eventsCursor.first, + eventsAfter: eventsCursor.after, eventsFilter: eventsFilterToExplorerSquidFilter(eventsFilter), + } ); @@ -486,8 +490,8 @@ async function searchNetworkByName( const result: NetworkSearchResult = { network, - accounts: emptyResponse(), - blocks: emptyResponse(), + accounts: emptyItemsResponse(0), + blocks: emptyItemsResponse(0), extrinsics: { ...extrinsics, totalCount: extrinsicsTotalCount @@ -502,42 +506,55 @@ async function searchNetworkByName( return result; } -function itemsToSearchResultItems(items: ItemsResponse): ItemsResponse, true> { +function itemsToSearchResultItems(items: ItemsResponse): ItemsResponse, true> { return { ...items, data: items.data.map(it => ({ + id: `${it.network}-${it.id}`, network: it.network, data: it })), }; } -function networkResultsToSearchResultItems( +function networkResultsToSearchResultItems( networkResults: NetworkSearchResult[], itemsType: keyof PickByType>, pagination: PaginationOptions ): ItemsResponse, true> { - const data = networkResults.flatMap>((result) => { - const {data, totalCount} = result[itemsType]; - - if (totalCount === 0) { - return []; + const data: SearchResultItem[] = []; + let totalCount = 0; + + for (const result of networkResults) { + const items = result[itemsType]; + + if (items.totalCount === 1) { + const item = items.data[0] as unknown as T; + + data.push({ + id: `${result.network.name}-${item.id}`, + network: result.network, + data: item, + }); + } else if (totalCount > 1) { + data.push({ + id: `${result.network.name}-grouped`, + network: result.network, + groupedCount: items.totalCount > 1 ? items.totalCount : undefined + }); } - return [{ - network: result.network, - data: totalCount === 1 ? data[0] as T : undefined, - groupedCount: totalCount > 1 ? totalCount : undefined - }]; - }); + totalCount += items.totalCount; + } return { data: data.slice((pagination.page - 1) * pagination.pageSize, pagination.page * pagination.pageSize), pageInfo: { page: pagination.page, pageSize: pagination.pageSize, - hasNextPage: pagination.page * pagination.pageSize < data.length + hasNextPage: pagination.page * pagination.pageSize < data.length, + totalPageCount: Math.ceil(data.length / pagination.pageSize) }, - totalCount: data.length + totalCount }; } diff --git a/src/services/transfersService.ts b/src/services/transfersService.ts index 20be4438..a6435029 100644 --- a/src/services/transfersService.ts +++ b/src/services/transfersService.ts @@ -6,6 +6,7 @@ import { Transfer } from "../model/transfer"; import { decodeAddress } from "../utils/address"; import { extractConnectionItems, paginationToConnectionCursor } from "../utils/itemsConnection"; +import { emptyItemsResponse } from "../utils/itemsResponse"; import { rawAmountToDecimal } from "../utils/number"; import { fetchArchive, fetchMainSquid } from "./fetchService"; @@ -26,15 +27,7 @@ export async function getTransfers( return getMainSquidTransfers(network, filter, order, pagination); } - return { - data: [], - pageInfo: { - page: 1, - pageSize: 10, // TODO constant - hasNextPage: false, - }, - totalCount: 0, - }; + return emptyItemsResponse(); } /*** PRIVATE ***/ diff --git a/src/utils/equal.ts b/src/utils/equal.ts new file mode 100644 index 00000000..fe863da3 --- /dev/null +++ b/src/utils/equal.ts @@ -0,0 +1,3 @@ +export function isDeepEqual(a: any, b: any) { + return JSON.stringify(a) === JSON.stringify(b); +} diff --git a/src/utils/itemsConnection.ts b/src/utils/itemsConnection.ts index f8888352..b864c179 100644 --- a/src/utils/itemsConnection.ts +++ b/src/utils/itemsConnection.ts @@ -5,9 +5,12 @@ import { PaginationOptions } from "../model/paginationOptions"; export function paginationToConnectionCursor(pagination: PaginationOptions) { const offset = (pagination.page - 1) * pagination.pageSize; + const first = pagination.pageSize; + const after = offset === 0 ? null : offset.toString(); + return { - after: offset === 0 ? null : offset.toString(), - first: pagination.pageSize + first, + after }; } @@ -22,18 +25,23 @@ export async function extractConnectionItems; diff --git a/src/utils/itemsResponse.ts b/src/utils/itemsResponse.ts index 23b44e0d..b52802dd 100644 --- a/src/utils/itemsResponse.ts +++ b/src/utils/itemsResponse.ts @@ -1,13 +1,16 @@ import { ItemsResponse } from "../model/itemsResponse"; -export function emptyResponse(): ItemsResponse { +export function emptyItemsResponse(totalCount: number): ItemsResponse +export function emptyItemsResponse(totalCount?: undefined): ItemsResponse +export function emptyItemsResponse(totalCount?: number): ItemsResponse { return { data: [], pageInfo: { page: 1, pageSize: 10, // TODO constant hasNextPage: false, + totalPageCount: 0 }, - totalCount: 0, + totalCount, }; } diff --git a/src/utils/router.ts b/src/utils/router.ts new file mode 100644 index 00000000..9ec30a00 --- /dev/null +++ b/src/utils/router.ts @@ -0,0 +1,24 @@ +import { Location, RouteObject, matchRoutes } from "react-router-dom"; + +export function setLocationParams(location: Location, newParams: Record, routes: RouteObject[]) { + const match = matchRoutes(routes, location); + + if (!match) { + return; + } + + const pathPattern = match.map(it => it.route.path).join("/").replace(/\/+/g, "/"); + + const currentParams = match[match.length - 1]?.params || {}; + + const paramEntries = Object.entries({ + ...currentParams, + ...newParams + }); + + const path = paramEntries.reduce((path, [param, value]) => { + return path.replace(new RegExp(`:${param}\\??`), value || ""); + }, pathPattern); + + return path; +}