Dashboard: every number leads somewhere, real counts, and new charts
- Cards and charts link to the page they're about: pending messages to the queue, bans to blocked IPs, report warnings to the reports, and so on, shown only to viewers who may open that page. - A one-line status under the greeting: what needs a look (failed tasks, messages retrying, recipients given up on) or, when nothing does, what's there. Each phrase is a link. - Counts from the server's own objects stand in for live metrics it can't report, and a live number with no source reads as unknown, not zero. - Who uses the space: a treemap of people sized by storage, colored by how near their quota they are, each tile opening the account. - Where mail is waiting: queued recipients by destination, split into waiting, retrying and given up, each row opening the filtered queue. - The weekly rhythm: messages by hour and weekday, shown once metric history exists. - Dashboard tabs are titled by their label.
This commit is contained in:
@@ -7,7 +7,10 @@
|
||||
|
||||
import { IconTile } from '@/components/common/IconTile';
|
||||
import { useMemo } from 'react';
|
||||
import { Info } from 'lucide-react';
|
||||
import { ArrowUpRight, Info } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { hrefFor, useDashLink } from '../links';
|
||||
import { LineChart, Line } from 'recharts';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -22,20 +25,27 @@ interface StatCardProps {
|
||||
card: CardSchema;
|
||||
historySamples: Metric[];
|
||||
historyWindow: { from: Date; to: Date };
|
||||
/** INBUXA: a value counted from the server's objects, used when live metrics aren't available. */
|
||||
fallback?: number;
|
||||
}
|
||||
|
||||
export function StatCard({ card, historySamples, historyWindow }: StatCardProps) {
|
||||
export function StatCard({ card, historySamples, historyWindow, fallback }: StatCardProps) {
|
||||
const liveSnapshot = useLiveMetricsStore((s) => s.snapshot);
|
||||
const liveStatus = useLiveMetricsStore((s) => s.status);
|
||||
const link = useDashLink(card.metrics);
|
||||
|
||||
const value = useMemo(() => {
|
||||
if (card.source === 'live' && fallback !== undefined && liveStatus !== 'open') return fallback;
|
||||
if (card.source === 'live') {
|
||||
const liveSamples = card.metrics.map((id) => liveSnapshot.get(id)).filter((m): m is Metric => m !== undefined);
|
||||
return cardValue(card, liveSamples);
|
||||
}
|
||||
return cardValue(card, historySamples);
|
||||
}, [card, liveSnapshot, historySamples]);
|
||||
}, [card, liveSnapshot, historySamples, fallback, liveStatus]);
|
||||
|
||||
const formattedValue = formatValue(value, card.format);
|
||||
// INBUXA: a live number the server can't report yet reads as unknown, not as zero.
|
||||
const unknown = card.source === 'live' && liveStatus !== 'open' && fallback === undefined;
|
||||
const formattedValue = unknown ? '—' : formatValue(value, card.format);
|
||||
|
||||
const { from, to } = historyWindow;
|
||||
|
||||
@@ -52,12 +62,21 @@ export function StatCard({ card, historySamples, historyWindow }: StatCardProps)
|
||||
return computeDelta(card, historySamples, from, to);
|
||||
}, [card, historySamples, from, to]);
|
||||
|
||||
return (
|
||||
<Card className="transition-shadow hover:shadow-md">
|
||||
const body = (
|
||||
<Card
|
||||
className={cn(
|
||||
'h-full transition-all hover:shadow-md',
|
||||
link &&
|
||||
'group-hover:-translate-y-0.5 group-hover:border-primary/50 group-focus-visible:ring-2 group-focus-visible:ring-ring',
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<IconTile name={card.icon} size="sm" />
|
||||
<span className="text-sm font-medium text-muted-foreground">{card.title}</span>
|
||||
{link && (
|
||||
<ArrowUpRight className="ml-auto h-4 w-4 shrink-0 text-muted-foreground/0 transition-colors group-hover:text-primary" />
|
||||
)}
|
||||
{card.description && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
@@ -102,4 +121,16 @@ export function StatCard({ card, historySamples, historyWindow }: StatCardProps)
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return link ? (
|
||||
<Link
|
||||
to={hrefFor(link)}
|
||||
className="group block focus-visible:outline-none"
|
||||
aria-label={`${card.title}: ${link.label}`}
|
||||
>
|
||||
{body}
|
||||
</Link>
|
||||
) : (
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user