"use client"; import { useCallback, useState } from "react"; import { Accordion, Badge, Center, Flex, Group, Paper, RingProgress, ScrollArea, SimpleGrid, Stack, Text, Tooltip, } from "@mantine/core"; import { useLocalStorage } from "@mantine/hooks"; import { IconArrowBarDown, IconArrowBarUp, IconBrain, IconCpu, IconTopologyBus } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; import type { FirewallInterfacesSummary } from "@homarr/integrations"; import type { TranslationFunction } from "@homarr/translation"; import { useI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../definition"; import { calculateBandwidth, formatBitsPerSec } from "./bandwidth"; import { FirewallMenu } from "./firewall-menu"; import { FirewallVersion, formatVersion } from "./firewall-version"; export interface Firewall { label: string; value: string; } interface FirewallQueryState { isPending: boolean; isFetching: boolean; isError: boolean; data?: readonly { error?: string; integration?: { id: string } }[]; } export const hasTotalFirewallFailure = (queries: readonly FirewallQueryState[]) => queries.every((query) => !query.isPending && !query.isFetching) && queries.every( (query) => (query.isError && query.data === undefined) || (query.data !== undefined && query.data.length > 0 && query.data.every(({ error }) => error)), ); export const hasFirewallPartialFailure = (firewallId: string, queries: readonly FirewallQueryState[]) => queries.some( (query) => query.isError || query.data?.some(({ error, integration }) => integration?.id === firewallId && Boolean(error)) === true, ); export default function FirewallWidget({ integrationIds, width, height, itemId, displayMode, }: WidgetComponentProps<"firewall">) { const [selectedFirewall, setSelectedFirewall] = useState(""); const isAdvanced = displayMode === "advanced"; const isTiny = !isAdvanced && (width < 256 || height < 180); const ringSize = isAdvanced ? 100 : height < 120 ? 44 : isTiny ? 64 : 100; const showInterfaces = isAdvanced || (!isTiny && height >= 120); const t = useI18n(); const handleSelect = useCallback((value: string | null) => { setSelectedFirewall(value ?? ""); }, []); const cpuQuery = clientApi.widget.firewall.getFirewallCpuStatus.useQuery({ integrationIds }); const memoryQuery = clientApi.widget.firewall.getFirewallMemoryStatus.useQuery({ integrationIds, }); const versionQuery = clientApi.widget.firewall.getFirewallVersionStatus.useQuery({ integrationIds, }); const interfacesQuery = clientApi.widget.firewall.getFirewallInterfacesStatus.useQuery({ integrationIds, }); const firewallsCpuData = cpuQuery.data ?? []; const firewallsMemoryData = memoryQuery.data ?? []; const firewallsVersionData = versionQuery.data ?? []; const firewallsInterfacesData = interfacesQuery.data ?? []; const queries = [cpuQuery, memoryQuery, versionQuery, interfacesQuery]; const hasTotalFailure = hasTotalFirewallFailure(queries); const [accordionValue, setAccordionValue] = useLocalStorage({ key: `homarr-${itemId}-firewall`, defaultValue: "interfaces", }); if (hasTotalFailure) { throw new Error(t("widget.firewall.error.internalServerError")); } const firewallMetadata = new Map( [...firewallsVersionData, ...firewallsCpuData, ...firewallsMemoryData, ...firewallsInterfacesData].map( ({ integration }) => [integration.id, integration] as const, ), ); const firewallIds = [...firewallMetadata.keys()]; const initialSelectedFirewall = firewallIds[0] ?? ""; const activeFirewall = firewallMetadata.has(selectedFirewall) ? selectedFirewall : initialSelectedFirewall; const displayedFirewallIds = isAdvanced ? firewallIds : activeFirewall ? [activeFirewall] : []; const firewallHasError = (firewallId: string) => hasFirewallPartialFailure(firewallId, queries); const dropdownItems = firewallIds.map((firewallId) => ({ label: firewallMetadata.get(firewallId)?.name ?? firewallId, value: firewallId, })); if (firewallIds.length === 0) { const isLoading = queries.some((query) => query.isPending || query.isFetching); return (
{isLoading ? t("common.action.loading") : t("widget.firewall.empty.noInterfaces")}
); } return ( {!isAdvanced && ( {activeFirewall && firewallHasError(activeFirewall) && ( {t("common.error")} )} !error)} selectedFirewall={activeFirewall} isTiny={isTiny} /> )} = 720 ? 2 : 1} spacing="xs"> {displayedFirewallIds.map((firewallId) => { const cpu = firewallsCpuData.find(({ integration }) => integration.id === firewallId); const memory = firewallsMemoryData.find(({ integration }) => integration.id === firewallId); const version = firewallsVersionData.find(({ integration }) => integration.id === firewallId); const interfaces = firewallsInterfacesData.find(({ integration }) => integration.id === firewallId); const hasError = firewallHasError(firewallId); const metadata = firewallMetadata.get(firewallId); return ( ); })} ); } interface FirewallPanelProps { name: string; kind?: string; version?: string; cpu?: number; memory?: number; interfaces?: FirewallInterfacesSummary[]; interfacesLoaded: boolean; interfacesError: boolean; hasError: boolean; isAdvanced: boolean; ringSize: number; showInterfaces: boolean; accordionValue: string | null; setAccordionValue: (value: string | null) => void; errorLabel: string; noDataLabel: string; loadingLabel: string; errorBadgeLabel: string; interfacesLabel: string; t: TranslationFunction; } const FirewallPanel = ({ name, kind, version, cpu, memory, interfaces, interfacesLoaded, interfacesError, hasError, isAdvanced, ringSize, showInterfaces, accordionValue, setAccordionValue, errorLabel, noDataLabel, loadingLabel, errorBadgeLabel, interfacesLabel, t, }: FirewallPanelProps) => ( {isAdvanced && ( {name} {kind && ( {kind} )} {hasError && ( {t("common.error")} )} {formatVersion(version ?? "", t("widget.firewall.versionUnknown"))} )} {cpu !== undefined && ( )} {memory !== undefined && ( )} {showInterfaces && ( )} ); interface MetricRingProps { value: number; icon: typeof IconCpu; size: number; label: string; t: TranslationFunction; } const MetricRing = ({ value, icon: Icon, size, label, t }: MetricRingProps) => { const safeValue = Math.max(0, Math.min(100, Number.isFinite(value) ? value : 0)); const status = getMetricStatus(safeValue); const statusLabel = t(`widget.firewall.status.${status}`); const showIcon = size >= 64; const showStatus = size >= 96; return ( {safeValue.toFixed(1)}% {showIcon && } {showStatus && {statusLabel}} } sections={[ { value: safeValue, color: status === "warning" ? "yellow" : status === "critical" ? "red" : "green", }, ]} /> ); }; const getMetricStatus = (value: number): "normal" | "warning" | "critical" => { if (value >= 75) return "critical"; if (value > 50) return "warning"; return "normal"; }; interface InterfacesPanelProps { summary: FirewallInterfacesSummary[]; hasResult: boolean; hasError: boolean; isAdvanced: boolean; accordionValue: string | null; setAccordionValue: (value: string | null) => void; errorLabel: string; errorBadgeLabel: string; noDataLabel: string; loadingLabel: string; label: string; } const InterfacesPanel = ({ summary, hasResult, hasError, isAdvanced, accordionValue, setAccordionValue, errorLabel, errorBadgeLabel, noDataLabel, loadingLabel, label, }: InterfacesPanelProps) => { const bandwidth = calculateBandwidth(summary).data; return ( }> {label} {hasError && ( {errorBadgeLabel} )} {bandwidth.map(({ name, receive, transmit }) => ( {name} {formatBitsPerSec(transmit, 2)} {formatBitsPerSec(receive, 2)} ))} {hasError ? ( {errorLabel} ) : bandwidth.length === 0 ? ( {hasResult ? noDataLabel : loadingLabel} ) : null} ); };