From c59708b0cfb9da60e767853feeb80ff8a608e34c Mon Sep 17 00:00:00 2001 From: karooolis Date: Mon, 14 Oct 2024 19:51:20 +0300 Subject: [PATCH 1/2] paginate table --- .../packages/client/src/App.tsx | 18 +++ .../observe/TransactionsTable.tsx | 125 +++++++++++++----- 2 files changed, 113 insertions(+), 30 deletions(-) diff --git a/examples/local-explorer/packages/client/src/App.tsx b/examples/local-explorer/packages/client/src/App.tsx index 82c60486a9..db417ece41 100644 --- a/examples/local-explorer/packages/client/src/App.tsx +++ b/examples/local-explorer/packages/client/src/App.tsx @@ -1,7 +1,14 @@ import { useMUD } from "./MUDContext"; +import { useEffect } from "react"; const styleUnset = { all: "unset" } as const; +// Function to generate random task descriptions +const generateRandomTask = () => { + const tasks = ["Buy groceries", "Walk the dog", "Do laundry", "Clean the house", "Pay bills"]; + return tasks[Math.floor(Math.random() * tasks.length)] + " " + Math.floor(Math.random() * 1000); +}; + export const App = () => { const { network: { tables, useStore }, @@ -14,6 +21,17 @@ export const App = () => { return records; }); + useEffect(() => { + const interval = setInterval(() => { + const randomTask = generateRandomTask(); + addTask(randomTask); + }, 5000); // 100ms interval for 10 tasks per second + + // TODO: Add a way to stop the interval (e.g., after a certain number of tasks or time) + + return () => clearInterval(interval); + }, [addTask]); + return ( <> diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionsTable.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionsTable.tsx index c3b5eacea0..b5775a44be 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionsTable.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionsTable.tsx @@ -1,10 +1,20 @@ "use client"; import { BoxIcon, CheckCheckIcon, ReceiptTextIcon, UserPenIcon, XIcon } from "lucide-react"; +import { parseAsInteger, useQueryState } from "nuqs"; import React, { useState } from "react"; -import { ExpandedState, flexRender, getCoreRowModel, getExpandedRowModel, useReactTable } from "@tanstack/react-table"; +import { + ExpandedState, + flexRender, + getCoreRowModel, + getExpandedRowModel, + getPaginationRowModel, + useReactTable, +} from "@tanstack/react-table"; import { createColumnHelper } from "@tanstack/react-table"; import { Badge } from "../../../../../../components/ui/Badge"; +import { Button } from "../../../../../../components/ui/Button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../../../components/ui/Select"; import { Skeleton } from "../../../../../../components/ui/Skeleton"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../../../../components/ui/Table"; import { TruncatedHex } from "../../../../../../components/ui/TruncatedHex"; @@ -96,48 +106,103 @@ export const columns = [ export function TransactionsTable() { const transactions = useTransactionWatcher(); const [expanded, setExpanded] = useState({}); + const [pageIndex, setPageIndex] = useQueryState("txPage", parseAsInteger.withDefault(0)); + const [pageSize, setPageSize] = useQueryState("txPageSize", parseAsInteger.withDefault(10)); const table = useReactTable({ data: transactions, columns, state: { expanded, + pagination: { + pageIndex, + pageSize, + }, }, getRowId: (row) => row.writeId, onExpandedChange: setExpanded, getCoreRowModel: getCoreRowModel(), getExpandedRowModel: getExpandedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onPaginationChange: (updater) => { + // TODO: check this more + if (typeof updater === "function") { + const newState = updater({ pageIndex, pageSize }); + setPageIndex(newState.pageIndex); + setPageSize(newState.pageSize); + } + }, }); 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) => ) - ) : ( - - -

- Waiting for - transactions… -

