From c3e20af8d04619edc01beebb4524fbeea1597955 Mon Sep 17 00:00:00 2001 From: ejmg Date: Sat, 13 Jul 2024 22:00:55 -0500 Subject: [PATCH 1/5] small cleaning up of props and default imports --- src/app/transactions/page.tsx | 2 +- src/components/BlocksTable/index.tsx | 4 ++-- src/components/TransactionsTable/getTransactions.ts | 2 +- src/components/TransactionsTable/index.tsx | 13 +++++++++---- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/app/transactions/page.tsx b/src/app/transactions/page.tsx index 09ea425..c4115e5 100644 --- a/src/app/transactions/page.tsx +++ b/src/app/transactions/page.tsx @@ -1,5 +1,5 @@ import { TransactionsTable } from "@/components/TransactionsTable"; -import getTransactions from "@/components/TransactionsTable/getTransactions"; +import { getTransactions } from "@/components/TransactionsTable/getTransactions"; import { getQueryClient } from "@/lib/utils"; import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; diff --git a/src/components/BlocksTable/index.tsx b/src/components/BlocksTable/index.tsx index 8c4ff7a..dab7d49 100644 --- a/src/components/BlocksTable/index.tsx +++ b/src/components/BlocksTable/index.tsx @@ -13,7 +13,7 @@ export interface QueryOptions { pageSize: number, } -interface PaginatedDataTableProps { +interface BlocksTableProps { className?: string, queryName: string, defaultQueryOptions: QueryOptions, @@ -28,7 +28,7 @@ export function BlocksTable ({ defaultQueryOptions, endpoint, errorMessage, -} : PaginatedDataTableProps) { +} : BlocksTableProps) { const [pagination, setPagination] = useState({...defaultQueryOptions}); diff --git a/src/components/TransactionsTable/getTransactions.ts b/src/components/TransactionsTable/getTransactions.ts index 4023a28..daf6b1b 100644 --- a/src/components/TransactionsTable/getTransactions.ts +++ b/src/components/TransactionsTable/getTransactions.ts @@ -1,6 +1,6 @@ import { TransactionsTableData } from "@/lib/validators/table"; -export default async function getTransactions({ endpoint, pageIndex } : { endpoint: string, pageIndex: number}) { +export async function getTransactions({ endpoint, pageIndex } : { endpoint: string, pageIndex: number}) { console.log(`Fetching: POST ${endpoint}?page=${pageIndex}`); const res = await fetch(`http://localhost:3000${endpoint}?page=${pageIndex}`, { method: "POST" }); const json = await res.json(); diff --git a/src/components/TransactionsTable/index.tsx b/src/components/TransactionsTable/index.tsx index e7d4f88..2c390cb 100644 --- a/src/components/TransactionsTable/index.tsx +++ b/src/components/TransactionsTable/index.tsx @@ -3,7 +3,7 @@ import { columns } from "./columns"; import { useSuspenseQuery } from "@tanstack/react-query"; import { PaginatedDataTable } from "../ui/paginated-data-table"; -import getTransactions from "./getTransactions"; +import { getTransactions } from "./getTransactions"; import { useState } from "react"; import { PaginationState, getCoreRowModel, useReactTable } from "@tanstack/react-table"; import { cn } from "@/lib/utils"; @@ -13,21 +13,26 @@ export interface QueryOptions { pageSize: number, } -interface PaginatedDataTableProps { +interface TransactionsTableProps { className?: string, queryName: string, defaultQueryOptions: QueryOptions, endpoint: string, errorMessage: string, } - +// NOTE: There isn't a good place to put this so I'll write it here. The reason why cuiloa has all these wrapper components around +// PaginatedDataTable instead of passing the querying and pagination logic into PaginatedDataTable is because of the limitations +// of how NextJS handles hydration + logic of React.Suspense + how tanstack/query implements Suspense for server hydration. +// TL;DR, without having explicit code splitting that handles isServer/isBrowser instantiations, NextJS completely breaks. +// A more pragmatic compromise would be to write a generic getter instead of trying to pass a query function. +// This would eliminate the need for intermediate *Table components and flatten the component tree, too. export function TransactionsTable ({ className, queryName, defaultQueryOptions, endpoint, errorMessage, -} : PaginatedDataTableProps) { +} : TransactionsTableProps) { const [pagination, setPagination] = useState({...defaultQueryOptions}); From 080c3a5cad75054de68719cd141f94ae14e21b1b Mon Sep 17 00:00:00 2001 From: ejmg Date: Sat, 13 Jul 2024 22:03:09 -0500 Subject: [PATCH 2/5] ibc/channels and related components updated to use suspense + prefetching + paginated tables. --- src/app/api/ibc/channels/route.ts | 107 ++++++++++-------- src/app/api/ibc/channels/route.types.ts | 19 +++- src/app/ibc/channels/page.tsx | 55 ++++----- src/components/ibc/channels/ChannelsTable.tsx | 74 +++++++++--- src/components/ibc/channels/getIbcChannels.ts | 14 +++ 5 files changed, 181 insertions(+), 88 deletions(-) create mode 100644 src/components/ibc/channels/getIbcChannels.ts diff --git a/src/app/api/ibc/channels/route.ts b/src/app/api/ibc/channels/route.ts index ee49c45..329b827 100644 --- a/src/app/api/ibc/channels/route.ts +++ b/src/app/api/ibc/channels/route.ts @@ -1,66 +1,83 @@ import { getPgClient } from "@/lib/db"; import { sql } from "@pgtyped/runtime"; import { IGetIbcChannelsQuery } from "./route.types"; +import { NextRequest } from "next/server"; -export async function GET() { +export async function GET(req: NextRequest) { console.log("SUCCESS: GET /api/ibc/channels"); try { + const pageParam = req.nextUrl.searchParams.get("page")?.trim() ?? ""; + const pageOffset = (parseInt(pageParam, 10)) * 10; + const pageLimit = 10; + console.log("Acquiring PgClient and querying for IBC channels"); const client = await getPgClient(); - // TODO: A lot to improve on here. Create stable views instead of CTE's per client request. Laterals not a bad idea, either. - // NOTE: The only suq-query that I'm not 100% on how to improve later is for getting the most up-to-date consensusHeight for a counterparty - // in type_consensus_by_client. + // TODO: Revisit with latteral joins like other IBC queries + // WARNING + // TODO: This code *MUST* be revisited once support for IBC chanClose* events is clarified. Only channels are ever possibly deleted but does Penumbra support this?. const getIbcChannels = sql` - WITH channel_connection (channel_id, connection_id) AS ( - SELECT ea.value as channel_id, _ea.value as connection_id + WITH channel_connection (channel_id, connection_id) AS ( + SELECT ea.value as channel_id, _ea.value as connection_id + FROM event_attributes ea + INNER JOIN event_attributes _ea + ON _ea.block_id=ea.block_id + AND _ea.composite_key='channel_open_init.connection_id' + WHERE + ea.composite_key='channel_open_init.channel_id' + ), connections_counterparty_by_client (client_id, connection_id, counterparty_client_id) AS ( + SELECT ea.value as client_id, c_ea.value as connection_id, p_ea.value as counterparty_client_id + FROM event_attributes ea + INNER JOIN event_attributes c_ea + ON ea.block_id=c_ea.block_id + AND c_ea.composite_key='connection_open_init.connection_id' + INNER JOIN event_attributes p_ea + ON p_ea.block_id=c_ea.block_id + AND p_ea.composite_key='connection_open_init.counterparty_client_id' + WHERE + ea.composite_key='connection_open_init.client_id' + ), type_consensus_by_client (client_id, client_type, consensus_height) AS ( + SELECT DISTINCT ON (updates.client_id, updates.client_type) + updates.client_id, updates.client_type, updates.consensus_height + FROM ( + SELECT ea.block_id, ea.value as client_id, t_ea.value as client_type, h_ea.value as consensus_height + FROM event_attributes ea + INNER JOIN event_attributes h_ea + ON h_ea.block_id=ea.block_id + AND h_ea.composite_key='update_client.consensus_height' + INNER JOIN event_attributes t_ea + ON t_ea.block_id=ea.block_id + AND t_ea.composite_key='update_client.client_type' + WHERE + ea.composite_key='update_client.client_id' + ORDER BY ea.block_id DESC + ) updates + ) + SELECT cc.channel_id, tcc.client_id, ccc.connection_id, tcc.client_type, ccc.counterparty_client_id, tcc.consensus_height + FROM channel_connection cc + LEFT JOIN connections_counterparty_by_client ccc ON cc.connection_id=ccc.connection_id + LEFT JOIN type_consensus_by_client tcc ON tcc.client_id=ccc.client_id + LIMIT $pageLimit OFFSET $pageOffset! + ;`; + + const getChannelsCount = sql` + SELECT COUNT(*)::int as "count!" FROM event_attributes ea - INNER JOIN event_attributes _ea - ON _ea.block_id=ea.block_id - AND _ea.composite_key='channel_open_init.connection_id' WHERE ea.composite_key='channel_open_init.channel_id' - ), connections_counterparty_by_client (client_id, connection_id, counterparty_client_id) AS ( - SELECT ea.value as client_id, c_ea.value as connection_id, p_ea.value as counterparty_client_id - FROM event_attributes ea - INNER JOIN event_attributes c_ea - ON ea.block_id=c_ea.block_id - AND c_ea.composite_key='connection_open_init.connection_id' - INNER JOIN event_attributes p_ea - ON p_ea.block_id=c_ea.block_id - AND p_ea.composite_key='connection_open_init.counterparty_client_id' - WHERE - ea.composite_key='connection_open_init.client_id' - ), type_consensus_by_client (client_id, client_type, consensus_height) AS ( - SELECT DISTINCT ON (updates.client_id, updates.client_type) - updates.client_id, updates.client_type, updates.consensus_height - FROM ( - SELECT ea.block_id, ea.value as client_id, t_ea.value as client_type, h_ea.value as consensus_height - FROM event_attributes ea - INNER JOIN event_attributes h_ea - ON h_ea.block_id=ea.block_id - AND h_ea.composite_key='update_client.consensus_height' - INNER JOIN event_attributes t_ea - ON t_ea.block_id=ea.block_id - AND t_ea.composite_key='update_client.client_type' - WHERE - ea.composite_key='update_client.client_id' - ORDER BY ea.block_id DESC - ) updates - ) - SELECT cc.channel_id, tcc.client_id, ccc.connection_id, tcc.client_type, ccc.counterparty_client_id, tcc.consensus_height - FROM channel_connection cc - LEFT JOIN connections_counterparty_by_client ccc ON cc.connection_id=ccc.connection_id - LEFT JOIN type_consensus_by_client tcc ON tcc.client_id=ccc.client_id; - `; + ;`; + + const channels = await getIbcChannels.run({ pageLimit, pageOffset }, client); + const [ { count },,] = await getChannelsCount.run(undefined, client); - const channels = await getIbcChannels.run(undefined, client); client.release(); - console.log("Successfully queried channels:", channels); + console.log("Successfully queried channels:", [channels, count]); + + const pages = Math.floor((count / 10) + 1); - return new Response(JSON.stringify(channels)); + return new Response(JSON.stringify({ results: channels, pages })); } catch (error) { console.error("GET request failed.", error); diff --git a/src/app/api/ibc/channels/route.types.ts b/src/app/api/ibc/channels/route.types.ts index 1f75e12..4099f1a 100644 --- a/src/app/api/ibc/channels/route.types.ts +++ b/src/app/api/ibc/channels/route.types.ts @@ -1,7 +1,10 @@ /** Types generated for queries found in "src/app/api/ibc/channels/route.ts" */ /** 'GetIbcChannels' parameters type */ -export type IGetIbcChannelsParams = void; +export interface IGetIbcChannelsParams { + pageLimit?: bigint | number | null | void; + pageOffset: bigint | number; +} /** 'GetIbcChannels' return type */ export interface IGetIbcChannelsResult { @@ -19,3 +22,17 @@ export interface IGetIbcChannelsQuery { result: IGetIbcChannelsResult; } +/** 'GetChannelsCount' parameters type */ +export type IGetChannelsCountParams = void; + +/** 'GetChannelsCount' return type */ +export interface IGetChannelsCountResult { + count: number; +} + +/** 'GetChannelsCount' query type */ +export interface IGetChannelsCountQuery { + params: IGetChannelsCountParams; + result: IGetChannelsCountResult; +} + diff --git a/src/app/ibc/channels/page.tsx b/src/app/ibc/channels/page.tsx index e767bd2..4f37877 100644 --- a/src/app/ibc/channels/page.tsx +++ b/src/app/ibc/channels/page.tsx @@ -1,40 +1,41 @@ -"use client"; +import { ChannelsTable } from "@/components/ibc/channels/ChannelsTable"; +import { getIbcChannels } from "@/components/ibc/channels/getIbcChannels"; +import { getQueryClient } from "@/lib/utils"; +import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; + -import ChannelsTable from "@/components/ibc/channels/ChannelsTable"; -import { useQuery } from "@tanstack/react-query"; -import axios from "axios"; const Page = () => { - const { data , isError } = useQuery({ - queryFn: async () => { - console.log("Fetching: GET /api/ibc/channels"); - const { data } = await axios.get("/api/ibc/channels"); - console.log("Fetched result:", data); - // TODO: enforce validation - return data; - }, - queryKey: ["IbcChannels"], - retry: false, + const queryClient = getQueryClient(); + + const defaultQueryOptions = { + pageIndex: 0, + pageSize: 0, + }; + + const endpoint = "/api/ibc/channels"; + const queryName = "IbcChannels"; + const errorMessage = "Failed to query for IBC Channels. Please try again."; + + queryClient.prefetchQuery({ + queryKey: [queryName, defaultQueryOptions.pageIndex], + queryFn: () => getIbcChannels({ endpoint, pageIndex: defaultQueryOptions.pageIndex }), meta: { - errorMessage: "Failed to query for IBC Channels. Please try again.", + errorMessage, }, }); - if (isError) { - return ( -
-

No results found.

-
- ); - } - - // TODO: clean this up. return (

IBC Channels

- {// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - data ? :

No results

- } + + +
); }; diff --git a/src/components/ibc/channels/ChannelsTable.tsx b/src/components/ibc/channels/ChannelsTable.tsx index cf82ffc..541bcef 100644 --- a/src/components/ibc/channels/ChannelsTable.tsx +++ b/src/components/ibc/channels/ChannelsTable.tsx @@ -1,23 +1,67 @@ +"use client"; + import { columns } from "./columns"; -import { DataTable } from "../../ui/data-table"; -import { type FC } from "react"; +import { useState, type FC } from "react"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { PaginationState, getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { getIbcChannels } from "./getIbcChannels"; +import { cn } from "@/lib/utils"; +import { PaginatedDataTable } from "@/components/ui/paginated-data-table"; + +export interface QueryOptions { + pageIndex: number, + pageSize: number, +} -interface Props { +interface ChannelsTableProps { className?: string, - data: Array<{ - channel_id: string, - client_id: string, - connection_id: string, - client_type: string, - counterparty_client_id: string, - consensus_height: bigint - }>, + queryName: string, + defaultQueryOptions: QueryOptions, + endpoint: string, + errorMessage: string, } -const ChannelsTable : FC = ({ className, data }) => { +export const ChannelsTable : FC = ({className, queryName, defaultQueryOptions, endpoint, errorMessage }) => { + + const [pagination, setPagination] = useState({...defaultQueryOptions}); + + const { data } : { + data: { results: Array<{ + channel_id: string, + client_id: string, + connection_id: string, + client_type: string, + counterparty_client_id: string, + consensus_height: bigint, + }>, + pages: number, + } + } = useSuspenseQuery({ + queryKey: [queryName, pagination.pageIndex], + queryFn: () => getIbcChannels({ endpoint, pageIndex: pagination.pageIndex }), + meta: { + errorMessage, + }, + }); + + + const { pages: pageCount, results: channelsData } = data ?? { pages: 0, results: []}; + + const table = useReactTable({ + data: channelsData, + columns, + pageCount, + state: { + pagination, + }, + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }); + return ( - +
+ +
); }; - -export default ChannelsTable; diff --git a/src/components/ibc/channels/getIbcChannels.ts b/src/components/ibc/channels/getIbcChannels.ts new file mode 100644 index 0000000..afca459 --- /dev/null +++ b/src/components/ibc/channels/getIbcChannels.ts @@ -0,0 +1,14 @@ +export async function getIbcChannels({ endpoint, pageIndex } : { endpoint: string, pageIndex: number}) { + console.log(`Fetching: GET ${endpoint}?page=${pageIndex}`); + const res = await fetch(`http://localhost:3000${endpoint}?page=${pageIndex}`, { method: "GET" }); + return await res.json(); + // const json = await res.json(); + // console.log("Fetched Result:", json); + // return json; + // const result = ChannelsTableData.safeParse(json); + // if (result.success) { + // return result.data; + // } else { + // throw new Error(result.error.message); + // } +} From 30bc1ee9271474099b5d46667d71843b1c941201 Mon Sep 17 00:00:00 2001 From: ejmg Date: Sat, 13 Jul 2024 23:27:15 -0500 Subject: [PATCH 3/5] ibc/connections now uses suspense + prefetch + paginated data tables --- src/app/api/ibc/channels/route.ts | 11 ++-- src/app/api/ibc/connections/route.ts | 28 ++++++--- src/app/api/ibc/connections/route.types.ts | 19 +++++- src/app/ibc/connections/page.tsx | 54 ++++++++-------- .../ibc/connections/ConnectionsTable.tsx | 63 ++++++++++++++++--- .../ibc/connections/getIbcConnections.ts | 5 ++ 6 files changed, 128 insertions(+), 52 deletions(-) create mode 100644 src/components/ibc/connections/getIbcConnections.ts diff --git a/src/app/api/ibc/channels/route.ts b/src/app/api/ibc/channels/route.ts index 329b827..99c486a 100644 --- a/src/app/api/ibc/channels/route.ts +++ b/src/app/api/ibc/channels/route.ts @@ -1,6 +1,6 @@ import { getPgClient } from "@/lib/db"; import { sql } from "@pgtyped/runtime"; -import { IGetIbcChannelsQuery } from "./route.types"; +import { IGetChannelsCountQuery, IGetIbcChannelsQuery } from "./route.types"; import { NextRequest } from "next/server"; export async function GET(req: NextRequest) { @@ -60,17 +60,14 @@ export async function GET(req: NextRequest) { LEFT JOIN type_consensus_by_client tcc ON tcc.client_id=ccc.client_id LIMIT $pageLimit OFFSET $pageOffset! ;`; - - const getChannelsCount = sql` + const getChannelsCount = sql` SELECT COUNT(*)::int as "count!" FROM event_attributes ea - WHERE - ea.composite_key='channel_open_init.channel_id' - ;`; + WHERE ea.composite_key='channel_open_init.channel_id' + ;`; const channels = await getIbcChannels.run({ pageLimit, pageOffset }, client); const [ { count },,] = await getChannelsCount.run(undefined, client); - client.release(); console.log("Successfully queried channels:", [channels, count]); diff --git a/src/app/api/ibc/connections/route.ts b/src/app/api/ibc/connections/route.ts index 9def96e..7909f9b 100644 --- a/src/app/api/ibc/connections/route.ts +++ b/src/app/api/ibc/connections/route.ts @@ -1,26 +1,40 @@ import { getPgClient } from "@/lib/db"; import { sql } from "@pgtyped/runtime"; -import { IGetConnectionsQuery } from "./route.types"; +import { IGetConnectionsCountQuery, IGetConnectionsQuery } from "./route.types"; +import { NextRequest } from "next/server"; -export async function GET() { +export async function GET(req: NextRequest) { console.log("SUCCESS: GET /api/ibc/connections"); try { + const pageParam = req.nextUrl.searchParams.get("page")?.trim() ?? ""; + const pageOffset = (parseInt(pageParam, 10)) * 10; + const pageLimit = 10; + console.log("Acquiring db and querying for IBC Connections."); const client = await getPgClient(); + const getConnections = sql` SELECT ea.value as "connection_id!" FROM event_attributes ea WHERE ea.composite_key='connection_open_init.connection_id' - ORDER BY ea.block_id DESC; - `; + ORDER BY ea.block_id DESC LIMIT $pageLimit OFFSET $pageOffset! + ;`; + const getConnectionsCount = sql` + SELECT COUNT(*)::int as "count!" + FROM event_attributes ea + WHERE ea.composite_key='connection_open_init.connection_id' + ;`; - const connections = await getConnections.run(undefined, client); + const connections = await getConnections.run({ pageOffset, pageLimit }, client); + const [{ count },, ] = await getConnectionsCount.run(undefined, client); client.release(); - console.log("Successfully queried:", connections); + console.log("Successfully queried:", [connections, count]); + + const pages = Math.floor((count / 10) + 1); - return new Response(JSON.stringify(connections)); + return new Response(JSON.stringify({ results: connections, pages })); } catch (error) { console.error("GET request failed.", error); diff --git a/src/app/api/ibc/connections/route.types.ts b/src/app/api/ibc/connections/route.types.ts index 534e48c..5bcd43f 100644 --- a/src/app/api/ibc/connections/route.types.ts +++ b/src/app/api/ibc/connections/route.types.ts @@ -1,7 +1,10 @@ /** Types generated for queries found in "src/app/api/ibc/connections/route.ts" */ /** 'GetConnections' parameters type */ -export type IGetConnectionsParams = void; +export interface IGetConnectionsParams { + pageLimit?: bigint | number | null | void; + pageOffset: bigint | number; +} /** 'GetConnections' return type */ export interface IGetConnectionsResult { @@ -14,3 +17,17 @@ export interface IGetConnectionsQuery { result: IGetConnectionsResult; } +/** 'GetConnectionsCount' parameters type */ +export type IGetConnectionsCountParams = void; + +/** 'GetConnectionsCount' return type */ +export interface IGetConnectionsCountResult { + count: number; +} + +/** 'GetConnectionsCount' query type */ +export interface IGetConnectionsCountQuery { + params: IGetConnectionsCountParams; + result: IGetConnectionsCountResult; +} + diff --git a/src/app/ibc/connections/page.tsx b/src/app/ibc/connections/page.tsx index 40c4d5e..63960d9 100644 --- a/src/app/ibc/connections/page.tsx +++ b/src/app/ibc/connections/page.tsx @@ -1,39 +1,39 @@ -"use client"; - import ConnectionsTable from "@/components/ibc/connections/ConnectionsTable"; -import { useQuery } from "@tanstack/react-query"; -import axios from "axios"; +import { getIbcConnections } from "@/components/ibc/connections/getIbcConnections"; +import { getQueryClient } from "@/lib/utils"; +import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; const Page = () => { - const { data , isError } = useQuery({ - queryFn: async () => { - console.log("Fetching: GET /api/ibc/connections"); - const { data } = await axios.get("/api/ibc/connections"); - console.log("Fetched result:", data); - // TODO: enforce validation - return data; - }, - queryKey: ["IbcConnections"], - retry: false, + const queryClient = getQueryClient(); + + const defaultQueryOptions = { + pageIndex: 0, + pageSize: 0, + }; + + const endpoint = "/api/ibc/connections"; + const queryName = "IbcConnections"; + const errorMessage = "Failed to query for IBC Connections. Please try again."; + + queryClient.prefetchQuery({ + queryKey: [queryName, defaultQueryOptions.pageIndex], + queryFn: () => getIbcConnections({ endpoint, pageIndex: defaultQueryOptions.pageIndex }), meta: { - errorMessage: "Failed to query for IBC Connections. Please try again.", + errorMessage, }, }); - if (isError) { - return ( -
-

No results found.

-
- ); - } - return (
-

IBC Connections

- {// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - data ? :

No results

- } +

IBC Channels

+ + +
); }; diff --git a/src/components/ibc/connections/ConnectionsTable.tsx b/src/components/ibc/connections/ConnectionsTable.tsx index 8b4aad4..fb06bf9 100644 --- a/src/components/ibc/connections/ConnectionsTable.tsx +++ b/src/components/ibc/connections/ConnectionsTable.tsx @@ -1,19 +1,62 @@ +"use client"; import { columns } from "./columns"; -import { DataTable } from "../../ui/data-table"; -import { type FC } from "react"; +import { useState, type FC } from "react"; +import { PaginatedDataTable } from "@/components/ui/paginated-data-table"; +import { cn } from "@/lib/utils"; +import { PaginationState, getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { getIbcConnections } from "./getIbcConnections"; +import { useSuspenseQuery } from "@tanstack/react-query"; -interface Props { +export interface QueryOptions { + pageIndex: number, + pageSize: number, +} + +interface ConnectionsTableProps { className?: string, - data: Array<{ - key: string, - value: string | null, - }>, + queryName: string, + defaultQueryOptions: QueryOptions, + endpoint: string, + errorMessage: string, } -const ConnectionsTable : FC = ({ className, data }) => { +const ConnectionsTable : FC = ({ className, queryName, defaultQueryOptions, endpoint, errorMessage }) => { + const [pagination, setPagination] = useState({...defaultQueryOptions}); + + const { data } : { + data: { results: Array<{ + connection_id: string, + }>, + pages: number, + } + } = useSuspenseQuery({ + queryKey: [queryName, pagination.pageIndex], + queryFn: () => getIbcConnections({ endpoint, pageIndex: pagination.pageIndex }), + meta: { + errorMessage, + }, + }); + + + const { pages: pageCount, results: connectionsData } = data ?? { pages: 0, results: []}; + + const table = useReactTable({ + data: connectionsData, + columns, + pageCount, + state: { + pagination, + }, + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }); + return ( - +
+ +
); }; -export default ConnectionsTable; \ No newline at end of file +export default ConnectionsTable; diff --git a/src/components/ibc/connections/getIbcConnections.ts b/src/components/ibc/connections/getIbcConnections.ts new file mode 100644 index 0000000..1f92069 --- /dev/null +++ b/src/components/ibc/connections/getIbcConnections.ts @@ -0,0 +1,5 @@ +export async function getIbcConnections({ endpoint, pageIndex } : { endpoint: string, pageIndex: number}) { + console.log(`Fetching: GET ${endpoint}?page=${pageIndex}`); + const res = await fetch(`http://localhost:3000${endpoint}?page=${pageIndex}`, { method: "GET" }); + return await res.json(); +} From 083ca67a5710631906918532d559415cae4a57a9 Mon Sep 17 00:00:00 2001 From: ejmg Date: Sun, 14 Jul 2024 19:13:18 -0500 Subject: [PATCH 4/5] small corrections to typings for blocks and channels api db queries --- src/app/api/blocks/route.ts | 5 ++--- src/app/api/blocks/route.types.ts | 2 +- src/app/api/ibc/channels/route.ts | 4 ++-- src/app/api/ibc/channels/route.types.ts | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/app/api/blocks/route.ts b/src/app/api/blocks/route.ts index 6b4a779..aaf3ddf 100644 --- a/src/app/api/blocks/route.ts +++ b/src/app/api/blocks/route.ts @@ -20,14 +20,13 @@ export async function POST(req: Request) { SELECT b.height, b.created_at FROM blocks b ORDER BY b.height DESC LIMIT $queryLimit! OFFSET $pageOffset!; `; - const getBlocksCount = sql`SELECT COUNT(*)::int as _count FROM blocks;`; + const getBlocksCount = sql`SELECT COUNT(*)::int as "count!" FROM blocks;`; console.log("Acquiring DB Client and Querying database for recent blocks."); const client = await getPgClient(); const blocks = await getBlocksByDesc.run({queryLimit, pageOffset}, client); - const [{ _count },,] = await getBlocksCount.run(undefined, client); - const count = _count ?? 1; + const [{ count },,] = await getBlocksCount.run(undefined, client); console.log("Successfully queried Blocks."); console.log([count, blocks]); diff --git a/src/app/api/blocks/route.types.ts b/src/app/api/blocks/route.types.ts index f186952..319f250 100644 --- a/src/app/api/blocks/route.types.ts +++ b/src/app/api/blocks/route.types.ts @@ -23,7 +23,7 @@ export type IGetBlocksCountParams = void; /** 'GetBlocksCount' return type */ export interface IGetBlocksCountResult { - _count: number | null; + count: number; } /** 'GetBlocksCount' query type */ diff --git a/src/app/api/ibc/channels/route.ts b/src/app/api/ibc/channels/route.ts index 99c486a..5ae183f 100644 --- a/src/app/api/ibc/channels/route.ts +++ b/src/app/api/ibc/channels/route.ts @@ -58,7 +58,7 @@ export async function GET(req: NextRequest) { FROM channel_connection cc LEFT JOIN connections_counterparty_by_client ccc ON cc.connection_id=ccc.connection_id LEFT JOIN type_consensus_by_client tcc ON tcc.client_id=ccc.client_id - LIMIT $pageLimit OFFSET $pageOffset! + LIMIT $pageLimit! OFFSET $pageOffset! ;`; const getChannelsCount = sql` SELECT COUNT(*)::int as "count!" @@ -67,7 +67,7 @@ export async function GET(req: NextRequest) { ;`; const channels = await getIbcChannels.run({ pageLimit, pageOffset }, client); - const [ { count },,] = await getChannelsCount.run(undefined, client); + const [{ count },,] = await getChannelsCount.run(undefined, client); client.release(); console.log("Successfully queried channels:", [channels, count]); diff --git a/src/app/api/ibc/channels/route.types.ts b/src/app/api/ibc/channels/route.types.ts index 4099f1a..01716dc 100644 --- a/src/app/api/ibc/channels/route.types.ts +++ b/src/app/api/ibc/channels/route.types.ts @@ -2,7 +2,7 @@ /** 'GetIbcChannels' parameters type */ export interface IGetIbcChannelsParams { - pageLimit?: bigint | number | null | void; + pageLimit: bigint | number; pageOffset: bigint | number; } From 2c92337ece6d885da93b337b8167300a69f42405 Mon Sep 17 00:00:00 2001 From: ejmg Date: Sun, 14 Jul 2024 19:14:03 -0500 Subject: [PATCH 5/5] ibc/clients now uses prefetch + suspense + paginated data table --- src/app/api/ibc/clients/route.ts | 38 ++++++++--- src/app/api/ibc/clients/route.types.ts | 19 +++++- src/app/ibc/clients/page.tsx | 54 ++++++++-------- src/components/ibc/clients/ClientsTable.tsx | 72 +++++++++++++++++---- src/components/ibc/clients/getIbcClients.ts | 5 ++ 5 files changed, 138 insertions(+), 50 deletions(-) create mode 100644 src/components/ibc/clients/getIbcClients.ts diff --git a/src/app/api/ibc/clients/route.ts b/src/app/api/ibc/clients/route.ts index 3b2ae7d..a9d2742 100644 --- a/src/app/api/ibc/clients/route.ts +++ b/src/app/api/ibc/clients/route.ts @@ -1,11 +1,17 @@ import { getPgClient } from "@/lib/db"; import { sql } from "@pgtyped/runtime"; +import { NextRequest } from "next/server"; +import { IGetClientsQuery } from "./route.types"; -export async function GET() { +export async function GET(req: NextRequest) { console.log("SUCCESS: GET /api/ibc/clients"); try { + const pageParam = req.nextUrl.searchParams.get("page")?.trim() ?? ""; + const pageOffset = (parseInt(pageParam, 10)) * 10; + const pageLimit = 10; + console.log("Querying indexer for IBC clients."); - const getClients = sql` + const getClients = sql` SELECT clients.client_id as "client_id!", clients.block_id as "block_id!", @@ -18,11 +24,10 @@ export async function GET() { SELECT DISTINCT ON (ea.value) ea.value as "client_id", ea.block_id, ea.tx_id FROM event_attributes ea WHERE - ea.composite_key='create_client.client_id' - OR ea.composite_key='update_client.client_id' + OR + ea.composite_key='create_client.client_id' ORDER BY client_id, ea.block_id DESC - LIMIT 10 ) clients LEFT JOIN LATERAL ( SELECT MAX(ea.value) as "max_value", ea.tx_id FROM event_attributes ea @@ -44,15 +49,32 @@ export async function GET() { LIMIT 1 ) hashes ON true ORDER BY clients.block_id DESC + LIMIT $pageLimit! OFFSET $pageOffset! + ;`; + // Filtering distinct and then counting is more efficient than both at the same time. Not that these queries are particularly performant to begin with. + const getClientsCount = sql` + SELECT COUNT(*)::int as "count!" + FROM ( + SELECT DISTINCT ON (ea.value) ea.value, ea.block_id + FROM event_attributes ea + WHERE + ea.composite_key='update_client.client_id' + OR + ea.composite_key='create_client.client_id' + ORDER BY ea.value, ea.block_id DESC + ) AS clients ;`; + const pgClient = await getPgClient(); - const clients = await getClients.run(undefined, pgClient); + const clients = await getClients.run({ pageLimit, pageOffset }, pgClient); + const [{ count },,] = await getClientsCount.run(undefined, pgClient); pgClient.release(); console.log("Successfully queried for IBC Clients."); - // console.dir(["pgClient query: ", clients], { depth: 4 }); - return new Response(JSON.stringify(clients)); + const pages = Math.floor((count / 10) + 1); + + return new Response(JSON.stringify({ results: clients, pages })); } catch (error) { console.error("GET request failed.", error); diff --git a/src/app/api/ibc/clients/route.types.ts b/src/app/api/ibc/clients/route.types.ts index d53c44e..7fa4d46 100644 --- a/src/app/api/ibc/clients/route.types.ts +++ b/src/app/api/ibc/clients/route.types.ts @@ -1,7 +1,10 @@ /** Types generated for queries found in "src/app/api/ibc/clients/route.ts" */ /** 'GetClients' parameters type */ -export type IGetClientsParams = void; +export interface IGetClientsParams { + pageLimit: bigint | number; + pageOffset: bigint | number; +} /** 'GetClients' return type */ export interface IGetClientsResult { @@ -18,3 +21,17 @@ export interface IGetClientsQuery { result: IGetClientsResult; } +/** 'GetClientsCount' parameters type */ +export type IGetClientsCountParams = void; + +/** 'GetClientsCount' return type */ +export interface IGetClientsCountResult { + count: number; +} + +/** 'GetClientsCount' query type */ +export interface IGetClientsCountQuery { + params: IGetClientsCountParams; + result: IGetClientsCountResult; +} + diff --git a/src/app/ibc/clients/page.tsx b/src/app/ibc/clients/page.tsx index 0e7b515..93e105d 100644 --- a/src/app/ibc/clients/page.tsx +++ b/src/app/ibc/clients/page.tsx @@ -1,39 +1,39 @@ -"use client"; - -import ClientsTable from "@/components/ibc/clients/ClientsTable"; -import { useQuery } from "@tanstack/react-query"; -import axios from "axios"; +import { ClientsTable } from "@/components/ibc/clients/ClientsTable"; +import { getIbcClients } from "@/components/ibc/clients/getIbcClients"; +import { getQueryClient } from "@/lib/utils"; +import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; const Page = () => { - const { data , isError } = useQuery({ - queryFn: async () => { - console.log("Fetching: GET /api/ibc/clients"); - const { data } = await axios.get("/api/ibc/clients"); - console.log("Fetched result:", data); - // TODO: enforce validation - return data; - }, - queryKey: ["IbcClients"], - retry: false, + const queryClient = getQueryClient(); + + const defaultQueryOptions = { + pageIndex: 0, + pageSize: 0, + }; + + const endpoint = "/api/ibc/clients"; + const queryName = "IbcClients"; + const errorMessage = "Failed to query for IBC Clients. Please try again."; + + queryClient.prefetchQuery({ + queryKey: [queryName, defaultQueryOptions.pageIndex], + queryFn: () => getIbcClients({ endpoint, pageIndex: defaultQueryOptions.pageIndex }), meta: { - errorMessage: "Failed to query for IBC Clients. Please try again.", + errorMessage, }, }); - if (isError) { - return ( -
-

No results found.

-
- ); - } - return (

IBC Clients

- {// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - data ? :

No results

- } + + +
); }; diff --git a/src/components/ibc/clients/ClientsTable.tsx b/src/components/ibc/clients/ClientsTable.tsx index 68a5bfc..32960a7 100644 --- a/src/components/ibc/clients/ClientsTable.tsx +++ b/src/components/ibc/clients/ClientsTable.tsx @@ -1,22 +1,66 @@ +"use client"; import { columns } from "./columns"; -import { DataTable } from "../../ui/data-table"; -import { type FC } from "react"; +import { useState, type FC } from "react"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { getIbcClients } from "./getIbcClients"; +import { PaginationState, getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { cn } from "@/lib/utils"; +import { PaginatedDataTable } from "@/components/ui/paginated-data-table"; -interface Props { +export interface QueryOptions { + pageIndex: number, + pageSize: number, +} + +interface ClientsTableProps { className?: string, - data: Array<{ - client_id: string, - block_id: bigint, - last_updated_at: string, - hash: string, - consensus_height: string | null - }>, + queryName: string, + defaultQueryOptions: QueryOptions, + endpoint: string, + errorMessage: string, } -const ClientsTable : FC = ({ className, data }) => { +export const ClientsTable : FC = ({className, queryName, defaultQueryOptions, endpoint, errorMessage}) => { + + const [pagination, setPagination] = useState({...defaultQueryOptions}); + + const { data } : { + data: { + results: Array<{ + client_id: string, + block_id: bigint, + last_updated_at: string, + hash: string, + consensus_height: string | null + }>, + pages: number + } + } = useSuspenseQuery({ + queryKey: [queryName, pagination.pageIndex], + queryFn: () => getIbcClients({ endpoint, pageIndex: pagination.pageIndex }), + meta: { + errorMessage, + }, + }); + + + const { pages: pageCount, results: clientsData } = data ?? { pages: 0, results: []}; + + const table = useReactTable({ + data: clientsData, + columns, + pageCount, + state: { + pagination, + }, + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }); + return ( - +
+ +
); }; - -export default ClientsTable; diff --git a/src/components/ibc/clients/getIbcClients.ts b/src/components/ibc/clients/getIbcClients.ts new file mode 100644 index 0000000..889f2a9 --- /dev/null +++ b/src/components/ibc/clients/getIbcClients.ts @@ -0,0 +1,5 @@ +export async function getIbcClients({ endpoint, pageIndex } : { endpoint: string, pageIndex: number }) { + console.log(`Fetching: GET ${endpoint}?page=${pageIndex}`); + const res = await fetch(`http://localhost:3000${endpoint}?page=${pageIndex}`, { method: "GET" }); + return await res.json(); +}