From 7f725b6e56b13c03ddaef9de423a1aa6c3fd4ce7 Mon Sep 17 00:00:00 2001 From: Aaron Sutula Date: Fri, 1 Mar 2024 21:16:42 -0600 Subject: [PATCH 01/13] working logs Signed-off-by: Aaron Sutula --- package-lock.json | 31 ++++++ .../[[...slug]]/_components/data-table.tsx | 5 +- .../web/app/_components/latest-projects.tsx | 2 +- .../web/app/_components/latest-tables.tsx | 3 +- .../web/app/_components/popular-tables.tsx | 3 +- .../_components => components}/paginator.tsx | 8 +- packages/web/components/sql-logs.tsx | 95 +++++++++++++++++++ packages/web/components/tableland-table.tsx | 17 +++- packages/web/components/ui/tabs.tsx | 55 +++++++++++ packages/web/lib/validator-queries.ts | 53 ++++++++++- packages/web/package.json | 1 + 11 files changed, 261 insertions(+), 12 deletions(-) rename packages/web/{app/_components => components}/paginator.tsx (80%) create mode 100644 packages/web/components/sql-logs.tsx create mode 100644 packages/web/components/ui/tabs.tsx diff --git a/package-lock.json b/package-lock.json index a627ec1e..40301091 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4150,6 +4150,36 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz", + "integrity": "sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", @@ -23234,6 +23264,7 @@ "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-tooltip": "^1.0.7", "@tableland/nonce": "^0.0.0-pre.1", diff --git a/packages/web/app/[team]/[project]/(project)/deployments/[[...slug]]/_components/data-table.tsx b/packages/web/app/[team]/[project]/(project)/deployments/[[...slug]]/_components/data-table.tsx index b2e842d3..d63b731a 100644 --- a/packages/web/app/[team]/[project]/(project)/deployments/[[...slug]]/_components/data-table.tsx +++ b/packages/web/app/[team]/[project]/(project)/deployments/[[...slug]]/_components/data-table.tsx @@ -56,8 +56,7 @@ export function DataTable({ return (
-
-

Table Data

