Skip to content

Commit 218fc00

Browse files
committed
feat: implement metrics page and project layout with navigation tabs
1 parent 13a3086 commit 218fc00

10 files changed

Lines changed: 544 additions & 190 deletions

File tree

apps/dashboard/src/layouts/AppLayout.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ const nav = [
3333
{ label: "Projects", href: "/projects", icon: FolderOpen },
3434
{ label: "Storage", href: "/storage", icon: HardDrive },
3535
{ label: "Logs", href: "/logs", icon: ScrollText },
36-
{ label: "Observability", href: "/observability", icon: BarChart2 },
36+
{ label: "Observability", href: "/observability", icon: Activity },
37+
{ label: "Metrics", href: "/metrics", icon: BarChart2 },
3738
{ label: "Audit Log", href: "/audit", icon: Shield },
3839
{ label: "Team", href: "/team", icon: Users },
3940
{

apps/dashboard/src/lib/utils.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@ export function cn(...inputs: ClassValue[]) {
55
return twMerge(clsx(inputs));
66
}
77

8-
export function formatDate(date: string | Date, opts?: Intl.DateTimeFormatOptions) {
8+
export function formatDate(
9+
date: string | Date | null | undefined,
10+
opts?: Intl.DateTimeFormatOptions,
11+
) {
12+
if (!date) return "N/A";
13+
const d = new Date(date);
14+
if (isNaN(d.getTime())) return "N/A";
915
return new Intl.DateTimeFormat("en-US", {
1016
year: "numeric",
1117
month: "short",
1218
day: "numeric",
1319
hour: "2-digit",
1420
minute: "2-digit",
1521
...opts,
16-
}).format(new Date(date));
22+
}).format(d);
1723
}
1824

1925
export function formatRelative(date: string | Date): string {
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
import { PageHeader } from "@/components/ui/PageHeader";
2+
import { PageSkeleton } from "@/components/ui/PageSkeleton";
3+
import { StatCard } from "@/components/ui/StatCard";
4+
import { api } from "@/lib/api";
5+
import { QK } from "@/lib/query-keys";
6+
import { useQuery } from "@tanstack/react-query";
7+
import { BarChart2, Clock, FolderOpen, TrendingUp, Users, Zap } from "lucide-react";
8+
import { useState } from "react";
9+
import { Area, AreaChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
10+
11+
type Period = "24h" | "7d" | "30d";
12+
13+
export default function MetricsPage() {
14+
const [period, setPeriod] = useState<Period>("24h");
15+
16+
const { data: metrics, isLoading } = useQuery({
17+
queryKey: QK.metricsOverview(),
18+
queryFn: () => api.get<{ metrics: any }>("/admin/metrics/overview"),
19+
refetchInterval: 30_000,
20+
});
21+
22+
const { data: latency } = useQuery({
23+
queryKey: QK.metricsLatency(period),
24+
queryFn: () => api.get<{ latency: any }>(`/admin/metrics/latency?period=${period}`),
25+
refetchInterval: 30_000,
26+
});
27+
28+
const { data: timeseries } = useQuery({
29+
queryKey: QK.metricsTimeseries("requests", period),
30+
queryFn: () => api.get<{ timeseries: any[] }>(`/admin/metrics/timeseries?period=${period}`),
31+
refetchInterval: 30_000,
32+
});
33+
34+
const { data: topEndpoints } = useQuery({
35+
queryKey: QK.metricsTopEndpoints(period),
36+
queryFn: () =>
37+
api.get<{ endpoints: any[] }>(`/admin/metrics/top-endpoints?period=${period}&limit=15`),
38+
refetchInterval: 30_000,
39+
});
40+
41+
if (isLoading) return <PageSkeleton />;
42+
43+
const m = metrics?.metrics;
44+
const l = latency?.latency;
45+
const ts = timeseries?.timeseries ?? [];
46+
const endpoints = topEndpoints?.endpoints ?? [];
47+
48+
return (
49+
<div>
50+
<PageHeader
51+
title="Metrics"
52+
description="Detailed performance metrics for your Betterbase instance"
53+
/>
54+
55+
<div className="px-8 pb-8 space-y-6">
56+
{/* Period selector */}
57+
<div className="flex gap-2">
58+
{(["24h", "7d", "30d"] as Period[]).map((p) => (
59+
<button
60+
key={p}
61+
onClick={() => setPeriod(p)}
62+
className={`px-3 py-1.5 rounded-md text-sm transition-colors ${
63+
period === p ? "font-medium" : "opacity-60 hover:opacity-100"
64+
}`}
65+
style={{
66+
background: period === p ? "var(--color-brand)" : "var(--color-surface)",
67+
color: period === p ? "white" : "var(--color-text-secondary)",
68+
border: `1px solid ${period === p ? "var(--color-brand)" : "var(--color-border)"}`,
69+
}}
70+
>
71+
{p}
72+
</button>
73+
))}
74+
</div>
75+
76+
{/* Key Metrics Cards */}
77+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
78+
<StatCard
79+
label="Total Projects"
80+
value={m?.projects ?? 0}
81+
icon={FolderOpen}
82+
color="brand"
83+
/>
84+
<StatCard
85+
label="Active Functions"
86+
value={m?.active_functions ?? 0}
87+
icon={Zap}
88+
color="success"
89+
/>
90+
<StatCard
91+
label="Total Users"
92+
value={m?.total_end_users ?? 0}
93+
icon={Users}
94+
color="default"
95+
/>
96+
<StatCard
97+
label="Avg Response Time"
98+
value={`${l?.avg ?? 0}ms`}
99+
icon={Clock}
100+
color="default"
101+
/>
102+
</div>
103+
104+
{/* Latency Distribution */}
105+
<div
106+
className="grid grid-cols-2 lg:grid-cols-4 gap-4"
107+
style={{
108+
background: "var(--color-surface)",
109+
border: "1px solid var(--color-border)",
110+
borderRadius: "12px",
111+
padding: "16px",
112+
}}
113+
>
114+
{[
115+
{ label: "P50 Latency", value: l?.p50 ?? 0, icon: TrendingUp },
116+
{ label: "P95 Latency", value: l?.p95 ?? 0, icon: TrendingUp },
117+
{ label: "P99 Latency", value: l?.p99 ?? 0, icon: TrendingUp },
118+
{ label: "Avg Latency", value: l?.avg ?? 0, icon: BarChart2 },
119+
].map(({ label, value, icon: Icon }) => (
120+
<div key={label} className="text-center">
121+
<div className="flex items-center justify-center gap-1 mb-1">
122+
<Icon size={14} style={{ color: "var(--color-text-muted)" }} />
123+
<span className="text-xs" style={{ color: "var(--color-text-muted)" }}>
124+
{label}
125+
</span>
126+
</div>
127+
<div
128+
className="text-2xl font-semibold"
129+
style={{ color: "var(--color-text-primary)" }}
130+
>
131+
{value}ms
132+
</div>
133+
</div>
134+
))}
135+
</div>
136+
137+
{/* Request Trends */}
138+
<div
139+
className="rounded-xl p-5"
140+
style={{ background: "var(--color-surface)", border: "1px solid var(--color-border)" }}
141+
>
142+
<h2 className="text-sm font-medium mb-4" style={{ color: "var(--color-text-primary)" }}>
143+
Request Trends — {period}
144+
</h2>
145+
<div className="h-64">
146+
<ResponsiveContainer width="100%" height="100%">
147+
<AreaChart data={ts} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
148+
<XAxis
149+
dataKey="bucket"
150+
tickFormatter={(v) => new Date(v).toLocaleTimeString([], { hour: "2-digit" })}
151+
stroke="var(--color-text-muted)"
152+
fontSize={11}
153+
/>
154+
<YAxis stroke="var(--color-text-muted)" fontSize={11} />
155+
<Tooltip
156+
contentStyle={{
157+
background: "var(--color-surface)",
158+
border: "1px solid var(--color-border)",
159+
borderRadius: "6px",
160+
}}
161+
/>
162+
<Area
163+
type="monotone"
164+
dataKey="total"
165+
stroke="var(--color-brand)"
166+
fill="var(--color-brand-muted)"
167+
strokeWidth={2}
168+
name="Total Requests"
169+
/>
170+
<Area
171+
type="monotone"
172+
dataKey="errors"
173+
stroke="#ef4444"
174+
fill="#fef2f2"
175+
strokeWidth={2}
176+
name="Errors"
177+
/>
178+
</AreaChart>
179+
</ResponsiveContainer>
180+
</div>
181+
</div>
182+
183+
{/* Top Endpoints Performance */}
184+
<div
185+
className="rounded-xl overflow-hidden"
186+
style={{ background: "var(--color-surface)", border: "1px solid var(--color-border)" }}
187+
>
188+
<h2
189+
className="text-sm font-medium px-5 py-4"
190+
style={{ color: "var(--color-text-primary)" }}
191+
>
192+
Top Endpoints by Requests — {period}
193+
</h2>
194+
<div className="overflow-x-auto">
195+
<table className="w-full text-sm">
196+
<thead>
197+
<tr
198+
className="text-left text-xs border-b"
199+
style={{ borderColor: "var(--color-border)", color: "var(--color-text-muted)" }}
200+
>
201+
<th className="px-5 py-3 font-medium">Rank</th>
202+
<th className="px-5 py-3 font-medium">Method</th>
203+
<th className="px-5 py-3 font-medium">Endpoint</th>
204+
<th className="px-5 py-3 font-medium text-right">Requests</th>
205+
<th className="px-5 py-3 font-medium text-right">Avg Latency</th>
206+
<th className="px-5 py-3 font-medium text-right">Errors</th>
207+
</tr>
208+
</thead>
209+
<tbody>
210+
{endpoints.length === 0 ? (
211+
<tr>
212+
<td
213+
colSpan={6}
214+
className="px-5 py-8 text-center"
215+
style={{ color: "var(--color-text-muted)" }}
216+
>
217+
No endpoint data available
218+
</td>
219+
</tr>
220+
) : (
221+
endpoints.map((ep: any, i: number) => (
222+
<tr key={i} className="border-t" style={{ borderColor: "var(--color-border)" }}>
223+
<td className="px-5 py-3" style={{ color: "var(--color-text-muted)" }}>
224+
#{i + 1}
225+
</td>
226+
<td className="px-5 py-3">
227+
<span
228+
className="px-2 py-0.5 rounded text-xs font-medium"
229+
style={{
230+
background: "var(--color-brand-muted)",
231+
color: "var(--color-brand)",
232+
}}
233+
>
234+
{ep.method}
235+
</span>
236+
</td>
237+
<td
238+
className="px-5 py-3 font-mono text-xs"
239+
style={{ color: "var(--color-text-secondary)" }}
240+
>
241+
{ep.path}
242+
</td>
243+
<td
244+
className="px-5 py-3 text-right font-medium"
245+
style={{ color: "var(--color-text-primary)" }}
246+
>
247+
{ep.requests?.toLocaleString() ?? ep.count?.toLocaleString() ?? 0}
248+
</td>
249+
<td
250+
className="px-5 py-3 text-right"
251+
style={{ color: "var(--color-text-primary)" }}
252+
>
253+
{ep.avg_ms}ms
254+
</td>
255+
<td
256+
className="px-5 py-3 text-right"
257+
style={{
258+
color: ep.errors > 0 ? "var(--color-danger)" : "var(--color-text-muted)",
259+
}}
260+
>
261+
{ep.errors}
262+
</td>
263+
</tr>
264+
))
265+
)}
266+
</tbody>
267+
</table>
268+
</div>
269+
</div>
270+
</div>
271+
</div>
272+
);
273+
}

0 commit comments

Comments
 (0)