Skip to content

Commit d0e4db2

Browse files
committed
[PRO-136] Add x402 requests chart in project overview (#8482)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing chart components and their presentation in the dashboard, including layout adjustments, new chart configurations, and improved user feedback for empty states. ### Detailed summary - Updated `Card` component in `bar-chart.tsx` to include additional class names. - Adjusted `aspect-auto` for `ThirdwebBarChart` in `AiTokenUsageChartCard`. - Modified `WaitingForIntegrationCard` to conditionally render `TabButtons`. - Enhanced empty state in `EmptyChartState` with a visual indicator and message. - Added `aspect-auto` class to `SkeletonBarChart`. - Introduced `X402RequestsChartCardUI` for handling X402 request data. - Refactored project analytics page to include `X402RequestsChartCardUI` and manage its data fetching. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added X402 requests analytics chart with per-period totals to the project dashboard. * **Bug Fixes** * Tab selector now appears only when multiple code tabs exist. * Time filters are shown only when relevant data is present. * Empty-chart state now shows a clear "No data available" display. * **Style** * Header styling refined for improved visuals on large screens. * Chart containers adjusted for better aspect/overflow handling. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 1056696 commit d0e4db2

File tree

7 files changed

+172
-37
lines changed

7 files changed

+172
-37
lines changed

apps/dashboard/src/@/components/analytics/empty-chart-state.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"use client";
2+
import { XIcon } from "lucide-react";
23
import { useId, useMemo } from "react";
34
import { Area, AreaChart, Bar, BarChart } from "recharts";
45
import { type ChartConfig, ChartContainer } from "@/components/ui/chart";
@@ -33,8 +34,15 @@ export function EmptyChartState({
3334

3435
return (
3536
<div className="relative z-0 h-full w-full">
36-
<div className="absolute inset-0 z-[1] flex flex-col items-center justify-center font-medium text-base text-muted-foreground">
37-
{children ?? "No data available"}
37+
<div className="absolute inset-0 z-[1] flex flex-col items-center justify-center text-base text-muted-foreground">
38+
{children ?? (
39+
<div className="flex items-center gap-3 flex-col">
40+
<div className="rounded-full border p-2 bg-background">
41+
<XIcon className="size-4" />
42+
</div>
43+
<p className="text-base"> No data available </p>
44+
</div>
45+
)}
3846
</div>
3947
<SkeletonBarChart data={barChartData} type={type} />
4048
</div>
@@ -61,7 +69,7 @@ function SkeletonBarChart(props: {
6169
const fillAreaSkeletonId = useId();
6270
return (
6371
<ChartContainer
64-
className="pointer-events-none h-full w-full blur-[5px]"
72+
className="pointer-events-none h-full w-full blur-[5px] aspect-auto"
6573
config={skeletonChartConfig}
6674
>
6775
{props.type === "bar" ? (

apps/dashboard/src/@/components/blocks/charts/bar-chart.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
5959
props.variant || configKeys.length > 4 ? "stacked" : "grouped";
6060

6161
return (
62-
<Card className={props.className}>
62+
<Card className={cn("overflow-hidden", props.className)}>
6363
{props.header && (
6464
<CardHeader>
6565
<CardTitle className={cn("mb-2", props.header.titleClassName)}>

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/analytics/chart/AiTokenUsageChartCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export function AiTokenUsageChartCardUI(props: {
6060

6161
return (
6262
<ThirdwebBarChart
63-
chartClassName="h-[275px] w-full"
63+
chartClassName="h-[275px] w-full aspect-auto"
6464
config={chartConfig}
6565
customHeader={
6666
props.viewMoreLink ? (

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/WaitingForIntegrationCard/WaitingForIntegrationCard.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,27 @@ export function WaitingForIntegrationCard(props: {
2020
const [selectedTab, setSelectedTab] = useState(props.codeTabs[0]?.label);
2121
return (
2222
<div className="rounded-lg border bg-card">
23-
<div className="border-b px-4 py-4 lg:px-6">
23+
<div className="border-b px-4 py-4 lg:px-6 lg:py-5 border-dashed">
2424
<h2 className="font-semibold text-xl tracking-tight">{props.title}</h2>
2525
</div>
2626

2727
<div className="px-4 py-6 lg:p-6">
2828
{props.children}
2929
{/* Code */}
3030
<div>
31-
<TabButtons
32-
tabs={props.codeTabs.map((tab) => ({
33-
isActive: tab.label === selectedTab,
34-
name: tab.label,
35-
onClick: () => setSelectedTab(tab.label),
36-
}))}
37-
/>
38-
<div className="h-2" />
31+
{props.codeTabs.length > 1 && (
32+
<>
33+
<TabButtons
34+
tabs={props.codeTabs.map((tab) => ({
35+
isActive: tab.label === selectedTab,
36+
name: tab.label,
37+
onClick: () => setSelectedTab(tab.label),
38+
}))}
39+
/>
40+
<div className="h-2" />
41+
</>
42+
)}
43+
3944
{props.codeTabs.find((tab) => tab.label === selectedTab)?.code}
4045
</div>
4146
</div>

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/page.tsx

Lines changed: 93 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
getInsightStatusCodeUsage,
1212
getRpcUsageByType,
1313
getUniversalBridgeUsage,
14+
getX402Settlements,
1415
isProjectActive,
1516
} from "@/api/analytics";
1617
import { getAuthToken } from "@/api/auth-token";
@@ -35,6 +36,7 @@ import { RequestsByStatusGraph } from "./gateway/indexer/components/RequestsBySt
3536
import { RPCRequestsChartUI } from "./gateway/rpc/components/RequestsGraph";
3637
import { ProjectHighlightsCard } from "./overview/highlights-card";
3738
import { AllWalletConnectionsChart } from "./wallets/analytics/chart/all-wallet-connections-chart";
39+
import { X402RequestsChartCardUI } from "./x402/analytics/x402-requests-chart";
3840

3941
type PageParams = {
4042
team_slug: string;
@@ -258,29 +260,55 @@ async function ProjectAnalytics(props: {
258260
</ResponsiveSuspense>
259261
</div>
260262

261-
{/* AI tokens */}
262-
<ResponsiveSuspense
263-
fallback={
264-
<AiTokenUsageChartCardUI
265-
isPending={true}
266-
title="AI token volume"
267-
viewMoreLink={`/team/${params.team_slug}/${params.project_slug}/ai/analytics`}
268-
aiUsageStats={[]}
263+
<div className="grid gap-6 md:grid-cols-2">
264+
{/* x402 */}
265+
<ResponsiveSuspense
266+
searchParamsUsed={["from", "to", "interval"]}
267+
fallback={
268+
<X402RequestsChartCardUI
269+
stats={[]}
270+
isPending={true}
271+
teamSlug={params.team_slug}
272+
projectSlug={params.project_slug}
273+
/>
274+
}
275+
>
276+
<AsyncX402RequestsChart
277+
from={range.from}
278+
teamSlug={params.team_slug}
279+
projectSlug={params.project_slug}
280+
to={range.to}
281+
interval={interval}
282+
projectId={project.id}
283+
teamId={project.teamId}
284+
authToken={authToken}
269285
/>
270-
}
271-
searchParamsUsed={["from", "to", "interval"]}
272-
>
273-
<AsyncAiAnalytics
274-
teamSlug={params.team_slug}
275-
projectSlug={params.project_slug}
276-
from={range.from}
277-
to={range.to}
278-
interval={interval}
279-
projectId={project.id}
280-
teamId={project.teamId}
281-
authToken={authToken}
282-
/>
283-
</ResponsiveSuspense>
286+
</ResponsiveSuspense>
287+
288+
{/* AI tokens */}
289+
<ResponsiveSuspense
290+
fallback={
291+
<AiTokenUsageChartCardUI
292+
isPending={true}
293+
title="AI token volume"
294+
viewMoreLink={`/team/${params.team_slug}/${params.project_slug}/ai/analytics`}
295+
aiUsageStats={[]}
296+
/>
297+
}
298+
searchParamsUsed={["from", "to", "interval"]}
299+
>
300+
<AsyncAiAnalytics
301+
teamSlug={params.team_slug}
302+
projectSlug={params.project_slug}
303+
from={range.from}
304+
to={range.to}
305+
interval={interval}
306+
projectId={project.id}
307+
teamId={project.teamId}
308+
authToken={authToken}
309+
/>
310+
</ResponsiveSuspense>
311+
</div>
284312

285313
{/* transactions */}
286314
<ResponsiveSuspense
@@ -483,3 +511,46 @@ async function AsyncAiAnalytics(props: {
483511
/>
484512
);
485513
}
514+
515+
async function AsyncX402RequestsChart(props: {
516+
from: Date;
517+
to: Date;
518+
interval: "day" | "week";
519+
projectId: string;
520+
teamId: string;
521+
authToken: string;
522+
teamSlug: string;
523+
projectSlug: string;
524+
}) {
525+
const stats = await getX402Settlements(
526+
{
527+
from: props.from,
528+
period: props.interval,
529+
projectId: props.projectId,
530+
teamId: props.teamId,
531+
to: props.to,
532+
},
533+
props.authToken,
534+
).catch((error) => {
535+
console.error(error);
536+
return [];
537+
});
538+
539+
const isAllEmpty = stats.every((stat) => stat.totalRequests === 0);
540+
541+
return (
542+
<X402RequestsChartCardUI
543+
stats={
544+
isAllEmpty
545+
? []
546+
: stats.map((stat) => ({
547+
requests: stat.totalRequests,
548+
time: stat.date,
549+
}))
550+
}
551+
isPending={false}
552+
teamSlug={props.teamSlug}
553+
projectSlug={props.projectSlug}
554+
/>
555+
);
556+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"use client";
2+
3+
import { format } from "date-fns";
4+
import { useMemo } from "react";
5+
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
6+
import { TotalValueChartHeader } from "@/components/blocks/charts/chart-header";
7+
8+
export function X402RequestsChartCardUI(props: {
9+
stats: Array<{ requests: number; time: string }>;
10+
isPending: boolean;
11+
teamSlug: string;
12+
projectSlug: string;
13+
}) {
14+
const total = useMemo(() => {
15+
return props.stats.reduce((acc, curr) => acc + curr.requests, 0);
16+
}, [props.stats]);
17+
18+
return (
19+
<ThirdwebBarChart
20+
chartClassName="w-full h-[275px]"
21+
data={props.stats}
22+
config={{
23+
requests: {
24+
color: "hsl(var(--chart-1))",
25+
label: "Requests",
26+
},
27+
}}
28+
isPending={props.isPending}
29+
hideLabel={false}
30+
toolTipLabelFormatter={(label) => {
31+
return format(label, "MMM dd");
32+
}}
33+
toolTipValueFormatter={(value) => {
34+
return compactNumberFormatter.format(value as number);
35+
}}
36+
customHeader={
37+
<TotalValueChartHeader
38+
total={total}
39+
isPending={props.isPending}
40+
title="X402 Requests"
41+
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/x402`}
42+
/>
43+
}
44+
/>
45+
);
46+
}
47+
48+
const compactNumberFormatter = new Intl.NumberFormat("en-US", {
49+
notation: "compact",
50+
maximumFractionDigits: 2,
51+
});

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/x402/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ export default async function Page(props: {
8080
return (
8181
<ResponsiveSearchParamsProvider value={searchParams}>
8282
<div className="flex flex-col gap-4 md:gap-6">
83-
<ResponsiveTimeFilters defaultRange={defaultRange} />
8483
{totalPayments === 0 ? (
8584
<X402EmptyState walletAddress={projectWallet?.address} />
8685
) : (
8786
<>
87+
<ResponsiveTimeFilters defaultRange={defaultRange} />
8888
<X402Summary
8989
authToken={authToken}
9090
projectId={project.id}

0 commit comments

Comments
 (0)