+
);
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;
+}