Skip to content

Commit 8bfdea9

Browse files
committed
add sponsored transactions table in team usage page
1 parent c653d70 commit 8bfdea9

File tree

10 files changed

+801
-27
lines changed

10 files changed

+801
-27
lines changed

apps/dashboard/src/@/actions/proxies.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ import { API_SERVER_URL } from "../constants/env";
55

66
type ProxyActionParams = {
77
pathname: string;
8-
searchParams?: Record<string, string>;
8+
searchParams?: Record<string, string | undefined>;
99
method: "GET" | "POST" | "PUT" | "DELETE";
1010
body?: string;
1111
headers?: Record<string, string>;
12+
parseAsText?: boolean;
1213
};
1314

14-
type ProxyActionResult<T extends object> =
15+
type ProxyActionResult<T> =
1516
| {
1617
status: number;
1718
ok: true;
@@ -23,7 +24,7 @@ type ProxyActionResult<T extends object> =
2324
error: string;
2425
};
2526

26-
async function proxy<T extends object>(
27+
async function proxy<T>(
2728
baseUrl: string,
2829
params: ProxyActionParams,
2930
): Promise<ProxyActionResult<T>> {
@@ -34,7 +35,10 @@ async function proxy<T extends object>(
3435
url.pathname = params.pathname;
3536
if (params.searchParams) {
3637
for (const key in params.searchParams) {
37-
url.searchParams.append(key, params.searchParams[key] as string);
38+
const value = params.searchParams[key];
39+
if (value) {
40+
url.searchParams.append(key, value);
41+
}
3842
}
3943
}
4044

@@ -67,23 +71,23 @@ async function proxy<T extends object>(
6771
return {
6872
status: res.status,
6973
ok: true,
70-
data: await res.json(),
74+
data: params.parseAsText ? await res.text() : await res.json(),
7175
};
7276
}
7377

74-
export async function apiServerProxy<T extends object = object>(
75-
params: ProxyActionParams,
76-
) {
78+
export async function apiServerProxy<T>(params: ProxyActionParams) {
7779
return proxy<T>(API_SERVER_URL, params);
7880
}
7981

80-
export async function payServerProxy<T extends object = object>(
81-
params: ProxyActionParams,
82-
) {
82+
export async function payServerProxy<T>(params: ProxyActionParams) {
8383
return proxy<T>(
8484
process.env.NEXT_PUBLIC_PAY_URL
8585
? `https://${process.env.NEXT_PUBLIC_PAY_URL}`
8686
: "https://pay.thirdweb-dev.com",
8787
params,
8888
);
8989
}
90+
91+
export async function analyticsServerProxy<T>(params: ProxyActionParams) {
92+
return proxy<T>(process.env.ANALYTICS_SERVICE_URL || "", params);
93+
}

apps/dashboard/src/@/components/blocks/ExportToCSVButton.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,20 @@ import { toast } from "sonner";
66
import { cn } from "../../lib/utils";
77

88
export function ExportToCSVButton(props: {
9-
getData: () => Promise<{ header: string[]; rows: string[][] }>;
9+
getData: () => Promise<{ header: string[]; rows: string[][] } | string>;
1010
fileName: string;
1111
disabled?: boolean;
1212
className?: string;
1313
}) {
1414
const exportMutation = useMutation({
1515
mutationFn: async () => {
1616
const data = await props.getData();
17-
exportToCSV(props.fileName, data);
17+
if (typeof data === "string") {
18+
exportToCSV(props.fileName, data);
19+
} else {
20+
const fileContent = convertToCSVFormat(data);
21+
exportToCSV(props.fileName, fileContent);
22+
}
1823
},
1924
onError: () => {
2025
toast.error("Failed to download CSV");
@@ -29,7 +34,7 @@ export function ExportToCSVButton(props: {
2934
return (
3035
<Button
3136
variant="outline"
32-
disabled={props.disabled}
37+
disabled={props.disabled || exportMutation.isPending}
3338
className={cn("flex items-center gap-2 border text-xs", props.className)}
3439
onClick={async () => {
3540
exportMutation.mutate();
@@ -50,15 +55,17 @@ export function ExportToCSVButton(props: {
5055
);
5156
}
5257

53-
function exportToCSV(
54-
fileName: string,
55-
data: { header: string[]; rows: string[][] },
56-
) {
58+
function convertToCSVFormat(data: { header: string[]; rows: string[][] }) {
5759
const { header, rows } = data;
58-
const csvContent = `data:text/csv;charset=utf-8,${header.join(",")}\n${rows
60+
const fileContent = `${header.join(",")}\n${rows
5961
.map((e) => e.join(","))
6062
.join("\n")}`;
6163

64+
return fileContent;
65+
}
66+
67+
function exportToCSV(fileName: string, fileContent: string) {
68+
const csvContent = `data:text/csv;charset=utf-8,${fileContent}`;
6269
const encodedUri = encodeURI(csvContent);
6370
const link = document.createElement("a");
6471
link.setAttribute("href", encodedUri);

apps/dashboard/src/@/components/blocks/Img.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export function Img(props: imgElementProps) {
4747
}, []);
4848

4949
return (
50-
<div className="relative">
50+
<div className="relative shrink-0">
5151
<img
5252
{...restProps}
5353
// avoid setting empty src string to prevent request to the entire page

apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export function SingleNetworkSelector(props: {
104104
side?: "left" | "right" | "top" | "bottom";
105105
disableChainId?: boolean;
106106
align?: "center" | "start" | "end";
107+
placeholder?: string;
107108
}) {
108109
const { allChains, idToChain } = useAllChainsData();
109110

@@ -180,7 +181,11 @@ export function SingleNetworkSelector(props: {
180181
props.onChange(Number(chainId));
181182
}}
182183
closeOnSelect={true}
183-
placeholder={isLoadingChains ? "Loading Chains..." : "Select Chain"}
184+
placeholder={
185+
isLoadingChains
186+
? "Loading Chains..."
187+
: props.placeholder || "Select Chain"
188+
}
184189
overrideSearchFn={searchFn}
185190
renderOption={renderOption}
186191
className={props.className}

apps/dashboard/src/@/components/blocks/wallet-address.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import {
88
import { useThirdwebClient } from "@/constants/thirdweb.client";
99
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
1010
import { useClipboard } from "hooks/useClipboard";
11-
import { Check, Copy } from "lucide-react";
11+
import { Check, Copy, XIcon } from "lucide-react";
1212
import { useMemo } from "react";
1313
import { type ThirdwebClient, isAddress } from "thirdweb";
1414
import { ZERO_ADDRESS } from "thirdweb";
1515
import { Blobbie, type SocialProfile, useSocialProfiles } from "thirdweb/react";
1616
import { cn } from "../../lib/utils";
1717
import { Badge } from "../ui/badge";
1818
import { Button } from "../ui/button";
19+
import { ToolTipLabel } from "../ui/tooltip";
1920
import { Img } from "./Img";
2021

2122
export function WalletAddress(props: {
@@ -44,7 +45,16 @@ export function WalletAddress(props: {
4445
const { onCopy, hasCopied } = useClipboard(address, 2000);
4546

4647
if (!isAddress(address)) {
47-
return <span>Invalid Address ({address})</span>;
48+
return (
49+
<ToolTipLabel label={address} hoverable>
50+
<span className="flex items-center gap-2 underline-offset-4 hover:underline">
51+
<div className="flex size-6 items-center justify-center rounded-full border bg-background">
52+
<XIcon className="size-4 text-muted-foreground" />
53+
</div>
54+
Invalid Address
55+
</span>
56+
</ToolTipLabel>
57+
);
4858
}
4959

5060
// special case for zero address
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"use client";
2+
import { analyticsServerProxy } from "@/actions/proxies";
3+
import { keepPreviousData, useQuery } from "@tanstack/react-query";
4+
import { useState } from "react";
5+
import type { ThirdwebClient } from "thirdweb/dist/types/client/client";
6+
import {
7+
type SponsoredTransaction,
8+
SponsoredTransactionsTableUI,
9+
} from "./SponsoredTransactionsTableUI";
10+
11+
type GetSponsoredTransactionsParams = {
12+
teamId: string;
13+
limit: number;
14+
offset: number;
15+
from: string;
16+
to: string;
17+
// optional
18+
projectId?: string;
19+
chainId?: string;
20+
};
21+
22+
const getSponsoredTransactions = async (
23+
params: GetSponsoredTransactionsParams,
24+
) => {
25+
const res = await analyticsServerProxy<{
26+
data: SponsoredTransaction[];
27+
pagination: {
28+
total: number;
29+
};
30+
}>({
31+
pathname: "/v2/bundler/sponsored-transactions",
32+
method: "GET",
33+
searchParams: {
34+
teamId: params.teamId,
35+
limit: params.limit.toString(),
36+
offset: params.offset.toString(),
37+
projectId: params.projectId,
38+
chainId: params.chainId,
39+
from: params.from,
40+
to: params.to,
41+
},
42+
});
43+
44+
if (!res.ok) {
45+
throw new Error(res.error);
46+
}
47+
48+
return res.data;
49+
};
50+
51+
async function getSponsoredTransactionsCSV(params: {
52+
teamId: string;
53+
from: string;
54+
to: string;
55+
}) {
56+
const res = await analyticsServerProxy<string>({
57+
method: "GET",
58+
pathname: "/v2/bundler/sponsored-transactions/export",
59+
parseAsText: true,
60+
searchParams: {
61+
teamId: params.teamId,
62+
from: params.from,
63+
to: params.to,
64+
},
65+
});
66+
67+
if (!res.ok) {
68+
throw new Error(res.error);
69+
}
70+
71+
return res.data;
72+
}
73+
74+
export function SponsoredTransactionsTable(props: {
75+
client: ThirdwebClient;
76+
teamId: string;
77+
from: string;
78+
to: string;
79+
projects: { id: string; name: string; image: string | null }[];
80+
}) {
81+
const pageSize = 10;
82+
const [page, setPage] = useState(1);
83+
84+
const [filters, setFilters] = useState<{
85+
chainId?: string;
86+
projectId?: string;
87+
}>({});
88+
89+
const params = {
90+
teamId: props.teamId,
91+
limit: pageSize,
92+
offset: (page - 1) * pageSize,
93+
from: props.from,
94+
to: props.to,
95+
chainId: filters.chainId,
96+
projectId: filters.projectId,
97+
};
98+
99+
const sponsoredTransactionsQuery = useQuery({
100+
queryKey: ["sponsored-transactions", params],
101+
queryFn: () => {
102+
return getSponsoredTransactions(params);
103+
},
104+
placeholderData: keepPreviousData,
105+
refetchOnWindowFocus: false,
106+
});
107+
108+
const totalPages = sponsoredTransactionsQuery.data
109+
? Math.ceil(sponsoredTransactionsQuery.data.pagination.total / pageSize)
110+
: 0;
111+
112+
return (
113+
<SponsoredTransactionsTableUI
114+
filters={filters}
115+
projects={props.projects}
116+
setFilters={(v) => {
117+
setFilters(v);
118+
setPage(1);
119+
}}
120+
client={props.client}
121+
isError={sponsoredTransactionsQuery.isError}
122+
getCSV={() => {
123+
return getSponsoredTransactionsCSV({
124+
teamId: props.teamId,
125+
from: props.from,
126+
to: props.to,
127+
});
128+
}}
129+
isPending={sponsoredTransactionsQuery.isFetching}
130+
sponsoredTransactions={sponsoredTransactionsQuery.data?.data ?? []}
131+
pageNumber={page}
132+
setPageNumber={setPage}
133+
pageSize={pageSize}
134+
teamId={props.teamId}
135+
totalPages={totalPages}
136+
/>
137+
);
138+
}

0 commit comments

Comments
 (0)