From 9ce27557b931cf20b58bbde1c1b5a53822a19003 Mon Sep 17 00:00:00 2001 From: ejmg Date: Wed, 3 Jul 2024 13:48:01 -0500 Subject: [PATCH 01/10] loading.tsx for base routes added + skeleton ui component --- src/app/loading.tsx | 11 +++++++++++ src/components/ui/skeleton.tsx | 15 +++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 src/app/loading.tsx create mode 100644 src/components/ui/skeleton.tsx diff --git a/src/app/loading.tsx b/src/app/loading.tsx new file mode 100644 index 0000000..8fbe080 --- /dev/null +++ b/src/app/loading.tsx @@ -0,0 +1,11 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function Loading() { + return ( +
+ + + +
+ ); +} diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..2cdf440 --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils"; + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +} + +export { Skeleton }; From 66c17f86b666ec073e053e25de3f16471f407d5e Mon Sep 17 00:00:00 2001 From: ejmg Date: Wed, 3 Jul 2024 13:49:52 -0500 Subject: [PATCH 02/10] more small styling improvements for skeleton/theme, updated navbar app name, updating reat-query usage --- src/app/block/[ht]/page.tsx | 30 +++-- src/app/globals.css | 4 +- src/components/Navbar/index.tsx | 2 +- src/components/ui/paginated-data-table.tsx | 145 ++++++++++++--------- 4 files changed, 100 insertions(+), 81 deletions(-) diff --git a/src/app/block/[ht]/page.tsx b/src/app/block/[ht]/page.tsx index d945898..3d49663 100644 --- a/src/app/block/[ht]/page.tsx +++ b/src/app/block/[ht]/page.tsx @@ -15,7 +15,7 @@ interface PageProps { const Page : FC = ({ params }) => { const { ht } = params; - const { data: blockData , isError } = useQuery({ + const { data: blockData , isError, isPending, isSuccess } = useQuery({ queryFn: async () => { console.log(`Fetching: GET /api/block?q=${ht}`); const { data } = await axios.get(`/api/block?q=${ht}`); @@ -34,32 +34,34 @@ const Page : FC = ({ params }) => { }, }); + if (isPending) { + return ( +
+

Loading...

