Skip to content

Commit e516b4d

Browse files
committed
[PRO-147] Dashboard: Update empty state for project analytics charts (#8499)
<!-- ## 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 improving the handling of empty chart states across various components in the dashboard by standardizing the `EmptyChartState` usage and enhancing the user experience with informative content. ### Detailed summary - Updated `EmptyChartState` components to use `content` prop instead of `type`. - Enhanced `EmptyChartState` with more descriptive text and links. - Introduced `EmptyChartStateGetStartedCTA` for better guidance in empty states. - Adjusted multiple charts to incorporate the new empty state handling. - Simplified chart rendering logic when no data is available. > ✨ 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 * **UI/UX Improvements** * Unified and simplified empty-chart visuals (smaller message, cleaner layout, adjusted loading background) and consistent spacing across analytics cards. * Empty states now show contextual CTAs linking to relevant configuration/details pages. * **New Features** * Reusable "Get Started" CTA for empty charts with title, description and action. * **Refactor** * Charts can now accept and render provided empty-state content for a more consistent UX. <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 dffa457 commit e516b4d

File tree

19 files changed

+162
-128
lines changed

19 files changed

+162
-128
lines changed
Lines changed: 40 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,23 @@
11
"use client";
2-
import { XIcon } from "lucide-react";
3-
import { useId, useMemo } from "react";
4-
import { Area, AreaChart, Bar, BarChart } from "recharts";
5-
import { type ChartConfig, ChartContainer } from "@/components/ui/chart";
2+
import { ArrowUpRightIcon, XIcon } from "lucide-react";
3+
import Link from "next/link";
64
import { LoadingDots } from "@/components/ui/LoadingDots";
75
import { cn } from "@/lib/utils";
6+
import { Button } from "../ui/button";
87

9-
type FakeCartData = {
10-
value: number;
11-
};
12-
13-
function generateRandomData(): FakeCartData[] {
14-
return Array.from({ length: 30 }, () => ({
15-
value: Math.floor(Math.random() * 100 + 30),
16-
}));
17-
}
18-
19-
const skeletonChartConfig = {
20-
value: {
21-
color: "hsl(var(--muted)/90%)",
22-
label: "Value",
23-
},
24-
} satisfies ChartConfig;
25-
26-
export function EmptyChartState({
27-
children,
28-
type,
29-
}: {
30-
children?: React.ReactNode;
31-
type: "bar" | "area" | "none";
32-
}) {
33-
const barChartData = useMemo(() => generateRandomData(), []);
34-
8+
export function EmptyChartState({ content }: { content?: React.ReactNode }) {
359
return (
3610
<div className="relative z-0 h-full w-full">
3711
<div className="absolute inset-0 z-[1] flex flex-col items-center justify-center text-base text-muted-foreground">
38-
{children || (
12+
{content || (
3913
<div className="flex items-center gap-3 flex-col">
4014
<div className="rounded-full border p-2 bg-background">
4115
<XIcon className="size-4" />
4216
</div>
43-
<p className="text-base"> No data available </p>
17+
<p className="text-sm"> No data available </p>
4418
</div>
4519
)}
4620
</div>
47-
{type !== "none" && <SkeletonBarChart data={barChartData} type={type} />}
4821
</div>
4922
);
5023
}
@@ -53,7 +26,7 @@ export function LoadingChartState({ className }: { className?: string }) {
5326
return (
5427
<div
5528
className={cn(
56-
"pointer-events-none flex h-full w-full items-center justify-center rounded-lg bg-muted/30",
29+
"pointer-events-none flex h-full w-full items-center justify-center bg-card rounded-lg",
5730
className,
5831
)}
5932
>
@@ -62,51 +35,40 @@ export function LoadingChartState({ className }: { className?: string }) {
6235
);
6336
}
6437

65-
function SkeletonBarChart(props: {
66-
data: FakeCartData[];
67-
type: "bar" | "area";
38+
export function EmptyChartStateGetStartedCTA(props: {
39+
link: {
40+
label: string;
41+
href: string;
42+
};
43+
title: string;
44+
description?: string;
6845
}) {
69-
const fillAreaSkeletonId = useId();
7046
return (
71-
<ChartContainer
72-
className="pointer-events-none h-full w-full blur-[5px] aspect-auto"
73-
config={skeletonChartConfig}
74-
>
75-
{props.type === "bar" ? (
76-
<BarChart
77-
data={props.data}
78-
margin={{
79-
top: 20,
80-
}}
81-
>
82-
<Bar dataKey="value" fill="var(--color-value)" radius={8} />
83-
</BarChart>
84-
) : (
85-
<AreaChart
86-
data={props.data}
87-
margin={{
88-
top: 20,
89-
}}
90-
>
91-
<defs>
92-
<linearGradient id={fillAreaSkeletonId} x1="0" x2="0" y1="0" y2="1">
93-
<stop
94-
offset="5%"
95-
stopColor={"hsl(var(--muted-foreground)/0.5)"}
96-
stopOpacity={0.8}
97-
/>
98-
<stop offset="95%" stopColor="transparent" stopOpacity={0.1} />
99-
</linearGradient>
100-
</defs>
101-
<Area
102-
dataKey="value"
103-
fill={`url(#${fillAreaSkeletonId})`}
104-
radius={8}
105-
stroke="hsl(var(--muted-foreground))"
106-
type="monotone"
107-
/>
108-
</AreaChart>
109-
)}
110-
</ChartContainer>
47+
<div className="flex items-center flex-col">
48+
<div className="rounded-full border p-2 mb-4">
49+
<XIcon className="size-4 text-foreground" />
50+
</div>
51+
<div className="mb-5 space-y-1">
52+
<h3 className="text-base text-foreground text-center font-medium">
53+
{props.title}
54+
</h3>
55+
{props.description && (
56+
<p className="text-center text-sm text-muted-foreground">
57+
{props.description}
58+
</p>
59+
)}
60+
</div>
61+
62+
<Button
63+
asChild
64+
className="rounded-full gap-2"
65+
variant="default"
66+
size="sm"
67+
>
68+
<Link href={props.link.href}>
69+
{props.link.label} <ArrowUpRightIcon className="size-4" />
70+
</Link>
71+
</Button>
72+
</div>
11173
);
11274
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,7 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
9090
{props.isPending ? (
9191
<LoadingChartState />
9292
) : props.data.length === 0 ? (
93-
<EmptyChartState type="area">
94-
{props.emptyChartState}
95-
</EmptyChartState>
93+
<EmptyChartState content={props.emptyChartState} />
9694
) : (
9795
<AreaChart
9896
accessibilityLayer

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,7 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
7878
{props.isPending ? (
7979
<LoadingChartState />
8080
) : props.data.length === 0 ? (
81-
<EmptyChartState type="bar">
82-
{props.emptyChartState}
83-
</EmptyChartState>
81+
<EmptyChartState content={props.emptyChartState} />
8482
) : (
8583
<BarChart accessibilityLayer data={props.data}>
8684
<CartesianGrid vertical={false} />

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/[chain_id]/_components/service-card.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,17 @@ function MetricPlaceholders({
6363
{metric.label}
6464
</span>
6565
<div className="h-32 w-full">
66-
<EmptyChartState type="area">
67-
{status === "active" ? (
68-
<Badge>Coming Soon</Badge>
69-
) : status === "pending" ? (
70-
<p className="text-xs">Activation in progress.</p>
71-
) : (
72-
<p className="text-xs">Activate service to view metrics.</p>
73-
)}
74-
</EmptyChartState>
66+
<EmptyChartState
67+
content={
68+
status === "active" ? (
69+
<Badge>Coming Soon</Badge>
70+
) : status === "pending" ? (
71+
<p className="text-xs">Activation in progress.</p>
72+
) : (
73+
<p className="text-xs">Activate service to view metrics.</p>
74+
)
75+
}
76+
/>
7577
</div>
7678
</Card>
7779
))}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/_analytics/ai-card.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ResponsiveSuspense } from "responsive-rsc";
22
import { getAiUsage } from "@/api/analytics";
3+
import { EmptyChartStateGetStartedCTA } from "@/components/analytics/empty-chart-state";
34
import { AiTokenUsageChartCardUI } from "../ai/analytics/chart/AiTokenUsageChartCard";
45

56
async function AsyncAiAnalytics(props: {
@@ -32,6 +33,16 @@ async function AsyncAiAnalytics(props: {
3233
isPending={false}
3334
aiUsageStats={stats}
3435
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/ai/analytics`}
36+
emptyChartState={
37+
<EmptyChartStateGetStartedCTA
38+
link={{
39+
label: "View more details",
40+
href: `/team/${props.teamSlug}/${props.projectSlug}/ai/analytics`,
41+
}}
42+
title="No data available"
43+
description="No AI usage found in selected time period"
44+
/>
45+
}
3546
/>
3647
);
3748
}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/_analytics/all-wallet-connections-chart.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ResponsiveSuspense } from "responsive-rsc";
22
import { getEOAAndInAppWalletConnections } from "@/api/analytics";
3+
import { EmptyChartStateGetStartedCTA } from "@/components/analytics/empty-chart-state";
34
import {
45
AutoMergeBarChart,
56
type StatData,
@@ -75,7 +76,16 @@ function AllWalletConnectionsChartUI(props: {
7576
isPending={props.isPending}
7677
exportButton={undefined}
7778
maxLabelsToShow={5}
78-
emptyChartState={undefined}
79+
emptyChartState={
80+
<EmptyChartStateGetStartedCTA
81+
link={{
82+
label: "View Configuration",
83+
href: `/team/${props.teamSlug}/${props.projectSlug}/wallets/user-wallets/configure`,
84+
}}
85+
title="No data available"
86+
description="No wallet connections found in selected time period"
87+
/>
88+
}
7989
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/wallets/user-wallets`}
8090
/>
8191
);

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/_analytics/bridge-card.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { ResponsiveSuspense } from "responsive-rsc";
22
import { getUniversalBridgeUsage } from "@/api/analytics";
3-
import { LoadingChartState } from "@/components/analytics/empty-chart-state";
3+
import {
4+
EmptyChartState,
5+
EmptyChartStateGetStartedCTA,
6+
LoadingChartState,
7+
} from "@/components/analytics/empty-chart-state";
48
import { TotalValueChartHeader } from "@/components/blocks/charts/chart-header";
59
import type { UniversalBridgeStats } from "@/types/analytics";
610
import { TotalVolumePieChart } from "../payments/components/TotalVolumePieChart";
@@ -76,7 +80,22 @@ function BridgeChartCardUI(props: {
7680
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/bridge`}
7781
/>
7882
<div className="p-6 grow flex items-center justify-center">
79-
<TotalVolumePieChart data={props.data} hideTotal={true} />
83+
{total === 0 ? (
84+
<EmptyChartState
85+
content={
86+
<EmptyChartStateGetStartedCTA
87+
link={{
88+
label: "View Configuration",
89+
href: `/team/${props.teamSlug}/${props.projectSlug}/bridge/configuration`,
90+
}}
91+
title="No data available"
92+
description="No bridge volume found in selected time period"
93+
/>
94+
}
95+
/>
96+
) : (
97+
<TotalVolumePieChart data={props.data} hideTotal={true} />
98+
)}
8099
</div>
81100
</div>
82101
);

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/_analytics/highlights-card-ui.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

3-
import { EmptyStateContent } from "app/(app)/team/components/Analytics/EmptyStateCard";
43
import { useSetResponsiveSearchParams } from "responsive-rsc";
4+
import { EmptyChartStateGetStartedCTA } from "@/components/analytics/empty-chart-state";
55
import type { InAppWalletStats, UniversalBridgeStats } from "@/types/analytics";
66
import { CombinedBarChartCard } from "../../../../components/Analytics/CombinedBarChartCard";
77

@@ -39,10 +39,13 @@ export function ProjectHighlightsCard(props: {
3939
feesCollected: {
4040
color: "hsl(var(--chart-4))",
4141
emptyContent: (
42-
<EmptyStateContent
43-
description="Your app hasn't collected any fees yet."
44-
link={`/team/${teamSlug}/${projectSlug}/bridge/configuration`}
45-
metric="Fees"
42+
<EmptyChartStateGetStartedCTA
43+
link={{
44+
label: "View Configuration",
45+
href: `/team/${teamSlug}/${projectSlug}/bridge/configuration`,
46+
}}
47+
title="No data available"
48+
description="No bridge revenue generated in selected time period"
4649
/>
4750
),
4851
isCurrency: true,

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/_analytics/indexer-card.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ResponsiveSuspense } from "responsive-rsc";
22
import { getInsightStatusCodeUsage } from "@/api/analytics";
3+
import { EmptyChartStateGetStartedCTA } from "@/components/analytics/empty-chart-state";
34
import { RequestsByStatusGraph } from "../gateway/indexer/components/RequestsByStatusGraph";
45

56
export function IndexerRequestsChartCard(props: {
@@ -63,6 +64,16 @@ async function AsyncIndexerRequestsChartCard(props: {
6364
data={requestsData && "data" in requestsData ? requestsData.data : []}
6465
isPending={false}
6566
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/gateway/indexer`}
67+
emptyChartState={
68+
<EmptyChartStateGetStartedCTA
69+
link={{
70+
label: "View more details",
71+
href: `/team/${props.teamSlug}/${props.projectSlug}/gateway/indexer`,
72+
}}
73+
title="No data available"
74+
description="No indexer requests found in selected time period"
75+
/>
76+
}
6677
/>
6778
);
6879
}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/_analytics/rpc-card.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ResponsiveSuspense } from "responsive-rsc";
22
import { getRpcUsageByType } from "@/api/analytics";
3+
import { EmptyChartStateGetStartedCTA } from "@/components/analytics/empty-chart-state";
34
import { RPCRequestsChartUI } from "../gateway/rpc/components/RequestsGraph";
45

56
async function AsyncRPCRequestsChartCard(props: {
@@ -23,11 +24,23 @@ async function AsyncRPCRequestsChartCard(props: {
2324
props.authToken,
2425
).catch(() => undefined);
2526

27+
const isEmpty = requestsData?.every((d) => d.count === 0);
28+
2629
return (
2730
<RPCRequestsChartUI
2831
isPending={false}
29-
data={requestsData || []}
32+
data={isEmpty ? [] : requestsData || []}
3033
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/gateway/rpc`}
34+
emptyChartState={
35+
<EmptyChartStateGetStartedCTA
36+
link={{
37+
label: "View more details",
38+
href: `/team/${props.teamSlug}/${props.projectSlug}/gateway/rpc`,
39+
}}
40+
title="No data available"
41+
description="No RPC requests found in selected time period"
42+
/>
43+
}
3144
/>
3245
);
3346
}

0 commit comments

Comments
 (0)