refactor(admin-web): replace hardcoded chart colors with theme tokens
This commit is contained in:
parent
a33f3cfbd6
commit
fab88a57a4
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user