Skip to content

Commit

Permalink
feat(explorer): copy button (#3423)
Browse files Browse the repository at this point in the history
  • Loading branch information
karooolis authored Jan 17, 2025
1 parent bf5e220 commit 54e5c06
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/five-berries-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/explorer": patch
---

Added 'Copy to Clipboard' button to relevant sections for easier data copying.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DownloadIcon } from "lucide-react";
import { stringify } from "viem";
import { Button } from "../../../../../../components/ui/Button";
import {
DropdownMenu,
Expand Down Expand Up @@ -29,7 +30,7 @@ export function ExportButton({ tableData, isLoading }: { tableData?: TData; isLo
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
const json = JSON.stringify(tableData?.rows, null, 2);
const json = stringify(tableData?.rows, null, 2);
exportTableData(json, "data.json", "application/json");
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import { Coins, ExternalLinkIcon, Eye, LoaderIcon, Send } from "lucide-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { toast } from "sonner";
import { Abi, AbiFunction, Address, Hex, decodeEventLog } from "viem";
import { Abi, AbiFunction, Address, Hex, decodeEventLog, stringify } from "viem";
import { useAccount, useConfig } from "wagmi";
import { readContract, waitForTransactionReceipt, writeContract } from "wagmi/actions";
import { z } from "zod";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useConnectModal } from "@rainbow-me/rainbowkit";
import { CopyButton } from "../../../../../../components/CopyButton";
import { Button } from "../../../../../../components/ui/Button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../../../../../../components/ui/Form";
import { Input } from "../../../../../../components/ui/Input";
Expand Down Expand Up @@ -79,7 +80,7 @@ export function FunctionField({ worldAbi, functionAbi }: Props) {
chainId,
});

setResult(JSON.stringify(result, null, 2));
setResult(stringify(result, null, 2));
} else {
toastId = toast.loading("Transaction submitted");
const txHash = await writeContract(wagmiConfig, {
Expand Down Expand Up @@ -167,9 +168,14 @@ export function FunctionField({ worldAbi, functionAbi }: Props) {
</form>
</Form>

{result && <pre className="text-md mt-4 rounded border p-3 text-sm">{result}</pre>}
{result && (
<pre className="text-md relative mt-4 rounded border p-3 text-sm">
{result}
<CopyButton value={result} className="absolute right-1.5 top-1.5" />
</pre>
)}
{events && (
<div className="mt-4 flex-grow break-all border border-white/20 p-2 pb-3">
<div className="relative mt-4 flex-grow break-all rounded border border-white/20 p-2 pb-3">
<ul>
{events.map((event, idx) => (
<li key={idx}>
Expand All @@ -188,6 +194,8 @@ export function FunctionField({ worldAbi, functionAbi }: Props) {
</li>
))}
</ul>

<CopyButton value={stringify(events, null, 2)} className="absolute right-1.5 top-1.5" />
</div>
)}
{txUrl && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { Coins, Eye, Send } from "lucide-react";
import { useQueryState } from "nuqs";
import { AbiFunction } from "viem";
import { AbiFunction, stringify } from "viem";
import { useDeferredValue, useMemo } from "react";
import { Input } from "../../../../../../components/ui/Input";
import { Separator } from "../../../../../../components/ui/Separator";
Expand Down Expand Up @@ -92,7 +92,7 @@ export function InteractForm() {

{data?.abi &&
filteredFunctions.map((abi) => (
<FunctionField key={JSON.stringify(abi)} worldAbi={data.abi} functionAbi={abi} />
<FunctionField key={stringify(abi)} worldAbi={data.abi} functionAbi={abi} />
))}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { formatEther } from "viem";
import { formatEther, stringify } from "viem";
import { Row, flexRender } from "@tanstack/react-table";
import { CopyButton } from "../../../../../../components/CopyButton";
import { Separator } from "../../../../../../components/ui/Separator";
import { Skeleton } from "../../../../../../components/ui/Skeleton";
import { TableCell, TableRow } from "../../../../../../components/ui/Table";
Expand Down Expand Up @@ -85,22 +86,20 @@ export function TransactionTableRow({ row }: { row: Row<ObservedTransaction> })
<div className="flex items-start gap-x-4">
<h3 className="w-[45px] flex-shrink-0 text-2xs font-bold uppercase">Inputs</h3>

<div className="flex w-full flex-col gap-y-4">
<div className="relative flex w-full flex-col gap-y-4">
{calls.map((call, idx) => {
if (!call.args || call.args.length === 0) {
return null;
}

return (
<div key={idx} className="min-w-0 flex-grow border border-white/20 p-2 pt-1">
<div key={idx} className="min-w-0 flex-grow rounded border border-white/20 p-2 pt-1">
<span className="text-xs">{call.functionName}:</span>
{call.args?.map((arg, argIdx) => (
<div key={argIdx} className="flex">
<span className="flex-shrink-0 text-xs text-white/60">arg {argIdx + 1}:</span>
<span className="ml-2 break-all text-xs">
{typeof arg === "object" && arg !== null
? JSON.stringify(arg, null, 2)
: String(arg)}
{typeof arg === "object" && arg !== null ? stringify(arg, null, 2) : String(arg)}
</span>
</div>
))}
Expand All @@ -111,6 +110,8 @@ export function TransactionTableRow({ row }: { row: Row<ObservedTransaction> })
</div>
);
})}

<CopyButton value={stringify(data.calls, null, 2)} className="absolute right-1.5 top-1.5" />
</div>
</div>
</>
Expand All @@ -121,7 +122,7 @@ export function TransactionTableRow({ row }: { row: Row<ObservedTransaction> })
<Separator className="my-5" />
<div className="flex items-start gap-x-4">
<h3 className="w-[45px] flex-shrink-0 text-2xs font-bold uppercase">Error</h3>
<div className="flex-grow whitespace-pre-wrap border border-red-500 p-2 font-mono text-xs">
<div className="flex-grow whitespace-pre-wrap rounded border border-red-500 p-2 font-mono text-xs">
{data.error.message}
</div>
</div>
Expand All @@ -134,7 +135,7 @@ export function TransactionTableRow({ row }: { row: Row<ObservedTransaction> })
<div className="flex items-start gap-x-4">
<h3 className="inline-block w-[45px] flex-shrink-0 text-2xs font-bold uppercase">Logs</h3>
{Array.isArray(logs) && logs.length > 0 ? (
<div className="flex-grow break-all border border-white/20 p-2 pb-3">
<div className="relative flex-grow break-all rounded border border-white/20 p-2 pb-3">
<ul>
{logs.map((log, idx) => {
const eventName = "eventName" in log ? log.eventName : null;
Expand All @@ -157,6 +158,8 @@ export function TransactionTableRow({ row }: { row: Row<ObservedTransaction> })
);
})}
</ul>

<CopyButton value={stringify(logs, null, 2)} className="absolute right-1.5 top-1.5" />
</div>
) : status === "pending" ? (
<Skeleton className="h-4 w-full" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useParams } from "next/navigation";
import { Hex } from "viem";
import { Hex, stringify } from "viem";
import { Table } from "@latticexyz/config";
import { useQuery } from "@tanstack/react-query";
import { useChain } from "../hooks/useChain";
Expand Down Expand Up @@ -32,7 +32,7 @@ export function useTableDataQuery({ table, query, isLiveQuery }: Props) {
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify([
body: stringify([
{
address: worldAddress as Hex,
query: decodedQuery,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useParams } from "next/navigation";
import { Hex } from "viem";
import { Hex, stringify } from "viem";
import { isDefined } from "@latticexyz/common/utils";
import { Table } from "@latticexyz/config";
import mudConfig from "@latticexyz/store/mud.config";
Expand Down Expand Up @@ -29,7 +29,7 @@ export function useTablesQuery() {
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify([
body: stringify([
{
address: worldAddress as Hex,
query,
Expand Down
43 changes: 43 additions & 0 deletions packages/explorer/src/components/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { CheckIcon, ClipboardIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { cn } from "../utils";
import { Button, ButtonProps } from "./ui/Button";

interface CopyButtonProps extends ButtonProps {
value: string;
}

function copyToClipboard(value: string) {
navigator.clipboard.writeText(value);
}

export function CopyButton({ className, variant = "outline", value, ...props }: CopyButtonProps) {
const [hasCopied, setHasCopied] = useState(false);

useEffect(() => {
if (hasCopied) {
setTimeout(() => {
setHasCopied(false);
}, 2000);
}
}, [hasCopied]);

return (
<Button
size="icon"
variant={variant}
className={cn(
"relative z-10 h-6 w-6 border-white/15 bg-transparent text-zinc-50 hover:bg-secondary hover:text-zinc-50 [&_svg]:h-3 [&_svg]:w-3",
className,
)}
onClick={() => {
copyToClipboard(value);
setHasCopied(true);
}}
{...props}
>
<span className="sr-only">Copy</span>
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
</Button>
);
}

0 comments on commit 54e5c06

Please sign in to comment.