mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
update
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
// Copyright (c) 2025-2026 rustmailer.com[](https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
@@ -10,7 +10,6 @@
|
||||
//
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, BarChart, Bar } from 'recharts';
|
||||
@@ -25,6 +24,7 @@ import { getToken } from '@/stores/authStore';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import useMinimalAccountList from '@/hooks/use-minimal-account-list';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import LongText from '@/components/long-text';
|
||||
|
||||
interface DailyActivity {
|
||||
date: string;
|
||||
@@ -75,8 +75,6 @@ const formatTooltipDate = (timestamp_ms: number, locale: string): string => {
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444'];
|
||||
|
||||
const MetricCardSkeleton = () => (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
@@ -91,7 +89,7 @@ const MetricCardSkeleton = () => (
|
||||
);
|
||||
|
||||
const EmptyChart = ({ title }: { title: string }) => (
|
||||
<div className="h-80 flex flex-col items-center justify-center text-muted-foreground">
|
||||
<div className="h-36 flex flex-col items-center justify-center text-muted-foreground">
|
||||
<Inbox className="h-12 w-12 mb-3 opacity-40" />
|
||||
<p className="text-sm font-medium">{title}</p>
|
||||
</div>
|
||||
@@ -157,13 +155,27 @@ export default function MailArchiveDashboard() {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleQuickAttachmentSearch = (filter: Record<string, any>) => {
|
||||
navigate({
|
||||
to: '/attachment',
|
||||
search: (prev: any) => ({
|
||||
page: 1,
|
||||
pageSize: prev.pageSize ?? 50,
|
||||
sortBy: prev.sortBy ?? "DATE",
|
||||
sortOrder: prev.sortOrder ?? "desc",
|
||||
q: JSON.stringify(filter),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const attachmentData = totalAttachments > 0
|
||||
? [
|
||||
{ name: 'With Attachments', value: attachmentRatio, fill: COLORS[1] },
|
||||
{ name: 'No Attachments', value: 1 - attachmentRatio, fill: '#e5e7eb' },
|
||||
{ name: 'With Attachments', value: attachmentRatio, fill: 'hsl(var(--primary))' },
|
||||
{ name: 'No Attachments', value: 1 - attachmentRatio, fill: 'hsl(var(--muted))' },
|
||||
]
|
||||
: [
|
||||
{ name: 'No Data', value: 1, fill: '#e5e7eb' },
|
||||
{ name: 'No Data', value: 1, fill: 'hsl(var(--muted))' },
|
||||
];
|
||||
|
||||
if (isLoading) {
|
||||
@@ -172,7 +184,7 @@ export default function MailArchiveDashboard() {
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{[...Array(4)].map((_, i) => <MetricCardSkeleton key={i} />)}
|
||||
</div>
|
||||
<Skeleton className="h-80 w-full" />
|
||||
<Skeleton className="h-36 w-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -182,37 +194,55 @@ export default function MailArchiveDashboard() {
|
||||
<FixedHeader />
|
||||
<Main higher>
|
||||
<div className="flex-1 space-y-6 p-6 md:p-8">
|
||||
{/* Top Metrics */}
|
||||
<div className="grid gap-4 grid-cols-1 md:grid-cols-12">
|
||||
<Card className="md:col-span-2 lg:col-span-2">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{t('dashboard.mailAccounts')}</CardTitle>
|
||||
<CardTitle className="text-xs font-bold uppercase tracking-wider">{t('dashboard.mailAccounts')}</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-4xl font-bold">{formatNumber(stats!.account_count)}</div>
|
||||
<div className="text-xl font-bold">{formatNumber(stats!.account_count)}</div>
|
||||
<p className="text-xs text-muted-foreground">{t('dashboard.connected')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="md:col-span-3 lg:col-span-3">
|
||||
<Card className="md:col-span-2 lg:col-span-2">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{t('dashboard.totalEmails')}</CardTitle>
|
||||
<CardTitle className="text-xs font-bold uppercase tracking-wider">{t('dashboard.totalEmails')}</CardTitle>
|
||||
<Mail className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-4xl font-bold">{formatNumber(stats!.email_count)}</div>
|
||||
<div className="text-xl font-bold">{formatNumber(stats!.email_count)}</div>
|
||||
<p className="text-xs text-muted-foreground">{t('dashboard.syncedLocally')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-primary/20 bg-primary/[0.01] md:col-span-4 lg:col-span-4">
|
||||
<Card className="md:col-span-2 lg:col-span-2">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-bold text-primary flex items-center gap-1 uppercase">
|
||||
<Zap className="h-3.5 w-3.5 fill-primary" />
|
||||
{t('dashboard.efficiency', 'Efficiency')}
|
||||
<CardTitle className="text-xs font-bold uppercase tracking-wider">{t('dashboard.systemVersion')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='text-xl font-bold truncate text-primary tracking-tighter'>
|
||||
{stats!.system_version ? (
|
||||
<a href={`https://github.com/rustmailer/bichon/releases/tag/${stats!.system_version}`} target="_blank" rel="noopener noreferrer" className="hover:underline">
|
||||
{stats!.system_version}
|
||||
</a>
|
||||
) : 'N/A'}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 mt-1">
|
||||
<GithubIcon className="h-5 w-5 text-muted-foreground" />
|
||||
<p className="text-xs text-muted-foreground font-mono truncate">{stats!.commit_hash?.substring(0, 7) ?? 'N/A'}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="md:col-span-6 lg:col-span-6">
|
||||
<CardHeader className="flex flex-row items-center justify-between pt-2 pb-1 px-4">
|
||||
<CardTitle className="text-xs font-bold flex items-center gap-1 uppercase">
|
||||
<Zap className="h-3.5 w-3.5" />
|
||||
{t('dashboard.efficiency')}
|
||||
</CardTitle>
|
||||
<div className="flex flex-col items-end leading-none">
|
||||
<span className="text-xl font-black text-primary">{savingsPercent}%</span>
|
||||
<span className="text-sm font-black text-primary">{savingsPercent}%</span>
|
||||
<span className="text-[9px] font-bold text-primary uppercase">{t('dashboard.saved', 'Saved')}</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -250,198 +280,282 @@ export default function MailArchiveDashboard() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="md:col-span-3 lg:col-span-3">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{t('dashboard.systemVersion')}</CardTitle>
|
||||
<Badge variant="secondary" className="text-[9px] h-4.5 font-bold px-1.5 uppercase tracking-wider">Community</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[3fr_1fr] gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-xs'>{t('dashboard.newEmails')}</CardTitle>
|
||||
<CardDescription className='text-xs'>{t('dashboard.messageDistribution')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='text-4xl font-bold truncate text-primary tracking-tighter'>
|
||||
{stats!.system_version ? (
|
||||
<a href={`https://github.com/rustmailer/bichon/releases/tag/${stats!.system_version}`} target="_blank" rel="noopener noreferrer" className="hover:underline">
|
||||
{stats!.system_version}
|
||||
</a>
|
||||
) : 'N/A'}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 mt-1">
|
||||
<GithubIcon className="h-5 w-5 text-muted-foreground" />
|
||||
<p className="text-[10px] text-muted-foreground font-mono truncate">{stats!.commit_hash?.substring(0, 7) ?? 'N/A'}</p>
|
||||
</div>
|
||||
<CardContent className="h-36">
|
||||
{hasRecentActivity ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={convertRecentActivity(stats!.recent_activity, currentLocale)} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} opacity={0.3} />
|
||||
<XAxis dataKey="date" tick={{ fontSize: 12 }} interval="preserveStart" tickCount={10} />
|
||||
<YAxis tick={{ fontSize: 12 }} />
|
||||
<Tooltip
|
||||
formatter={(v) => formatNumber(v as number)}
|
||||
content={({ payload }) => {
|
||||
if (payload && payload.length) {
|
||||
const dataPoint = payload[0].payload;
|
||||
const fullDate = formatTooltipDate(dataPoint.timestamp_ms, currentLocale);
|
||||
return (
|
||||
<div className="p-2 border rounded-lg shadow-md bg-white dark:bg-gray-800">
|
||||
<p className="font-semibold text-xs mb-1">{fullDate}</p>
|
||||
<p className="text-xs">{t('dashboard.emails')}: {formatNumber(dataPoint.count)}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey="count" fill="currentColor" className="text-primary" radius={[4, 4, 0, 0]} barSize={26} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<EmptyChart title={t('dashboard.noRecentActivity')} />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-xs'>{t('dashboard.attachmentRatio')}</CardTitle>
|
||||
<CardDescription className='text-xs'>
|
||||
{totalAttachments > 0
|
||||
? t('dashboard.attachmentRatioDesc', { percent: (attachmentRatio * 100).toFixed(1) })
|
||||
: t('dashboard.noEmailsSynced')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-center h-36">
|
||||
<ResponsiveContainer width={220} height={220}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={attachmentData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={38}
|
||||
outerRadius={68}
|
||||
paddingAngle={4}
|
||||
dataKey="value"
|
||||
stroke="none"
|
||||
>
|
||||
{attachmentData.map((entry, i) => (
|
||||
<Cell key={`cell-${i}`} fill={entry.fill} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
formatter={(v) =>
|
||||
totalAttachments > 0 ? `${((v as number) * 100).toFixed(1)}%` : '0%'
|
||||
}
|
||||
contentStyle={{
|
||||
fontSize: '12px',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '6px',
|
||||
}}
|
||||
itemStyle={{
|
||||
fontSize: '12px',
|
||||
}}
|
||||
labelStyle={{
|
||||
fontSize: '12px',
|
||||
}} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="!px-4 !pt-4 !pb-1">
|
||||
<CardTitle className="text-xs font-bold uppercase tracking-wider">{t('dashboard.top10Senders')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{hasTopSenders ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-xs">{t('dashboard.sender')}</TableHead>
|
||||
<TableHead className="text-right text-xs">{t('dashboard.count')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{stats!.top_senders.map((s) => (
|
||||
<TableRow key={s.key}>
|
||||
<TableCell>
|
||||
<div className="group relative flex items-center w-full min-w-0 h-full px-2 overflow-hidden">
|
||||
<div className="absolute left-0 top-0 bottom-0 w-[2px] bg-primary opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="text-xs flex flex-wrap gap-x-1 min-w-0 flex-1">
|
||||
<span className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleQuickSearch({ from: s.key })
|
||||
}}
|
||||
className="hover:text-primary hover:underline transition-colors truncate max-w-[258px]"
|
||||
>
|
||||
{s.key}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono text-xs">{formatNumber(s.count)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<EmptyTable title={t('dashboard.noSendersData')} />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="!px-4 !pt-4 !pb-1">
|
||||
<CardTitle className="text-xs font-bold uppercase tracking-wider">{t('dashboard.top10LargestEmails')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{hasTopEmails ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-xs">{t('dashboard.subject')}</TableHead>
|
||||
<TableHead className="text-right text-xs">{t('dashboard.size')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{stats!.top_largest_emails.map((m, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell>
|
||||
<div className="group relative flex items-center w-full min-w-0 h-full px-2 overflow-hidden">
|
||||
<div className="absolute left-0 top-0 bottom-0 w-[2px] bg-primary opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="text-xs flex flex-wrap gap-x-1 min-w-0 flex-1">
|
||||
<span className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleQuickSearch({ id: m.id })
|
||||
}}
|
||||
className="hover:text-primary hover:underline transition-colors truncate max-w-[258px]"
|
||||
>
|
||||
{m.subject || t('dashboard.noSubject')}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono text-xs">{formatBytes(m.size_bytes)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<EmptyTable title={t('dashboard.noLargeEmails')} />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="!px-4 !pt-4 !pb-1">
|
||||
<CardTitle className="text-xs font-bold uppercase tracking-wider">{t('dashboard.top10Accounts')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{hasTopAccounts ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-xs">{t('dashboard.account')}</TableHead>
|
||||
<TableHead className="text-right text-xs">{t('dashboard.emails')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{stats!.top_accounts.map((acc) => (
|
||||
<TableRow key={acc.key}>
|
||||
<TableCell>
|
||||
<div className="group relative flex items-center w-full min-w-0 h-full px-2 overflow-hidden">
|
||||
<div className="absolute left-0 top-0 bottom-0 w-[2px] bg-primary opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="text-xs flex flex-wrap gap-x-1 min-w-0 flex-1">
|
||||
<span className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleQuickSearch({ account_ids: [getAccountIdByEmail(acc.key) || 0] })
|
||||
}}
|
||||
className="hover:text-primary hover:underline transition-colors truncate max-w-[258px]"
|
||||
>
|
||||
{acc.key}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono text-xs">{formatNumber(acc.count)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<EmptyTable title={t('dashboard.noAccountData')} />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="!px-4 !pt-4 !pb-1">
|
||||
<CardTitle className="text-xs font-bold uppercase tracking-wider">
|
||||
{t('dashboard.top10LargestAttachments')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{stats?.top_largest_attachments?.length ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-xs">Name</TableHead>
|
||||
<TableHead className="text-right text-xs">{t('dashboard.size')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{stats.top_largest_attachments.slice(0, 10).map((a, i) => (
|
||||
<TableRow
|
||||
key={i}
|
||||
onClick={() => handleQuickAttachmentSearch({ id: a.id })}
|
||||
>
|
||||
<TableCell>
|
||||
<div className="group relative flex items-center w-full min-w-0 h-full px-2 overflow-hidden">
|
||||
<div className="absolute left-0 top-0 bottom-0 w-[2px] bg-primary opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="text-xs flex flex-wrap gap-x-1 min-w-0 flex-1">
|
||||
<span className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleQuickAttachmentSearch({ id: a.id })
|
||||
}}
|
||||
className="hover:text-primary hover:underline transition-colors truncate max-w-[258px]"
|
||||
>
|
||||
{a.name || 'Unnamed'}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono text-xs">
|
||||
{formatBytes(a.size_bytes)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<EmptyTable title="No attachment data" />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="trend" className="space-y-4">
|
||||
<TabsList className="grid w-full grid-cols-3 lg:w-auto">
|
||||
<TabsTrigger value="trend">{t('dashboard.dayTrend')}</TabsTrigger>
|
||||
<TabsTrigger value="attachment">{t('dashboard.attachments')}</TabsTrigger>
|
||||
<TabsTrigger value="top">{t('dashboard.topLists')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="trend" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('dashboard.newEmails')}</CardTitle>
|
||||
<CardDescription>{t('dashboard.messageDistribution')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="h-80">
|
||||
{hasRecentActivity ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={convertRecentActivity(stats!.recent_activity, currentLocale)} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} opacity={0.3} />
|
||||
<XAxis dataKey="date" tick={{ fontSize: 12 }} interval="preserveStart" tickCount={10} />
|
||||
<YAxis tick={{ fontSize: 12 }} />
|
||||
<Tooltip
|
||||
formatter={(v) => formatNumber(v as number)}
|
||||
content={({ payload }) => {
|
||||
if (payload && payload.length) {
|
||||
const dataPoint = payload[0].payload;
|
||||
const fullDate = formatTooltipDate(dataPoint.timestamp_ms, currentLocale);
|
||||
return (
|
||||
<div className="p-2 border rounded-lg shadow-md bg-white dark:bg-gray-800">
|
||||
<p className="font-semibold text-sm mb-1">{fullDate}</p>
|
||||
<p className="text-xs">{t('dashboard.emails')}: {formatNumber(dataPoint.count)}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey="count" fill="currentColor" className="text-primary" radius={[4, 4, 0, 0]} barSize={28} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<EmptyChart title={t('dashboard.noRecentActivity')} />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="attachment" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('dashboard.attachmentRatio')}</CardTitle>
|
||||
<CardDescription>
|
||||
{totalAttachments > 0
|
||||
? t('dashboard.attachmentRatioDesc', { percent: (attachmentRatio * 100).toFixed(1) })
|
||||
: t('dashboard.noEmailsSynced')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-center h-80">
|
||||
<ResponsiveContainer width={300} height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={attachmentData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={100}
|
||||
paddingAngle={5}
|
||||
dataKey="value"
|
||||
stroke="none"
|
||||
>
|
||||
{attachmentData.map((entry, i) => (
|
||||
<Cell key={`cell-${i}`} fill={entry.fill} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip formatter={(v) => totalAttachments > 0 ? `${((v as number) * 100).toFixed(1)}%` : '0%'} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="top" className="space-y-6">
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm font-bold uppercase">{t('dashboard.top10Senders')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{hasTopSenders ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-xs">{t('dashboard.sender')}</TableHead>
|
||||
<TableHead className="text-right text-xs">{t('dashboard.count')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{stats!.top_senders.map((s) => (
|
||||
<TableRow key={s.key} className="cursor-pointer hover:bg-accent/50 group" onClick={() => handleQuickSearch({ from: s.key })}>
|
||||
<TableCell className="max-w-[200px] truncate text-sm group-hover:text-primary transition-colors">{s.key}</TableCell>
|
||||
<TableCell className="text-right font-mono text-sm">{formatNumber(s.count)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<EmptyTable title={t('dashboard.noSendersData')} />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm font-bold uppercase">{t('dashboard.top10LargestEmails')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{hasTopEmails ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-xs">{t('dashboard.subject')}</TableHead>
|
||||
<TableHead className="text-right text-xs">{t('dashboard.size')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{stats!.top_largest_emails.map((m, i) => (
|
||||
<TableRow key={i} className="cursor-pointer hover:bg-accent/50 group" onClick={() => handleQuickSearch({ subject: m.subject })}>
|
||||
<TableCell className="max-w-[200px] truncate text-sm group-hover:text-primary transition-colors">{m.subject || t('dashboard.noSubject')}</TableCell>
|
||||
<TableCell className="text-right font-mono text-sm text-orange-600">{formatBytes(m.size_bytes)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<EmptyTable title={t('dashboard.noLargeEmails')} />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm font-bold uppercase">{t('dashboard.top10Accounts')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{hasTopAccounts ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-xs">{t('dashboard.account')}</TableHead>
|
||||
<TableHead className="text-right text-xs">{t('dashboard.emails')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{stats!.top_accounts.map((acc) => (
|
||||
<TableRow key={acc.key} className="cursor-pointer hover:bg-accent/50 group" onClick={() => handleQuickSearch({ account_ids: [getAccountIdByEmail(acc.key) || 0] })}>
|
||||
<TableCell className="max-w-[200px] truncate text-sm group-hover:text-primary transition-colors">{acc.key}</TableCell>
|
||||
<TableCell className="text-right font-mono text-sm">{formatNumber(acc.count)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<EmptyTable title={t('dashboard.noAccountData')} />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Main>
|
||||
|
||||
<div className="mt-auto p-6 text-center text-xs text-muted-foreground border-t">
|
||||
© 2025-2026 <a href="https://rustmailer.com" target="_blank" rel="noopener noreferrer" className="hover:underline">rustmailer.com</a> - Bichon Email Archiving Project
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user