diff --git a/apps/dashboard/src/@/components/blocks/charts/bar-chart.tsx b/apps/dashboard/src/@/components/blocks/charts/bar-chart.tsx index 678b46f2c26..42de009738b 100644 --- a/apps/dashboard/src/@/components/blocks/charts/bar-chart.tsx +++ b/apps/dashboard/src/@/components/blocks/charts/bar-chart.tsx @@ -108,6 +108,8 @@ export function ThirdwebBarChart( : [0, 0, 0, 0] : [4, 4, 4, 4] } + strokeWidth={1} + className="stroke-background" /> ))} diff --git a/apps/dashboard/src/@/components/pagination-buttons.stories.tsx b/apps/dashboard/src/@/components/pagination-buttons.stories.tsx new file mode 100644 index 00000000000..9c6d522472b --- /dev/null +++ b/apps/dashboard/src/@/components/pagination-buttons.stories.tsx @@ -0,0 +1,58 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { useState } from "react"; +import { BadgeContainer, mobileViewport } from "../../stories/utils"; +import { PaginationButtons } from "./pagination-buttons"; + +const meta = { + title: "blocks/PaginationButtons", + component: Story, + parameters: { + nextjs: { + appDirectory: true, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Desktop: Story = { + args: {}, +}; + +export const Mobile: Story = { + args: {}, + parameters: { + viewport: mobileViewport("iphone14"), + }, +}; + +function Story() { + return ( +
+ + + + + +
+ ); +} + +function Variant(props: { + label: string; + totalPages: number; +}) { + const [activePage, setActivePage] = useState(1); + return ( + +
+ +
+
+ ); +} diff --git a/apps/dashboard/src/@/components/pagination-buttons.tsx b/apps/dashboard/src/@/components/pagination-buttons.tsx index f0e74925785..0074d246fe2 100644 --- a/apps/dashboard/src/@/components/pagination-buttons.tsx +++ b/apps/dashboard/src/@/components/pagination-buttons.tsx @@ -9,6 +9,11 @@ import { PaginationNext, PaginationPrevious, } from "@/components/ui/pagination"; +import { ArrowUpRightIcon } from "lucide-react"; +import { useState } from "react"; +import { cn } from "../lib/utils"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; export const PaginationButtons = (props: { activePage: number; @@ -16,6 +21,47 @@ export const PaginationButtons = (props: { onPageClick: (page: number) => void; }) => { const { activePage, totalPages, onPageClick: setPage } = props; + const [inputHasError, setInputHasError] = useState(false); + const [pageNumberInput, setPageNumberInput] = useState(""); + + if (totalPages === 1) { + return null; + } + + function handlePageInputSubmit() { + const page = Number(pageNumberInput); + + setInputHasError(false); + if (Number.isInteger(page) && page > 0 && page <= totalPages) { + setPage(page); + setPageNumberInput(""); + } else { + setInputHasError(true); + } + } + + // just render all the page buttons directly + if (totalPages <= 6) { + const pages = [...Array(totalPages)].map((_, i) => i + 1); + return ( + + + {pages.map((page) => ( + + { + setPage(page); + }} + > + {page} + + + ))} + + + ); + } return ( @@ -28,24 +74,28 @@ export const PaginationButtons = (props: { }} /> + + {/* First page + ... */} {activePage - 3 > 0 && ( - - - - )} - {activePage - 2 > 0 && ( - - { - setPage(activePage - 2); - }} - > - {activePage - 2} - - + <> + + { + setPage(1); + }} + > + 1 + + + + + + + )} + {activePage - 1 > 0 && ( - + { setPage(activePage - 1); @@ -55,11 +105,13 @@ export const PaginationButtons = (props: { )} + {activePage} + {activePage + 1 <= totalPages && ( - + { setPage(activePage + 1); @@ -69,22 +121,26 @@ export const PaginationButtons = (props: { )} - {activePage + 2 <= totalPages && ( - - { - setPage(activePage + 2); - }} - > - {activePage + 2} - - - )} + + {/* ... + Last page */} {activePage + 3 <= totalPages && ( - - - + <> + + + + + + { + setPage(totalPages); + }} + > + {totalPages} + + + )} + + +
+ { + setInputHasError(false); + setPageNumberInput(e.target.value); + }} + type="number" + placeholder="Page" + className={cn( + "w-[60px] bg-transparent [appearance:textfield] max-sm:placeholder:text-sm lg:w-[100px] lg:pr-8 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none", + inputHasError && "text-red-500", + )} + onKeyDown={(e) => { + if (e.key === "Enter") { + handlePageInputSubmit(); + } + }} + /> + +
); diff --git a/apps/dashboard/src/@/components/ui/chart.tsx b/apps/dashboard/src/@/components/ui/chart.tsx index 16aacc0eb2d..a452dbddc36 100644 --- a/apps/dashboard/src/@/components/ui/chart.tsx +++ b/apps/dashboard/src/@/components/ui/chart.tsx @@ -186,7 +186,7 @@ const ChartTooltipContent = React.forwardRef<
diff --git a/apps/dashboard/src/@/components/ui/pagination.tsx b/apps/dashboard/src/@/components/ui/pagination.tsx index c8a53d9dee9..3c4f0cd7ca3 100644 --- a/apps/dashboard/src/@/components/ui/pagination.tsx +++ b/apps/dashboard/src/@/components/ui/pagination.tsx @@ -63,11 +63,11 @@ const PaginationPrevious = ({ - Previous + Previous ); PaginationPrevious.displayName = "PaginationPrevious"; @@ -79,10 +79,10 @@ const PaginationNext = ({ - Next + Next ); diff --git a/apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts b/apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts index 4d99b6174ac..410bf2026da 100644 --- a/apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts +++ b/apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts @@ -76,8 +76,8 @@ export const engineKeys = { [...engineKeys.all, address, "instances"] as const, backendWallets: (instance: string) => [...engineKeys.all, instance, "backendWallets"] as const, - transactions: (instance: string) => - [...engineKeys.all, instance, "transactions"] as const, + transactions: (instance: string, params: object) => + [...engineKeys.all, instance, "transactions", params] as const, permissions: (instance: string) => [...engineKeys.all, instance, "permissions"] as const, accessTokens: (instance: string) => diff --git a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts index f081fccb168..43292d66a48 100644 --- a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts +++ b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts @@ -1,12 +1,18 @@ "use client"; import { apiServerProxy } from "@/actions/proxies"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + keepPreviousData, + useMutation, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; import type { ResultItem } from "app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/metrics/components/StatusCodes"; import type { EngineBackendWalletType } from "lib/engine"; import { useState } from "react"; import { useActiveAccount, useActiveWalletChain } from "thirdweb/react"; import invariant from "tiny-invariant"; +import type { EngineStatus } from "../../../app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table"; import { engineKeys } from "../cache-keys"; export type EngineTier = "STARTER" | "PREMIUM" | "ENTERPRISE"; @@ -383,13 +389,29 @@ export function useEngineTransactions(params: { instanceUrl: string; autoUpdate: boolean; authToken: string; + queryParams?: { + limit?: number; + page?: number; + status?: EngineStatus; + }; }) { const { instanceUrl, autoUpdate, authToken } = params; return useQuery({ - queryKey: engineKeys.transactions(instanceUrl), + queryKey: engineKeys.transactions(instanceUrl, params), queryFn: async () => { - const res = await fetch(`${instanceUrl}transaction/get-all`, { + const url = new URL(`${instanceUrl}transaction/get-all`); + if (params.queryParams) { + for (const key in params.queryParams) { + const value = + params.queryParams[key as keyof typeof params.queryParams]; + if (value !== undefined) { + url.searchParams.append(key, value.toString()); + } + } + } + + const res = await fetch(url, { method: "GET", headers: getEngineRequestHeaders(authToken), }); @@ -398,8 +420,8 @@ export function useEngineTransactions(params: { return (json.result as TransactionResponse) || {}; }, - enabled: !!instanceUrl, refetchInterval: autoUpdate ? 4_000 : false, + placeholderData: keepPreviousData, }); } diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/analytics/components/EcosystemWalletUsersChartCard.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/analytics/components/EcosystemWalletUsersChartCard.tsx index 92bb499ad4f..b2659843626 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/analytics/components/EcosystemWalletUsersChartCard.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/analytics/components/EcosystemWalletUsersChartCard.tsx @@ -228,8 +228,8 @@ export function EcosystemWalletUsersChartCard(props: { fill={chartConfig[authMethod]?.color} radius={4} stackId="a" - strokeWidth={1.5} - className="stroke-muted" + strokeWidth={1} + className="stroke-background" /> ); })} diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/backend-wallets-table.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/backend-wallets-table.tsx index c2f96627af7..4c20a4bec26 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/backend-wallets-table.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/backend-wallets-table.tsx @@ -182,6 +182,7 @@ export const BackendWalletsTable: React.FC = ({ isPending={isPending} isFetched={isFetched} tableScrollableClassName="max-h-[1000px]" + tableContainerClassName="border-x-0 rounded-t-none border-b-0" onMenuClick={[ { icon: , diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/create-backend-wallet-button.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/create-backend-wallet-button.tsx index 892088b0c16..6dba57a63ca 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/create-backend-wallet-button.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/create-backend-wallet-button.tsx @@ -31,7 +31,7 @@ import { EngineBackendWalletOptions, type EngineBackendWalletType, } from "lib/engine"; -import { CircleAlertIcon } from "lucide-react"; +import { CircleAlertIcon, PlusIcon } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -120,7 +120,10 @@ export const CreateBackendWalletButton: React.FC< return ( <> - + = ({ teamSlug={teamSlug} authToken={authToken} /> -
- +
+ +
+
); }; @@ -55,23 +53,23 @@ function BackendWalletsSection(props: { }); return ( -
-
+
+
-
-

+
+

Backend Wallets

-

+

Engine sends blockchain transactions from backend wallets you own and manage.

-

+

Set up other wallet types from the{" "} Configuration {" "} @@ -79,45 +77,43 @@ function BackendWalletsSection(props: { learn more about backend wallets.

- {walletConfig && ( -
- - -
- )} -
+
+ {walletConfig && ( +
+ + +
+ )} -
- -
-
- Show balance for - {/* TODO - Replace with simple network selector - there's no need for user to switch chain */} -
- +
+
+ Show balance for + {/* TODO - Replace with simple network selector - there's no need for user to switch chain */} +
+ +
+
-
- ); } - -function TransactionsSection(props: { - instanceUrl: string; - authToken: string; -}) { - const { instanceUrl, authToken } = props; - const [autoUpdate, setAutoUpdate] = useState(true); - const transactionsQuery = useEngineTransactions({ - instanceUrl, - autoUpdate, - authToken, - }); - - return ( -
-
-
-

- Transactions -

-

- View transactions sent from your backend wallets -

-
- -
-
- - setAutoUpdate(!!v)} - id="auto-update" - /> -
-
-
- -
- - -
- ); -} diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/import-backend-wallet-button.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/import-backend-wallet-button.tsx index 975e0980e48..f4e69cbec8b 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/import-backend-wallet-button.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/import-backend-wallet-button.tsx @@ -33,7 +33,7 @@ import { EngineBackendWalletOptions, type EngineBackendWalletType, } from "lib/engine"; -import { CircleAlertIcon } from "lucide-react"; +import { CircleAlertIcon, CloudDownloadIcon } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -136,7 +136,13 @@ export const ImportBackendWalletButton: React.FC< return ( <> - diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table.tsx index 2d232c3cd8f..f0f9c21c1de 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table.tsx @@ -1,45 +1,60 @@ import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart"; import { WalletAddress } from "@/components/blocks/wallet-address"; +import { PaginationButtons } from "@/components/pagination-buttons"; import { CopyAddressButton } from "@/components/ui/CopyAddressButton"; import { CopyTextButton } from "@/components/ui/CopyTextButton"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import type { ChartConfig } from "@/components/ui/chart"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Sheet, SheetContent, SheetHeader, SheetTitle, } from "@/components/ui/sheet"; -import type { Transaction } from "@3rdweb-sdk/react/hooks/useEngine"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Switch } from "@/components/ui/switch"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { ToolTipLabel } from "@/components/ui/tooltip"; import { - Collapse, - Divider, - Flex, - FormControl, - Tooltip, - useDisclosure, -} from "@chakra-ui/react"; -import { createColumnHelper } from "@tanstack/react-table"; + type Transaction, + useEngineTransactions, +} from "@3rdweb-sdk/react/hooks/useEngine"; +import { Collapse, Divider, useDisclosure } from "@chakra-ui/react"; import { ChainIcon } from "components/icons/ChainIcon"; import { formatDistanceToNowStrict } from "date-fns"; import { format, formatDate } from "date-fns/format"; import { useAllChainsData } from "hooks/chains/allChains"; -import { InfoIcon, MoveLeftIcon, MoveRightIcon } from "lucide-react"; +import { + ExternalLinkIcon, + InfoIcon, + MoveLeftIcon, + MoveRightIcon, +} from "lucide-react"; +import Link from "next/link"; import { type Dispatch, type SetStateAction, useMemo, useState } from "react"; import { toTokens } from "thirdweb"; -import { Button, Card, FormLabel, LinkButton, Text } from "tw-components"; -import { TWTable } from "../../../../../../../../../../components/shared/TWTable"; +import { FormLabel, LinkButton, Text } from "tw-components"; import { TransactionTimeline } from "./transaction-timeline"; -interface TransactionsTableProps { - transactions: Transaction[]; - isPending: boolean; - isFetched: boolean; - instanceUrl: string; - authToken: string; -} - -type EngineStatus = +export type EngineStatus = | "errored" | "mined" | "cancelled" @@ -48,22 +63,34 @@ type EngineStatus = | "processed" | "queued" | "user-op-sent"; + const statusDetails: Record< EngineStatus, { name: string; type: "success" | "destructive" | "warning"; - showTooltipIcon?: boolean; } > = { - processed: { - name: "Processed", - type: "warning", + mined: { + name: "Mined", + type: "success", + }, + errored: { + name: "Failed", + type: "destructive", + }, + cancelled: { + name: "Cancelled", + type: "destructive", }, queued: { name: "Queued", type: "warning", }, + processed: { + name: "Processed", + type: "warning", + }, sent: { name: "Sent", type: "warning", @@ -72,193 +99,396 @@ const statusDetails: Record< name: "User Op Sent", type: "warning", }, - mined: { - name: "Mined", - type: "success", - }, retried: { name: "Retried", type: "success", }, - errored: { - name: "Failed", - type: "destructive", - }, - cancelled: { - name: "Cancelled", - type: "destructive", - }, }; -const columnHelper = createColumnHelper(); - -export const TransactionsTable: React.FC = ({ - transactions, - isPending, - isFetched, - instanceUrl, - authToken, -}) => { - const { idToChain } = useAllChainsData(); +// TODO - add Status selector dropdown here +export function TransactionsTable(props: { + instanceUrl: string; + authToken: string; +}) { const transactionDisclosure = useDisclosure(); const [selectedTransaction, setSelectedTransaction] = useState(null); + const [autoUpdate, setAutoUpdate] = useState(true); + const [page, setPage] = useState(1); + const [status, setStatus] = useState(undefined); + + const pageSize = 10; + const transactionsQuery = useEngineTransactions({ + instanceUrl: props.instanceUrl, + autoUpdate, + authToken: props.authToken, + queryParams: { + limit: pageSize, + page: page, + status, + }, + }); + + const transactions = transactionsQuery.data?.transactions ?? []; - const columns = [ - columnHelper.accessor("queueId", { - header: "Queue ID", - cell: (cell) => { - return ( - pageSize; + + const showSkeleton = + (transactionsQuery.isPlaceholderData && transactionsQuery.isFetching) || + (transactionsQuery.isLoading && !transactionsQuery.isPlaceholderData); + + return ( +
+
+
+

+ Transaction History +

+

+ Transactions sent from your backend wallets +

+
+ +
+
+ + setAutoUpdate(!!v)} + id="auto-update" + /> +
+ { + setStatus(v); + // reset page + setPage(1); + }} /> - ); - }, - }), - columnHelper.accessor("chainId", { - header: "Chain", - cell: (cell) => { - const chainId = cell.getValue(); - if (!chainId) { - return; - } +
+
+ + + + + + Queue ID + Chain + Status + From + Tx Hash + Queued + + + + {showSkeleton ? ( + <> + {new Array(pageSize).fill(0).map((_, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: + + ))} + + ) : ( + <> + {transactions.map((tx) => ( + { + setSelectedTransaction(tx); + transactionDisclosure.onOpen(); + }} + key={`${tx.queueId}${tx.chainId}${tx.blockNumber}`} + className="cursor-pointer hover:bg-accent/50" + > + {/* Queue ID */} + + + + + {/* Chain Id */} + + + + + {/* Status */} + + + + + {/* From Address */} + + {tx.fromAddress ? ( + + ) : ( + "N/A" + )} + + + {/* Tx Hash */} + + + + + {/* Queued At */} + + + + + ))} + + )} + +
+ + {!showSkeleton && transactions.length === 0 && ( +
+ No transactions found +
+ )} +
+ + {showPagination && ( +
+ +
+ )} - const chain = idToChain.get(Number.parseInt(chainId)); - if (chain) { - return ( - - - - {chain?.name ?? "N/A"} - - - ); - } - }, - }), - columnHelper.accessor("status", { - header: "Status", - cell: (cell) => { - const transaction = cell.row.original; - const { errorMessage, minedAt } = transaction; - const status = (transaction.status as EngineStatus) ?? null; - if (!status) { - return null; - } + {transactionDisclosure.isOpen && selectedTransaction && ( + 0 + ? () => setSelectedTransaction(transactions[idx - 1] || null) + : undefined + } + onClickNext={ + idx < transactions.length - 1 + ? () => setSelectedTransaction(transactions[idx + 1] || null) + : undefined + } + setSelectedTransaction={setSelectedTransaction} + /> + )} +
+ ); +} - const tooltip = - status === "errored" - ? errorMessage - : (status === "mined" || status === "retried") && minedAt - ? `Completed ${format(new Date(minedAt), "PP pp")}` - : undefined; - - return ( - - - {tooltip} - - ) : undefined - } - > -
- - - {statusDetails[status].name} - {statusDetails[status].showTooltipIcon && ( - - )} - - -
-
-
- ); - }, - }), - columnHelper.accessor("fromAddress", { - header: "From", - cell: (cell) => { - return ; - }, - }), - columnHelper.accessor("transactionHash", { - header: "Tx Hash", - cell: (cell) => { - const { chainId, transactionHash } = cell.row.original; - if (!chainId || !transactionHash) { - return; - } +function StatusSelector(props: { + status: EngineStatus | undefined; + setStatus: (value: EngineStatus | undefined) => void; +}) { + const statuses = Object.keys(statusDetails) as EngineStatus[]; - const chain = idToChain.get(Number.parseInt(chainId)); - if (chain) { - const explorer = chain.explorers?.[0]; - if (!explorer) { + return ( + + ); +} - return ( - - ); - } - }, - }), - columnHelper.accessor("queuedAt", { - header: "Queued", - cell: (cell) => { - const value = cell.getValue(); - if (!value) { - return; - } +function SkeletonRow() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} - const date = new Date(value); - return ( - - {format(date, "PP pp z")} - - } - shouldWrapChildren - > - {formatDistanceToNowStrict(date, { addSuffix: true })} - - ); - }, - }), - ]; +function TxChainCell(props: { chainId: string | undefined }) { + const { chainId } = props; + const { idToChain } = useAllChainsData(); - const idx = selectedTransaction - ? transactions.indexOf(selectedTransaction) - : 0; + if (!chainId) { + return "N/A"; + } + + const chain = idToChain.get(Number.parseInt(chainId)); + + if (!chain) { + return `Chain ID: ${chainId}`; + } + + return ( +
+ +
+ {chain.name ?? `Chain ID: ${chainId}`} +
+
+ ); +} + +function TxStatusCell(props: { + transaction: Transaction; +}) { + const { transaction } = props; + const { errorMessage, minedAt } = transaction; + const status = (transaction.status as EngineStatus) ?? null; + if (!status) { + return null; + } + + const tooltip = + status === "errored" + ? errorMessage + : (status === "mined" || status === "retried") && minedAt + ? `Completed ${format(new Date(minedAt), "PP pp")}` + : undefined; + + return ( + + + {statusDetails[status].name} + {errorMessage && } + + + ); +} + +function TxHashCell(props: { transaction: Transaction }) { + const { idToChain } = useAllChainsData(); + const { chainId, transactionHash } = props.transaction; + if (!transactionHash) { + return "N/A"; + } + + const chain = chainId ? idToChain.get(Number.parseInt(chainId)) : undefined; + const explorer = chain?.explorers?.[0]; + + const shortHash = `${transactionHash.slice(0, 6)}...${transactionHash.slice(-4)}`; + if (!explorer) { + return ( + + ); + } + + return ( + + ); +} + +function TxQueuedAtCell(props: { transaction: Transaction }) { + const value = props.transaction.queuedAt; + if (!value) { + return; + } + + const date = new Date(value); + return ( + +

{formatDistanceToNowStrict(date, { addSuffix: true })}

+
+ ); +} + +export function TransactionCharts(props: { + authToken: string; + instanceUrl: string; +}) { + const transactionsQuery = useEngineTransactions({ + instanceUrl: props.instanceUrl, + autoUpdate: false, + authToken: props.authToken, + queryParams: { + limit: 10000, + page: 1, + }, + }); + + const transactions = transactionsQuery.data?.transactions ?? []; const { analyticsData, chartConfig } = useMemo(() => { + if (!transactions.length) { + return { + analyticsData: [], + chartConfig: {}, + }; + } const dayToTxCountMap: Map> = new Map(); const uniqueStatuses = new Set(); @@ -302,62 +532,27 @@ export const TransactionsTable: React.FC = ({ }, [transactions]); return ( - <> - { - if (Array.isArray(item)) { - const time = item[0].payload.time as number; - return formatDate(new Date(time), "MMM d, yyyy"); - } - return undefined; - }} - /> - -
- - { - setSelectedTransaction(row); - transactionDisclosure.onOpen(); - }} - /> - - {transactionDisclosure.isOpen && selectedTransaction && ( - 0 - ? () => setSelectedTransaction(transactions[idx - 1] || null) - : undefined - } - onClickNext={ - idx < transactions.length - 1 - ? () => setSelectedTransaction(transactions[idx + 1] || null) - : undefined - } - setSelectedTransaction={setSelectedTransaction} - /> - )} - + { + if (Array.isArray(item)) { + const time = item[0].payload.time as number; + return formatDate(new Date(time), "MMM d, yyyy"); + } + return undefined; + }} + /> ); -}; +} const TransactionDetailsDrawer = ({ transaction, @@ -418,27 +613,27 @@ const TransactionDetailsDrawer = ({
- +
Queue ID {transaction.queueId} - +
- +
Chain - - - {chain?.name} - - +
+ + {chain?.name} +
+
{functionCalled && ( - +
Function {functionCalled} - +
)} - +
{transaction.accountAddress ? "Signer Address" : "From Address"} @@ -454,10 +649,10 @@ const TransactionDetailsDrawer = ({ > {transaction.fromAddress} - +
{transaction.accountAddress && ( - +
Account Address {transaction.accountAddress} - +
)} - +
{/* The "to" address is usually a contract except for native token transfers. */} {functionCalled === "transfer" @@ -493,23 +688,18 @@ const TransactionDetailsDrawer = ({ > {transaction.toAddress} - +
{transaction.errorMessage && ( - +
Error {transaction.errorMessage} - - +
)} @@ -524,15 +714,14 @@ const TransactionDetailsDrawer = ({ {/* On-chain details */} - +
Value - - +
{transaction.value @@ -540,11 +729,11 @@ const TransactionDetailsDrawer = ({ : 0}{" "} {symbol} - +
{transaction.transactionHash && ( <> - +
Transaction Hash - +
- +
Transaction Fee {txFeeDisplay} - +
{transaction.nonce && ( - +
Nonce - + - +
{transaction.nonce ?? "N/A"} - +
)} {transaction.gasLimit && ( - +
Gas Units - + - +
{Number(transaction.gasLimit).toLocaleString()} - +
)} {transaction.gasPrice && ( - +
Gas Price - + - +
{Number(transaction.gasPrice).toLocaleString()} - +
)} {transaction.blockNumber && ( - +
Block {transaction.blockNumber} - +
)}
@@ -643,9 +823,8 @@ const TransactionDetailsDrawer = ({
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/RpcMethodBarChartCard/RpcMethodBarChartCardUI.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/RpcMethodBarChartCard/RpcMethodBarChartCardUI.tsx index 03216b210e0..d72c5491788 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/RpcMethodBarChartCard/RpcMethodBarChartCardUI.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/RpcMethodBarChartCard/RpcMethodBarChartCardUI.tsx @@ -177,6 +177,8 @@ export function RpcMethodBarChartCardUI({ idx === 0 ? 4 : 0, ]} fill={`hsl(var(--chart-${idx + 1}))`} + strokeWidth={1} + className="stroke-background" /> ))} diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/DailyConnectionsChartCard.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/DailyConnectionsChartCard.tsx index 947548f3c1f..ea9f3b1f749 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/DailyConnectionsChartCard.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/DailyConnectionsChartCard.tsx @@ -222,6 +222,8 @@ export function DailyConnectionsChartCard(props: { dataKey={chartToShow} fill={`var(--color-${chartToShow})`} radius={8} + strokeWidth={1} + className="stroke-background" > {barChartData.length < 50 && ( ); })} diff --git a/apps/dashboard/src/app/team/components/Analytics/BarChart.tsx b/apps/dashboard/src/app/team/components/Analytics/BarChart.tsx index 255049f5811..14b3ebf17fe 100644 --- a/apps/dashboard/src/app/team/components/Analytics/BarChart.tsx +++ b/apps/dashboard/src/app/team/components/Analytics/BarChart.tsx @@ -98,6 +98,8 @@ export function BarChart({ dataKey={activeKey} radius={4} fill={chartConfig[activeKey]?.color ?? "hsl(var(--chart-1))"} + strokeWidth={1} + className="stroke-background" /> )} diff --git a/apps/dashboard/src/app/team/components/Analytics/PieChart.tsx b/apps/dashboard/src/app/team/components/Analytics/PieChart.tsx index 395b182a0dd..8d24142fe6d 100644 --- a/apps/dashboard/src/app/team/components/Analytics/PieChart.tsx +++ b/apps/dashboard/src/app/team/components/Analytics/PieChart.tsx @@ -47,7 +47,14 @@ export function PieChart({ /> } /> - + ); diff --git a/apps/dashboard/src/components/analytics/empty-chart-state.tsx b/apps/dashboard/src/components/analytics/empty-chart-state.tsx index 1d4bd6c6785..9dfd382f935 100644 --- a/apps/dashboard/src/components/analytics/empty-chart-state.tsx +++ b/apps/dashboard/src/components/analytics/empty-chart-state.tsx @@ -37,7 +37,7 @@ export function EmptyChartState({ children }: { children?: React.ReactNode }) { export function LoadingChartState() { return ( -
+
); diff --git a/apps/dashboard/src/components/analytics/interval-selector.tsx b/apps/dashboard/src/components/analytics/interval-selector.tsx index ef8f3d6ff75..003b0658320 100644 --- a/apps/dashboard/src/components/analytics/interval-selector.tsx +++ b/apps/dashboard/src/components/analytics/interval-selector.tsx @@ -5,7 +5,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { cn } from "../../@/lib/utils"; +import { cn } from "@/lib/utils"; export function IntervalSelector(props: { intervalType: "day" | "week"; @@ -19,7 +19,9 @@ export function IntervalSelector(props: { props.setIntervalType(value); }} > - + diff --git a/apps/dashboard/src/components/embedded-wallets/Analytics/InAppWalletUsersChartCard.tsx b/apps/dashboard/src/components/embedded-wallets/Analytics/InAppWalletUsersChartCard.tsx index 8cfe049dc98..8728f1f502a 100644 --- a/apps/dashboard/src/components/embedded-wallets/Analytics/InAppWalletUsersChartCard.tsx +++ b/apps/dashboard/src/components/embedded-wallets/Analytics/InAppWalletUsersChartCard.tsx @@ -228,8 +228,8 @@ export function InAppWalletUsersChartCardUI(props: { fill={chartConfig[authMethod]?.color} radius={4} stackId="a" - strokeWidth={1.5} - className="stroke-muted" + strokeWidth={1} + className="stroke-background" /> ); })} diff --git a/apps/dashboard/src/components/pay/PayAnalytics/components/PaymentHistory.tsx b/apps/dashboard/src/components/pay/PayAnalytics/components/PaymentHistory.tsx index e7f3355c249..14a2df4acd4 100644 --- a/apps/dashboard/src/components/pay/PayAnalytics/components/PaymentHistory.tsx +++ b/apps/dashboard/src/components/pay/PayAnalytics/components/PaymentHistory.tsx @@ -160,15 +160,15 @@ function RenderData(props: {
No data available
- ) : ( -
+ ) : props.query.data ? ( +
- )} + ) : null}
); diff --git a/apps/dashboard/src/components/pay/PayAnalytics/components/Payouts.tsx b/apps/dashboard/src/components/pay/PayAnalytics/components/Payouts.tsx index dcaf20b6279..8e558579ae0 100644 --- a/apps/dashboard/src/components/pay/PayAnalytics/components/Payouts.tsx +++ b/apps/dashboard/src/components/pay/PayAnalytics/components/Payouts.tsx @@ -186,6 +186,8 @@ function RenderData(props: { fill={barColor} radius={8} barSize={20} + strokeWidth={1} + className="stroke-background" /> {props.query.data && ( diff --git a/apps/dashboard/src/components/pay/PayAnalytics/components/common.tsx b/apps/dashboard/src/components/pay/PayAnalytics/components/common.tsx index 4bc1b097167..a1de25c8536 100644 --- a/apps/dashboard/src/components/pay/PayAnalytics/components/common.tsx +++ b/apps/dashboard/src/components/pay/PayAnalytics/components/common.tsx @@ -23,7 +23,7 @@ export function FailedToLoad() { export function NoDataOverlay() { return ( -
+
No data available
); @@ -87,7 +87,7 @@ export function IntervalSelector(props: { props.setIntervalType(value); }} > - + diff --git a/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard.tsx b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard.tsx index 1788ae451f1..f1192192fed 100644 --- a/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard.tsx +++ b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard.tsx @@ -193,8 +193,8 @@ export function SponsoredTransactionsChartCard(props: { fill={chartConfig[chainId]?.color} radius={4} stackId="a" - strokeWidth={1.5} - className="stroke-muted" + strokeWidth={1} + className="stroke-background" /> ); })} diff --git a/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/TotalSponsoredChartCard.tsx b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/TotalSponsoredChartCard.tsx index 96fe3fe82cb..b3d67af4c4e 100644 --- a/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/TotalSponsoredChartCard.tsx +++ b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/TotalSponsoredChartCard.tsx @@ -228,8 +228,8 @@ export function TotalSponsoredChartCard(props: { fill={chartConfig[chainId]?.color} radius={4} stackId="a" - strokeWidth={1.5} - className="stroke-muted" + strokeWidth={1} + className="stroke-background" /> ); })}