Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/two-coats-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/explorer": patch
---

For the hosted Explorer, transactions are now preloaded, with infinite loading available for previous transactions.
1 change: 1 addition & 0 deletions packages/explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.52.1",
"react-intersection-observer": "^9.15.1",
"react18-json-view": "^0.2.9",
"sonner": "^1.5.0",
"sql-autocomplete": "^1.1.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
"use client";

import { BoxIcon, CheckCheckIcon, ReceiptTextIcon, UserPenIcon, XIcon } from "lucide-react";
import React, { useState } from "react";
import { parseAsString, useQueryState } from "nuqs";
import React, { useEffect, useMemo, useState } from "react";
import { useInView } from "react-intersection-observer";
import { ExpandedState, flexRender, getCoreRowModel, getExpandedRowModel, useReactTable } from "@tanstack/react-table";
import { createColumnHelper } from "@tanstack/react-table";
import { Badge } from "../../../../../../components/ui/Badge";
import { Input } from "../../../../../../components/ui/Input";
import { Skeleton } from "../../../../../../components/ui/Skeleton";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../../../../components/ui/Table";
import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "../../../../../../components/ui/Table";
import { TruncatedHex } from "../../../../../../components/ui/TruncatedHex";
import { cn } from "../../../../../../utils";
import { useChain } from "../../../../hooks/useChain";
import { useIndexerForChainId } from "../../../../hooks/useIndexerForChainId";
import { useTransactionsQuery } from "../../../../queries/useTransactionsQuery";
import { BlockExplorerLink } from "./BlockExplorerLink";
import { TimeAgo } from "./TimeAgo";
import { TimingRowHeader } from "./TimingRowHeader";
Expand All @@ -16,7 +31,7 @@ import { ObservedTransaction, useMergedTransactions } from "./useMergedTransacti

const columnHelper = createColumnHelper<ObservedTransaction>();
export const columns = [
columnHelper.accessor("receipt.blockNumber", {
columnHelper.accessor("blockNumber", {
header: "Block",
cell: (row) => {
const status = row.row.original.status;
Expand Down Expand Up @@ -101,11 +116,53 @@ export const columns = [
];

export function TransactionsTable() {
const { ref, inView } = useInView();
const { id: chainId } = useChain();
const indexer = useIndexerForChainId(chainId);
const transactions = useMergedTransactions();
const { data: indexedTransactions, fetchNextPage } = useTransactionsQuery();
const loadedInitialTransactions = Array.isArray(indexedTransactions) && indexedTransactions.length > 0;
const [expanded, setExpanded] = useState<ExpandedState>({});

const [blockNumberFilter, setBlockNumberFilter] = useQueryState("blockNumber", parseAsString.withDefault(""));
const [fromFilter, setFromFilter] = useQueryState("from", parseAsString.withDefault(""));
const [callsFilter, setCallsFilter] = useQueryState("calls", parseAsString.withDefault(""));
const [hashFilter, setHashFilter] = useQueryState("hash", parseAsString.withDefault(""));
const [timestampFilter, setTimestampFilter] = useQueryState("timestamp", parseAsString.withDefault(""));

useEffect(() => {
if (inView) {
fetchNextPage();
}
}, [fetchNextPage, inView]);

// Filter transactions based on filter values
const filteredTransactions = useMemo(() => {
return transactions.filter((transaction) => {
if (blockNumberFilter && !transaction.blockNumber?.toString().includes(blockNumberFilter)) {
return false;
}
if (fromFilter && !transaction.from?.toLowerCase().includes(fromFilter.toLowerCase())) {
return false;
}
if (
callsFilter &&
!transaction.calls?.some((call) => call.functionName?.toLowerCase().includes(callsFilter.toLowerCase()))
) {
return false;
}
if (hashFilter && !transaction.hash?.toLowerCase().includes(hashFilter.toLowerCase())) {
return false;
}
if (timestampFilter && !transaction.timestamp?.toString().includes(timestampFilter)) {
return false;
}
return true;
});
}, [transactions, blockNumberFilter, fromFilter, callsFilter, hashFilter, timestampFilter]);

const table = useReactTable({
data: transactions,
data: filteredTransactions,
columns,
state: {
expanded,
Expand All @@ -117,34 +174,110 @@ export function TransactionsTable() {
});

return (
<Table>
<TableHeader className="sticky top-0 z-10 bg-[var(--color-background)]">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} className="text-xs uppercase">
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
<div className="space-y-4">
{/* Filter Row */}
<div className="grid grid-cols-5 gap-4 rounded-md border bg-muted/20 p-4">
<div className="space-y-2">
<label className="text-xs font-medium uppercase text-muted-foreground">Block</label>
<Input
placeholder="Filter block..."
value={blockNumberFilter}
onChange={(e) => setBlockNumberFilter(e.target.value)}
className="h-8 text-xs"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-medium uppercase text-muted-foreground">From</label>
<Input
placeholder="Filter address..."
value={fromFilter}
onChange={(e) => setFromFilter(e.target.value)}
className="h-8 text-xs"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-medium uppercase text-muted-foreground">Functions</label>
<Input
placeholder="Filter functions..."
value={callsFilter}
onChange={(e) => setCallsFilter(e.target.value)}
className="h-8 text-xs"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-medium uppercase text-muted-foreground">Tx Hash</label>
<Input
placeholder="Filter hash..."
value={hashFilter}
onChange={(e) => setHashFilter(e.target.value)}
className="h-8 text-xs"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-medium uppercase text-muted-foreground">Time</label>
<Input
placeholder="Filter time..."
value={timestampFilter}
onChange={(e) => setTimestampFilter(e.target.value)}
className="h-8 text-xs"
/>
</div>
</div>

<Table>
<TableHeader className="sticky top-0 z-10 bg-[var(--color-background)]">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} className="text-xs uppercase">
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => <TransactionTableRow key={row.id} row={row} />)
) : (
<TableRow>
<TableCell colSpan={columns.length}>
<p className="flex items-center justify-center gap-3 py-4 font-mono text-xs font-bold uppercase text-muted-foreground">
<span className="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-muted-foreground" /> Waiting
for transactions…
</p>
</TableCell>
</TableRow>
)}
</TableBody>

{indexer.type === "hosted" && (
<TableFooter
className={cn("border-t-transparent bg-transparent hover:bg-transparent", {
"border-t-muted": loadedInitialTransactions,
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => <TransactionTableRow key={row.id} row={row} />)
) : (
<TableRow>
<TableCell colSpan={columns.length}>
<p className="flex items-center justify-center gap-3 py-4 font-mono text-xs font-bold uppercase text-muted-foreground">
<span className="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-muted-foreground" /> Waiting for
transactions…
</p>
</TableCell>
</TableRow>
>
<TableRow>
<TableCell colSpan={columns.length}>
<div
ref={ref}
className={cn(
"hidden items-center justify-center gap-3 py-4 font-mono text-xs font-bold uppercase text-muted-foreground",
{
flex: loadedInitialTransactions,
},
)}
>
<span className="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-muted-foreground" />
Loading more transactions...
</div>
</TableCell>
</TableRow>
</TableFooter>
)}
</TableBody>
</Table>
</Table>
</div>
);
}
Loading
Loading