+
{!!data.length && ( @@ -87,7 +86,7 @@ export function DataTable({ )}
-
+
{table.getHeaderGroups().map((headerGroup) => ( diff --git a/packages/web/app/_components/latest-projects.tsx b/packages/web/app/_components/latest-projects.tsx index c53a7802..887d9dd0 100644 --- a/packages/web/app/_components/latest-projects.tsx +++ b/packages/web/app/_components/latest-projects.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import TimeAgo from "javascript-time-ago"; import { useState } from "react"; -import { Paginator } from "./paginator"; +import { Paginator } from "@/components/paginator"; import { TypographyH3 } from "@/components/typography-h3"; import { type store } from "@/lib/store"; import TeamAvatar from "@/components/team-avatar"; diff --git a/packages/web/app/_components/latest-tables.tsx b/packages/web/app/_components/latest-tables.tsx index 5d29ce64..f1ca6140 100644 --- a/packages/web/app/_components/latest-tables.tsx +++ b/packages/web/app/_components/latest-tables.tsx @@ -4,7 +4,7 @@ import Link from "next/link"; import TimeAgo from "javascript-time-ago"; import { useEffect, useState } from "react"; import { chainsMap } from "../../lib/chains-map"; -import { Paginator } from "./paginator"; +import { Paginator } from "@/components/paginator"; import { TypographyH3 } from "@/components/typography-h3"; import ChainSelector from "@/components/chain-selector"; import { Label } from "@/components/ui/label"; @@ -85,6 +85,7 @@ export function LatestTables({ initialData }: { initialData: Table[] }) { pageSize={pageSize} page={page} setPage={setPage} + disabled={loading} /> ); diff --git a/packages/web/app/_components/popular-tables.tsx b/packages/web/app/_components/popular-tables.tsx index 8e50514e..5a14a56a 100644 --- a/packages/web/app/_components/popular-tables.tsx +++ b/packages/web/app/_components/popular-tables.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { useEffect, useState } from "react"; import { chainsMap } from "../../lib/chains-map"; -import { Paginator } from "./paginator"; +import { Paginator } from "@/components/paginator"; import { TypographyH3 } from "@/components/typography-h3"; import ChainSelector from "@/components/chain-selector"; import { Label } from "@/components/ui/label"; @@ -101,6 +101,7 @@ export function PopularTables({ pageSize={pageSize} page={page} setPage={setPage} + disabled={loading} /> ); diff --git a/packages/web/app/_components/paginator.tsx b/packages/web/components/paginator.tsx similarity index 80% rename from packages/web/app/_components/paginator.tsx rename to packages/web/components/paginator.tsx index 33f4dfbd..e5cd5cd4 100644 --- a/packages/web/app/_components/paginator.tsx +++ b/packages/web/components/paginator.tsx @@ -1,16 +1,18 @@ import { ChevronLeft, ChevronRight } from "lucide-react"; -import { Button } from "@/components/ui/button"; +import { Button } from "./ui/button"; export function Paginator({ numItems, pageSize, page, setPage, + disabled, }: { numItems: number; pageSize: number; page: number; setPage: (page: number) => void; + disabled?: boolean; }) { return (
@@ -18,7 +20,7 @@ export function Paginator({ variant="ghost" className="px-2" onClick={() => setPage(page === 0 ? page : page - 1)} - disabled={page === 0} + disabled={page === 0 || disabled} > @@ -27,7 +29,7 @@ export function Paginator({ variant="ghost" className="px-2" onClick={() => setPage(page + 1)} - disabled={numItems < pageSize * (page + 1)} + disabled={numItems < pageSize * (page + 1) || disabled} > diff --git a/packages/web/components/sql-logs.tsx b/packages/web/components/sql-logs.tsx new file mode 100644 index 00000000..42566336 --- /dev/null +++ b/packages/web/components/sql-logs.tsx @@ -0,0 +1,95 @@ +"use client"; + +import { Paginator } from "./paginator"; +import { Button } from "./ui/button"; +import { cn } from "@/lib/utils"; +import { SqlLog, getSqlLogs } from "@/lib/validator-queries"; +import { AlertCircle, RefreshCw } from "lucide-react"; +import Link from "next/link"; +import { useEffect, useState } from "react"; + +export default function SQLLogs({ + chain, + tableId, +}: { + chain: number; + tableId: string; +}) { + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(undefined); + const [page, setPage] = useState(0); + const [loadedPage, setLoadedPage] = useState(-1); + const [maxLoadedPage, setMaxLoadedPage] = useState(-1); + const [pageSize] = useState(20); + useEffect(() => { + async function loadLogs() { + if (page <= maxLoadedPage) { + setLoadedPage(page); + return; + } + let beforeTimestamp = !!logs.length + ? logs[logs.length - 1].timestamp + : undefined; + if (beforeTimestamp && maxLoadedPage === -1) { + beforeTimestamp = undefined; + } + setLoading(true); + const res = await getSqlLogs(chain, tableId, pageSize, beforeTimestamp); + setLoading(false); + setLogs(maxLoadedPage === -1 ? res : [...logs, ...res]); + setLoadedPage(page); + setMaxLoadedPage(page); + } + + loadLogs().catch((err) => { + setLoading(false); + setError(err.message); + }); + }, [page, pageSize, maxLoadedPage]); + + function refresh() { + setMaxLoadedPage(-1); + setPage(0); + } + + const offset = loadedPage * pageSize; + + return ( +
+ + {error &&
{error}
} + {logs.slice(offset, offset + pageSize).map((log) => ( + + {log.error && } +
+ {new Date(log.timestamp * 1000).toLocaleString()} +
+
{log.statement}
+ + ))} + +
+ ); +} diff --git a/packages/web/components/tableland-table.tsx b/packages/web/components/tableland-table.tsx index 252371ef..b2f540c7 100644 --- a/packages/web/components/tableland-table.tsx +++ b/packages/web/components/tableland-table.tsx @@ -7,7 +7,9 @@ import Link from "next/link"; import { DataTable } from "../app/[team]/[project]/(project)/deployments/[[...slug]]/_components/data-table"; import { openSeaLinks } from "@/lib/open-sea"; import { blockExplorers } from "@/lib/block-explorers"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; +import SQLLogs from "./sql-logs"; const timeAgo = new TimeAgo("en-US"); @@ -175,7 +177,18 @@ export default async function TablelandTable({ )}
- + + + Table Data + SQL Logs + + + + + + + + ); } diff --git a/packages/web/components/ui/tabs.tsx b/packages/web/components/ui/tabs.tsx new file mode 100644 index 00000000..0f4caebb --- /dev/null +++ b/packages/web/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/packages/web/lib/validator-queries.ts b/packages/web/lib/validator-queries.ts index 15113c35..54b2920d 100644 --- a/packages/web/lib/validator-queries.ts +++ b/packages/web/lib/validator-queries.ts @@ -43,7 +43,7 @@ export async function getPopularTables( const d = new Date(); const dayAgo = Math.round(d.getTime() / 1000) - 24 * 60 * 60; - let query = `select tr.chain_id, tr.table_id, prefix, r.controller, max(timestamp) as max_timestamp, count(*) as count from system_evm_events e join system_evm_blocks b on e.block_number = b.block_number and e.chain_id = b.chain_id inner join system_txn_receipts tr on tr.txn_hash = e.tx_hash inner join registry r on r.chain_id = tr.chain_id and r.id = tr.table_id where event_type = 'ContractRunSQL' and error is null and timestamp > ${dayAgo} `; + let query = `select tr.chain_id, tr.table_id, prefix, r.controller, max(timestamp) as max_timestamp, count(*) as count from system_evm_events e inner join system_evm_blocks b on e.block_number = b.block_number and e.chain_id = b.chain_id inner join system_txn_receipts tr on tr.txn_hash = e.tx_hash inner join registry r on r.chain_id = tr.chain_id and r.id = tr.table_id where event_type = 'ContractRunSQL' and error is null and timestamp > ${dayAgo} `; if (typeof chain === "number") { query += `and tr.chain_id = ${chain} `; } @@ -65,6 +65,57 @@ export async function getPopularTables( return res.json() as unknown as PopularTable[]; } +export interface SqlLog { + blockNumber: number; + caller: string; + error: string | null; + eventIndex: number; + eventType: "ContractRunSQL" | "ContractCreateTable"; + statement: string; + timestamp: number; + txHash: string; +} + +export async function getSqlLogs( + chain: number, + tableId: string, + limit: number, + beforeTimestamp?: number, +) { + // TODO: Just filtering on timestamp is probably going to miss some logs. Need to investigate. + const query = ` + SELECT + e.block_number as blockNumber, + e.event_index as eventIndex, + tx_hash as txHash, + event_type as eventType, + json_extract(event_json,'$.Caller') as caller, + json_extract(event_json,'$.Statement') as statement, + error, + timestamp + FROM + system_evm_events e join system_evm_blocks b on e.block_number = b.block_number and e.chain_id = b.chain_id inner join system_txn_receipts tr on tr.txn_hash = e.tx_hash AND tr.chain_id = e.chain_id + WHERE + ${beforeTimestamp ? `timestamp < ${beforeTimestamp} AND` : ""} + json_extract(event_json,'$.TableId') = ${tableId} AND + e.chain_id = ${chain} AND + (eventType = 'ContractCreateTable' OR eventType = 'ContractRunSQL') + ORDER BY + blockNumber DESC, eventIndex DESC + LIMIT ${limit} + `; + + const uri = encodeURI(`${baseUrlForChain(chain)}/query?statement=${query}`); + const res = await fetch(uri); + + if (!res.ok) { + // This will activate the closest `error.js` Error Boundary + throw new Error("Failed to fetch data"); + } + + return res.json() as unknown as SqlLog[]; +} + function baseUrlForChain(chainId: number | "mainnets" | "testnets") { if (chainId === "mainnets") { return helpers.getBaseUrl(1); diff --git a/packages/web/package.json b/packages/web/package.json index cc5e3cd0..0c9be151 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -32,6 +32,7 @@ "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-tooltip": "^1.0.7", "@tableland/nonce": "^0.0.0-pre.1", From f69c13aeb620cde977fed495c981d6027bf408ea Mon Sep 17 00:00:00 2001 From: Aaron Sutula Date: Fri, 1 Mar 2024 21:45:24 -0600 Subject: [PATCH 02/13] lint fixes Signed-off-by: Aaron Sutula --- packages/web/components/sql-logs.tsx | 15 +++++++-------- packages/web/components/tableland-table.tsx | 4 ++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/web/components/sql-logs.tsx b/packages/web/components/sql-logs.tsx index 42566336..59eba802 100644 --- a/packages/web/components/sql-logs.tsx +++ b/packages/web/components/sql-logs.tsx @@ -1,12 +1,12 @@ "use client"; -import { Paginator } from "./paginator"; -import { Button } from "./ui/button"; -import { cn } from "@/lib/utils"; -import { SqlLog, getSqlLogs } from "@/lib/validator-queries"; import { AlertCircle, RefreshCw } from "lucide-react"; import Link from "next/link"; import { useEffect, useState } from "react"; +import { Paginator } from "./paginator"; +import { Button } from "./ui/button"; +import { cn } from "@/lib/utils"; +import { type SqlLog, getSqlLogs } from "@/lib/validator-queries"; export default function SQLLogs({ chain, @@ -21,16 +21,15 @@ export default function SQLLogs({ const [page, setPage] = useState(0); const [loadedPage, setLoadedPage] = useState(-1); const [maxLoadedPage, setMaxLoadedPage] = useState(-1); - const [pageSize] = useState(20); + const [pageSize] = useState(10); useEffect(() => { async function loadLogs() { if (page <= maxLoadedPage) { setLoadedPage(page); return; } - let beforeTimestamp = !!logs.length - ? logs[logs.length - 1].timestamp - : undefined; + let beforeTimestamp = + logs.length > 0 ? logs[logs.length - 1].timestamp : undefined; if (beforeTimestamp && maxLoadedPage === -1) { beforeTimestamp = undefined; } diff --git a/packages/web/components/tableland-table.tsx b/packages/web/components/tableland-table.tsx index b2f540c7..6859b668 100644 --- a/packages/web/components/tableland-table.tsx +++ b/packages/web/components/tableland-table.tsx @@ -5,11 +5,11 @@ import TimeAgo from "javascript-time-ago"; import { Blocks, Coins, Hash, Rocket, Table2 } from "lucide-react"; import Link from "next/link"; import { DataTable } from "../app/[team]/[project]/(project)/deployments/[[...slug]]/_components/data-table"; -import { openSeaLinks } from "@/lib/open-sea"; -import { blockExplorers } from "@/lib/block-explorers"; import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; import SQLLogs from "./sql-logs"; +import { blockExplorers } from "@/lib/block-explorers"; +import { openSeaLinks } from "@/lib/open-sea"; const timeAgo = new TimeAgo("en-US"); From 4f1bc39f0ecaafd8c7627d1aaccfc131cdbe9c38 Mon Sep 17 00:00:00 2001 From: Aaron Sutula Date: Mon, 4 Mar 2024 16:09:25 -0600 Subject: [PATCH 03/13] txn page Signed-off-by: Aaron Sutula --- .../txn/[txnHash]/_components/card.tsx | 71 ++++++++ .../[txnHash]/_components/explorer-button.tsx | 24 +++ .../chain/[chainId]/txn/[txnHash]/page.tsx | 157 ++++++++++++++++++ packages/web/components/address-display.tsx | 36 +++- packages/web/components/sql-logs.tsx | 2 +- packages/web/lib/validator-queries.ts | 34 ++++ 6 files changed, 315 insertions(+), 9 deletions(-) create mode 100644 packages/web/app/chain/[chainId]/txn/[txnHash]/_components/card.tsx create mode 100644 packages/web/app/chain/[chainId]/txn/[txnHash]/_components/explorer-button.tsx create mode 100644 packages/web/app/chain/[chainId]/txn/[txnHash]/page.tsx diff --git a/packages/web/app/chain/[chainId]/txn/[txnHash]/_components/card.tsx b/packages/web/app/chain/[chainId]/txn/[txnHash]/_components/card.tsx new file mode 100644 index 00000000..a58ca457 --- /dev/null +++ b/packages/web/app/chain/[chainId]/txn/[txnHash]/_components/card.tsx @@ -0,0 +1,71 @@ +import { cn } from "@/lib/utils"; +import React, { ComponentPropsWithoutRef } from "react"; + +export function Card({ + children, + className, + ...rest +}: ComponentPropsWithoutRef<"div">) { + return ( +
+ {children} +
+ ); +} + +export function CardTitle({ + children, + className, + ...rest +}: ComponentPropsWithoutRef<"div">) { + return ( +
+ {children} +
+ ); +} + +export function CardContent({ + children, + className, + ...rest +}: ComponentPropsWithoutRef<"div">) { + return ( +
+ {children} +
+ ); +} + +export function CardMainContent({ + children, + className, + ...rest +}: ComponentPropsWithoutRef<"div">) { + return ( +
+ {children} +
+ ); +} + +export function CardSubContent({ + children, + className, + ...rest +}: ComponentPropsWithoutRef<"div">) { + return ( +
+ {children} +
+ ); +} diff --git a/packages/web/app/chain/[chainId]/txn/[txnHash]/_components/explorer-button.tsx b/packages/web/app/chain/[chainId]/txn/[txnHash]/_components/explorer-button.tsx new file mode 100644 index 00000000..1edcf46c --- /dev/null +++ b/packages/web/app/chain/[chainId]/txn/[txnHash]/_components/explorer-button.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { ExternalLink } from "lucide-react"; + +export default function ExplorerButton({ + explorerName, + txnUrl, +}: { + explorerName: string; + txnUrl: string; +}) { + return ( + + ); +} diff --git a/packages/web/app/chain/[chainId]/txn/[txnHash]/page.tsx b/packages/web/app/chain/[chainId]/txn/[txnHash]/page.tsx new file mode 100644 index 00000000..4f2dff26 --- /dev/null +++ b/packages/web/app/chain/[chainId]/txn/[txnHash]/page.tsx @@ -0,0 +1,157 @@ +import AddressDisplay from "@/components/address-display"; +import { chainsMap } from "@/lib/chains-map"; +import { cn } from "@/lib/utils"; +import { getSqlLog } from "@/lib/validator-queries"; +import TimeAgo from "javascript-time-ago"; +import { AlertCircle, ExternalLink } from "lucide-react"; +import { notFound } from "next/navigation"; +import { + Card, + CardContent, + CardMainContent, + CardSubContent, + CardTitle, +} from "./_components/card"; +import { blockExplorers } from "@/lib/block-explorers"; +import ExplorerButton from "./_components/explorer-button"; +import Link from "next/link"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +const timeAgo = new TimeAgo("en-US"); + +export default async function TxnPage({ + params, +}: { + params: { chainId: string; txnHash: string }; +}) { + const chainNumber = parseInt(params.chainId, 10); + const log = await getSqlLog(chainNumber, params.txnHash); + + const chain = chainsMap.get(chainNumber); + + if (!chain) { + notFound(); + } + + const explorer = blockExplorers.get(chainNumber); + + return ( +
+
+ {log.error && ( + + + + + + Txn Status: Error + + + )} + + {explorer && ( + + )} +
+
+ + Sent by + + + + + + + + Timestamp + + + {timeAgo.format(log.timestamp * 1000)} + + + {new Date(log.timestamp * 1000).toLocaleString()} + + + + + Status + + {log.error ? "Error" : "Success"} + + + + Chain ID + + {chainNumber} + {chain.name} + + + + Block Number + + {log.blockNumber} + {explorer && ( + + + View on {explorer.explorer} + + + )} + + + + Event Index + + {log.eventIndex} + + + + Event Type + + + {log.eventType === "ContractCreateTable" + ? "Create Table" + : "Run SQL"} + + + +
+
+ +
+          {log.statement}
+        
+
+ {log.error && ( +
+ +
+            {log.error}
+          
+
+ )} +
+ ); +} diff --git a/packages/web/components/address-display.tsx b/packages/web/components/address-display.tsx index c04d7a9a..a0e53441 100644 --- a/packages/web/components/address-display.tsx +++ b/packages/web/components/address-display.tsx @@ -9,12 +9,17 @@ import { TooltipTrigger, } from "./ui/tooltip"; import { useToast } from "@/components/ui/use-toast"; +import { HTMLProps } from "react"; +import { cn } from "@/lib/utils"; export default function AddressDisplay({ address, numCharacters = 5, copy = false, -}: { + name, + className, + ...rest +}: HTMLProps & { address: string; numCharacters?: number; copy?: boolean; @@ -30,13 +35,15 @@ export default function AddressDisplay({ .then(function () { toast({ title: "Done!", - description: "The address has been copied to your clipboard.", + description: `The ${ + name || "address" + } has been copied to your clipboard.`, duration: 2000, }); }) .catch(function (err) { const errMessage = [ - "Could not copy the address to your clipboard.", + `Could not copy the ${name || "address"} to your clipboard.`, typeof err.message === "string" ? err.message : undefined, ] .filter((s) => s) @@ -52,9 +59,22 @@ export default function AddressDisplay({ return (
-

- {address.slice(0, numCharacters)}...{address.slice(-numCharacters)} -

+ + + + + {address.slice(0, numCharacters)}... + {address.slice(-numCharacters)} + + + +

{address}

+
+
+
{copy && ( @@ -65,11 +85,11 @@ export default function AddressDisplay({ onClick={handleCopy} > - Copy address + Copy {name || "address"} -

Click to copy address

+

Click to copy {name || "address"}

diff --git a/packages/web/components/sql-logs.tsx b/packages/web/components/sql-logs.tsx index 59eba802..77276cca 100644 --- a/packages/web/components/sql-logs.tsx +++ b/packages/web/components/sql-logs.tsx @@ -73,7 +73,7 @@ export default function SQLLogs({ "flex items-center gap-4 rounded-sm border border-gray-200 p-2 transition-all", log.error ? "bg-red-200 hover:bg-red-300" : "hover:bg-accent", )} - href={``} + href={`/chain/${chain}/txn/${log.txHash}`} > {log.error && }
diff --git a/packages/web/lib/validator-queries.ts b/packages/web/lib/validator-queries.ts index 54b2920d..16044130 100644 --- a/packages/web/lib/validator-queries.ts +++ b/packages/web/lib/validator-queries.ts @@ -116,6 +116,40 @@ export async function getSqlLogs( return res.json() as unknown as SqlLog[]; } +export async function getSqlLog(chainId: number, txnHash: string) { + const query = ` + SELECT + e.block_number as blockNumber, + e.event_index as eventIndex, + tx_hash as txHash, + event_type as eventType, + json_extract(event_json,'$.Caller') as caller, + json_extract(event_json,'$.Statement') as statement, + error, + timestamp + FROM + system_evm_events e join system_evm_blocks b on e.block_number = b.block_number and e.chain_id = b.chain_id inner join system_txn_receipts tr on tr.txn_hash = e.tx_hash AND tr.chain_id = e.chain_id + WHERE + txHash = '${txnHash}' AND + e.chain_id = ${chainId} + LIMIT 1 + `; + + const uri = encodeURI(`${baseUrlForChain(chainId)}/query?statement=${query}`); + const res = await fetch(uri); + + if (!res.ok) { + // This will activate the closest `error.js` Error Boundary + throw new Error("Failed to fetch data"); + } + + const array = (await res.json()) as unknown as SqlLog[]; + if (array.length === 0) { + throw new Error("Log not found"); + } + return array[0]; +} + function baseUrlForChain(chainId: number | "mainnets" | "testnets") { if (chainId === "mainnets") { return helpers.getBaseUrl(1); From 01e28c45675650e27401dec3970169398aa04a92 Mon Sep 17 00:00:00 2001 From: Aaron Sutula Date: Mon, 4 Mar 2024 16:12:11 -0600 Subject: [PATCH 04/13] make table data the default tab Signed-off-by: Aaron Sutula --- packages/web/components/tableland-table.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/components/tableland-table.tsx b/packages/web/components/tableland-table.tsx index 6859b668..08674ae9 100644 --- a/packages/web/components/tableland-table.tsx +++ b/packages/web/components/tableland-table.tsx @@ -177,7 +177,7 @@ export default async function TablelandTable({ )}
- + Table Data SQL Logs From b4e0f4979012871cf4d7015fbae30307b3a11408 Mon Sep 17 00:00:00 2001 From: Aaron Sutula Date: Mon, 4 Mar 2024 16:23:38 -0600 Subject: [PATCH 05/13] lint fixes Signed-off-by: Aaron Sutula --- .../[chainId]/txn/[txnHash]/_components/card.tsx | 2 +- .../txn/[txnHash]/_components/explorer-button.tsx | 2 +- .../web/app/chain/[chainId]/txn/[txnHash]/page.tsx | 14 +++++++------- packages/web/components/address-display.tsx | 10 +++++----- packages/web/components/sql-logs.tsx | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/web/app/chain/[chainId]/txn/[txnHash]/_components/card.tsx b/packages/web/app/chain/[chainId]/txn/[txnHash]/_components/card.tsx index a58ca457..290b7e08 100644 --- a/packages/web/app/chain/[chainId]/txn/[txnHash]/_components/card.tsx +++ b/packages/web/app/chain/[chainId]/txn/[txnHash]/_components/card.tsx @@ -1,5 +1,5 @@ +import React, { type ComponentPropsWithoutRef } from "react"; import { cn } from "@/lib/utils"; -import React, { ComponentPropsWithoutRef } from "react"; export function Card({ children, diff --git a/packages/web/app/chain/[chainId]/txn/[txnHash]/_components/explorer-button.tsx b/packages/web/app/chain/[chainId]/txn/[txnHash]/_components/explorer-button.tsx index 1edcf46c..a695faf3 100644 --- a/packages/web/app/chain/[chainId]/txn/[txnHash]/_components/explorer-button.tsx +++ b/packages/web/app/chain/[chainId]/txn/[txnHash]/_components/explorer-button.tsx @@ -1,7 +1,7 @@ "use client"; -import { Button } from "@/components/ui/button"; import { ExternalLink } from "lucide-react"; +import { Button } from "@/components/ui/button"; export default function ExplorerButton({ explorerName, diff --git a/packages/web/app/chain/[chainId]/txn/[txnHash]/page.tsx b/packages/web/app/chain/[chainId]/txn/[txnHash]/page.tsx index 4f2dff26..7c726f0c 100644 --- a/packages/web/app/chain/[chainId]/txn/[txnHash]/page.tsx +++ b/packages/web/app/chain/[chainId]/txn/[txnHash]/page.tsx @@ -1,10 +1,7 @@ -import AddressDisplay from "@/components/address-display"; -import { chainsMap } from "@/lib/chains-map"; -import { cn } from "@/lib/utils"; -import { getSqlLog } from "@/lib/validator-queries"; import TimeAgo from "javascript-time-ago"; -import { AlertCircle, ExternalLink } from "lucide-react"; +import { AlertCircle } from "lucide-react"; import { notFound } from "next/navigation"; +import Link from "next/link"; import { Card, CardContent, @@ -12,9 +9,12 @@ import { CardSubContent, CardTitle, } from "./_components/card"; -import { blockExplorers } from "@/lib/block-explorers"; import ExplorerButton from "./_components/explorer-button"; -import Link from "next/link"; +import AddressDisplay from "@/components/address-display"; +import { chainsMap } from "@/lib/chains-map"; +import { cn } from "@/lib/utils"; +import { getSqlLog } from "@/lib/validator-queries"; +import { blockExplorers } from "@/lib/block-explorers"; import { Tooltip, TooltipContent, diff --git a/packages/web/components/address-display.tsx b/packages/web/components/address-display.tsx index a0e53441..392dbebb 100644 --- a/packages/web/components/address-display.tsx +++ b/packages/web/components/address-display.tsx @@ -1,6 +1,7 @@ "use client"; import { Copy } from "lucide-react"; +import { type HTMLProps } from "react"; import { Button } from "./ui/button"; import { Tooltip, @@ -9,7 +10,6 @@ import { TooltipTrigger, } from "./ui/tooltip"; import { useToast } from "@/components/ui/use-toast"; -import { HTMLProps } from "react"; import { cn } from "@/lib/utils"; export default function AddressDisplay({ @@ -36,14 +36,14 @@ export default function AddressDisplay({ toast({ title: "Done!", description: `The ${ - name || "address" + name ?? "address" } has been copied to your clipboard.`, duration: 2000, }); }) .catch(function (err) { const errMessage = [ - `Could not copy the ${name || "address"} to your clipboard.`, + `Could not copy the ${name ?? "address"} to your clipboard.`, typeof err.message === "string" ? err.message : undefined, ] .filter((s) => s) @@ -85,11 +85,11 @@ export default function AddressDisplay({ onClick={handleCopy} > - Copy {name || "address"} + Copy {name ?? "address"} -

Click to copy {name || "address"}

+

Click to copy {name ?? "address"}

diff --git a/packages/web/components/sql-logs.tsx b/packages/web/components/sql-logs.tsx index 77276cca..8b86cdc2 100644 --- a/packages/web/components/sql-logs.tsx +++ b/packages/web/components/sql-logs.tsx @@ -45,7 +45,7 @@ export default function SQLLogs({ setLoading(false); setError(err.message); }); - }, [page, pageSize, maxLoadedPage]); + }, [page, pageSize, maxLoadedPage, chain, logs, tableId]); function refresh() { setMaxLoadedPage(-1); From 818ad55214ebca087d5511f4bfe251b379641b12 Mon Sep 17 00:00:00 2001 From: Aaron Sutula Date: Mon, 4 Mar 2024 17:05:51 -0600 Subject: [PATCH 06/13] open log page in new tab to not loose state of sql logs list Signed-off-by: Aaron Sutula --- packages/web/components/sql-logs.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web/components/sql-logs.tsx b/packages/web/components/sql-logs.tsx index 8b86cdc2..094843fb 100644 --- a/packages/web/components/sql-logs.tsx +++ b/packages/web/components/sql-logs.tsx @@ -74,6 +74,7 @@ export default function SQLLogs({ log.error ? "bg-red-200 hover:bg-red-300" : "hover:bg-accent", )} href={`/chain/${chain}/txn/${log.txHash}`} + target="_blank" > {log.error && }
From 9b8c0df496dd85acb9b6dad6b02627e5215f61c9 Mon Sep 17 00:00:00 2001 From: Aaron Sutula Date: Mon, 4 Mar 2024 21:47:36 -0600 Subject: [PATCH 07/13] css fix Signed-off-by: Aaron Sutula --- packages/web/app/chain/[chainId]/txn/[txnHash]/page.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/web/app/chain/[chainId]/txn/[txnHash]/page.tsx b/packages/web/app/chain/[chainId]/txn/[txnHash]/page.tsx index 7c726f0c..dd824af0 100644 --- a/packages/web/app/chain/[chainId]/txn/[txnHash]/page.tsx +++ b/packages/web/app/chain/[chainId]/txn/[txnHash]/page.tsx @@ -41,7 +41,7 @@ export default async function TxnPage({ const explorer = blockExplorers.get(chainNumber); return ( -
+
{log.error && ( @@ -58,7 +58,10 @@ export default async function TxnPage({ name="txn hash" numCharacters={8} copy - className={cn("text-3xl font-bold", log.error && "text-red-500")} + className={cn( + "text-3xl font-bold text-foreground", + log.error && "text-red-500", + )} /> {explorer && ( From 2313238a20c63e0ba02e38eab6baac56918bebb3 Mon Sep 17 00:00:00 2001 From: Aaron Sutula Date: Tue, 5 Mar 2024 14:52:31 -0600 Subject: [PATCH 08/13] make event idx part of the sql log query and display Signed-off-by: Aaron Sutula --- .../{ => log/[index]}/_components/card.tsx | 0 .../[index]}/_components/explorer-button.tsx | 0 .../txn/[txnHash]/{ => log/[index]}/page.tsx | 34 +++++++++++-------- packages/web/components/sql-logs.tsx | 2 +- packages/web/lib/validator-queries.ts | 10 +++++- 5 files changed, 30 insertions(+), 16 deletions(-) rename packages/web/app/chain/[chainId]/txn/[txnHash]/{ => log/[index]}/_components/card.tsx (100%) rename packages/web/app/chain/[chainId]/txn/[txnHash]/{ => log/[index]}/_components/explorer-button.tsx (100%) rename packages/web/app/chain/[chainId]/txn/[txnHash]/{ => log/[index]}/page.tsx (84%) diff --git a/packages/web/app/chain/[chainId]/txn/[txnHash]/_components/card.tsx b/packages/web/app/chain/[chainId]/txn/[txnHash]/log/[index]/_components/card.tsx similarity index 100% rename from packages/web/app/chain/[chainId]/txn/[txnHash]/_components/card.tsx rename to packages/web/app/chain/[chainId]/txn/[txnHash]/log/[index]/_components/card.tsx diff --git a/packages/web/app/chain/[chainId]/txn/[txnHash]/_components/explorer-button.tsx b/packages/web/app/chain/[chainId]/txn/[txnHash]/log/[index]/_components/explorer-button.tsx similarity index 100% rename from packages/web/app/chain/[chainId]/txn/[txnHash]/_components/explorer-button.tsx rename to packages/web/app/chain/[chainId]/txn/[txnHash]/log/[index]/_components/explorer-button.tsx diff --git a/packages/web/app/chain/[chainId]/txn/[txnHash]/page.tsx b/packages/web/app/chain/[chainId]/txn/[txnHash]/log/[index]/page.tsx similarity index 84% rename from packages/web/app/chain/[chainId]/txn/[txnHash]/page.tsx rename to packages/web/app/chain/[chainId]/txn/[txnHash]/log/[index]/page.tsx index dd824af0..b195812d 100644 --- a/packages/web/app/chain/[chainId]/txn/[txnHash]/page.tsx +++ b/packages/web/app/chain/[chainId]/txn/[txnHash]/log/[index]/page.tsx @@ -27,10 +27,10 @@ const timeAgo = new TimeAgo("en-US"); export default async function TxnPage({ params, }: { - params: { chainId: string; txnHash: string }; + params: { chainId: string; txnHash: string; index: number }; }) { const chainNumber = parseInt(params.chainId, 10); - const log = await getSqlLog(chainNumber, params.txnHash); + const log = await getSqlLog(chainNumber, params.txnHash, params.index); const chain = chainsMap.get(chainNumber); @@ -53,16 +53,22 @@ export default async function TxnPage({ )} - +
+ +
+ Log index:{" "} + {log.eventIndex} +
+
{explorer && ( - Event Index + Txn Index - {log.eventIndex} + {log.txIndex} diff --git a/packages/web/components/sql-logs.tsx b/packages/web/components/sql-logs.tsx index 094843fb..13006a01 100644 --- a/packages/web/components/sql-logs.tsx +++ b/packages/web/components/sql-logs.tsx @@ -73,7 +73,7 @@ export default function SQLLogs({ "flex items-center gap-4 rounded-sm border border-gray-200 p-2 transition-all", log.error ? "bg-red-200 hover:bg-red-300" : "hover:bg-accent", )} - href={`/chain/${chain}/txn/${log.txHash}`} + href={`/chain/${chain}/txn/${log.txHash}/log/${log.eventIndex}`} target="_blank" > {log.error && } diff --git a/packages/web/lib/validator-queries.ts b/packages/web/lib/validator-queries.ts index 16044130..43f7c2e8 100644 --- a/packages/web/lib/validator-queries.ts +++ b/packages/web/lib/validator-queries.ts @@ -67,6 +67,7 @@ export async function getPopularTables( export interface SqlLog { blockNumber: number; + txIndex: number; caller: string; error: string | null; eventIndex: number; @@ -86,6 +87,7 @@ export async function getSqlLogs( const query = ` SELECT e.block_number as blockNumber, + e.tx_index as txIndex, e.event_index as eventIndex, tx_hash as txHash, event_type as eventType, @@ -116,10 +118,15 @@ export async function getSqlLogs( return res.json() as unknown as SqlLog[]; } -export async function getSqlLog(chainId: number, txnHash: string) { +export async function getSqlLog( + chainId: number, + txnHash: string, + eventIndex: number, +) { const query = ` SELECT e.block_number as blockNumber, + e.tx_index as txIndex, e.event_index as eventIndex, tx_hash as txHash, event_type as eventType, @@ -131,6 +138,7 @@ export async function getSqlLog(chainId: number, txnHash: string) { system_evm_events e join system_evm_blocks b on e.block_number = b.block_number and e.chain_id = b.chain_id inner join system_txn_receipts tr on tr.txn_hash = e.tx_hash AND tr.chain_id = e.chain_id WHERE txHash = '${txnHash}' AND + eventIndex = ${eventIndex} AND e.chain_id = ${chainId} LIMIT 1 `; From be22aa5b4adcba3a9c865dfd6118c9725baed671 Mon Sep 17 00:00:00 2001 From: Aaron Sutula Date: Wed, 6 Mar 2024 10:57:05 -0600 Subject: [PATCH 09/13] use search params for sql log page Signed-off-by: Aaron Sutula --- .../log/[index] => sql-log}/_components/card.tsx | 0 .../_components/explorer-button.tsx | 0 .../txn/[txnHash]/log/[index] => sql-log}/page.tsx | 14 +++++++++----- packages/web/components/sql-logs.tsx | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) rename packages/web/app/{chain/[chainId]/txn/[txnHash]/log/[index] => sql-log}/_components/card.tsx (100%) rename packages/web/app/{chain/[chainId]/txn/[txnHash]/log/[index] => sql-log}/_components/explorer-button.tsx (100%) rename packages/web/app/{chain/[chainId]/txn/[txnHash]/log/[index] => sql-log}/page.tsx (94%) diff --git a/packages/web/app/chain/[chainId]/txn/[txnHash]/log/[index]/_components/card.tsx b/packages/web/app/sql-log/_components/card.tsx similarity index 100% rename from packages/web/app/chain/[chainId]/txn/[txnHash]/log/[index]/_components/card.tsx rename to packages/web/app/sql-log/_components/card.tsx diff --git a/packages/web/app/chain/[chainId]/txn/[txnHash]/log/[index]/_components/explorer-button.tsx b/packages/web/app/sql-log/_components/explorer-button.tsx similarity index 100% rename from packages/web/app/chain/[chainId]/txn/[txnHash]/log/[index]/_components/explorer-button.tsx rename to packages/web/app/sql-log/_components/explorer-button.tsx diff --git a/packages/web/app/chain/[chainId]/txn/[txnHash]/log/[index]/page.tsx b/packages/web/app/sql-log/page.tsx similarity index 94% rename from packages/web/app/chain/[chainId]/txn/[txnHash]/log/[index]/page.tsx rename to packages/web/app/sql-log/page.tsx index b195812d..88089aff 100644 --- a/packages/web/app/chain/[chainId]/txn/[txnHash]/log/[index]/page.tsx +++ b/packages/web/app/sql-log/page.tsx @@ -25,12 +25,16 @@ import { const timeAgo = new TimeAgo("en-US"); export default async function TxnPage({ - params, + searchParams, }: { - params: { chainId: string; txnHash: string; index: number }; + searchParams: { chainId: string; txnHash: string; index: number }; }) { - const chainNumber = parseInt(params.chainId, 10); - const log = await getSqlLog(chainNumber, params.txnHash, params.index); + const chainNumber = parseInt(searchParams.chainId, 10); + const log = await getSqlLog( + chainNumber, + searchParams.txnHash, + searchParams.index, + ); const chain = chainsMap.get(chainNumber); @@ -72,7 +76,7 @@ export default async function TxnPage({ {explorer && ( )}
diff --git a/packages/web/components/sql-logs.tsx b/packages/web/components/sql-logs.tsx index 13006a01..5115f2f6 100644 --- a/packages/web/components/sql-logs.tsx +++ b/packages/web/components/sql-logs.tsx @@ -73,7 +73,7 @@ export default function SQLLogs({ "flex items-center gap-4 rounded-sm border border-gray-200 p-2 transition-all", log.error ? "bg-red-200 hover:bg-red-300" : "hover:bg-accent", )} - href={`/chain/${chain}/txn/${log.txHash}/log/${log.eventIndex}`} + href={`/sql-log?chainId=${chain}&txnHash=${log.txHash}&index=${log.eventIndex}`} target="_blank" > {log.error && } From 88dfc480b2d5a76f1cd24b1f0c00ef0823abfbce Mon Sep 17 00:00:00 2001 From: Aaron Sutula Date: Wed, 6 Mar 2024 13:22:10 -0600 Subject: [PATCH 10/13] use our metric-card customization of the shadcn card Signed-off-by: Aaron Sutula --- packages/web/app/sql-log/_components/card.tsx | 71 --------- packages/web/app/sql-log/page.tsx | 148 +++++++++--------- packages/web/components/address-display.tsx | 4 +- packages/web/components/metric-card.tsx | 105 +++++++++++++ 4 files changed, 184 insertions(+), 144 deletions(-) delete mode 100644 packages/web/app/sql-log/_components/card.tsx create mode 100644 packages/web/components/metric-card.tsx diff --git a/packages/web/app/sql-log/_components/card.tsx b/packages/web/app/sql-log/_components/card.tsx deleted file mode 100644 index 290b7e08..00000000 --- a/packages/web/app/sql-log/_components/card.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { type ComponentPropsWithoutRef } from "react"; -import { cn } from "@/lib/utils"; - -export function Card({ - children, - className, - ...rest -}: ComponentPropsWithoutRef<"div">) { - return ( -
- {children} -
- ); -} - -export function CardTitle({ - children, - className, - ...rest -}: ComponentPropsWithoutRef<"div">) { - return ( -
- {children} -
- ); -} - -export function CardContent({ - children, - className, - ...rest -}: ComponentPropsWithoutRef<"div">) { - return ( -
- {children} -
- ); -} - -export function CardMainContent({ - children, - className, - ...rest -}: ComponentPropsWithoutRef<"div">) { - return ( -
- {children} -
- ); -} - -export function CardSubContent({ - children, - className, - ...rest -}: ComponentPropsWithoutRef<"div">) { - return ( -
- {children} -
- ); -} diff --git a/packages/web/app/sql-log/page.tsx b/packages/web/app/sql-log/page.tsx index 88089aff..94d12e39 100644 --- a/packages/web/app/sql-log/page.tsx +++ b/packages/web/app/sql-log/page.tsx @@ -3,12 +3,12 @@ import { AlertCircle } from "lucide-react"; import { notFound } from "next/navigation"; import Link from "next/link"; import { - Card, - CardContent, - CardMainContent, - CardSubContent, - CardTitle, -} from "./_components/card"; + MetricCard, + MetricCardContent, + MetricCardFooter, + MetricCardHeader, + MetricCardTitle, +} from "@/components/metric-card"; import ExplorerButton from "./_components/explorer-button"; import AddressDisplay from "@/components/address-display"; import { chainsMap } from "@/lib/chains-map"; @@ -81,71 +81,77 @@ export default async function TxnPage({ )}
- - Sent by - - - - - - - - Timestamp - - - {timeAgo.format(log.timestamp * 1000)} - - - {new Date(log.timestamp * 1000).toLocaleString()} - - - - - Status - - {log.error ? "Error" : "Success"} - - - - Chain ID - - {chainNumber} - {chain.name} - - - - Block Number - - {log.blockNumber} - {explorer && ( - - - View on {explorer.explorer} - - - )} - - - - Txn Index - - {log.txIndex} - - - - Event Type - - - {log.eventType === "ContractCreateTable" - ? "Create Table" - : "Run SQL"} - - - + + + Sent by + + + + + + + + Timestamp + + + {timeAgo.format(log.timestamp * 1000)} + + + {new Date(log.timestamp * 1000).toLocaleString()} + + + + + Status + + + {log.error ? "Error" : "Success"} + + + + + Chain Id + + {chainNumber} + {chain.name} + + + + + Block Number + + + {log.blockNumber} + {explorer && ( + + + View on {explorer.explorer} + + + )} + + + + Txn Index + + {log.txIndex} + + + + + Event Type + + + + {log.eventType === "ContractCreateTable" + ? "Create Table" + : "Run SQL"} + +