"use client"; import type { ReactNode } from "react"; import { Badge, Group, ScrollArea, Text } from "@mantine/core"; import { IconAlertTriangle, IconCircleCheck, IconDoorEnter, IconRoute, IconServer, IconStack2, } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; import type { TraefikDashboardData, TraefikProtocolSummary, TraefikResourceSummary } from "@homarr/integrations/types"; import { useScopedI18n } from "@homarr/translation/client"; import { WidgetEmptyState } from "../common/empty-state"; import type { WidgetComponentProps } from "../definition"; import { NoIntegrationDataError } from "../errors/no-data-integration"; import classes from "./component.module.css"; const emptySummary: TraefikResourceSummary = { total: 0, enabled: 0, warnings: 0, errors: 0, }; const emptyProtocol: TraefikProtocolSummary = { routers: emptySummary, services: emptySummary, middlewares: emptySummary, }; const emptyDashboard: TraefikDashboardData = { version: null, entryPoints: [], http: emptyProtocol, tcp: emptyProtocol, udp: { routers: emptySummary, services: emptySummary, }, }; type ProtocolKey = "http" | "tcp" | "udp"; type ResourceKey = "routers" | "services" | "middlewares"; export default function TraefikWidget({ integrationIds, options, width }: WidgetComponentProps<"traefik">) { if (integrationIds.length === 0) { throw new NoIntegrationDataError(); } const t = useScopedI18n("widget.traefik"); const { data } = clientApi.widget.traefik.getDashboard.useQuery({ integrationIds }); if (!data) return ; const combined = data.reduce( (acc, item) => ({ version: acc.version ?? item.dashboard.version, entryPoints: [...acc.entryPoints, ...item.dashboard.entryPoints], http: combineProtocol(acc.http, item.dashboard.http), tcp: combineProtocol(acc.tcp, item.dashboard.tcp), udp: { routers: combineSummary(acc.udp.routers, item.dashboard.udp.routers), services: combineSummary(acc.udp.services, item.dashboard.udp.services), }, }), emptyDashboard, ); const totalRouters = combined.http.routers.total + combined.tcp.routers.total + combined.udp.routers.total; const totalErrors = getProtocolErrors(combined.http) + getProtocolErrors(combined.tcp) + getUdpErrors(combined.udp); const totalWarnings = getProtocolWarnings(combined.http) + getProtocolWarnings(combined.tcp) + getUdpWarnings(combined.udp); const protocolKeys: ProtocolKey[] = ["http"]; if (options.showTcp) protocolKeys.push("tcp"); if (options.showUdp) protocolKeys.push("udp"); return (
{data.length === 1 ? data[0]?.integrationName : t("instances")} {combined.version ? `v${combined.version}` : t("versionUnknown")}
} label={t("summary.routers")} value={totalRouters} /> } label={t("summary.entryPoints")} value={dedupe(combined.entryPoints).length} />
{protocolKeys.map((protocol) => ( ))}
{options.showEntryPoints && combined.entryPoints.length > 0 && (
{dedupe(combined.entryPoints) .slice(0, getEntryPointLimit(width)) .map((entryPoint) => ( {entryPoint} ))}
)}
); } function ProtocolCard({ protocol, data }: { protocol: ProtocolKey; data: TraefikDashboardData[ProtocolKey] }) { const t = useScopedI18n("widget.traefik"); const rows: { key: ResourceKey; icon: typeof IconRoute; summary: TraefikResourceSummary }[] = [ { key: "routers", icon: IconRoute, summary: data.routers }, { key: "services", icon: IconServer, summary: data.services }, ]; if ("middlewares" in data) { rows.push({ key: "middlewares", icon: IconStack2, summary: data.middlewares }); } return (
{t(`protocol.${protocol}`)}
{rows.map(({ key, icon: Icon, summary }) => (
{t(`resource.${key}`)}
))}
); } function StatusCount({ summary }: { summary: TraefikResourceSummary }) { return ( {summary.total} {summary.errors > 0 && ( {summary.errors} )} {summary.warnings > 0 && ( {summary.warnings} )} ); } function SummaryMetric({ icon, label, value }: { icon: ReactNode; label: string; value: number }) { return (
{icon}
{value} {label}
); } function HealthBadge({ errors, warnings }: { errors: number; warnings: number }) { const t = useScopedI18n("widget.traefik"); if (errors > 0) { return ( } radius="sm"> {t("status.errors", { count: errors })} ); } if (warnings > 0) { return ( } radius="sm"> {t("status.warnings", { count: warnings })} ); } return ( } radius="sm"> {t("status.healthy")} ); } function combineProtocol(left: TraefikProtocolSummary, right: TraefikProtocolSummary): TraefikProtocolSummary { return { routers: combineSummary(left.routers, right.routers), services: combineSummary(left.services, right.services), middlewares: combineSummary(left.middlewares, right.middlewares), }; } function combineSummary(left: TraefikResourceSummary, right: TraefikResourceSummary): TraefikResourceSummary { return { total: left.total + right.total, enabled: left.enabled + right.enabled, warnings: left.warnings + right.warnings, errors: left.errors + right.errors, }; } function getProtocolErrors(protocol: TraefikProtocolSummary) { return protocol.routers.errors + protocol.services.errors + protocol.middlewares.errors; } function getProtocolWarnings(protocol: TraefikProtocolSummary) { return protocol.routers.warnings + protocol.services.warnings + protocol.middlewares.warnings; } function getUdpErrors(protocol: TraefikDashboardData["udp"]) { return protocol.routers.errors + protocol.services.errors; } function getUdpWarnings(protocol: TraefikDashboardData["udp"]) { return protocol.routers.warnings + protocol.services.warnings; } function dedupe(values: string[]) { return Array.from(new Set(values)); } function getHeroIconSize(width: number) { if (width < 220) return 16; if (width < 340) return 18; return 22; } function getEntryPointLimit(width: number) { if (width < 220) return 2; if (width < 360) return 4; return 8; }