Merge pull request #189 from BEDOLAGA-DEV/fix/risk-display-improvements

fix: improve risk assessment display with GB/d values
This commit is contained in:
Egor
2026-02-07 13:01:15 +03:00
committed by GitHub

View File

@@ -91,31 +91,74 @@ const getRiskLevel = (ratio: number): RiskLevel => {
return 'critical'; return 'critical';
}; };
const getCompositeRatio = ( interface RiskResult {
ratio: number;
gbPerDay: number; // the dominant daily value (total or worst node)
totalRatio: number;
maxNodeRatio: number;
}
const getCompositeRisk = (
row: UserTrafficItem, row: UserTrafficItem,
totalThreshold: number, totalThreshold: number,
nodeThreshold: number, nodeThreshold: number,
days: number, days: number,
): number => { ): RiskResult => {
const dailyTotal = bytesToGbPerDay(row.total_bytes, days); const dailyTotal = bytesToGbPerDay(row.total_bytes, days);
const totalR = totalThreshold > 0 ? getRatio(dailyTotal, totalThreshold) : 0; const totalR = totalThreshold > 0 ? getRatio(dailyTotal, totalThreshold) : 0;
const nodeR =
nodeThreshold > 0 let maxNodeR = 0;
? Math.max( let worstNodeGbPerDay = 0;
0, if (nodeThreshold > 0) {
...Object.values(row.node_traffic).map((b) => for (const b of Object.values(row.node_traffic)) {
getRatio(bytesToGbPerDay(b || 0, days), nodeThreshold), const daily = bytesToGbPerDay(b || 0, days);
), const r = getRatio(daily, nodeThreshold);
) if (r > maxNodeR) {
: 0; maxNodeR = r;
return Math.max(totalR, nodeR); worstNodeGbPerDay = daily;
}
}
}
// The dominant metric determines what GB/d we show
const ratio = Math.max(totalR, maxNodeR);
const gbPerDay = totalR >= maxNodeR ? dailyTotal : worstNodeGbPerDay;
return { ratio, gbPerDay, totalRatio: totalR, maxNodeRatio: maxNodeR };
}; };
const RISK_STYLES: Record<RiskLevel, { dot: string; text: string }> = { const RISK_STYLES: Record<RiskLevel, { dot: string; text: string; bar: string; bg: string }> = {
low: { dot: 'bg-success-400', text: 'text-success-400' }, low: {
medium: { dot: 'bg-warning-400', text: 'text-warning-400' }, dot: 'bg-success-400',
high: { dot: 'bg-orange-400', text: 'text-orange-400' }, text: 'text-success-400',
critical: { dot: 'bg-error-400 animate-pulse', text: 'text-error-400' }, bar: 'bg-success-400',
bg: 'bg-success-400/10',
},
medium: {
dot: 'bg-warning-400',
text: 'text-warning-400',
bar: 'bg-warning-400',
bg: 'bg-warning-400/10',
},
high: {
dot: 'bg-orange-400',
text: 'text-orange-400',
bar: 'bg-orange-400',
bg: 'bg-orange-400/10',
},
critical: {
dot: 'bg-error-400 animate-pulse',
text: 'text-error-400',
bar: 'bg-error-400',
bg: 'bg-error-400/10',
},
};
const formatGbPerDay = (gbPerDay: number): string => {
if (gbPerDay < 0.01) return '<0.01';
if (gbPerDay < 10) return gbPerDay.toFixed(2);
if (gbPerDay < 100) return gbPerDay.toFixed(1);
return Math.round(gbPerDay).toString();
}; };
// ============ Icons ============ // ============ Icons ============
@@ -811,17 +854,34 @@ function NodeFilter({
// ============ Risk Badge ============ // ============ Risk Badge ============
function RiskBadge({ level, ratio }: { level: RiskLevel; ratio: number }) { function RiskBadge({
const { t } = useTranslation(); level,
ratio,
gbPerDay,
}: {
level: RiskLevel;
ratio: number;
gbPerDay: number;
}) {
const style = RISK_STYLES[level]; const style = RISK_STYLES[level];
const labelKey = `admin.trafficUsage.risk${level.charAt(0).toUpperCase() + level.slice(1)}`; const barWidth = Math.min(ratio * 100, 100);
const pct = Math.round(ratio * 100);
return ( return (
<div className="flex items-center justify-center gap-1.5"> <div className="flex flex-col items-center gap-0.5">
<span className={`inline-block h-2 w-2 rounded-full ${style.dot}`} /> <div className="flex items-center gap-1">
<span className={`text-[11px] font-medium ${style.text}`}>{pct}%</span> <span className={`inline-block h-1.5 w-1.5 rounded-full ${style.dot}`} />
<span className={`text-[10px] ${style.text} opacity-70`}>{t(labelKey)}</span> <span className={`text-[11px] font-semibold tabular-nums ${style.text}`}>
{formatGbPerDay(gbPerDay)}
</span>
<span className={`text-[10px] ${style.text} opacity-60`}>GB/d</span>
</div>
{/* Mini progress bar showing ratio to threshold */}
<div className={`h-1 w-full max-w-[60px] rounded-full ${style.bg}`}>
<div
className={`h-full rounded-full ${style.bar} transition-all`}
style={{ width: `${barWidth}%` }}
/>
</div>
</div> </div>
); );
} }
@@ -1155,15 +1215,25 @@ export default function AdminTrafficUsage() {
const nodeRatio = hasNodeThreshold ? getRatio(dailyNode, nodeThresholdNum) : 0; const nodeRatio = hasNodeThreshold ? getRatio(dailyNode, nodeThresholdNum) : 0;
const textColor = hasNodeThreshold ? getNodeTextColor(nodeRatio) : undefined; const textColor = hasNodeThreshold ? getNodeTextColor(nodeRatio) : undefined;
return ( return (
<span <div className="flex flex-col items-center">
className="text-xs text-dark-300" <span
style={{ className="text-xs text-dark-300"
color: textColor, style={{
fontWeight: nodeRatio > 0.8 ? 600 : undefined, color: textColor,
}} fontWeight: nodeRatio > 0.8 ? 600 : undefined,
> }}
{formatBytes(bytes)} >
</span> {formatBytes(bytes)}
</span>
{hasNodeThreshold && (
<span
className="text-[9px] leading-tight opacity-60"
style={{ color: textColor }}
>
{formatGbPerDay(dailyNode)} GB/d
</span>
)}
</div>
); );
}, },
}), }),
@@ -1178,13 +1248,20 @@ export default function AdminTrafficUsage() {
size: 100, size: 100,
minSize: 80, minSize: 80,
meta: { align: 'center' as const }, meta: { align: 'center' as const },
accessorFn: (row) => accessorFn: (row) => {
getCompositeRatio(row, totalThresholdNum, nodeThresholdNum, periodDays), const result = getCompositeRisk(row, totalThresholdNum, nodeThresholdNum, periodDays);
return result.ratio;
},
enableSorting: false, enableSorting: false,
cell: ({ getValue }) => { cell: ({ row }) => {
const maxRatio = getValue() as number; const result = getCompositeRisk(
const level = getRiskLevel(maxRatio); row.original,
return <RiskBadge level={level} ratio={maxRatio} />; totalThresholdNum,
nodeThresholdNum,
periodDays,
);
const level = getRiskLevel(result.ratio);
return <RiskBadge level={level} ratio={result.ratio} gbPerDay={result.gbPerDay} />;
}, },
}); });
} }
@@ -1198,10 +1275,19 @@ export default function AdminTrafficUsage() {
meta: { align: 'center' as const, bold: true }, meta: { align: 'center' as const, bold: true },
cell: ({ getValue }) => { cell: ({ getValue }) => {
const bytes = getValue() as number; const bytes = getValue() as number;
if (bytes <= 0) {
return <span className="text-xs font-semibold text-dark-100">{'\u2014'}</span>;
}
const dailyTotal = bytesToGbPerDay(bytes, periodDays);
return ( return (
<span className="text-xs font-semibold text-dark-100"> <div className="flex flex-col items-center">
{bytes > 0 ? formatBytes(bytes) : '\u2014'} <span className="text-xs font-semibold text-dark-100">{formatBytes(bytes)}</span>
</span> {hasTotalThreshold && (
<span className="text-[9px] leading-tight text-dark-400">
{formatGbPerDay(dailyTotal)} GB/d
</span>
)}
</div>
); );
}, },
}); });
@@ -1429,12 +1515,12 @@ export default function AdminTrafficUsage() {
<tbody> <tbody>
{table.getRowModel().rows.map((row) => { {table.getRowModel().rows.map((row) => {
const compositeRatio = hasAnyThreshold const compositeRatio = hasAnyThreshold
? getCompositeRatio( ? getCompositeRisk(
row.original, row.original,
totalThresholdNum, totalThresholdNum,
nodeThresholdNum, nodeThresholdNum,
periodDays, periodDays,
) ).ratio
: 0; : 0;
const rowBg = hasAnyThreshold ? getRowBgColor(compositeRatio) : undefined; const rowBg = hasAnyThreshold ? getRowBgColor(compositeRatio) : undefined;