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

View File

@ -365,8 +365,8 @@ export default function DashboardPage() {
<AreaChart data={dailyMetrics}>
<defs>
<linearGradient id="colorUsers" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="hsl(221, 83%, 53%)" stopOpacity={0.3} />
<stop offset="95%" stopColor="hsl(221, 83%, 53%)" stopOpacity={0} />
<stop offset="5%" stopColor="hsl(var(--chart-1))" stopOpacity={0.3} />
<stop offset="95%" stopColor="hsl(var(--chart-1))" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
@ -381,7 +381,7 @@ export default function DashboardPage() {
<Area
type="monotone"
dataKey="activeUsers"
stroke="hsl(221, 83%, 53%)"
stroke="hsl(var(--chart-1))"
fill="url(#colorUsers)"
strokeWidth={2}
/>
@ -408,7 +408,7 @@ export default function DashboardPage() {
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>
</ResponsiveContainer>
</CardContent>

View File

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

View File

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