axonhub / frontend /src /features /dashboard /components /requests-by-api-key-chart.tsx
llzai's picture
Upload 1793 files
9853396 verified
'use client';
import { useTranslation } from 'react-i18next';
import { Bar, BarChart, CartesianGrid, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis, type TooltipProps } from 'recharts';
import { formatNumber } from '@/utils/format-number';
import { Skeleton } from '@/components/ui/skeleton';
import { useRequestsByAPIKey } from '../data/dashboard';
const COLORS = ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)', 'var(--chart-6)', 'var(--chart-7)', 'var(--chart-8)', 'var(--chart-9)', 'var(--chart-10)'];
export function RequestsByAPIKeyChart() {
const { t } = useTranslation();
const { data: apiKeyData, isLoading, error } = useRequestsByAPIKey();
if (isLoading) {
return (
<div className='flex h-[300px] items-center justify-center'>
<Skeleton className='h-[250px] w-full rounded-md' />
</div>
);
}
if (error) {
return (
<div className='flex h-[300px] items-center justify-center'>
<div className='text-sm text-red-500'>
{t('dashboard.charts.errorLoadingAPIKeyData')} {error.message}
</div>
</div>
);
}
if (!apiKeyData || apiKeyData.length === 0) {
return (
<div className='flex h-[300px] items-center justify-center'>
<div className='text-muted-foreground text-sm'>{t('dashboard.charts.noAPIKeyData')}</div>
</div>
);
}
const total = apiKeyData.reduce((sum, item) => sum + item.count, 0);
// Backend already returns data sorted by count descending
const chartData = apiKeyData.map((item) => ({
name: item.apiKeyName,
value: item.count,
}));
const legendItems = chartData.map((item, index) => ({
...item,
index: index + 1,
color: COLORS[index % COLORS.length],
percent: total ? (item.value / total) * 100 : 0,
}));
type APIKeyTooltipProps = TooltipProps<number, string> & {
payload?: Array<{
name?: string;
value?: number;
payload?: {
name: string;
value: number;
};
}>;
};
const tooltipContent = (props: APIKeyTooltipProps) => {
const payload = props.payload;
if (!props.active || !payload?.length) return null;
const [{ value }] = payload;
const name = payload[0].payload?.name;
const percent = total ? ((value ?? 0) / total) * 100 : 0;
return (
<div className='bg-background/90 rounded-md border px-3 py-2 text-xs shadow-sm backdrop-blur'>
<div className='text-foreground text-sm font-medium'>{name}</div>
<div className='text-muted-foreground'>
{value?.toLocaleString()} ({percent.toFixed(0)}%)
</div>
</div>
);
};
return (
<div className='space-y-6'>
<ResponsiveContainer width='100%' height={320}>
<BarChart data={chartData} barSize={32}>
<CartesianGrid strokeDasharray='3 3' stroke='var(--border)' vertical={false} />
<XAxis dataKey='name' hide />
<YAxis tickLine={false} axisLine={false} width={60} tick={{ fontSize: 12, fill: 'var(--muted-foreground)' }} />
<Tooltip content={tooltipContent} cursor={{ fill: 'var(--muted)' }} />
<Bar dataKey='value' radius={[6, 6, 0, 0]}>
{chartData.map((_, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
<div className='grid gap-4 sm:grid-cols-2'>
{legendItems.map((item) => (
<div key={item.name} className='grid w-full grid-cols-[auto_auto_1fr_auto] items-start gap-3'>
<span className='text-muted-foreground w-8 text-right text-sm font-semibold tabular-nums'>
{item.index.toString().padStart(2, '0')}.
</span>
<span className='mt-1 h-2.5 w-2.5 rounded-full' style={{ backgroundColor: item.color }} />
<span className='text-foreground min-w-0 text-sm font-medium break-words'>{item.name}</span>
<div className='text-right leading-tight'>
<div className='text-foreground text-sm font-medium tabular-nums'>{formatNumber(item.value)}</div>
<div className='text-muted-foreground text-xs tabular-nums'>{item.percent.toFixed(0)}%</div>
</div>
</div>
))}
</div>
</div>
);
}