mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 07:52:10 +01:00
fix: implement comprehensive UI/UX and code organization improvements
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
This commit is contained in:
@ -41,19 +41,20 @@ function buildWhereClause(
|
||||
}
|
||||
|
||||
// Date Range Filter
|
||||
const dateFilters: { gte?: Date; lt?: Date } = {};
|
||||
|
||||
if (startDate) {
|
||||
whereClause.startTime = {
|
||||
...((whereClause.startTime as object) || {}),
|
||||
gte: new Date(startDate),
|
||||
};
|
||||
dateFilters.gte = new Date(startDate);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
const inclusiveEndDate = new Date(endDate);
|
||||
inclusiveEndDate.setDate(inclusiveEndDate.getDate() + 1);
|
||||
whereClause.startTime = {
|
||||
...((whereClause.startTime as object) || {}),
|
||||
lt: inclusiveEndDate,
|
||||
};
|
||||
dateFilters.lt = inclusiveEndDate;
|
||||
}
|
||||
|
||||
if (Object.keys(dateFilters).length > 0) {
|
||||
whereClause.startTime = dateFilters;
|
||||
}
|
||||
|
||||
return whereClause;
|
||||
|
||||
@ -367,8 +367,16 @@ export default function AuditLogsPage() {
|
||||
{auditLogs.map((log) => (
|
||||
<TableRow
|
||||
key={log.id}
|
||||
className="cursor-pointer hover:bg-gray-50"
|
||||
className="cursor-pointer hover:bg-gray-50 focus:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset"
|
||||
onClick={() => setSelectedLog(log)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
setSelectedLog(log);
|
||||
}
|
||||
}}
|
||||
tabIndex={0}
|
||||
aria-label={`View details for ${eventTypeLabels[log.eventType] || log.eventType} event`}
|
||||
>
|
||||
<TableCell className="font-mono text-sm">
|
||||
{formatDistanceToNow(new Date(log.timestamp), {
|
||||
|
||||
@ -16,6 +16,7 @@ interface CSRFProtectedFormProps {
|
||||
action: string;
|
||||
method?: "POST" | "PUT" | "DELETE" | "PATCH";
|
||||
onSubmit?: (formData: FormData) => Promise<void> | void;
|
||||
onError?: (error: Error) => void;
|
||||
className?: string;
|
||||
encType?: string;
|
||||
}
|
||||
@ -28,6 +29,7 @@ export function CSRFProtectedForm({
|
||||
action,
|
||||
method = "POST",
|
||||
onSubmit,
|
||||
onError,
|
||||
className,
|
||||
encType,
|
||||
}: CSRFProtectedFormProps) {
|
||||
@ -59,7 +61,14 @@ export function CSRFProtectedForm({
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Form submission error:", error);
|
||||
// You might want to show an error message to the user here
|
||||
|
||||
// Notify user of the error
|
||||
if (onError && error instanceof Error) {
|
||||
onError(error);
|
||||
} else {
|
||||
// Fallback: show alert if no error handler provided
|
||||
alert("An error occurred while submitting the form. Please try again.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -90,8 +99,11 @@ export function ExampleCSRFForm() {
|
||||
|
||||
const handleCustomSubmit = async (formData: FormData) => {
|
||||
// Custom form submission logic
|
||||
// Filter out CSRF token for security when logging
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
console.log("Form data:", data);
|
||||
// biome-ignore lint/correctness/noUnusedVariables: csrf_token is intentionally extracted and discarded for security
|
||||
const { csrf_token, ...safeData } = data;
|
||||
console.log("Form data (excluding CSRF token):", safeData);
|
||||
|
||||
// You can process the form data here before submission
|
||||
// The CSRF token is automatically included in formData
|
||||
|
||||
@ -8,114 +8,48 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { COUNTRY_NAMES } from "../../lib/constants/countries";
|
||||
|
||||
interface GeographicThreatMapProps {
|
||||
geoDistribution: Record<string, number>;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
// Simple country code to name mapping for common countries
|
||||
const countryNames: Record<string, string> = {
|
||||
USA: "United States",
|
||||
GBR: "United Kingdom",
|
||||
DEU: "Germany",
|
||||
FRA: "France",
|
||||
JPN: "Japan",
|
||||
CHN: "China",
|
||||
IND: "India",
|
||||
BRA: "Brazil",
|
||||
CAN: "Canada",
|
||||
AUS: "Australia",
|
||||
RUS: "Russia",
|
||||
ESP: "Spain",
|
||||
ITA: "Italy",
|
||||
NLD: "Netherlands",
|
||||
KOR: "South Korea",
|
||||
MEX: "Mexico",
|
||||
CHE: "Switzerland",
|
||||
SWE: "Sweden",
|
||||
NOR: "Norway",
|
||||
DNK: "Denmark",
|
||||
FIN: "Finland",
|
||||
POL: "Poland",
|
||||
BEL: "Belgium",
|
||||
AUT: "Austria",
|
||||
NZL: "New Zealand",
|
||||
SGP: "Singapore",
|
||||
THA: "Thailand",
|
||||
IDN: "Indonesia",
|
||||
MYS: "Malaysia",
|
||||
PHL: "Philippines",
|
||||
VNM: "Vietnam",
|
||||
ARE: "UAE",
|
||||
SAU: "Saudi Arabia",
|
||||
ISR: "Israel",
|
||||
ZAF: "South Africa",
|
||||
EGY: "Egypt",
|
||||
TUR: "Turkey",
|
||||
GRC: "Greece",
|
||||
PRT: "Portugal",
|
||||
CZE: "Czech Republic",
|
||||
HUN: "Hungary",
|
||||
ROU: "Romania",
|
||||
BGR: "Bulgaria",
|
||||
HRV: "Croatia",
|
||||
SVN: "Slovenia",
|
||||
SVK: "Slovakia",
|
||||
EST: "Estonia",
|
||||
LVA: "Latvia",
|
||||
LTU: "Lithuania",
|
||||
LUX: "Luxembourg",
|
||||
MLT: "Malta",
|
||||
CYP: "Cyprus",
|
||||
ISL: "Iceland",
|
||||
IRL: "Ireland",
|
||||
ARG: "Argentina",
|
||||
CHL: "Chile",
|
||||
COL: "Colombia",
|
||||
PER: "Peru",
|
||||
URY: "Uruguay",
|
||||
ECU: "Ecuador",
|
||||
BOL: "Bolivia",
|
||||
PRY: "Paraguay",
|
||||
VEN: "Venezuela",
|
||||
UKR: "Ukraine",
|
||||
BLR: "Belarus",
|
||||
MDA: "Moldova",
|
||||
GEO: "Georgia",
|
||||
ARM: "Armenia",
|
||||
AZE: "Azerbaijan",
|
||||
KAZ: "Kazakhstan",
|
||||
UZB: "Uzbekistan",
|
||||
KGZ: "Kyrgyzstan",
|
||||
TJK: "Tajikistan",
|
||||
TKM: "Turkmenistan",
|
||||
MNG: "Mongolia",
|
||||
};
|
||||
// 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) {
|
||||
const sortedCountries = Object.entries(geoDistribution)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 12);
|
||||
|
||||
// Calculate values once for efficiency
|
||||
const totalEvents = Object.values(geoDistribution).reduce(
|
||||
(sum, count) => sum + count,
|
||||
0
|
||||
);
|
||||
const maxEventCount = Math.max(...Object.values(geoDistribution));
|
||||
|
||||
const getThreatLevel = (count: number, total: number) => {
|
||||
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 { level: "high", color: "destructive" };
|
||||
if (percentage > 20) return { level: "medium", color: "secondary" };
|
||||
if (percentage > 5) return { level: "low", color: "outline" };
|
||||
return { level: "minimal", color: "outline" };
|
||||
if (percentage > 50) return "high";
|
||||
if (percentage > 20) return "medium";
|
||||
if (percentage > 5) return "low";
|
||||
return "minimal";
|
||||
};
|
||||
|
||||
const getCountryName = (code: string) => {
|
||||
return countryNames[code] || code;
|
||||
return COUNTRY_NAMES[code] || code;
|
||||
};
|
||||
|
||||
return (
|
||||
@ -135,7 +69,7 @@ export function GeographicThreatMap({
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{sortedCountries.map(([countryCode, count]) => {
|
||||
const threat = getThreatLevel(count, totalEvents);
|
||||
const threatLevel = getThreatLevel(count, totalEvents);
|
||||
const percentage = ((count / totalEvents) * 100).toFixed(1);
|
||||
|
||||
return (
|
||||
@ -150,7 +84,7 @@ export function GeographicThreatMap({
|
||||
</span>
|
||||
<Badge
|
||||
variant={
|
||||
threat.color as
|
||||
THREAT_LEVELS[threatLevel].color as
|
||||
| "default"
|
||||
| "secondary"
|
||||
| "destructive"
|
||||
@ -158,7 +92,7 @@ export function GeographicThreatMap({
|
||||
}
|
||||
className="text-xs"
|
||||
>
|
||||
{threat.level}
|
||||
{threatLevel}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@ -170,17 +104,9 @@ export function GeographicThreatMap({
|
||||
<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.level === "high"
|
||||
? "bg-red-500"
|
||||
: threat.level === "medium"
|
||||
? "bg-yellow-500"
|
||||
: threat.level === "low"
|
||||
? "bg-blue-500"
|
||||
: "bg-gray-400"
|
||||
}`}
|
||||
className={`h-2 rounded-full ${THREAT_LEVELS[threatLevel].bgColor}`}
|
||||
style={{
|
||||
width: `${Math.min(100, (count / Math.max(...Object.values(geoDistribution))) * 100)}%`,
|
||||
width: `${Math.min(100, (count / maxEventCount) * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
84
lib/constants/countries.ts
Normal file
84
lib/constants/countries.ts
Normal file
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Country Constants
|
||||
*
|
||||
* Country code to name mapping for common countries
|
||||
* Used throughout the application for geographic data display
|
||||
*/
|
||||
|
||||
export const COUNTRY_NAMES: Record<string, string> = {
|
||||
USA: "United States",
|
||||
GBR: "United Kingdom",
|
||||
DEU: "Germany",
|
||||
FRA: "France",
|
||||
JPN: "Japan",
|
||||
CHN: "China",
|
||||
IND: "India",
|
||||
BRA: "Brazil",
|
||||
CAN: "Canada",
|
||||
AUS: "Australia",
|
||||
RUS: "Russia",
|
||||
ESP: "Spain",
|
||||
ITA: "Italy",
|
||||
NLD: "Netherlands",
|
||||
KOR: "South Korea",
|
||||
MEX: "Mexico",
|
||||
CHE: "Switzerland",
|
||||
SWE: "Sweden",
|
||||
NOR: "Norway",
|
||||
DNK: "Denmark",
|
||||
FIN: "Finland",
|
||||
POL: "Poland",
|
||||
BEL: "Belgium",
|
||||
AUT: "Austria",
|
||||
NZL: "New Zealand",
|
||||
SGP: "Singapore",
|
||||
THA: "Thailand",
|
||||
IDN: "Indonesia",
|
||||
MYS: "Malaysia",
|
||||
PHL: "Philippines",
|
||||
VNM: "Vietnam",
|
||||
ARE: "UAE",
|
||||
SAU: "Saudi Arabia",
|
||||
ISR: "Israel",
|
||||
ZAF: "South Africa",
|
||||
EGY: "Egypt",
|
||||
TUR: "Turkey",
|
||||
GRC: "Greece",
|
||||
PRT: "Portugal",
|
||||
CZE: "Czech Republic",
|
||||
HUN: "Hungary",
|
||||
ROU: "Romania",
|
||||
BGR: "Bulgaria",
|
||||
HRV: "Croatia",
|
||||
SVN: "Slovenia",
|
||||
SVK: "Slovakia",
|
||||
EST: "Estonia",
|
||||
LVA: "Latvia",
|
||||
LTU: "Lithuania",
|
||||
LUX: "Luxembourg",
|
||||
MLT: "Malta",
|
||||
CYP: "Cyprus",
|
||||
ISL: "Iceland",
|
||||
IRL: "Ireland",
|
||||
ARG: "Argentina",
|
||||
CHL: "Chile",
|
||||
COL: "Colombia",
|
||||
PER: "Peru",
|
||||
URY: "Uruguay",
|
||||
ECU: "Ecuador",
|
||||
BOL: "Bolivia",
|
||||
PRY: "Paraguay",
|
||||
VEN: "Venezuela",
|
||||
UKR: "Ukraine",
|
||||
BLR: "Belarus",
|
||||
MDA: "Moldova",
|
||||
GEO: "Georgia",
|
||||
ARM: "Armenia",
|
||||
AZE: "Azerbaijan",
|
||||
KAZ: "Kazakhstan",
|
||||
UZB: "Uzbekistan",
|
||||
KGZ: "Kyrgyzstan",
|
||||
TJK: "Tajikistan",
|
||||
TKM: "Turkmenistan",
|
||||
MNG: "Mongolia",
|
||||
};
|
||||
Reference in New Issue
Block a user