Skip to content

Commit a50a2a1

Browse files
committed
add sponsored transactions table in team usage page
1 parent 834ed83 commit a50a2a1

File tree

10 files changed

+856
-27
lines changed

10 files changed

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

0 commit comments

Comments
 (0)