refactor(admin-web): replace hardcoded chart colors with theme tokens

This commit is contained in:
saravanakumardb1 2026-03-04 17:32:35 -08:00
parent a33f3cfbd6
commit fab88a57a4
4 changed files with 121 additions and 70 deletions

View File

@ -1,7 +1,16 @@
'use client'; 'use client';
import { useEffect, useState, useCallback, useMemo } from 'react'; import { useEffect, useState, useCallback, useMemo } from 'react';
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Legend } from 'recharts'; import {
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
CartesianGrid,
Legend,
} from 'recharts';
import { import {
AlertTriangle, AlertTriangle,
Bug, Bug,
@ -44,12 +53,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from '@/components/ui/table'; } from '@/components/ui/table';
import { import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/components/ui/tabs';
// ─── Types ────────────────────────────────────────────────────── // ─── Types ──────────────────────────────────────────────────────
@ -192,7 +196,9 @@ function ClusterTimelineChart({ clusters }: { clusters: TelemetryCluster[] }) {
else if (c.severity === 'error') b.error += c.totalCount; else if (c.severity === 'error') b.error += c.totalCount;
else if (c.severity === 'fatal') b.fatal += c.totalCount; else if (c.severity === 'fatal') b.fatal += c.totalCount;
} }
return Array.from(buckets.values()).sort((a, b) => a.date.localeCompare(b.date)).slice(-14); return Array.from(buckets.values())
.sort((a, b) => a.date.localeCompare(b.date))
.slice(-14);
}, [clusters]); }, [clusters]);
if (chartData.length === 0) return null; if (chartData.length === 0) return null;
@ -218,9 +224,15 @@ function ClusterTimelineChart({ clusters }: { clusters: TelemetryCluster[] }) {
}} }}
/> />
<Legend wrapperStyle={{ fontSize: 12 }} /> <Legend wrapperStyle={{ fontSize: 12 }} />
<Bar dataKey="fatal" stackId="a" fill="#ef4444" name="Fatal" /> <Bar dataKey="fatal" stackId="a" fill="hsl(var(--destructive))" name="Fatal" />
<Bar dataKey="error" stackId="a" fill="#f97316" name="Error" /> <Bar dataKey="error" stackId="a" fill="hsl(var(--chart-5))" name="Error" />
<Bar dataKey="warn" stackId="a" fill="#eab308" name="Warn" radius={[2, 2, 0, 0]} /> <Bar
dataKey="warn"
stackId="a"
fill="hsl(var(--chart-4))"
name="Warn"
radius={[2, 2, 0, 0]}
/>
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</CardContent> </CardContent>
@ -345,7 +357,7 @@ export default function ClientLogsPage() {
// Fetch ingestion metrics // Fetch ingestion metrics
useEffect(() => { useEffect(() => {
fetch('/api/telemetry/metrics') fetch('/api/telemetry/metrics')
.then(r => r.ok ? r.json() : null) .then(r => (r.ok ? r.json() : null))
.then(d => d && setIngestionMetrics(d)) .then(d => d && setIngestionMetrics(d))
.catch(() => {}); .catch(() => {});
}, []); }, []);
@ -356,20 +368,26 @@ export default function ClientLogsPage() {
setGeoLoading(true); setGeoLoading(true);
const sevenDaysAgo = new Date(Date.now() - 7 * 86400000).toISOString(); const sevenDaysAgo = new Date(Date.now() - 7 * 86400000).toISOString();
fetch(`/api/telemetry/geo?from=${encodeURIComponent(sevenDaysAgo)}`) fetch(`/api/telemetry/geo?from=${encodeURIComponent(sevenDaysAgo)}`)
.then(r => r.ok ? r.json() : null) .then(r => (r.ok ? r.json() : null))
.then(d => d && setGeoData(d.distribution || [])) .then(d => d && setGeoData(d.distribution || []))
.catch(() => {}) .catch(() => {})
.finally(() => setGeoLoading(false)); .finally(() => setGeoLoading(false));
}, [tab]); }, [tab]);
const handleClusterAction = async (cluster: TelemetryCluster, action: 'resolved' | 'ignored' | 'open') => { const handleClusterAction = async (
cluster: TelemetryCluster,
action: 'resolved' | 'ignored' | 'open'
) => {
const pk = cluster.pk || ''; const pk = cluster.pk || '';
try { try {
await fetch(`/api/telemetry/clusters/${encodeURIComponent(cluster.id)}?pk=${encodeURIComponent(pk)}`, { await fetch(
method: 'PATCH', `/api/telemetry/clusters/${encodeURIComponent(cluster.id)}?pk=${encodeURIComponent(pk)}`,
headers: { 'Content-Type': 'application/json' }, {
body: JSON.stringify({ status: action }), method: 'PATCH',
}); headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: action }),
}
);
fetchEvents(); fetchEvents();
} catch { } catch {
// best effort // best effort
@ -378,7 +396,8 @@ export default function ClientLogsPage() {
const handleGdprErasure = async () => { const handleGdprErasure = async () => {
if (!gdprUserId.trim()) return; if (!gdprUserId.trim()) return;
if (!confirm(`Delete ALL telemetry data for user "${gdprUserId}"? This cannot be undone.`)) return; if (!confirm(`Delete ALL telemetry data for user "${gdprUserId}"? This cannot be undone.`))
return;
setGdprLoading(true); setGdprLoading(true);
setGdprResult(null); setGdprResult(null);
try { try {
@ -402,7 +421,7 @@ export default function ClientLogsPage() {
}; };
// Client-side text filter // Client-side text filter
const filteredEvents = events.filter((e) => { const filteredEvents = events.filter(e => {
if (!searchQuery) return true; if (!searchQuery) return true;
const q = searchQuery.toLowerCase(); const q = searchQuery.toLowerCase();
return ( return (
@ -417,10 +436,10 @@ export default function ClientLogsPage() {
}); });
// Stats // Stats
const errorCount = events.filter((e) => e.eventType === 'error' || e.eventType === 'fatal').length; const errorCount = events.filter(e => e.eventType === 'error' || e.eventType === 'fatal').length;
const warnCount = events.filter((e) => e.eventType === 'warn').length; const warnCount = events.filter(e => e.eventType === 'warn').length;
const keyboardCount = events.filter((e) => e.channel === 'keyboard_extension').length; const keyboardCount = events.filter(e => e.channel === 'keyboard_extension').length;
const uniqueSessions = new Set(events.map((e) => e.sessionId)).size; const uniqueSessions = new Set(events.map(e => e.sessionId)).size;
return ( return (
<div className="flex-1 space-y-4 p-8 pt-6"> <div className="flex-1 space-y-4 p-8 pt-6">
@ -539,7 +558,7 @@ export default function ClientLogsPage() {
<Input <Input
placeholder="Module..." placeholder="Module..."
value={module} value={module}
onChange={(e) => setModule(e.target.value)} onChange={e => setModule(e.target.value)}
/> />
</div> </div>
<div className="flex-1 min-w-[200px]"> <div className="flex-1 min-w-[200px]">
@ -549,7 +568,7 @@ export default function ClientLogsPage() {
placeholder="Search events, errors, users..." placeholder="Search events, errors, users..."
className="pl-8" className="pl-8"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={e => setSearchQuery(e.target.value)}
/> />
</div> </div>
</div> </div>
@ -612,18 +631,17 @@ export default function ClientLogsPage() {
) : filteredEvents.length === 0 ? ( ) : filteredEvents.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground"> <TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
No telemetry events found. Events will appear here once clients start sending data. No telemetry events found. Events will appear here once clients start
sending data.
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
filteredEvents.map((evt) => ( filteredEvents.map(evt => (
<> <>
<TableRow <TableRow
key={evt.id} key={evt.id}
className="cursor-pointer hover:bg-muted/50" className="cursor-pointer hover:bg-muted/50"
onClick={() => onClick={() => setExpandedEvent(expandedEvent === evt.id ? null : evt.id)}
setExpandedEvent(expandedEvent === evt.id ? null : evt.id)
}
> >
<TableCell> <TableCell>
{expandedEvent === evt.id ? ( {expandedEvent === evt.id ? (
@ -690,9 +708,7 @@ export default function ClientLogsPage() {
</div> </div>
<div> <div>
<span className="text-muted-foreground block">User ID</span> <span className="text-muted-foreground block">User ID</span>
<span className="font-medium font-mono"> <span className="font-medium font-mono">{evt.userId || '—'}</span>
{evt.userId || '—'}
</span>
</div> </div>
<div> <div>
<span className="text-muted-foreground block">Install ID</span> <span className="text-muted-foreground block">Install ID</span>
@ -716,7 +732,9 @@ export default function ClientLogsPage() {
</div> </div>
{evt.errorDomain && ( {evt.errorDomain && (
<div> <div>
<span className="text-muted-foreground block">Error Domain</span> <span className="text-muted-foreground block">
Error Domain
</span>
<span className="font-medium font-mono">{evt.errorDomain}</span> <span className="font-medium font-mono">{evt.errorDomain}</span>
</div> </div>
)} )}
@ -783,11 +801,12 @@ export default function ClientLogsPage() {
) : clusters.length === 0 ? ( ) : clusters.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground"> <TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
No error clusters found. Clusters form automatically when warn/error/fatal events arrive. No error clusters found. Clusters form automatically when warn/error/fatal
events arrive.
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
clusters.map((c) => ( clusters.map(c => (
<TableRow key={c.id}> <TableRow key={c.id}>
<TableCell>{eventTypeBadge(c.severity)}</TableCell> <TableCell>{eventTypeBadge(c.severity)}</TableCell>
<TableCell> <TableCell>
@ -816,15 +835,24 @@ export default function ClientLogsPage() {
</TableCell> </TableCell>
<TableCell> <TableCell>
{c.status === 'resolved' ? ( {c.status === 'resolved' ? (
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20"> <Badge
variant="outline"
className="bg-green-500/10 text-green-500 border-green-500/20"
>
<CheckCircle className="mr-1 h-3 w-3" /> Resolved <CheckCircle className="mr-1 h-3 w-3" /> Resolved
</Badge> </Badge>
) : c.status === 'ignored' ? ( ) : c.status === 'ignored' ? (
<Badge variant="outline" className="bg-gray-500/10 text-gray-400 border-gray-500/20"> <Badge
variant="outline"
className="bg-gray-500/10 text-gray-400 border-gray-500/20"
>
<XCircle className="mr-1 h-3 w-3" /> Ignored <XCircle className="mr-1 h-3 w-3" /> Ignored
</Badge> </Badge>
) : ( ) : (
<Badge variant="outline" className="bg-orange-500/10 text-orange-500 border-orange-500/20"> <Badge
variant="outline"
className="bg-orange-500/10 text-orange-500 border-orange-500/20"
>
Open Open
</Badge> </Badge>
)} )}
@ -903,7 +931,8 @@ export default function ClientLogsPage() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{ingestionMetrics.totalPiiBlocked} PII blocked, {ingestionMetrics.totalDuplicatesDropped} duplicates {ingestionMetrics.totalPiiBlocked} PII blocked,{' '}
{ingestionMetrics.totalDuplicatesDropped} duplicates
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@ -951,15 +980,27 @@ export default function ClientLogsPage() {
</div> </div>
) : geoData.length === 0 ? ( ) : geoData.length === 0 ? (
<p className="text-sm text-muted-foreground py-8 text-center"> <p className="text-sm text-muted-foreground py-8 text-center">
No geo data available. Enable geo enrichment with <code>TELEMETRY_GEO_ENABLED=true</code> and configure <code>TELEMETRY_GEO_API_URL</code>. No geo data available. Enable geo enrichment with{' '}
<code>TELEMETRY_GEO_ENABLED=true</code> and configure{' '}
<code>TELEMETRY_GEO_API_URL</code>.
</p> </p>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
<ResponsiveContainer width="100%" height={Math.max(200, geoData.length * 32)}> <ResponsiveContainer width="100%" height={Math.max(200, geoData.length * 32)}>
<BarChart data={geoData} layout="vertical" margin={{ left: 40, right: 20 }}> <BarChart data={geoData} layout="vertical" margin={{ left: 40, right: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" /> <CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis type="number" tick={{ fontSize: 11 }} stroke="hsl(var(--muted-foreground))" /> <XAxis
<YAxis type="category" dataKey="countryCode" tick={{ fontSize: 12 }} width={50} stroke="hsl(var(--muted-foreground))" /> type="number"
tick={{ fontSize: 11 }}
stroke="hsl(var(--muted-foreground))"
/>
<YAxis
type="category"
dataKey="countryCode"
tick={{ fontSize: 12 }}
width={50}
stroke="hsl(var(--muted-foreground))"
/>
<Tooltip <Tooltip
contentStyle={{ contentStyle={{
backgroundColor: 'hsl(var(--card))', backgroundColor: 'hsl(var(--card))',
@ -967,7 +1008,10 @@ export default function ClientLogsPage() {
borderRadius: '8px', borderRadius: '8px',
fontSize: 12, fontSize: 12,
}} }}
formatter={(value: number | undefined) => [(value ?? 0).toLocaleString(), 'Events']} formatter={(value: number | undefined) => [
(value ?? 0).toLocaleString(),
'Events',
]}
/> />
<Bar dataKey="count" fill="hsl(var(--primary))" radius={[0, 4, 4, 0]} /> <Bar dataKey="count" fill="hsl(var(--primary))" radius={[0, 4, 4, 0]} />
</BarChart> </BarChart>
@ -1010,8 +1054,8 @@ export default function ClientLogsPage() {
GDPR Data Erasure GDPR Data Erasure
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
Delete all telemetry events for a specific user. This action is irreversible Delete all telemetry events for a specific user. This action is irreversible and
and will be logged in the audit trail. will be logged in the audit trail.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
@ -1019,7 +1063,7 @@ export default function ClientLogsPage() {
<Input <Input
placeholder="Enter user ID..." placeholder="Enter user ID..."
value={gdprUserId} value={gdprUserId}
onChange={(e) => setGdprUserId(e.target.value)} onChange={e => setGdprUserId(e.target.value)}
/> />
<Button <Button
variant="destructive" variant="destructive"
@ -1030,9 +1074,11 @@ export default function ClientLogsPage() {
</Button> </Button>
</div> </div>
{gdprResult && ( {gdprResult && (
<p className={`text-sm ${ <p
gdprResult.startsWith('Error') ? 'text-red-500' : 'text-green-500' className={`text-sm ${
}`}> gdprResult.startsWith('Error') ? 'text-red-500' : 'text-green-500'
}`}
>
{gdprResult} {gdprResult}
</p> </p>
)} )}

View File

@ -365,8 +365,8 @@ export default function DashboardPage() {
<AreaChart data={dailyMetrics}> <AreaChart data={dailyMetrics}>
<defs> <defs>
<linearGradient id="colorUsers" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="colorUsers" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="hsl(221, 83%, 53%)" stopOpacity={0.3} /> <stop offset="5%" stopColor="hsl(var(--chart-1))" stopOpacity={0.3} />
<stop offset="95%" stopColor="hsl(221, 83%, 53%)" stopOpacity={0} /> <stop offset="95%" stopColor="hsl(var(--chart-1))" stopOpacity={0} />
</linearGradient> </linearGradient>
</defs> </defs>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" /> <CartesianGrid strokeDasharray="3 3" className="opacity-30" />
@ -381,7 +381,7 @@ export default function DashboardPage() {
<Area <Area
type="monotone" type="monotone"
dataKey="activeUsers" dataKey="activeUsers"
stroke="hsl(221, 83%, 53%)" stroke="hsl(var(--chart-1))"
fill="url(#colorUsers)" fill="url(#colorUsers)"
strokeWidth={2} strokeWidth={2}
/> />
@ -408,7 +408,7 @@ export default function DashboardPage() {
border: '1px solid hsl(var(--border))', border: '1px solid hsl(var(--border))',
}} }}
/> />
<Bar dataKey="revenue" fill="hsl(142, 71%, 45%)" radius={[4, 4, 0, 0]} /> <Bar dataKey="revenue" fill="hsl(var(--chart-2))" radius={[4, 4, 0, 0]} />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</CardContent> </CardContent>

View File

@ -56,10 +56,10 @@ import {
} from 'recharts'; } from 'recharts';
const COLORS = [ const COLORS = [
'hsl(221, 83%, 53%)', 'hsl(var(--chart-1))',
'hsl(142, 71%, 45%)', 'hsl(var(--chart-2))',
'hsl(38, 92%, 50%)', 'hsl(var(--chart-4))',
'hsl(280, 67%, 55%)', 'hsl(var(--chart-5))',
]; ];
function usageRecordsToDailyMetrics(records: ApiUsageRecord[]): DailyMetric[] { function usageRecordsToDailyMetrics(records: ApiUsageRecord[]): DailyMetric[] {
@ -88,8 +88,12 @@ export default function UsagePage() {
const [timeRange, setTimeRange] = useState('30d'); const [timeRange, setTimeRange] = useState('30d');
const [dailyMetrics, setDailyMetrics] = useState<DailyMetric[]>(mockDailyMetrics); const [dailyMetrics, setDailyMetrics] = useState<DailyMetric[]>(mockDailyMetrics);
const [modelUsage, setModelUsage] = useState<(ApiModelBreakdown & { percentage: number })[]>([]); const [modelUsage, setModelUsage] = useState<(ApiModelBreakdown & { percentage: number })[]>([]);
const [sourceUsage, setSourceUsage] = useState<(ApiSourceBreakdown & { percentage: number })[]>([]); const [sourceUsage, setSourceUsage] = useState<(ApiSourceBreakdown & { percentage: number })[]>(
const [productUsage, setProductUsage] = useState<(ApiProductBreakdown & { percentage: number })[]>([]); []
);
const [productUsage, setProductUsage] = useState<
(ApiProductBreakdown & { percentage: number })[]
>([]);
const [users, setUsers] = useState<User[]>(mockUsers); const [users, setUsers] = useState<User[]>(mockUsers);
const [cohorts, setCohorts] = useState<RetentionCohort[]>([]); const [cohorts, setCohorts] = useState<RetentionCohort[]>([]);
const [selectedUserId, setSelectedUserId] = useState<string | null>(null); const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
@ -211,7 +215,8 @@ export default function UsagePage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<UserIcon className="h-4 w-4 text-blue-600" /> <UserIcon className="h-4 w-4 text-blue-600" />
<span className="text-sm font-medium text-blue-800 dark:text-blue-200"> <span className="text-sm font-medium text-blue-800 dark:text-blue-200">
Viewing usage for: {users.find(u => u.id === selectedUserId)?.name || selectedUserId} Viewing usage for:{' '}
{users.find(u => u.id === selectedUserId)?.name || selectedUserId}
</span> </span>
<span className="text-xs text-blue-600 dark:text-blue-400"> <span className="text-xs text-blue-600 dark:text-blue-400">
({users.find(u => u.id === selectedUserId)?.email}) ({users.find(u => u.id === selectedUserId)?.email})
@ -310,8 +315,8 @@ export default function UsagePage() {
<AreaChart data={dailyMetrics}> <AreaChart data={dailyMetrics}>
<defs> <defs>
<linearGradient id="colorTokens" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="colorTokens" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="hsl(221, 83%, 53%)" stopOpacity={0.3} /> <stop offset="5%" stopColor="hsl(var(--chart-1))" stopOpacity={0.3} />
<stop offset="95%" stopColor="hsl(221, 83%, 53%)" stopOpacity={0} /> <stop offset="95%" stopColor="hsl(var(--chart-1))" stopOpacity={0} />
</linearGradient> </linearGradient>
</defs> </defs>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" /> <CartesianGrid strokeDasharray="3 3" className="opacity-30" />
@ -327,7 +332,7 @@ export default function UsagePage() {
<Area <Area
type="monotone" type="monotone"
dataKey="totalTokens" dataKey="totalTokens"
stroke="hsl(221, 83%, 53%)" stroke="hsl(var(--chart-1))"
fill="url(#colorTokens)" fill="url(#colorTokens)"
strokeWidth={2} strokeWidth={2}
name="Tokens" name="Tokens"
@ -356,7 +361,7 @@ export default function UsagePage() {
/> />
<Bar <Bar
dataKey="totalRequests" dataKey="totalRequests"
fill="hsl(142, 71%, 45%)" fill="hsl(var(--chart-2))"
radius={[4, 4, 0, 0]} radius={[4, 4, 0, 0]}
name="Requests" name="Requests"
/> />

View File

@ -276,8 +276,8 @@ export default function UserDetailPage() {
<AreaChart data={dailyUsage}> <AreaChart data={dailyUsage}>
<defs> <defs>
<linearGradient id="colorDict" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="colorDict" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="hsl(221, 83%, 53%)" stopOpacity={0.3} /> <stop offset="5%" stopColor="hsl(var(--chart-1))" stopOpacity={0.3} />
<stop offset="95%" stopColor="hsl(221, 83%, 53%)" stopOpacity={0} /> <stop offset="95%" stopColor="hsl(var(--chart-1))" stopOpacity={0} />
</linearGradient> </linearGradient>
</defs> </defs>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" /> <CartesianGrid strokeDasharray="3 3" className="opacity-30" />
@ -292,7 +292,7 @@ export default function UserDetailPage() {
<Area <Area
type="monotone" type="monotone"
dataKey="dictations" dataKey="dictations"
stroke="hsl(221, 83%, 53%)" stroke="hsl(var(--chart-1))"
fill="url(#colorDict)" fill="url(#colorDict)"
strokeWidth={2} strokeWidth={2}
name="Dictations" name="Dictations"