Initial commit
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { Info } from 'lucide-react';
|
||||
import { LineChart, Line } from 'recharts';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import type { Card as CardSchema } from '../types/schema';
|
||||
import type { Metric } from '../types/metrics';
|
||||
import { cardValue, formatValue, sparklineData, computeDelta } from '../helpers';
|
||||
import { useLiveMetricsStore } from '../stores/liveMetricsStore';
|
||||
import { getChartColor } from '@/components/ui/chart';
|
||||
|
||||
const warnedIcons = new Set<string>();
|
||||
|
||||
function LucideIcon({ name, className }: { name: string; className?: string }) {
|
||||
const formatted = name
|
||||
.split('-')
|
||||
.map((s) => s[0].toUpperCase() + s.slice(1))
|
||||
.join('');
|
||||
const IconComp = (LucideIcons as Record<string, unknown>)[formatted] as LucideIcons.LucideIcon | undefined;
|
||||
if (!IconComp) {
|
||||
if (import.meta.env.DEV && !warnedIcons.has(name)) {
|
||||
warnedIcons.add(name);
|
||||
console.warn(`Unknown icon name: "${name}"`);
|
||||
}
|
||||
return <LucideIcons.HelpCircle className={className} />;
|
||||
}
|
||||
return <IconComp className={className} />;
|
||||
}
|
||||
|
||||
interface StatCardProps {
|
||||
card: CardSchema;
|
||||
historySamples: Metric[];
|
||||
historyWindow: { from: Date; to: Date };
|
||||
}
|
||||
|
||||
export function StatCard({ card, historySamples, historyWindow }: StatCardProps) {
|
||||
const liveSnapshot = useLiveMetricsStore((s) => s.snapshot);
|
||||
|
||||
const value = useMemo(() => {
|
||||
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]);
|
||||
|
||||
const formattedValue = formatValue(value, card.format);
|
||||
|
||||
const { from, to } = historyWindow;
|
||||
|
||||
const sparkline = useMemo(() => {
|
||||
if (card.source !== 'history' || !card.sparkline) return null;
|
||||
return sparklineData(card, historySamples, from, to).map((v, i) => ({
|
||||
v,
|
||||
i,
|
||||
}));
|
||||
}, [card, historySamples, from, to]);
|
||||
|
||||
const delta = useMemo(() => {
|
||||
if (card.source !== 'history' || !card.delta) return null;
|
||||
return computeDelta(card, historySamples, from, to);
|
||||
}, [card, historySamples, from, to]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<LucideIcon name={card.icon} className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium text-muted-foreground">{card.title}</span>
|
||||
{card.description && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-xs">
|
||||
<p className="text-xs">{card.description}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-2 text-2xl font-bold">{formattedValue}</div>
|
||||
|
||||
{(delta || sparkline) && (
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
{delta && (
|
||||
<Badge variant="secondary" className="text-xs font-normal text-muted-foreground">
|
||||
{delta.direction === 'up'
|
||||
? `\u2191 ${Math.abs(delta.pct)}%`
|
||||
: delta.direction === 'down'
|
||||
? `\u2193 ${Math.abs(delta.pct)}%`
|
||||
: '\u2013'}
|
||||
</Badge>
|
||||
)}
|
||||
{sparkline && (
|
||||
<LineChart width={64} height={32} data={sparkline}>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="v"
|
||||
stroke={getChartColor(0)}
|
||||
strokeWidth={1.5}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</LineChart>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user