-
-
- )} -
-
+ <> + + + {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) => ) + ) : ( + + +

+ Waiting + for transactions… +

+
+
+ )} +
+
+
+
+ + +
+
+ + Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()} + + + +
+
+ ); } From 29b0c2cc5c9da48a94f8244819805658481de8ff Mon Sep 17 00:00:00 2001 From: karooolis Date: Tue, 15 Oct 2024 10:51:39 +0300 Subject: [PATCH 2/2] wip --- .../observe/useTransactionWatcher.ts | 86 ++++++++++--------- packages/explorer/src/observer/store.ts | 42 ++++++--- 2 files changed, 74 insertions(+), 54 deletions(-) diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/useTransactionWatcher.ts b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/useTransactionWatcher.ts index 58d56c2a9e..3ace531431 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/useTransactionWatcher.ts +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/useTransactionWatcher.ts @@ -16,9 +16,11 @@ import { useConfig, useWatchBlocks } from "wagmi"; import { getTransaction, simulateContract, waitForTransactionReceipt } from "wagmi/actions"; import { useStore } from "zustand"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { observer } from "../../../../../../observer/decorator"; import { Message } from "../../../../../../observer/messages"; import { type Write, store } from "../../../../../../observer/store"; import { useChain } from "../../../../hooks/useChain"; +import { usePrevious } from "../../../../hooks/usePrevious"; import { useWorldAbiQuery } from "../../../../queries/useWorldAbiQuery"; export type WatchedTransaction = { @@ -45,6 +47,10 @@ export function useTransactionWatcher() { const [transactions, setTransactions] = useState([]); const observerWrites = useStore(store, (state) => state.writes); + // const observerWritesLen = observerWrites.length; + // const prevObserverWritesLen = usePrevious(observerWrites.length); + // const latestUpdatesLen = observerWritesLen - (prevObserverWritesLen || 0); + const handleTransaction = useCallback( async (hash: Hex, timestamp: bigint) => { if (!abi) return; @@ -65,7 +71,7 @@ export function useTransactionWatcher() { functionName = transaction.input.length > 10 ? transaction.input.slice(0, 10) : "unknown"; } - const write = Object.values(observerWrites).find((write) => write.hash === hash); + const write = observerWrites.find((write) => write.hash === hash); setTransactions((prevTransactions) => [ { hash, @@ -133,7 +139,7 @@ export function useTransactionWatcher() { ); useEffect(() => { - for (const write of Object.values(observerWrites)) { + for (const write of observerWrites.slice(0, 50)) { const hash = write.hash; if (write.type === "waitForTransactionReceipt" && hash && write.address === worldAddress) { const transaction = transactions.find((transaction) => transaction.hash === hash); @@ -155,42 +161,42 @@ export function useTransactionWatcher() { pollingInterval: 500, }); - const mergedTransactions = useMemo((): WatchedTransaction[] => { - const mergedMap = new Map(); - - for (const write of Object.values(observerWrites)) { - if (write.address !== worldAddress) continue; - - const parsedAbiItem = parseAbiItem(`function ${write.functionSignature}`) as AbiFunction; - const writeResult = write.events.find((event): event is Message<"write:result"> => event.type === "write:result"); - - mergedMap.set(write.hash || write.writeId, { - hash: write.hash, - writeId: write.writeId, - from: write.from, - status: writeResult?.status === "rejected" ? "rejected" : "pending", - timestamp: BigInt(write.time) / 1000n, - functionData: { - functionName: parsedAbiItem.name, - args: write.args, - }, - value: write.value, - error: writeResult && "reason" in writeResult ? (writeResult.reason as BaseError) : undefined, - write, - }); - } - - for (const transaction of transactions) { - const existing = mergedMap.get(transaction.hash); - if (existing) { - mergedMap.set(transaction.hash, { ...transaction, write: existing.write }); - } else { - mergedMap.set(transaction.hash, { ...transaction }); - } - } - - return Array.from(mergedMap.values()).sort((a, b) => Number(b.timestamp ?? 0n) - Number(a.timestamp ?? 0n)); - }, [observerWrites, worldAddress, transactions]); - - return mergedTransactions; + // const mergedTransactions = useMemo((): WatchedTransaction[] => { + // const mergedMap = new Map(); + + // for (const write of observerWrites) { + // if (write.address !== worldAddress) continue; + + // const parsedAbiItem = parseAbiItem(`function ${write.functionSignature}`) as AbiFunction; + // const writeResult = write.events.find((event): event is Message<"write:result"> => event.type === "write:result"); + + // mergedMap.set(write.hash || write.writeId, { + // hash: write.hash, + // writeId: write.writeId, + // from: write.from, + // status: writeResult?.status === "rejected" ? "rejected" : "pending", + // timestamp: BigInt(write.time) / 1000n, + // functionData: { + // functionName: parsedAbiItem.name, + // args: write.args, + // }, + // value: write.value, + // error: writeResult && "reason" in writeResult ? (writeResult.reason as BaseError) : undefined, + // write, + // }); + // } + + // for (const transaction of transactions) { + // const existing = mergedMap.get(transaction.hash); + // if (existing) { + // mergedMap.set(transaction.hash, { ...transaction, write: existing.write }); + // } else { + // mergedMap.set(transaction.hash, { ...transaction }); + // } + // } + + // return Array.from(mergedMap.values()).sort((a, b) => Number(b.timestamp ?? 0n) - Number(a.timestamp ?? 0n)); + // }, [observerWrites, worldAddress, transactions]); + + return transactions; } diff --git a/packages/explorer/src/observer/store.ts b/packages/explorer/src/observer/store.ts index 19c2196dae..9fa1262ad8 100644 --- a/packages/explorer/src/observer/store.ts +++ b/packages/explorer/src/observer/store.ts @@ -21,13 +21,11 @@ export type Write = { }; export type State = { - writes: { - [id: string]: Write; - }; + writes: Write[]; }; export const store = createStore(() => ({ - writes: {}, + writes: [], })); debug("listening for relayed messages", relayChannelName); @@ -35,17 +33,33 @@ const channel = new BroadcastChannel(relayChannelName); channel.addEventListener("message", ({ data }: MessageEvent) => { if (data.type === "ping") return; store.setState((state) => { - const write = data.type === "write" ? ({ ...data, events: [] } satisfies Write) : state.writes[data.writeId]; + const writeIndex = state.writes.findIndex((w) => w.writeId === data.writeId); + const write = + writeIndex !== -1 + ? state.writes[writeIndex] + : data.type === "write" + ? ({ ...data, events: [] } satisfies Write) + : undefined; + + if (!write) return state; + + const updatedWrite = { + ...write, + type: data.type, + hash: data.type === "waitForTransactionReceipt" ? data.hash : write.hash, + events: [...write.events, data], + }; + + const newWrites = [...state.writes]; + if (writeIndex !== -1) { + newWrites.splice(writeIndex, 1); + newWrites.unshift(updatedWrite); + } else { + newWrites.unshift(updatedWrite); + } + return { - writes: { - ...state.writes, - [data.writeId]: { - ...write, - type: data.type, - hash: data.type === "waitForTransactionReceipt" ? data.hash : write.hash, - events: [...write.events, data], - }, - }, + writes: newWrites, }; }); });