From 4bd4efce4bb85da65c327e6638ed06b87e854650 Mon Sep 17 00:00:00 2001 From: iuwqyir Date: Wed, 2 Jul 2025 19:58:55 +0000 Subject: [PATCH] add rpc tab with analytics (#7489) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [Dashboard] Feature: Add RPC Edge analytics page --- ## PR-Codex overview This PR focuses on enhancing the RPC (Remote Procedure Call) analytics feature in the dashboard. It introduces new components, updates existing ones, and adds functionality for fetching and displaying RPC usage data and statistics. ### Detailed summary - Added `RpcMethodStats` and `RpcUsageTypeStats` interfaces in `analytics.ts`. - Introduced `getRpcUsageByType` function to fetch RPC usage data. - Updated `RpcMethodBarChartCardUI` to use new data structure. - Added new `RPC` link in `ProjectSidebarLayout`. - Created `RpcAnalyticsFilter` for filtering RPC analytics. - Implemented `RequestsGraph` to visualize RPC requests over time. - Developed `TopRPCMethodsTable` to display top EVM methods called. - Added `RpcFTUX` component for user onboarding in RPC section. - Updated layout files to integrate new components and improve styling. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` ## Summary by CodeRabbit * **New Features** * Introduced a dedicated "RPC" section in the project sidebar for easy access to RPC analytics. * Added a comprehensive RPC analytics dashboard, including: * Interactive requests graph and top RPC methods table. * Date range and interval filtering for analytics. * First-time user experience (FTUX) with ready-to-use code examples and documentation links. * Enhanced layout and navigation for the new RPC analytics section. * **Style** * Improved UI spacing, sizing, and visual consistency for analytics components and placeholders. --- apps/dashboard/src/@/api/analytics.ts | 33 ++++- apps/dashboard/src/@/types/analytics.ts | 6 - .../components/ProjectSidebarLayout.tsx | 6 + .../RpcMethodBarChartCard.stories.tsx | 2 +- .../RpcMethodBarChartCardUI.tsx | 2 +- .../insight/components/InsightAnalytics.tsx | 18 +-- .../(sidebar)/insight/layout.tsx | 4 +- .../(sidebar)/rpc/components/MethodsTable.tsx | 132 ++++++++++++++++++ .../rpc/components/RequestsGraph.tsx | 54 +++++++ .../(sidebar)/rpc/components/RpcAnalytics.tsx | 98 +++++++++++++ .../rpc/components/RpcAnalyticsFilter.tsx | 59 ++++++++ .../(sidebar)/rpc/components/RpcFtux.tsx | 104 ++++++++++++++ .../[project_slug]/(sidebar)/rpc/layout.tsx | 46 ++++++ .../[project_slug]/(sidebar)/rpc/page.tsx | 61 ++++++++ 14 files changed, 602 insertions(+), 23 deletions(-) create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/MethodsTable.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/RequestsGraph.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/RpcAnalytics.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/RpcAnalyticsFilter.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/RpcFtux.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/layout.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/page.tsx diff --git a/apps/dashboard/src/@/api/analytics.ts b/apps/dashboard/src/@/api/analytics.ts index 39a89144148..7229331fd2e 100644 --- a/apps/dashboard/src/@/api/analytics.ts +++ b/apps/dashboard/src/@/api/analytics.ts @@ -6,7 +6,6 @@ import type { EcosystemWalletStats, EngineCloudStats, InAppWalletStats, - RpcMethodStats, TransactionStats, UniversalBridgeStats, UniversalBridgeWalletStats, @@ -40,6 +39,18 @@ interface InsightUsageStats { totalRequests: number; } +export interface RpcMethodStats { + date: string; + evmMethod: string; + count: number; +} + +export interface RpcUsageTypeStats { + date: string; + usageType: string; + count: number; +} + async function fetchAnalytics( input: string | URL, init?: RequestInit, @@ -251,6 +262,26 @@ export async function getRpcMethodUsage( return json.data as RpcMethodStats[]; } +export async function getRpcUsageByType( + params: AnalyticsQueryParams, +): Promise { + const searchParams = buildSearchParams(params); + const res = await fetchAnalytics( + `v2/rpc/usage-types?${searchParams.toString()}`, + { + method: "GET", + }, + ); + + if (res?.status !== 200) { + console.error("Failed to fetch RPC usage"); + return []; + } + + const json = await res.json(); + return json.data as RpcUsageTypeStats[]; +} + export async function getWalletUsers( params: AnalyticsQueryParams, ): Promise { diff --git a/apps/dashboard/src/@/types/analytics.ts b/apps/dashboard/src/@/types/analytics.ts index d294b537cde..1c91f29e00a 100644 --- a/apps/dashboard/src/@/types/analytics.ts +++ b/apps/dashboard/src/@/types/analytics.ts @@ -39,12 +39,6 @@ export interface TransactionStats { count: number; } -export interface RpcMethodStats { - date: string; - evmMethod: string; - count: number; -} - export interface EngineCloudStats { date: string; chainId: string; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx index 69c24ba4a42..1b8de7d71f8 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx @@ -7,6 +7,7 @@ import { CoinsIcon, HomeIcon, LockIcon, + RssIcon, SettingsIcon, WalletIcon, } from "lucide-react"; @@ -103,6 +104,11 @@ export function ProjectSidebarLayout(props: { icon: SmartAccountIcon, label: "Account Abstraction", }, + { + href: `${layoutPath}/rpc`, + icon: RssIcon, + label: "RPC", + }, { href: `${layoutPath}/vault`, icon: LockIcon, diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/RpcMethodBarChartCard/RpcMethodBarChartCard.stories.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/RpcMethodBarChartCard/RpcMethodBarChartCard.stories.tsx index 9d90c4569ec..0021be1fee5 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/RpcMethodBarChartCard/RpcMethodBarChartCard.stories.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/RpcMethodBarChartCard/RpcMethodBarChartCard.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/nextjs"; +import type { RpcMethodStats } from "@/api/analytics"; import { BadgeContainer } from "@/storybook/utils"; -import type { RpcMethodStats } from "@/types/analytics"; import { RpcMethodBarChartCardUI } from "./RpcMethodBarChartCardUI"; const meta = { diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/RpcMethodBarChartCard/RpcMethodBarChartCardUI.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/RpcMethodBarChartCard/RpcMethodBarChartCardUI.tsx index 1b51463aaa4..0c43b5e2f08 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/RpcMethodBarChartCard/RpcMethodBarChartCardUI.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/RpcMethodBarChartCard/RpcMethodBarChartCardUI.tsx @@ -7,6 +7,7 @@ import { BarChart as RechartsBarChart, XAxis, } from "recharts"; +import type { RpcMethodStats } from "@/api/analytics"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { type ChartConfig, @@ -14,7 +15,6 @@ import { ChartTooltip, ChartTooltipContent, } from "@/components/ui/chart"; -import type { RpcMethodStats } from "@/types/analytics"; import { EmptyStateCard } from "../../../../../components/Analytics/EmptyStateCard"; export function RpcMethodBarChartCardUI({ diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/InsightAnalytics.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/InsightAnalytics.tsx index 40f2b0b35cb..2e905a0ca98 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/InsightAnalytics.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/InsightAnalytics.tsx @@ -121,7 +121,7 @@ export async function InsightAnalytics(props: { if (!hasVolume) { return ( -
+
); @@ -134,22 +134,18 @@ export async function InsightAnalytics(props: {
+
- - -
- -
- - -
+ +
+ +
} searchParamsUsed={["from", "to", "interval"]} > -
+
-
+

Insight @@ -36,8 +36,6 @@ export default async function Layout(props: {

- -
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/MethodsTable.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/MethodsTable.tsx new file mode 100644 index 00000000000..a49f84b2fcd --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/MethodsTable.tsx @@ -0,0 +1,132 @@ +"use client"; +import { useMemo, useState } from "react"; +import type { ThirdwebClient } from "thirdweb"; +import { shortenLargeNumber } from "thirdweb/utils"; +import type { RpcMethodStats } from "@/api/analytics"; +import { PaginationButtons } from "@/components/blocks/pagination-buttons"; +import { Card } from "@/components/ui/card"; +import { SkeletonContainer } from "@/components/ui/skeleton"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { CardHeading } from "../../universal-bridge/components/common"; + +export function TopRPCMethodsTable(props: { + data: RpcMethodStats[]; + client: ThirdwebClient; +}) { + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 30; + + const sortedData = useMemo(() => { + return props.data?.sort((a, b) => b.count - a.count) || []; + }, [props.data]); + + const totalPages = useMemo(() => { + return Math.ceil(sortedData.length / itemsPerPage); + }, [sortedData.length]); + + const tableData = useMemo(() => { + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + return sortedData.slice(startIndex, endIndex); + }, [sortedData, currentPage]); + + const isEmpty = useMemo(() => sortedData.length === 0, [sortedData]); + + return ( + + {/* header */} +
+ Top EVM Methods Called +
+ +
+ + + + + Method + Requests + + + + {tableData.map((method, i) => { + return ( + + ); + })} + +
+ {isEmpty && ( +
+ No data available +
+ )} +
+ + {!isEmpty && totalPages > 1 && ( +
+ +
+ )} + + ); +} + +function MethodTableRow(props: { + method?: { + evmMethod: string; + count: number; + }; + client: ThirdwebClient; + rowIndex: number; +}) { + const delayAnim = { + animationDelay: `${props.rowIndex * 100}ms`, + }; + + return ( + + + ( +

+ {v} +

+ )} + skeletonData="..." + style={delayAnim} + /> +
+ + { + return

{shortenLargeNumber(v)}

; + }} + skeletonData={0} + style={delayAnim} + /> +
+
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/RequestsGraph.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/RequestsGraph.tsx new file mode 100644 index 00000000000..c1964d2c2f9 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/RequestsGraph.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { format } from "date-fns"; +import { shortenLargeNumber } from "thirdweb/utils"; +import type { RpcUsageTypeStats } from "@/api/analytics"; +import { ThirdwebAreaChart } from "@/components/blocks/charts/area-chart"; + +export function RequestsGraph(props: { data: RpcUsageTypeStats[] }) { + return ( + new Date(a.date).getTime() - new Date(b.date).getTime()) + .reduce( + (acc, curr) => { + const existingEntry = acc.find((e) => e.time === curr.date); + if (existingEntry) { + existingEntry.requests += curr.count; + } else { + acc.push({ + requests: curr.count, + time: curr.date, + }); + } + return acc; + }, + [] as { requests: number; time: string }[], + )} + header={{ + description: "Requests over time.", + title: "RPC Requests", + }} + hideLabel={false} + isPending={false} + showLegend + toolTipLabelFormatter={(label) => { + return format(label, "MMM dd, HH:mm"); + }} + toolTipValueFormatter={(value) => { + return shortenLargeNumber(value as number); + }} + xAxis={{ + sameDay: true, + }} + yAxis + /> + ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/RpcAnalytics.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/RpcAnalytics.tsx new file mode 100644 index 00000000000..a3854c546f5 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/RpcAnalytics.tsx @@ -0,0 +1,98 @@ +import { ActivityIcon } from "lucide-react"; +import { ResponsiveSuspense } from "responsive-rsc"; +import type { ThirdwebClient } from "thirdweb"; +import { getRpcMethodUsage, getRpcUsageByType } from "@/api/analytics"; +import type { Range } from "@/components/analytics/date-range-selector"; +import { StatCard } from "@/components/analytics/stat"; +import { Skeleton } from "@/components/ui/skeleton"; +import { TopRPCMethodsTable } from "./MethodsTable"; +import { RequestsGraph } from "./RequestsGraph"; +import { RpcAnalyticsFilter } from "./RpcAnalyticsFilter"; +import { RpcFTUX } from "./RpcFtux"; + +export async function RPCAnalytics(props: { + projectClientId: string; + client: ThirdwebClient; + projectId: string; + teamId: string; + range: Range; + interval: "day" | "week"; +}) { + const { projectId, teamId, range, interval } = props; + + // TODO: add requests by status code, but currently not performant enough + const allRequestsByUsageTypePromise = getRpcUsageByType({ + from: range.from, + period: "all", + projectId: projectId, + teamId: teamId, + to: range.to, + }); + const requestsByUsageTypePromise = getRpcUsageByType({ + from: range.from, + period: interval, + projectId: projectId, + teamId: teamId, + to: range.to, + }); + const evmMethodsPromise = getRpcMethodUsage({ + from: range.from, + period: "all", + projectId: projectId, + teamId: teamId, + to: range.to, + }).catch((error) => { + console.error(error); + return []; + }); + + const [allUsageData, usageData, evmMethodsData] = await Promise.all([ + allRequestsByUsageTypePromise, + requestsByUsageTypePromise, + evmMethodsPromise, + ]); + + const totalRequests = allUsageData.reduce((acc, curr) => acc + curr.count, 0); + + if (totalRequests < 1) { + return ( +
+ +
+ ); + } + + return ( +
+
+ +
+ + + + +
+ } + searchParamsUsed={["from", "to", "interval"]} + > +
+
+ +
+ + +
+ +
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/RpcAnalyticsFilter.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/RpcAnalyticsFilter.tsx new file mode 100644 index 00000000000..2ebf01ba6c6 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/RpcAnalyticsFilter.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { + useResponsiveSearchParams, + useSetResponsiveSearchParams, +} from "responsive-rsc"; +import { DateRangeSelector } from "@/components/analytics/date-range-selector"; +import { IntervalSelector } from "@/components/analytics/interval-selector"; +import { getFiltersFromSearchParams, normalizeTimeISOString } from "@/lib/time"; + +type SearchParams = { + from?: string; + to?: string; + interval?: "day" | "week"; +}; + +export function RpcAnalyticsFilter() { + const responsiveSearchParams = useResponsiveSearchParams(); + const setResponsiveSearchParams = useSetResponsiveSearchParams(); + + const { range, interval } = getFiltersFromSearchParams({ + defaultRange: "last-30", + from: responsiveSearchParams.from, + interval: responsiveSearchParams.interval, + to: responsiveSearchParams.to, + }); + + return ( +
+ { + setResponsiveSearchParams((v: SearchParams) => { + const newParams = { + ...v, + from: normalizeTimeISOString(newRange.from), + to: normalizeTimeISOString(newRange.to), + }; + return newParams; + }); + }} + /> + + { + setResponsiveSearchParams((v: SearchParams) => { + const newParams = { + ...v, + interval: newInterval, + }; + return newParams; + }); + }} + /> +
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/RpcFtux.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/RpcFtux.tsx new file mode 100644 index 00000000000..90d87f6bba7 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/components/RpcFtux.tsx @@ -0,0 +1,104 @@ +import { CodeServer } from "@/components/ui/code/code.server"; +import { isProd } from "@/constants/env-utils"; +import { ClientIDSection } from "../../components/ProjectFTUX/ClientIDSection"; +import { WaitingForIntegrationCard } from "../../components/WaitingForIntegrationCard/WaitingForIntegrationCard"; + +export function RpcFTUX(props: { clientId: string }) { + return ( + + ), + label: "JavaScript", + }, + { + code: ( + + ), + label: "Python", + }, + { + code: ( + + ), + label: "Curl", + }, + ]} + ctas={[ + { + href: "https://portal.thirdweb.com/rpc-edge", + label: "View Docs", + }, + ]} + title="Start Using RPC" + > + +
+ + ); +} + +const twDomain = isProd ? "thirdweb" : "thirdweb-dev"; + +const jsCode = (clientId: string) => `\ +// Example: Get latest block number on Ethereum +const res = await fetch("https://1.rpc.${twDomain}.com/${clientId}", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "eth_blockNumber", + params: [], + id: 1, + }), +}); +const data = await res.json(); +console.log("Latest block number:", parseInt(data.result, 16)); +`; + +const curlCode = (clientId: string) => `\ +# Example: Get latest block number on Ethereum +curl -X POST "https://1.rpc.${twDomain}.com/${clientId}" \\ + -H "Content-Type: application/json" \\ + -d '{ + "jsonrpc": "2.0", + "method": "eth_blockNumber", + "params": [], + "id": 1 + }' +`; + +const pythonCode = (clientId: string) => `\ +# Example: Get latest block number on Ethereum +import requests +import json + +response = requests.post( + "https://1.rpc.${twDomain}.com/${clientId}", + headers={"Content-Type": "application/json"}, + json={ + "jsonrpc": "2.0", + "method": "eth_blockNumber", + "params": [], + "id": 1 + } +) +data = response.json() +print("Latest block number:", int(data["result"], 16)) +`; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/layout.tsx new file mode 100644 index 00000000000..1fbfa67c9f2 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/layout.tsx @@ -0,0 +1,46 @@ +import { redirect } from "next/navigation"; +import { getProject } from "@/api/projects"; +import { UnderlineLink } from "@/components/ui/UnderlineLink"; + +export default async function Layout(props: { + params: Promise<{ + team_slug: string; + project_slug: string; + }>; + children: React.ReactNode; +}) { + const params = await props.params; + const project = await getProject(params.team_slug, params.project_slug); + + if (!project) { + redirect(`/team/${params.team_slug}`); + } + + return ( +
+
+
+

+ RPC +

+

+ Remote Procedure Call (RPC) provides reliable access to querying + data and interacting with the blockchain through global edge RPCs.{" "} + + Learn more + +

+
+
+ +
+
+ {props.children} +
+
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/page.tsx new file mode 100644 index 00000000000..6730b6c1d22 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/rpc/page.tsx @@ -0,0 +1,61 @@ +import { loginRedirect } from "@app/login/loginRedirect"; +import { redirect } from "next/navigation"; +import { ResponsiveSearchParamsProvider } from "responsive-rsc"; +import { getAuthToken } from "@/api/auth-token"; +import { getProject } from "@/api/projects"; +import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; +import { getFiltersFromSearchParams } from "@/lib/time"; +import { RPCAnalytics } from "./components/RpcAnalytics"; + +export default async function Page(props: { + params: Promise<{ + team_slug: string; + project_slug: string; + }>; + searchParams: Promise<{ + from?: string | undefined | string[]; + to?: string | undefined | string[]; + interval?: string | undefined | string[]; + }>; +}) { + const [params, authToken] = await Promise.all([props.params, getAuthToken()]); + + const project = await getProject(params.team_slug, params.project_slug); + + if (!authToken) { + loginRedirect(`/team/${params.team_slug}/${params.project_slug}/rpc`); + } + + if (!project) { + redirect(`/team/${params.team_slug}`); + } + + const searchParams = await props.searchParams; + const { range, interval } = getFiltersFromSearchParams({ + defaultRange: "last-30", + from: searchParams.from, + interval: searchParams.interval, + to: searchParams.to, + }); + + const client = getClientThirdwebClient({ + jwt: authToken, + teamId: project.teamId, + }); + + return ( + +
+ +
+
+ + ); +}