+
+ ); + } + if (isError) { return ( -
+

No results found.

); } - // TODO: Replace with data table component views once those are fleshed out. - return ( -
- {blockData ? ( + if (isSuccess) { + return ( +

Block Summary

- ) : ( -
-

No block event

-

To be frank... You shouldn't be able to see this.

-
- )} -
- ); +
+ ); + } }; export default Page; diff --git a/src/app/globals.css b/src/app/globals.css index f0c901d..e0d61f2 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -26,7 +26,7 @@ --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 0 0% 96% / 1.0; + --muted: 0 0% 96% / 0.9; --muted-foreground: 0 0% 45% / 1.0; --accent: 210 40% 96.1%; @@ -60,7 +60,7 @@ --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; - --muted: 270 34% 17%; + --muted: 270 34% 17% / 0.9; --muted-foreground: 0 0% 45% / 1.0; --accent: 217.2 32.6% 17.5%; diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index 4875fc7..986639d 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -10,7 +10,7 @@ import { ThemeToggleButton } from "../ThemeToggleButton"; const Navbar : FC = () => { return (
-

Penumbra Explorer

+

Cuiloa Block Explorer

diff --git a/src/components/ui/paginated-data-table.tsx b/src/components/ui/paginated-data-table.tsx index c70c2c6..d329d0b 100644 --- a/src/components/ui/paginated-data-table.tsx +++ b/src/components/ui/paginated-data-table.tsx @@ -61,7 +61,7 @@ export function PaginatedDataTable({ pageSize, }; - const { data } = useQuery({ + const { data, isError, isPending, isSuccess } = useQuery({ queryKey: [queryName, queryOptions], queryFn: async () => await fetcher({ endpoint, pageIndex }), meta: { @@ -91,70 +91,87 @@ export function PaginatedDataTable({ manualPagination: true, }); - return ( -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} + if (isSuccess){ + return ( +
+
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} - )) - ) : ( - - - No results. - - - )} - -
+ ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + + +
+
+ + +
+
+ ); + } + if (isPending) { + return ( +
+

Loading...

-
- - + ); + } + + if (isError) { + return ( +
+

No results found.

-
- ); + ); + } } From 640f253f3b874166e037ec35dd31434dd0cacb73 Mon Sep 17 00:00:00 2001 From: ejmg Date: Mon, 8 Jul 2024 11:36:04 -0500 Subject: [PATCH 03/10] preparing tanstack/query for streaming --- package-lock.json | 16 ++++++------ package.json | 2 +- src/components/Providers/index.tsx | 39 +++++++++++++++++++++++++----- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 80f0370..fae5ed4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.5", - "@tanstack/react-query": "^5.0.5", + "@tanstack/react-query": "^5.49.2", "@tanstack/react-table": "^8.10.6", "@types/pg": "^8.11.6", "axios": "^1.5.1", @@ -2223,9 +2223,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.40.0.tgz", - "integrity": "sha512-eD8K8jsOIq0Z5u/QbvOmfvKKE/XC39jA7yv4hgpl/1SRiU+J8QCIwgM/mEHuunQsL87dcvnHqSVLmf9pD4CiaA==", + "version": "5.49.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.49.1.tgz", + "integrity": "sha512-JnC9ndmD1KKS01Rt/ovRUB1tmwO7zkyXAyIxN9mznuJrcNtOrkmOnQqdJF2ib9oHzc2VxHomnEG7xyfo54Npkw==", "license": "MIT", "funding": { "type": "github", @@ -2233,12 +2233,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.40.0.tgz", - "integrity": "sha512-iv/W0Axc4aXhFzkrByToE1JQqayxTPNotCoSCnarR/A1vDIHaoKpg7FTIfP3Ev2mbKn1yrxq0ZKYUdLEJxs6Tg==", + "version": "5.49.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.49.2.tgz", + "integrity": "sha512-6rfwXDK9BvmHISbNFuGd+wY3P44lyW7lWiA9vIFGT/T0P9aHD1VkjTvcM4SDAIbAQ9ygEZZoLt7dlU1o3NjMVA==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.40.0" + "@tanstack/query-core": "5.49.1" }, "funding": { "type": "github", diff --git a/package.json b/package.json index cd0e904..d6b4994 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.5", - "@tanstack/react-query": "^5.0.5", + "@tanstack/react-query": "^5.49.2", "@tanstack/react-table": "^8.10.6", "@types/pg": "^8.11.6", "axios": "^1.5.1", diff --git a/src/components/Providers/index.tsx b/src/components/Providers/index.tsx index 025c8e8..c19c221 100644 --- a/src/components/Providers/index.tsx +++ b/src/components/Providers/index.tsx @@ -1,11 +1,11 @@ "use client"; import React, { useState } from "react"; -import { QueryCache, QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { QueryCache, QueryClient, QueryClientProvider, isServer } from "@tanstack/react-query"; import { createGrpcWebTransport } from "@connectrpc/connect-web"; import { TransportProvider } from "@connectrpc/connect-query"; import { Toaster } from "../ui/toaster"; -import { useToast } from "../ui/use-toast"; +import { toast } from "../ui/use-toast"; import { ThemeProvider as NextThemesProvider } from "next-themes"; import { type ThemeProviderProps } from "next-themes/dist/types"; @@ -17,9 +17,11 @@ const penumbraTransport = createGrpcWebTransport({ baseUrl: "https://grpc.testnet.penumbra.zone", }); -const Providers = ({ children } : { children: React.ReactNode }) => { - const { toast } = useToast(); - const [ queryClient ] = useState(() => new QueryClient({ +let browserQueryClient: QueryClient | undefined = undefined; + +const makeQueryClient = () => { + // const { toast } = useToast(); + return new QueryClient({ defaultOptions: { queries: { // Direct suggestion by tanstack, to prevent over-eager refetching from the client. @@ -57,7 +59,32 @@ const Providers = ({ children } : { children: React.ReactNode }) => { } }, }), - })); + }); +}; + +const getQueryClient = () => { + if (isServer) { + // Server: always make a new query client + return makeQueryClient(); + } else { + // Browser: make a new query client if we don't already have one + // This is very important, so we don't re-make a new client if React + // suspends during the initial render. This may not be needed if we + // have a suspense boundary BELOW the creation of the query client + if (!browserQueryClient) browserQueryClient = makeQueryClient(); + return browserQueryClient; + } +}; + + +const Providers = ({ children } : { children: React.ReactNode }) => { + // NOTE: there is a very explicit warning in the TanStack docs about using useState for handling QueryClient (de)hydration within the provider in the scenario where + // there is no Suspense boundary between the instantiation (here) and the context that is being wrapped; however, it is more or less considered best practice to + // use useState for QueryClient because... this is how you keep a stable reference to the client. I believe it is safe to use useState here because + // NextJS, in theory, injects a React.Suspense boundary around app/page.tsx when loading.tsx is provided (as I have). This is a detail that goes undocumented and + // unconnected in the reference docs for using this functionality with NextJS itself. If this note is confusing/unnerving, then you are having a healthy response + // to the current state of the core React ecosystem. I am being serious. This is what passes for non-beta software. + const [ queryClient ] = useState(() => getQueryClient()); return ( From f0837be17c2e7b42439b18b6c7e6632c0a6dc94c Mon Sep 17 00:00:00 2001 From: ejmg Date: Wed, 10 Jul 2024 00:00:59 -0500 Subject: [PATCH 04/10] Reworked entire structure and handling of TransactionsTable + management of tanstack/query + paginated-data-table to work half-correctly with Suspense. --- src/app/transactions/page.tsx | 35 ++- src/components/Providers/index.tsx | 9 +- .../TransactionsTable/getTransactions.ts | 2 - src/components/TransactionsTable/index.tsx | 91 +++---- src/components/ui/paginated-data-table.tsx | 243 ++++++------------ 5 files changed, 154 insertions(+), 226 deletions(-) diff --git a/src/app/transactions/page.tsx b/src/app/transactions/page.tsx index cdac52b..155aa99 100644 --- a/src/app/transactions/page.tsx +++ b/src/app/transactions/page.tsx @@ -1,12 +1,41 @@ -import TransactionsTable from "@/components/TransactionsTable"; +import { TransactionsTable } from "@/components/TransactionsTable"; +import getTransactions from "@/components/TransactionsTable/getTransactions"; +import { HydrationBoundary, QueryClient, dehydrate } from "@tanstack/react-query"; +// TODO: do we want this anymore? what is the story of caching between the client and events. export const dynamic = "force-dynamic"; -const Page = async () => { +const Page = () => { + const queryClient = new QueryClient(); + + const defaultQueryOptions = { + pageIndex: 0, + pageSize: 10, + }; + + const endpoint = "/api/transactions"; + const queryName = "TransactionsTable"; + const errorMessage = "Failed to query data while trying to generate event table, please try reloading the page."; + + queryClient.prefetchQuery({ + queryFn: () => getTransactions({ endpoint, pageIndex: 0}), + queryKey: [queryName, defaultQueryOptions], + meta: { + errorMessage, + }, + }); + return (

Recent Transactions

- + + +
); }; diff --git a/src/components/Providers/index.tsx b/src/components/Providers/index.tsx index c19c221..53ee003 100644 --- a/src/components/Providers/index.tsx +++ b/src/components/Providers/index.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useState } from "react"; -import { QueryCache, QueryClient, QueryClientProvider, isServer } from "@tanstack/react-query"; +import { QueryCache, QueryClient, QueryClientProvider, defaultShouldDehydrateQuery, isServer } from "@tanstack/react-query"; import { createGrpcWebTransport } from "@connectrpc/connect-web"; import { TransportProvider } from "@connectrpc/connect-query"; import { Toaster } from "../ui/toaster"; @@ -26,6 +26,13 @@ const makeQueryClient = () => { queries: { // Direct suggestion by tanstack, to prevent over-eager refetching from the client. staleTime: 60 * 1000, + refetchOnWindowFocus: false, + }, + dehydrate: { + // only successful and pending Queries are included per defaults + shouldDehydrateQuery: (query) => + defaultShouldDehydrateQuery(query) || + query.state.status === "pending", }, }, queryCache: new QueryCache({ diff --git a/src/components/TransactionsTable/getTransactions.ts b/src/components/TransactionsTable/getTransactions.ts index 416593d..4023a28 100644 --- a/src/components/TransactionsTable/getTransactions.ts +++ b/src/components/TransactionsTable/getTransactions.ts @@ -1,5 +1,3 @@ -"use server"; - import { TransactionsTableData } from "@/lib/validators/table"; export default async function getTransactions({ endpoint, pageIndex } : { endpoint: string, pageIndex: number}) { diff --git a/src/components/TransactionsTable/index.tsx b/src/components/TransactionsTable/index.tsx index 7d70c33..2c57b2c 100644 --- a/src/components/TransactionsTable/index.tsx +++ b/src/components/TransactionsTable/index.tsx @@ -1,67 +1,60 @@ +"use client"; + import { columns } from "./columns"; -import { HydrationBoundary, QueryClient, dehydrate } from "@tanstack/react-query"; +import { useSuspenseQuery } from "@tanstack/react-query"; import { PaginatedDataTable } from "../ui/paginated-data-table"; import getTransactions from "./getTransactions"; +import { useState } from "react"; +import { PaginationState, getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { cn } from "@/lib/utils"; -// TODO: resolve these typings and that with zod and how to navigate between them. -// NOTE: is it possible to derive a tuple type that encodes the valid combinations of event attributes and their types? -export type TransactionType = "tx" | "action_spend" | "action_output" | "action_delegate" | "action_undelegate" | "action_position_open"| "action_postion_close" | "action_swap" | "action_swap_claim" | "action_position_withdraw" | "action_spend" ; - -export type TransactionKey = "amount" | "hash" | "height" | "note_commitment" | "nullifier" | "position_id" | "reserves_1" | "reserves_2" | "trading_fee" | "trading_p1" | "trading_p2" | "trading_pair" | "validator"; - -export interface Attribute { - key: TransactionKey, - value: string | number, +export interface QueryOptions { + pageIndex: number, + pageSize: number, } -export interface Event { - type: TransactionType, - attributes: Attribute[], +interface PaginatedDataTableProps { + className?: string, + queryName: string, + defaultQueryOptions: QueryOptions, + endpoint: string, + errorMessage: string, } -export interface TransactionResult { - height: number, - createdAt: Date, - chain_id: string, - hash: string, - // TODO: is string actually wanted here for the representation of this buffer - result: string, - events: Event[], -}; - -// TODO: Could try extracting out a minimal data table representation that can then be modified for different query types -// such as Blocks vs Block Events vs Transaction Results vs Transaction Events etc. -const TransactionsTable = async ({ className } : { className?: string }) => { - const queryClient = new QueryClient(); +export function TransactionsTable ({ + className, + queryName, + defaultQueryOptions, + endpoint, + errorMessage, +} : PaginatedDataTableProps) { - const defaultQueryOptions = { - pageIndex: 0, - pageSize: 10, - }; + const [pagination, setPagination] = useState({...defaultQueryOptions}); - const endpoint = "/api/transactions"; - const queryName = "TransactionsTable"; - const errorMessage = "Failed to query data while trying to generate event table, please try reloading the page."; - await queryClient.prefetchQuery({ - queryFn: async () => await getTransactions({ endpoint, pageIndex: 0}), - queryKey: [queryName, defaultQueryOptions], + const { data } = useSuspenseQuery({ + queryKey: [queryName, pagination], + queryFn: () => getTransactions({ endpoint, pageIndex: pagination.pageIndex }), meta: { errorMessage, }, }); + const { pages: pageCount, results: tableData } = data ?? { pages: 0, results: []}; + + const table = useReactTable({ + data: tableData, + columns, + pageCount, + state: { + pagination, + }, + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }); return ( - - - +
+ +
); }; - -export default TransactionsTable; diff --git a/src/components/ui/paginated-data-table.tsx b/src/components/ui/paginated-data-table.tsx index d329d0b..b4f8e7d 100644 --- a/src/components/ui/paginated-data-table.tsx +++ b/src/components/ui/paginated-data-table.tsx @@ -1,177 +1,78 @@ -"use client"; +import { ColumnDef, Table as TableT, flexRender } from "@tanstack/react-table"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./Table"; +import { Button } from "./button"; -import { - type ColumnDef, - flexRender, - getCoreRowModel, - useReactTable, - type PaginationState, -} from "@tanstack/react-table"; - -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/Table"; - -import { Button } from "@/components/ui/button"; -import { useMemo, useState } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { type z } from "zod"; - -export interface QueryOptions { - pageIndex: number, - pageSize: number, -} - -interface FetcherParams { - endpoint: string, - pageIndex: number -}; - -type Fetcher = ({ endpoint, pageIndex} : FetcherParams) => Promise>; - -interface PaginatedDataTableProps { - className?: string, +interface PaginatedDataTableProps { + table: TableT, columns: Array> - queryName: string, - defaultQueryOptions: QueryOptions, - endpoint: string, - fetcher: Fetcher, - errorMessage: string, } -export function PaginatedDataTable({ - className, - columns, - queryName, - defaultQueryOptions, - endpoint, - fetcher, - errorMessage, -}: PaginatedDataTableProps) { - - const [{ pageIndex, pageSize }, setPagination] = useState({...defaultQueryOptions}); - - const queryOptions = { - pageIndex, - pageSize, - }; - - const { data, isError, isPending, isSuccess } = useQuery({ - queryKey: [queryName, queryOptions], - queryFn: async () => await fetcher({ endpoint, pageIndex }), - meta: { - errorMessage, - }, - }); - - const { pages: pageCount, results: tableData } : z.infer = data ?? { pages: 0, results: []}; - - const pagination = useMemo( - () => ({ - pageIndex, - pageSize, - }), - [pageIndex, pageSize], - ); - - const table = useReactTable({ - data: tableData as TData[], - columns, - pageCount, - state: { - pagination, - }, - onPaginationChange: setPagination, - getCoreRowModel: getCoreRowModel(), - manualPagination: true, - }); - - if (isSuccess){ - return ( -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - - No results. +export function PaginatedDataTable ({ table, columns } : PaginatedDataTableProps ) { + + return ( +
+
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} - - )} - -
-
-
- - -
-
- ); - } - if (isPending) { - return ( -
-

Loading...

-
- ); - } - - if (isError) { - return ( -
-

No results found.

-
- ); - } + ))} + + )) + ) : ( + + + No results. + + + )} + + +
+
+ + +
+
+ ); } From 81198cc7577e131b16379fedd73a65e776e4dad9 Mon Sep 17 00:00:00 2001 From: ejmg Date: Wed, 10 Jul 2024 00:19:32 -0500 Subject: [PATCH 05/10] updated /blocks components to work with new suspense support --- src/app/blocks/page.tsx | 28 ++++++++- src/components/BlocksTable/getBlocks.ts | 2 - src/components/BlocksTable/index.tsx | 78 +++++++++++++++---------- 3 files changed, 74 insertions(+), 34 deletions(-) diff --git a/src/app/blocks/page.tsx b/src/app/blocks/page.tsx index 9c6d5b7..ca87b76 100644 --- a/src/app/blocks/page.tsx +++ b/src/app/blocks/page.tsx @@ -1,13 +1,39 @@ import BlocksTable from "@/components/BlocksTable"; +import getBlocks from "@/components/BlocksTable/getBlocks"; +import { HydrationBoundary, QueryClient, dehydrate } from "@tanstack/react-query"; export const dynamic = "force-dynamic"; const Page = () => { + const queryClient = new QueryClient(); + + const defaultQueryOptions = { + pageIndex: 0, + pageSize: 10, + }; + const queryName = "BlocksTable"; + const endpoint = "/api/blocks"; + const errorMessage = "Failed to query data while trying to generate blocks table, please try reloading the page."; + + queryClient.prefetchQuery({ + queryFn: () => getBlocks({ endpoint, pageIndex: 0}), + queryKey: [queryName, defaultQueryOptions], + meta: { + errorMessage, + }, + }); return (

Recent Blocks

- + + +
); }; diff --git a/src/components/BlocksTable/getBlocks.ts b/src/components/BlocksTable/getBlocks.ts index f0020f6..9fd6b08 100644 --- a/src/components/BlocksTable/getBlocks.ts +++ b/src/components/BlocksTable/getBlocks.ts @@ -1,5 +1,3 @@ -"use server"; - import { BlocksTableQuery } from "@/lib/validators/table"; export default async function getBlocks ({ endpoint, pageIndex } : ({ endpoint: string, pageIndex: number })) { diff --git a/src/components/BlocksTable/index.tsx b/src/components/BlocksTable/index.tsx index f871dc1..7f2d273 100644 --- a/src/components/BlocksTable/index.tsx +++ b/src/components/BlocksTable/index.tsx @@ -1,48 +1,64 @@ -"use server"; +"use client"; import { columns } from "./columns"; -import { HydrationBoundary, QueryClient, dehydrate } from "@tanstack/react-query"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { useState } from "react"; import { PaginatedDataTable } from "../ui/paginated-data-table"; +import { PaginationState, getCoreRowModel, useReactTable } from "@tanstack/react-table"; import getBlocks from "./getBlocks"; +import { cn } from "@/lib/utils"; -export interface BlocksTableRow { - height: number, - createdAt: Date, - // TODO: It would be nice to associate all events with a given row. Need to get testnet setup again and pull in example data for building this out. - // events: any[], -}; +export interface QueryOptions { + pageIndex: number, + pageSize: number, +} + +interface PaginatedDataTableProps { + className?: string, + queryName: string, + defaultQueryOptions: QueryOptions, + endpoint: string, + errorMessage: string, +} -const BlocksTable = async ({ className } : { className: string }) => { - const queryClient = new QueryClient(); - const defaultQueryOptions = { - pageIndex: 0, - pageSize: 10, - }; - const queryName = "BlocksTable"; - const endpoint = "/api/blocks"; - const errorMessage = "Failed to query data while trying to generate blocks table, please try reloading the page."; +export function BlocksTable ({ + className, + queryName, + defaultQueryOptions, + endpoint, + errorMessage, +} : PaginatedDataTableProps) { - await queryClient.prefetchQuery({ - queryFn: async () => await getBlocks({ endpoint, pageIndex: 0}), - queryKey: [queryName, defaultQueryOptions], + const [pagination, setPagination] = useState({...defaultQueryOptions}); + + const { data } = useSuspenseQuery({ + queryKey: [queryName, pagination], + queryFn: () => getBlocks({ endpoint, pageIndex: pagination.pageIndex }), meta: { errorMessage, }, }); + const { pages: pageCount, results: tableData } = data ?? { pages: 0, results: []}; + + const table = useReactTable({ + data: tableData, + columns, + pageCount, + state: { + pagination, + }, + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }); + return ( - - - +
+ +
); }; -export default BlocksTable; \ No newline at end of file +export default BlocksTable; From a12286cdafa2afe69fdaea2e040b6521008baf0f Mon Sep 17 00:00:00 2001 From: ejmg Date: Thu, 11 Jul 2024 14:34:27 -0500 Subject: [PATCH 06/10] small fixes to query key, default exports --- src/app/blocks/page.tsx | 5 +++-- src/app/transactions/page.tsx | 2 +- src/components/BlocksTable/index.tsx | 4 +--- src/components/Providers/index.tsx | 1 - src/components/TransactionsTable/index.tsx | 2 +- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/app/blocks/page.tsx b/src/app/blocks/page.tsx index ca87b76..c526f1c 100644 --- a/src/app/blocks/page.tsx +++ b/src/app/blocks/page.tsx @@ -1,4 +1,4 @@ -import BlocksTable from "@/components/BlocksTable"; +import { BlocksTable } from "@/components/BlocksTable"; import getBlocks from "@/components/BlocksTable/getBlocks"; import { HydrationBoundary, QueryClient, dehydrate } from "@tanstack/react-query"; @@ -11,13 +11,14 @@ const Page = () => { pageIndex: 0, pageSize: 10, }; + const queryName = "BlocksTable"; const endpoint = "/api/blocks"; const errorMessage = "Failed to query data while trying to generate blocks table, please try reloading the page."; queryClient.prefetchQuery({ queryFn: () => getBlocks({ endpoint, pageIndex: 0}), - queryKey: [queryName, defaultQueryOptions], + queryKey: [queryName, defaultQueryOptions.pageIndex], meta: { errorMessage, }, diff --git a/src/app/transactions/page.tsx b/src/app/transactions/page.tsx index 155aa99..24c5dc1 100644 --- a/src/app/transactions/page.tsx +++ b/src/app/transactions/page.tsx @@ -19,7 +19,7 @@ const Page = () => { queryClient.prefetchQuery({ queryFn: () => getTransactions({ endpoint, pageIndex: 0}), - queryKey: [queryName, defaultQueryOptions], + queryKey: [queryName, defaultQueryOptions.pageIndex], meta: { errorMessage, }, diff --git a/src/components/BlocksTable/index.tsx b/src/components/BlocksTable/index.tsx index 7f2d273..8c4ff7a 100644 --- a/src/components/BlocksTable/index.tsx +++ b/src/components/BlocksTable/index.tsx @@ -33,7 +33,7 @@ export function BlocksTable ({ const [pagination, setPagination] = useState({...defaultQueryOptions}); const { data } = useSuspenseQuery({ - queryKey: [queryName, pagination], + queryKey: [queryName, pagination.pageIndex], queryFn: () => getBlocks({ endpoint, pageIndex: pagination.pageIndex }), meta: { errorMessage, @@ -60,5 +60,3 @@ export function BlocksTable ({
); }; - -export default BlocksTable; diff --git a/src/components/Providers/index.tsx b/src/components/Providers/index.tsx index 53ee003..9f16541 100644 --- a/src/components/Providers/index.tsx +++ b/src/components/Providers/index.tsx @@ -26,7 +26,6 @@ const makeQueryClient = () => { queries: { // Direct suggestion by tanstack, to prevent over-eager refetching from the client. staleTime: 60 * 1000, - refetchOnWindowFocus: false, }, dehydrate: { // only successful and pending Queries are included per defaults diff --git a/src/components/TransactionsTable/index.tsx b/src/components/TransactionsTable/index.tsx index 2c57b2c..e7d4f88 100644 --- a/src/components/TransactionsTable/index.tsx +++ b/src/components/TransactionsTable/index.tsx @@ -32,7 +32,7 @@ export function TransactionsTable ({ const [pagination, setPagination] = useState({...defaultQueryOptions}); const { data } = useSuspenseQuery({ - queryKey: [queryName, pagination], + queryKey: [queryName, pagination.pageIndex], queryFn: () => getTransactions({ endpoint, pageIndex: pagination.pageIndex }), meta: { errorMessage, From a0e39fa2cd02ebb3ec5db998031cfe4f861e7ee9 Mon Sep 17 00:00:00 2001 From: ejmg Date: Thu, 11 Jul 2024 22:52:31 -0500 Subject: [PATCH 07/10] added dev tools for tanstack/query --- package-lock.json | 44 ++++++++++++++++++++++++----- package.json | 1 + src/app/layout.tsx | 2 ++ src/lib/utils.ts | 69 +++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 108 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index fae5ed4..570f99a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "@next/eslint-plugin-next": "^14.2.3", "@pgtyped/cli": "^2.3.0", "@stylistic/eslint-plugin": "^2.1.0", + "@tanstack/react-query-devtools": "^5.51.1", "@types/eslint": "^8.56.10", "@types/node": "^20", "@types/react": "^18", @@ -2223,9 +2224,20 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.49.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.49.1.tgz", - "integrity": "sha512-JnC9ndmD1KKS01Rt/ovRUB1tmwO7zkyXAyIxN9mznuJrcNtOrkmOnQqdJF2ib9oHzc2VxHomnEG7xyfo54Npkw==", + "version": "5.51.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.1.tgz", + "integrity": "sha512-fJBMQMpo8/KSsWW5ratJR5+IFr7YNJ3K2kfP9l5XObYHsgfVy1w3FJUWU4FT2fj7+JMaEg33zOcNDBo0LMwHnw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.51.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.51.1.tgz", + "integrity": "sha512-rehG0WmL3EXER6MAI2uHQia/n0b5c3ZROohpYm7u3G7yg4q+HsfQy6nuAo6uy40NzHUe3FmnfWCZQ0Vb/3lE6g==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -2233,12 +2245,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.49.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.49.2.tgz", - "integrity": "sha512-6rfwXDK9BvmHISbNFuGd+wY3P44lyW7lWiA9vIFGT/T0P9aHD1VkjTvcM4SDAIbAQ9ygEZZoLt7dlU1o3NjMVA==", + "version": "5.51.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.1.tgz", + "integrity": "sha512-s47HKFnQ4HOJAHoIiXcpna/roMMPZJPy6fJ6p4ZNVn8+/onlLBEDd1+xc8OnDuwgvecqkZD7Z2mnSRbcWefrKw==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.49.1" + "@tanstack/query-core": "5.51.1" }, "funding": { "type": "github", @@ -2248,6 +2260,24 @@ "react": "^18.0.0" } }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.51.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.51.1.tgz", + "integrity": "sha512-bRShIVKGpUOHpwziGKT8Aq1Ty0lIlGmNI7E0KbGYtmyOaImErpdElTdxfES1bRaI7i/j+mf2hLy+E6q7SrCwPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.51.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.51.1", + "react": "^18 || ^19" + } + }, "node_modules/@tanstack/react-table": { "version": "8.17.3", "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.17.3.tgz", diff --git a/package.json b/package.json index d6b4994..cb25969 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@next/eslint-plugin-next": "^14.2.3", "@pgtyped/cli": "^2.3.0", "@stylistic/eslint-plugin": "^2.1.0", + "@tanstack/react-query-devtools": "^5.51.1", "@types/eslint": "^8.56.10", "@types/node": "^20", "@types/react": "^18", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5dfd1d3..7a3b257 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,6 +5,7 @@ import { cn } from "@/lib/utils"; import "@/lib/patch-toJSON-BigInt"; import Navbar from "@/components/Navbar"; import Providers from "@/components/Providers"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { CodeIcon } from "lucide-react"; import Link from "next/link"; @@ -41,6 +42,7 @@ export default function RootLayout({ + diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 9f49245..6ea825a 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,73 @@ +import { toast } from "@/components/ui/use-toast"; +import { QueryCache, QueryClient, defaultShouldDehydrateQuery, isServer } from "@tanstack/react-query"; import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; - + export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +let browserQueryClient: QueryClient | undefined = undefined; + +const makeQueryClient = () => { + // const { toast } = useToast(); + return new QueryClient({ + defaultOptions: { + queries: { + // Direct suggestion by tanstack, to prevent over-eager refetching from the client. + staleTime: 60 * 1000, + }, + dehydrate: { + // only successful and pending Queries are included per defaults + shouldDehydrateQuery: (query) => + defaultShouldDehydrateQuery(query) || + query.state.status === "pending", + }, + }, + queryCache: new QueryCache({ + onError: (error, query) => { + // TODO: Overall model is fine but need to change how meta is used. + // Idea: Add a `errorTitle` field instead that can be used in place of "Error" below. This gives a top level, succinct explanation. + // `description` becomes whatever value we store inside our error value. This is what needs to be refactored to make all queries play nicely. + // This allows each error to clearly signal its nature while also giving detail where appropriate. The issue of that detail is delegated to the useQuery callsite + // and any component/route that throws errors. + // There may be a more elegant way of expressing this but the general typing of onError's `error` and `query` arguments requires some amount of refinement for safety. + // https://tanstack.com/query/latest/docs/react/reference/QueryCache + let errorMessage = ""; + const meta = query?.meta ?? undefined ; + if (meta) { + // Precondition for this type cast: meta is of type Record where any query with a meta object containing the property `errorMessage` has a value of type string. + errorMessage = meta?.errorMessage as string ?? ""; + } + if (errorMessage !== "") { + toast({ + variant: "destructive", + title: "Error", + description: `${errorMessage}`, + }); + } else { + // TODO: Realistically, this will not be a useful error and should be improved further. + toast({ + variant: "destructive", + title: "Error", + description: `${error.message}`, + }); + } + }, + }), + }); +}; + +export const getQueryClient = () => { + if (isServer) { + // Server: always make a new query client + return makeQueryClient(); + } else { + // Browser: make a new query client if we don't already have one + // This is very important, so we don't re-make a new client if React + // suspends during the initial render. This may not be needed if we + // have a suspense boundary BELOW the creation of the query client + if (!browserQueryClient) browserQueryClient = makeQueryClient(); + return browserQueryClient; + } +}; From 1f38b3113db1bc2317d491ab36fe764485667960 Mon Sep 17 00:00:00 2001 From: ejmg Date: Thu, 11 Jul 2024 22:54:31 -0500 Subject: [PATCH 08/10] FIX: turns out tanstack docs suck and break your code --- src/app/blocks/page.tsx | 5 +- src/app/transaction/[hash]/page.tsx | 61 ++++++----------- src/components/Providers/index.tsx | 69 +------------------- src/components/Transaction/getTransaction.ts | 14 ++++ src/components/Transaction/index.tsx | 23 ++++--- 5 files changed, 53 insertions(+), 119 deletions(-) create mode 100644 src/components/Transaction/getTransaction.ts diff --git a/src/app/blocks/page.tsx b/src/app/blocks/page.tsx index c526f1c..a4df102 100644 --- a/src/app/blocks/page.tsx +++ b/src/app/blocks/page.tsx @@ -1,11 +1,12 @@ import { BlocksTable } from "@/components/BlocksTable"; import getBlocks from "@/components/BlocksTable/getBlocks"; -import { HydrationBoundary, QueryClient, dehydrate } from "@tanstack/react-query"; +import { getQueryClient } from "@/lib/utils"; +import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; export const dynamic = "force-dynamic"; const Page = () => { - const queryClient = new QueryClient(); + const queryClient = getQueryClient(); const defaultQueryOptions = { pageIndex: 0, diff --git a/src/app/transaction/[hash]/page.tsx b/src/app/transaction/[hash]/page.tsx index fd9a9b6..0b09eb2 100644 --- a/src/app/transaction/[hash]/page.tsx +++ b/src/app/transaction/[hash]/page.tsx @@ -1,10 +1,9 @@ -"use client"; - +// "use client"; import { type FC } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { TransactionResult } from "@/lib/validators/search"; -import axios from "axios"; -import Transaction from "@/components/Transaction"; +import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; +import { Transaction } from "@/components/Transaction"; +import getTransaction from "@/components/Transaction/getTransaction"; +import { getQueryClient } from "@/lib/utils"; interface PageProps { params: { @@ -12,51 +11,31 @@ interface PageProps { } } -// TODO: this entire page could probably be rendered statically on the server via layout.ts & some minor optimization via tanstack query context. const Page : FC = ({ params }) => { const { hash } = params; - const { data: txData , isError } = useQuery({ - queryFn: async () => { - console.log(`Fetching: GET /api/transaction?q=${hash}`); - const { data } = await axios.get(`/api/transaction?q=${hash}`); - console.log("Fetched result:", data); - const result = TransactionResult.safeParse(data); + const queryClient = getQueryClient(); - if (result.success) { - return result.data; - } else { - throw new Error(result.error.message); - } - }, - queryKey: ["txQuery", hash], - retry: false, + const endpoint = "/api/transaction/"; + const queryName = "txQuery"; + const errorMessage = "Failed to query transaction, please try reloading the page."; + + queryClient.prefetchQuery({ + queryKey: [queryName, hash], + queryFn: () => getTransaction({ endpoint, hash }), meta: { - errorMessage: "Failed to find transaction event with provided hash. Please check hash or try a different query.", + errorMessage, }, }); - if (isError) { - return ( -
-

No results found.

-
- ); - } - - // TODO: Replace with data table component views once those are fleshed out. - // TODO: add Suspense return ( -
- {txData ? ( -
-

Transaction Event Summary

-
- -
+
+

Transaction Summary

+ +
+
- ) :

No results

- } +
); }; diff --git a/src/components/Providers/index.tsx b/src/components/Providers/index.tsx index 9f16541..7fed8f6 100644 --- a/src/components/Providers/index.tsx +++ b/src/components/Providers/index.tsx @@ -1,13 +1,13 @@ "use client"; import React, { useState } from "react"; -import { QueryCache, QueryClient, QueryClientProvider, defaultShouldDehydrateQuery, isServer } from "@tanstack/react-query"; +import { QueryClientProvider } from "@tanstack/react-query"; import { createGrpcWebTransport } from "@connectrpc/connect-web"; import { TransportProvider } from "@connectrpc/connect-query"; import { Toaster } from "../ui/toaster"; -import { toast } from "../ui/use-toast"; import { ThemeProvider as NextThemesProvider } from "next-themes"; import { type ThemeProviderProps } from "next-themes/dist/types"; +import { getQueryClient } from "@/lib/utils"; export function ThemeProvider({ children, ...props }: ThemeProviderProps) { return {children}; @@ -17,71 +17,6 @@ const penumbraTransport = createGrpcWebTransport({ baseUrl: "https://grpc.testnet.penumbra.zone", }); -let browserQueryClient: QueryClient | undefined = undefined; - -const makeQueryClient = () => { - // const { toast } = useToast(); - return new QueryClient({ - defaultOptions: { - queries: { - // Direct suggestion by tanstack, to prevent over-eager refetching from the client. - staleTime: 60 * 1000, - }, - dehydrate: { - // only successful and pending Queries are included per defaults - shouldDehydrateQuery: (query) => - defaultShouldDehydrateQuery(query) || - query.state.status === "pending", - }, - }, - queryCache: new QueryCache({ - onError: (error, query) => { - // TODO: Overall model is fine but need to change how meta is used. - // Idea: Add a `errorTitle` field instead that can be used in place of "Error" below. This gives a top level, succinct explanation. - // `description` becomes whatever value we store inside our error value. This is what needs to be refactored to make all queries play nicely. - // This allows each error to clearly signal its nature while also giving detail where appropriate. The issue of that detail is delegated to the useQuery callsite - // and any component/route that throws errors. - // There may be a more elegant way of expressing this but the general typing of onError's `error` and `query` arguments requires some amount of refinement for safety. - // https://tanstack.com/query/latest/docs/react/reference/QueryCache - let errorMessage = ""; - const meta = query?.meta ?? undefined ; - if (meta) { - // Precondition for this type cast: meta is of type Record where any query with a meta object containing the property `errorMessage` has a value of type string. - errorMessage = meta?.errorMessage as string ?? ""; - } - if (errorMessage !== "") { - toast({ - variant: "destructive", - title: "Error", - description: `${errorMessage}`, - }); - } else { - // TODO: Realistically, this will not be a useful error and should be improved further. - toast({ - variant: "destructive", - title: "Error", - description: `${error.message}`, - }); - } - }, - }), - }); -}; - -const getQueryClient = () => { - if (isServer) { - // Server: always make a new query client - return makeQueryClient(); - } else { - // Browser: make a new query client if we don't already have one - // This is very important, so we don't re-make a new client if React - // suspends during the initial render. This may not be needed if we - // have a suspense boundary BELOW the creation of the query client - if (!browserQueryClient) browserQueryClient = makeQueryClient(); - return browserQueryClient; - } -}; - const Providers = ({ children } : { children: React.ReactNode }) => { // NOTE: there is a very explicit warning in the TanStack docs about using useState for handling QueryClient (de)hydration within the provider in the scenario where diff --git a/src/components/Transaction/getTransaction.ts b/src/components/Transaction/getTransaction.ts new file mode 100644 index 0000000..a02b47b --- /dev/null +++ b/src/components/Transaction/getTransaction.ts @@ -0,0 +1,14 @@ +import { TransactionData } from "@/lib/validators/search"; + +export default async function getTransaction({ endpoint, hash } : { endpoint: string, hash: string}) { + console.log(`Fetching: POST ${endpoint}?q=${hash}`); + const res = await fetch(`http://localhost:3000${endpoint}?q=${hash}`, { method: "GET" }); + const json = await res.json(); + console.log("Fetched Result:", json); + const result = TransactionData.safeParse(json); + if (result.success) { + return result.data; + } else { + throw new Error(result.error.message); + } +} diff --git a/src/components/Transaction/index.tsx b/src/components/Transaction/index.tsx index d28069c..97ea8ed 100644 --- a/src/components/Transaction/index.tsx +++ b/src/components/Transaction/index.tsx @@ -1,19 +1,26 @@ -import { type TransactionResultPayload } from "@/lib/validators/search"; +"use client"; import { JsonView, allExpanded, defaultStyles } from "react-json-view-lite"; import "react-json-view-lite/dist/index.css"; import Link from "next/link"; import { type FC } from "react"; import { TransactionView } from "../TransactionView"; -import { ibcRegistry } from "@/lib/protobuf"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import getTransaction from "./getTransaction"; interface TransactionProps { - txData: TransactionResultPayload + // txData: TransactionDataPayload + endpoint: string, + queryName: string, + hash: string, } -const Transaction : FC = ({ txData }) => { - const [txResult, penumbraTx] = txData; - // const abciEvents = txResult.events; +export const Transaction : FC = ({ endpoint, queryName, hash }) => { + const { data } = useSuspenseQuery({ + queryKey: [queryName, hash], + queryFn: () => getTransaction({ endpoint, hash }), + }); + const [ txResult, penumbraTx ] = data; return (
@@ -34,7 +41,7 @@ const Transaction : FC = ({ txData }) => {

Transaction Data

-            
+            
           
@@ -42,5 +49,3 @@ const Transaction : FC = ({ txData }) => {
); }; - -export default Transaction; From 9d3e2fca92e7a3d1a55fd0d571e2f5526d780bc6 Mon Sep 17 00:00:00 2001 From: ejmg Date: Thu, 11 Jul 2024 22:55:36 -0500 Subject: [PATCH 09/10] transaction/[hash] now use suspense, updated validators to work with server component rework --- src/app/transactions/page.tsx | 5 +++-- src/components/TransactionView/index.tsx | 7 ++++--- src/lib/validators/json.ts | 2 ++ src/lib/validators/search.ts | 9 ++++----- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/app/transactions/page.tsx b/src/app/transactions/page.tsx index 24c5dc1..09ea425 100644 --- a/src/app/transactions/page.tsx +++ b/src/app/transactions/page.tsx @@ -1,12 +1,13 @@ import { TransactionsTable } from "@/components/TransactionsTable"; import getTransactions from "@/components/TransactionsTable/getTransactions"; -import { HydrationBoundary, QueryClient, dehydrate } from "@tanstack/react-query"; +import { getQueryClient } from "@/lib/utils"; +import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; // TODO: do we want this anymore? what is the story of caching between the client and events. export const dynamic = "force-dynamic"; const Page = () => { - const queryClient = new QueryClient(); + const queryClient = getQueryClient(); const defaultQueryOptions = { pageIndex: 0, diff --git a/src/components/TransactionView/index.tsx b/src/components/TransactionView/index.tsx index 02b8a42..6aedc76 100644 --- a/src/components/TransactionView/index.tsx +++ b/src/components/TransactionView/index.tsx @@ -1,9 +1,10 @@ import { type FC } from "react"; -import { TransactionView as TransactionViewSchema, type Transaction, TransactionBodyView as TransactionBodyViewSchema, MemoView } from "@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1/transaction_pb"; +import { TransactionView as TransactionViewSchema, Transaction, TransactionBodyView as TransactionBodyViewSchema, MemoView } from "@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1/transaction_pb"; import { makeActionView } from "@/lib/protobuf"; import { TransactionBodyView } from "./TransactionBodyView"; import { FlexRow } from "../ui/flex"; import { ActionRow } from "../ActionView"; +import { ZodJson } from "@/lib/validators/json"; const BindingSig = ActionRow; const MerkleRoot = ActionRow; @@ -34,11 +35,11 @@ const makeTransactionView = ({ body, ...tx }: Transaction) : TransactionViewSche }; interface TransactionViewProps { - tx: Transaction + tx: ZodJson } export const TransactionView : FC = ({ tx }) => { - const txView = makeTransactionView(tx); + const txView = makeTransactionView(Transaction.fromJson(tx)); return (

Transaction View

diff --git a/src/lib/validators/json.ts b/src/lib/validators/json.ts index ad7a9e5..5df4dc7 100644 --- a/src/lib/validators/json.ts +++ b/src/lib/validators/json.ts @@ -9,3 +9,5 @@ type Json = Literal | { [key: string]: Json } | Json[]; export const jsonSchema: z.ZodType = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]), ); + +export type ZodJson = z.infer; diff --git a/src/lib/validators/search.ts b/src/lib/validators/search.ts index aecade8..8a55db4 100644 --- a/src/lib/validators/search.ts +++ b/src/lib/validators/search.ts @@ -1,7 +1,5 @@ -import { Transaction } from "@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1/transaction_pb"; import { z } from "zod"; import { jsonSchema } from "./json"; -import { ibcRegistry } from "../protobuf"; // This validator is to check whether a sha256 hash conforms to what is expected by the `tx_hash` column // of the `tx_result` table defined in cometbft's psql indexer schema. @@ -130,14 +128,15 @@ const EventAttribute = z.array( ); // zod schema equivalent to the /parsed/ JSON data returned by GET /api/transaction?q= -export const TransactionResult = z.tuple([ +export const TransactionData = z.tuple([ z.object({ tx_hash: z.string(), height: z.coerce.bigint(), created_at: z.string().datetime(), events: jsonSchema.pipe(EventAttribute), }), - jsonSchema.transform((json) => Transaction.fromJson(json, { typeRegistry: ibcRegistry })), + jsonSchema, + // jsonSchema.transform((json) => Transaction.fromJson(json, { typeRegistry: ibcRegistry })), ]); // Schema for JSON data by GET /api/block?q= @@ -198,5 +197,5 @@ export const BlockData = z.array( return { tx_hashes, created_at, events }; }); -export type TransactionResultPayload = z.infer; +export type TransactionDataPayload = z.infer; export type BlockDataPayload = z.infer; From 31b52729043a232b33147695bce0bf29ac2cf421 Mon Sep 17 00:00:00 2001 From: ejmg Date: Fri, 12 Jul 2024 15:03:53 -0500 Subject: [PATCH 10/10] /block/[ht]/page.tsx now correctly uses prefetching + suspense. removed more default exports. --- src/app/block/[ht]/page.tsx | 66 ++++++-------------- src/app/transaction/[hash]/page.tsx | 3 +- src/components/Block/getBlock.ts | 14 +++++ src/components/Block/index.tsx | 27 +++++--- src/components/Transaction/getTransaction.ts | 4 +- src/components/Transaction/index.tsx | 3 +- 6 files changed, 56 insertions(+), 61 deletions(-) create mode 100644 src/components/Block/getBlock.ts diff --git a/src/app/block/[ht]/page.tsx b/src/app/block/[ht]/page.tsx index 3d49663..2ea41c1 100644 --- a/src/app/block/[ht]/page.tsx +++ b/src/app/block/[ht]/page.tsx @@ -1,10 +1,7 @@ -"use client"; - import { type FC } from "react"; -import { useQuery } from "@tanstack/react-query"; -import axios from "axios"; -import { BlockData } from "@/lib/validators/search"; -import Block from "@/components/Block"; +import { Block } from "@/components/Block"; +import { getBlock } from "@/components/Block/getBlock"; +import { getQueryClient } from "@/lib/utils"; interface PageProps { params: { @@ -15,53 +12,30 @@ interface PageProps { const Page : FC = ({ params }) => { const { ht } = params; - const { data: blockData , isError, isPending, isSuccess } = useQuery({ - queryFn: async () => { - console.log(`Fetching: GET /api/block?q=${ht}`); - const { data } = await axios.get(`/api/block?q=${ht}`); - console.log("Fetching result:", data); - const result = BlockData.safeParse(data); - if (result.success) { - return result.data; - } else { - throw new Error(result.error.message); - } - }, + const queryClient = getQueryClient(); + + const endpoint = "/api/block/"; + const queryName = "htQuery"; + const errorMessage = "Failed to query block with provided height, please check height or try a different query"; + + queryClient.prefetchQuery({ + queryFn: () => getBlock({ endpoint, ht }), queryKey: ["htQuery", ht], - retry: false, meta: { - errorMessage: "Failed to find block event with provided height. Please check height or try a different query.", + errorMessage, }, }); - if (isPending) { - return ( -
-

Loading...

-
- ); - } - - if (isError) { - return ( -
-

No results found.

-
- ); - } - - if (isSuccess) { - return ( -
-
-

Block Summary

-
- -
+ return ( +
+
+

Block Summary

+
+
- ); - } +
+ ); }; export default Page; diff --git a/src/app/transaction/[hash]/page.tsx b/src/app/transaction/[hash]/page.tsx index 0b09eb2..b4912d6 100644 --- a/src/app/transaction/[hash]/page.tsx +++ b/src/app/transaction/[hash]/page.tsx @@ -1,8 +1,7 @@ -// "use client"; import { type FC } from "react"; import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; import { Transaction } from "@/components/Transaction"; -import getTransaction from "@/components/Transaction/getTransaction"; +import { getTransaction } from "@/components/Transaction/getTransaction"; import { getQueryClient } from "@/lib/utils"; interface PageProps { diff --git a/src/components/Block/getBlock.ts b/src/components/Block/getBlock.ts new file mode 100644 index 0000000..4b7822d --- /dev/null +++ b/src/components/Block/getBlock.ts @@ -0,0 +1,14 @@ +import { BlockData } from "@/lib/validators/search"; + +export async function getBlock({ endpoint, ht } : { endpoint: string, ht: string}) { + console.log(`Fetching: GET ${endpoint}?q=${ht}`); + const res = await fetch(`http://localhost:3000${endpoint}?q=${ht}`, { method: "GET" }); + const json = await res.json(); + console.log("Fetched Result:", json); + const result = BlockData.safeParse(json); + if (result.success) { + return result.data; + } else { + throw new Error(result.error.message); + } +} diff --git a/src/components/Block/index.tsx b/src/components/Block/index.tsx index cbc65c4..274b6b5 100644 --- a/src/components/Block/index.tsx +++ b/src/components/Block/index.tsx @@ -1,21 +1,32 @@ -import { type BlockDataPayload } from "@/lib/validators/search"; +"use client"; + import Link from "next/link"; import { type FC } from "react"; import ABCIEventsTable from "../ABCIEventsTable"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { getBlock } from "./getBlock"; - -interface BlockProps extends BlockDataPayload { - height: string +interface BlockProps { + endpoint: string, + queryName: string, + ht: string, } -// TODO: Similar to TransactionEvent, it looks increasingly likely that tanstack/table will actually work here so pulling out different DataTable representations will need to happen. -const Block : FC = ({ height, created_at, tx_hashes, events }) => { +export const Block : FC = ({ endpoint, queryName, ht }) => { + + const { data } = useSuspenseQuery({ + queryKey: [queryName, ht], + queryFn: () => getBlock({ endpoint, ht }), + }); + + const { created_at, tx_hashes, events } = data; + return (

Block Height

-
{height}
+
{ht}

Timestamp

@@ -40,5 +51,3 @@ const Block : FC = ({ height, created_at, tx_hashes, events }) => {
); }; - -export default Block; diff --git a/src/components/Transaction/getTransaction.ts b/src/components/Transaction/getTransaction.ts index a02b47b..4297a08 100644 --- a/src/components/Transaction/getTransaction.ts +++ b/src/components/Transaction/getTransaction.ts @@ -1,7 +1,7 @@ import { TransactionData } from "@/lib/validators/search"; -export default async function getTransaction({ endpoint, hash } : { endpoint: string, hash: string}) { - console.log(`Fetching: POST ${endpoint}?q=${hash}`); +export async function getTransaction({ endpoint, hash } : { endpoint: string, hash: string}) { + console.log(`Fetching: GET ${endpoint}?q=${hash}`); const res = await fetch(`http://localhost:3000${endpoint}?q=${hash}`, { method: "GET" }); const json = await res.json(); console.log("Fetched Result:", json); diff --git a/src/components/Transaction/index.tsx b/src/components/Transaction/index.tsx index 97ea8ed..bc6bb33 100644 --- a/src/components/Transaction/index.tsx +++ b/src/components/Transaction/index.tsx @@ -5,10 +5,9 @@ import Link from "next/link"; import { type FC } from "react"; import { TransactionView } from "../TransactionView"; import { useSuspenseQuery } from "@tanstack/react-query"; -import getTransaction from "./getTransaction"; +import { getTransaction } from "./getTransaction"; interface TransactionProps { - // txData: TransactionDataPayload endpoint: string, queryName: string, hash: string,