diff --git a/apps/dashboard/src/@/actions/proxies.ts b/apps/dashboard/src/@/actions/proxies.ts index 268b8e78533..cbbeb923145 100644 --- a/apps/dashboard/src/@/actions/proxies.ts +++ b/apps/dashboard/src/@/actions/proxies.ts @@ -5,13 +5,14 @@ import { API_SERVER_URL } from "../constants/env"; type ProxyActionParams = { pathname: string; - searchParams?: Record; + searchParams?: Record; method: "GET" | "POST" | "PUT" | "DELETE"; body?: string; headers?: Record; + parseAsText?: boolean; }; -type ProxyActionResult = +type ProxyActionResult = | { status: number; ok: true; @@ -23,7 +24,7 @@ type ProxyActionResult = error: string; }; -async function proxy( +async function proxy( baseUrl: string, params: ProxyActionParams, ): Promise> { @@ -34,7 +35,10 @@ async function proxy( url.pathname = params.pathname; if (params.searchParams) { for (const key in params.searchParams) { - url.searchParams.append(key, params.searchParams[key] as string); + const value = params.searchParams[key]; + if (value) { + url.searchParams.append(key, value); + } } } @@ -67,19 +71,15 @@ async function proxy( return { status: res.status, ok: true, - data: await res.json(), + data: params.parseAsText ? await res.text() : await res.json(), }; } -export async function apiServerProxy( - params: ProxyActionParams, -) { +export async function apiServerProxy(params: ProxyActionParams) { return proxy(API_SERVER_URL, params); } -export async function payServerProxy( - params: ProxyActionParams, -) { +export async function payServerProxy(params: ProxyActionParams) { return proxy( process.env.NEXT_PUBLIC_PAY_URL ? `https://${process.env.NEXT_PUBLIC_PAY_URL}` @@ -87,3 +87,7 @@ export async function payServerProxy( params, ); } + +export async function analyticsServerProxy(params: ProxyActionParams) { + return proxy(process.env.ANALYTICS_SERVICE_URL || "", params); +} diff --git a/apps/dashboard/src/@/components/blocks/ExportToCSVButton.tsx b/apps/dashboard/src/@/components/blocks/ExportToCSVButton.tsx index d3e209f5ed4..a95dd84f21f 100644 --- a/apps/dashboard/src/@/components/blocks/ExportToCSVButton.tsx +++ b/apps/dashboard/src/@/components/blocks/ExportToCSVButton.tsx @@ -2,11 +2,12 @@ import { Spinner } from "@/components/ui/Spinner/Spinner"; import { Button } from "@/components/ui/button"; import { useMutation } from "@tanstack/react-query"; import { DownloadIcon } from "lucide-react"; +import Papa from "papaparse"; import { toast } from "sonner"; import { cn } from "../../lib/utils"; export function ExportToCSVButton(props: { - getData: () => Promise<{ header: string[]; rows: string[][] }>; + getData: () => Promise<{ header: string[]; rows: string[][] } | string>; fileName: string; disabled?: boolean; className?: string; @@ -14,7 +15,12 @@ export function ExportToCSVButton(props: { const exportMutation = useMutation({ mutationFn: async () => { const data = await props.getData(); - exportToCSV(props.fileName, data); + if (typeof data === "string") { + exportToCSV(props.fileName, data); + } else { + const fileContent = convertToCSVFormat(data); + exportToCSV(props.fileName, fileContent); + } }, onError: () => { toast.error("Failed to download CSV"); @@ -29,7 +35,7 @@ export function ExportToCSVButton(props: { return ( + ); + } + + return ( + + ); +} + +function ChainCell(props: { chainId: string }) { + const { idToChain, allChains } = useAllChainsData(); + const chain = idToChain.get(Number(props.chainId)); + + if (allChains.length === 0) { + return ; + } + + return ( +
+ + + {chain ? chain.name : `Chain #${props.chainId}`} + +
+ ); +} + +function ProjectCell(props: { + teamSlug: string; + project: + | { + id: string; + name: string; + image: string | null; + slug: string; + } + | undefined; + client: ThirdwebClient; +}) { + // just typeguard - never actually happens + if (!props.project) { + return ; + } + + return ( +
+ + + {props.project.name} + +
+ ); +} + +function ChainFilter(props: { + chainId: string | undefined; + setChainId: (chainId: string | undefined) => void; +}) { + const isChainFilterActive = props.chainId !== undefined; + + return ( +
+ {isChainFilterActive && ( + + + + )} + + props.setChainId(chainId.toString())} + popoverContentClassName="!w-[80vw] md:!w-[350px]" + align="end" + placeholder="All Chains" + disableChainId + /> +
+ ); +} + +function ProjectFilter(props: { + projectId: string | undefined; + setProjectId: (projectId: string | undefined) => void; + projects: { id: string; name: string; image: string | null }[]; + client: ThirdwebClient; +}) { + const isProjectFilterActive = props.projectId !== undefined; + + return ( +
+ {isProjectFilterActive && ( + + + + )} + + props.setProjectId(value)} + options={props.projects.map((project) => ({ + label: project.name, + value: project.id, + }))} + value={props.projectId} + placeholder="All Projects" + popoverContentClassName="!w-[80vw] md:!w-[350px]" + align="end" + className={cn( + "min-w-[160px]", + isProjectFilterActive && "rounded-l-none", + )} + renderOption={(option) => { + const project = props.projects.find((p) => p.id === option.value); + if (!project) { + return <>; + } + return ( +
+ + {project.name} +
+ ); + }} + /> +
+ ); +} + +// for values >= 0.01, show 2 decimal places +const normalValueUSDFormatter = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + notation: "standard", + maximumFractionDigits: 2, +}); + +function formatTransactionFee(usdValue: number) { + if (usdValue >= 0.01 || usdValue === 0) { + return normalValueUSDFormatter.format(usdValue); + } + + return "< $0.01"; +} + +function TransactionFeeCell(props: { usdValue: number }) { + return ( + + {formatTransactionFee(props.usdValue)} + + ); +} diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/overview/components/Usage.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/overview/components/Usage.tsx index ebe48bd8944..04f3bf01b4e 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/overview/components/Usage.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/overview/components/Usage.tsx @@ -10,8 +10,10 @@ import { InAppWalletUsersChartCardUI } from "components/embedded-wallets/Analyti import { subMonths } from "date-fns"; import Link from "next/link"; import { Suspense, useMemo } from "react"; +import type { ThirdwebClient } from "thirdweb"; import { toPercent, toSize } from "utils/number"; import { TotalSponsoredChartCardUI } from "../../../../_components/TotalSponsoredCard"; +import { SponsoredTransactionsTable } from "./SponsoredTransactionsTable"; import { UsageCard } from "./UsageCard"; type UsageProps = { @@ -20,6 +22,13 @@ type UsageProps = { subscriptions: TeamSubscription[]; account: Account; team: Team; + client: ThirdwebClient; + projects: { + id: string; + name: string; + image: string | null; + slug: string; + }[]; }; export const Usage: React.FC = ({ @@ -27,6 +36,8 @@ export const Usage: React.FC = ({ subscriptions, account, team, + client, + projects, }) => { // TODO - get this from team instead of account const storageMetrics = useMemo(() => { @@ -105,6 +116,16 @@ export const Usage: React.FC = ({ to={currentPeriodEnd} /> + + ; }) { const params = await props.params; - const account = await getValidAccount(`/team/${params.team_slug}/~/usage`); - const team = await getTeamBySlug(params.team_slug); + const [account, team, authToken] = await Promise.all([ + getValidAccount(`/team/${params.team_slug}/~/usage`), + getTeamBySlug(params.team_slug), + getAuthToken(), + ]); if (!team) { redirect("/team"); } - const [accountUsage, subscriptions] = await Promise.all([ + const [accountUsage, subscriptions, projects] = await Promise.all([ getAccountUsage(), getTeamSubscriptions(team.slug), + getProjects(team.slug), ]); - if (!accountUsage || !subscriptions) { + if (!accountUsage || !subscriptions || !authToken) { return (
Something went wrong. Please try again later. @@ -31,6 +38,8 @@ export default async function Page(props: { ); } + const client = getThirdwebClient(authToken); + return ( ({ + id: project.id, + name: project.name, + image: project.image, + slug: project.slug, + }))} /> ); } diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/page.tsx index a4057f281b6..63f59d1b133 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/page.tsx @@ -1,12 +1,14 @@ import { getUserOpUsage } from "@/api/analytics"; import { getProject } from "@/api/projects"; +import { getThirdwebClient } from "@/constants/thirdweb.server"; import { type Range, getLastNDaysRange, } from "components/analytics/date-range-selector"; import { AccountAbstractionAnalytics } from "components/smart-wallets/AccountAbstractionAnalytics"; -import { redirect } from "next/navigation"; +import { notFound, redirect } from "next/navigation"; import type { SearchParams } from "nuqs/server"; +import { getAuthToken } from "../../../../../api/lib/getAuthToken"; import { searchParamLoader } from "./search-params"; interface PageParams { @@ -19,11 +21,16 @@ export default async function Page(props: { searchParams: Promise; children: React.ReactNode; }) { - const [params, searchParams] = await Promise.all([ + const [params, searchParams, authToken] = await Promise.all([ props.params, searchParamLoader(props.searchParams), + getAuthToken(), ]); + if (!authToken) { + notFound(); + } + const project = await getProject(params.team_slug, params.project_slug); if (!project) { @@ -53,5 +60,13 @@ export default async function Page(props: { period: interval, }); - return ; + return ( + + ); } diff --git a/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/index.tsx b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/index.tsx index d6c2a4b0ec8..df0b8a88a74 100644 --- a/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/index.tsx +++ b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/index.tsx @@ -8,13 +8,19 @@ import { IntervalSelector } from "components/analytics/interval-selector"; import { differenceInDays } from "date-fns"; import { useQueryState } from "nuqs"; import { useTransition } from "react"; +import type { ThirdwebClient } from "thirdweb"; import type { UserOpStats } from "types/analytics"; +import { SponsoredTransactionsTable } from "../../../app/team/[team_slug]/(team)/~/usage/overview/components/SponsoredTransactionsTable"; import { searchParams } from "../../../app/team/[team_slug]/[project_slug]/connect/account-abstraction/search-params"; import { SponsoredTransactionsChartCard } from "./SponsoredTransactionsChartCard"; import { TotalSponsoredChartCard } from "./TotalSponsoredChartCard"; export function AccountAbstractionAnalytics(props: { userOpStats: UserOpStats[]; + teamId: string; + teamSlug: string; + client: ThirdwebClient; + projectId: string; }) { const [isLoading, startTransition] = useTransition(); @@ -98,6 +104,16 @@ export function AccountAbstractionAnalytics(props: { userOpStats={props.userOpStats} isPending={isLoading} /> + +
);