mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 09:32:08 +01:00
CSRF Form Enhancements: - Add optional onError callback prop for better error handling - Remove CSRF token from console logging for security - Provide user-friendly error notifications instead of silent failures Date Filter Optimization: - Refactor sessions route to avoid object mutation issues - Build date filters cleanly without relying on spreading existing objects - Prevent potential undefined startTime mutations Geographic Threat Map Optimization: - Extract country names to reusable constants in lib/constants/countries.ts - Calculate max values once to avoid repeated expensive operations - Centralize threat level color mapping to eliminate duplicated logic - Replace repeated color assignments with centralized THREAT_LEVELS configuration Accessibility Improvements: - Add keyboard support to audit log table rows (Enter/Space keys) - Include proper ARIA labels and focus management - Add tabIndex for screen reader compatibility - Enhance focus indicators with ring styling Performance & Code Organization: - Move COUNTRY_NAMES to shared constants for reusability - Optimize calculation patterns in threat mapping components - Reduce redundant logic and improve maintainability
133 lines
4.3 KiB
TypeScript
133 lines
4.3 KiB
TypeScript
"use client";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { COUNTRY_NAMES } from "../../lib/constants/countries";
|
|
|
|
interface GeographicThreatMapProps {
|
|
geoDistribution: Record<string, number>;
|
|
title?: string;
|
|
}
|
|
|
|
// Threat level configuration with colors
|
|
const THREAT_LEVELS = {
|
|
high: { color: "destructive", bgColor: "bg-red-500" },
|
|
medium: { color: "secondary", bgColor: "bg-yellow-500" },
|
|
low: { color: "outline", bgColor: "bg-blue-500" },
|
|
minimal: { color: "outline", bgColor: "bg-gray-400" },
|
|
} as const;
|
|
|
|
type ThreatLevel = keyof typeof THREAT_LEVELS;
|
|
|
|
export function GeographicThreatMap({
|
|
geoDistribution,
|
|
title = "Geographic Threat Distribution",
|
|
}: GeographicThreatMapProps) {
|
|
// Calculate values once for efficiency
|
|
const totalEvents = Object.values(geoDistribution).reduce(
|
|
(sum, count) => sum + count,
|
|
0
|
|
);
|
|
const maxEventCount = Math.max(...Object.values(geoDistribution));
|
|
|
|
const sortedCountries = Object.entries(geoDistribution)
|
|
.sort(([, a], [, b]) => b - a)
|
|
.slice(0, 12);
|
|
|
|
const getThreatLevel = (count: number, total: number): ThreatLevel => {
|
|
const percentage = (count / total) * 100;
|
|
if (percentage > 50) return "high";
|
|
if (percentage > 20) return "medium";
|
|
if (percentage > 5) return "low";
|
|
return "minimal";
|
|
};
|
|
|
|
const getCountryName = (code: string) => {
|
|
return COUNTRY_NAMES[code] || code;
|
|
};
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{title}</CardTitle>
|
|
<CardDescription>
|
|
Security events by country ({totalEvents} total events)
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{sortedCountries.length === 0 ? (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
<p>No geographic data available</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{sortedCountries.map(([countryCode, count]) => {
|
|
const threatLevel = getThreatLevel(count, totalEvents);
|
|
const percentage = ((count / totalEvents) * 100).toFixed(1);
|
|
|
|
return (
|
|
<div
|
|
key={countryCode}
|
|
className="flex items-center justify-between p-3 border rounded-lg"
|
|
>
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium">
|
|
{getCountryName(countryCode)}
|
|
</span>
|
|
<Badge
|
|
variant={
|
|
THREAT_LEVELS[threatLevel].color as
|
|
| "default"
|
|
| "secondary"
|
|
| "destructive"
|
|
| "outline"
|
|
}
|
|
className="text-xs"
|
|
>
|
|
{threatLevel}
|
|
</Badge>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">
|
|
{count} events ({percentage}%)
|
|
</p>
|
|
</div>
|
|
|
|
<div className="text-right">
|
|
<div className="text-2xl font-bold">{count}</div>
|
|
<div className="w-16 bg-gray-200 rounded-full h-2">
|
|
<div
|
|
className={`h-2 rounded-full ${THREAT_LEVELS[threatLevel].bgColor}`}
|
|
style={{
|
|
width: `${Math.min(100, (count / maxEventCount) * 100)}%`,
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{Object.keys(geoDistribution).length > 12 && (
|
|
<div className="text-center pt-4 border-t">
|
|
<p className="text-sm text-muted-foreground">
|
|
And {Object.keys(geoDistribution).length - 12} more
|
|
countries...
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|