Skip to content

Commit adaec4f

Browse files
committed
[NEB-95] Dashboard: Nebula Analytics UI updates
1 parent b7158c2 commit adaec4f

File tree

7 files changed

+267
-208
lines changed

7 files changed

+267
-208
lines changed
Lines changed: 96 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
"use client";
22

3+
import {
4+
Card,
5+
CardContent,
6+
CardDescription,
7+
CardHeader,
8+
CardTitle,
9+
} from "@/components/ui/card";
310
import {
411
type ChartConfig,
512
ChartContainer,
@@ -15,8 +22,14 @@ import {
1522
EmptyChartState,
1623
LoadingChartState,
1724
} from "../../../../components/analytics/empty-chart-state";
25+
import { cn } from "../../../lib/utils";
1826

1927
type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
28+
header?: {
29+
title: string;
30+
description?: string;
31+
titleClassName?: string;
32+
};
2033
// chart config
2134
config: TConfig;
2235
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
@@ -25,78 +38,99 @@ type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
2538
// chart className
2639
chartClassName?: string;
2740
isPending: boolean;
41+
hideLabel?: boolean;
42+
toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode;
2843
};
2944

3045
export function ThirdwebAreaChart<TConfig extends ChartConfig>(
3146
props: ThirdwebAreaChartProps<TConfig>,
3247
) {
3348
const configKeys = useMemo(() => Object.keys(props.config), [props.config]);
49+
3450
return (
35-
<div className="rounded-lg border border-border px-4 pt-10 pb-4">
36-
<ChartContainer config={props.config} className={props.chartClassName}>
37-
{props.isPending ? (
38-
<LoadingChartState />
39-
) : props.data.length === 0 ? (
40-
<EmptyChartState />
41-
) : (
42-
<AreaChart
43-
accessibilityLayer
44-
data={props.data}
45-
margin={{
46-
left: 12,
47-
right: 12,
48-
}}
49-
>
50-
<CartesianGrid vertical={false} />
51-
<XAxis
52-
dataKey="time"
53-
tickLine={false}
54-
axisLine={false}
55-
tickMargin={20}
56-
tickFormatter={(value) => formatDate(new Date(value), "MMM dd")}
57-
/>
58-
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
59-
<defs>
51+
<Card>
52+
{props.header && (
53+
<CardHeader>
54+
<CardTitle className={cn("mb-2", props.header.titleClassName)}>
55+
{props.header.title}
56+
</CardTitle>
57+
{props.header.description && (
58+
<CardDescription>{props.header.description}</CardDescription>
59+
)}
60+
</CardHeader>
61+
)}
62+
63+
<CardContent className={cn(!props.header && "pt-6")}>
64+
<ChartContainer config={props.config} className={props.chartClassName}>
65+
{props.isPending ? (
66+
<LoadingChartState />
67+
) : props.data.length === 0 ? (
68+
<EmptyChartState />
69+
) : (
70+
<AreaChart accessibilityLayer data={props.data}>
71+
<CartesianGrid vertical={false} />
72+
<XAxis
73+
dataKey="time"
74+
tickLine={false}
75+
axisLine={false}
76+
tickMargin={20}
77+
tickFormatter={(value) => formatDate(new Date(value), "MMM dd")}
78+
/>
79+
<ChartTooltip
80+
cursor={false}
81+
content={
82+
<ChartTooltipContent
83+
hideLabel={
84+
props.hideLabel !== undefined ? props.hideLabel : true
85+
}
86+
labelFormatter={props.toolTipLabelFormatter}
87+
/>
88+
}
89+
/>
90+
<defs>
91+
{configKeys.map((key) => (
92+
<linearGradient
93+
key={key}
94+
id={`fill_${key}`}
95+
x1="0"
96+
y1="0"
97+
x2="0"
98+
y2="1"
99+
>
100+
<stop
101+
offset="5%"
102+
stopColor={`var(--color-${key})`}
103+
stopOpacity={0.8}
104+
/>
105+
<stop
106+
offset="95%"
107+
stopColor={`var(--color-${key})`}
108+
stopOpacity={0.1}
109+
/>
110+
</linearGradient>
111+
))}
112+
</defs>
60113
{configKeys.map((key) => (
61-
<linearGradient
114+
<Area
62115
key={key}
63-
id={`fill_${key}`}
64-
x1="0"
65-
y1="0"
66-
x2="0"
67-
y2="1"
68-
>
69-
<stop
70-
offset="5%"
71-
stopColor={`var(--color-${key})`}
72-
stopOpacity={0.8}
73-
/>
74-
<stop
75-
offset="95%"
76-
stopColor={`var(--color-${key})`}
77-
stopOpacity={0.1}
78-
/>
79-
</linearGradient>
116+
dataKey={key}
117+
type="natural"
118+
fill={`url(#fill_${key})`}
119+
fillOpacity={0.4}
120+
stroke={`var(--color-${key})`}
121+
stackId="a"
122+
/>
80123
))}
81-
</defs>
82-
{configKeys.map((key) => (
83-
<Area
84-
key={key}
85-
dataKey={key}
86-
type="natural"
87-
fill={`url(#fill_${key})`}
88-
fillOpacity={0.4}
89-
stroke={`var(--color-${key})`}
90-
stackId="a"
91-
/>
92-
))}
93124

94-
{props.showLegend && (
95-
<ChartLegend content={<ChartLegendContent />} className="pt-8" />
96-
)}
97-
</AreaChart>
98-
)}
99-
</ChartContainer>
100-
</div>
125+
{props.showLegend && (
126+
<ChartLegend
127+
content={<ChartLegendContent className="pt-8" />}
128+
/>
129+
)}
130+
</AreaChart>
131+
)}
132+
</ChartContainer>
133+
</CardContent>
134+
</Card>
101135
);
102136
}

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/analytics/ContractAnalyticsPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ const AnalyticsStat: React.FC<AnalyticsStatProps> = ({
319319

320320
const AnalyticsSkeleton: React.FC<{ label: string }> = ({ label }) => {
321321
return (
322-
<Card as={Stat}>
322+
<Card as={Stat} className="bg-card">
323323
<StatLabel mb={{ base: 1, md: 0 }}>{label}</StatLabel>
324324
<Skeleton isLoaded={false}>
325325
<StatNumber>{0}</StatNumber>
@@ -350,7 +350,7 @@ const AnalyticsData: React.FC<AnalyticsStatProps> = ({
350350
}, [totalQuery.data]);
351351

352352
return (
353-
<Card as={Stat}>
353+
<Card as={Stat} className="bg-card">
354354
<StatLabel mb={{ base: 1, md: 0 }}>{label}</StatLabel>
355355
<Skeleton isLoaded={totalQuery.isFetched}>
356356
<StatNumber>{data.toLocaleString()}</StatNumber>

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/Analytics.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ function OverviewAnalytics(props: ChartProps) {
135135
data={mergedData || []}
136136
isPending={isPending}
137137
showLegend
138-
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
138+
chartClassName="aspect-[1.5] lg:aspect-[3.5]"
139139
/>
140140
);
141141
}

apps/dashboard/src/app/team/[team_slug]/(team)/~/nebula/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { getTeamBySlug } from "@/api/team";
22
import { getValidAccount } from "../../../../../account/settings/getAccount";
33
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
44
import { loginRedirect } from "../../../../../login/loginRedirect";
5-
import { NebulaAnalyticsPage } from "../../../[project_slug]/nebula/components/analytics/nebula-analytics-ui";
5+
import { NebulaAnalyticsPage } from "../../../[project_slug]/nebula/components/analytics/nebula-analytics-page";
66
import { NebulaWaitListPage } from "../../../[project_slug]/nebula/components/nebula-waitlist-page";
77

88
export default async function Page(props: {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import {
2+
ResponsiveSearchParamsProvider,
3+
ResponsiveSuspense,
4+
} from "responsive-rsc";
5+
import { normalizeTimeISOString } from "../../../../../../../lib/time";
6+
import { fetchNebulaAnalytics } from "./fetch-nebula-analytics";
7+
import { NebulaAnalyticsFilter } from "./nebula-analytics-filter";
8+
import { NebulaAnalyticsDashboardUI } from "./nebula-analytics-ui";
9+
import { getNebulaAnalyticsRangeFromSearchParams } from "./utils";
10+
11+
export function NebulaAnalyticsPage(props: {
12+
searchParams: {
13+
from: string | undefined | string[];
14+
to: string | undefined | string[];
15+
interval: string | undefined | string[];
16+
};
17+
accountId: string;
18+
authToken: string;
19+
}) {
20+
return (
21+
<ResponsiveSearchParamsProvider value={props.searchParams}>
22+
<header className="border-b">
23+
<div className="container flex flex-col items-start gap-3 py-10 md:flex-row md:items-center">
24+
<div className="flex-1">
25+
<h1 className="font-semibold text-2xl tracking-tight md:text-3xl">
26+
Nebula
27+
</h1>
28+
</div>
29+
<NebulaAnalyticsFilter />
30+
</div>
31+
</header>
32+
33+
<div className="container pt-8 pb-20">
34+
<ResponsiveSuspense
35+
searchParamsUsed={["from", "to", "interval"]}
36+
fallback={<NebulaAnalyticsDashboardUI data={[]} isPending={true} />}
37+
>
38+
<NebulaAnalyticDashboard
39+
searchParams={props.searchParams}
40+
accountId={props.accountId}
41+
authToken={props.authToken}
42+
/>
43+
</ResponsiveSuspense>
44+
</div>
45+
</ResponsiveSearchParamsProvider>
46+
);
47+
}
48+
49+
async function NebulaAnalyticDashboard(props: {
50+
accountId: string;
51+
authToken: string;
52+
searchParams: {
53+
from: string | undefined | string[];
54+
to: string | undefined | string[];
55+
interval: string | undefined | string[];
56+
};
57+
}) {
58+
const { range, interval } = getNebulaAnalyticsRangeFromSearchParams(
59+
props.searchParams,
60+
);
61+
62+
const res = await fetchNebulaAnalytics({
63+
accountId: props.accountId,
64+
authToken: props.authToken,
65+
from: normalizeTimeISOString(range.from),
66+
to: normalizeTimeISOString(range.to),
67+
interval,
68+
});
69+
70+
if (!res.ok) {
71+
return (
72+
<div className="flex min-h-[300px] grow items-center justify-center rounded-lg border">
73+
<div>
74+
<p className="mb-2 text-center text-destructive-text">
75+
Failed to fetch Nebula analytics
76+
</p>
77+
<p className="text-muted-foreground">{res.error}</p>
78+
</div>
79+
</div>
80+
);
81+
}
82+
83+
return <NebulaAnalyticsDashboardUI data={res.data} isPending={false} />;
84+
}

apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/analytics/nebula-analytics-ui.stories.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,6 @@ function Story() {
3737
return (
3838
<div className="container flex flex-col gap-8 py-10">
3939
<div>
40-
<h2 className="mb-1 font-semibold text-xl tracking-tight">
41-
Story Variants
42-
</h2>
4340
<TabButtons
4441
tabs={[
4542
{
@@ -103,13 +100,13 @@ function generateRandomNebulaAnalyticsData(
103100
): NebulaAnalyticsDataItem[] {
104101
return Array.from({ length: days }, (_, i) => ({
105102
date: subDays(new Date(), i).toISOString(),
106-
totalPromptTokens: randomInt(1000),
107-
totalCompletionTokens: randomInt(1000),
108-
totalSessions: randomInt(100),
109-
totalRequests: randomInt(4000),
103+
totalPromptTokens: randomInt(500, 700 + i * 100),
104+
totalCompletionTokens: randomInt(1000, 2000 + i * 100),
105+
totalSessions: randomInt(400, 1000 + i * 100),
106+
totalRequests: randomInt(4000, 5000 + i * 100),
110107
}));
111108
}
112109

113-
function randomInt(max: number) {
114-
return Math.floor(Math.random() * max);
110+
function randomInt(min: number, max: number) {
111+
return Math.floor(Math.random() * (max - min + 1)) + min;
115112
}

0 commit comments

Comments
 (0)