"use client"; import type { ReactNode } from "react"; import { Badge, Box, Group, ScrollArea, SimpleGrid, Stack, 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 { useI18n } from "@homarr/translation/client"; import { zoomCompensatedSize } from "@homarr/ui"; import { WidgetEmptyState } from "../common/empty-state"; import { IntegrationErrorIndicator } from "../common/integration-error-indicator"; import type { WidgetComponentProps } from "../definition"; import { getUsableWidgetQueryData } from "../common/query-state"; 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: [], resources: [], failedEndpoints: [], http: emptyProtocol, tcp: emptyProtocol, udp: { routers: emptySummary, services: emptySummary, }, }; type ProtocolKey = "http" | "tcp" | "udp"; type ResourceKey = "routers" | "services" | "middlewares"; interface TraefikDashboardSource { integrationId: string; integrationName: string; dashboard: TraefikDashboardData; } export const getTraefikSourceDetails = (sources: readonly TraefikDashboardSource[]) => ({ resources: sources.flatMap(({ integrationId, integrationName, dashboard }) => dashboard.resources.map((resource, index) => ({ ...resource, integrationId, integrationName, key: `${integrationId}:${resource.protocol}:${resource.type}:${resource.name}:${index}`, })), ), failedEndpoints: sources.flatMap(({ integrationId, integrationName, dashboard }) => dashboard.failedEndpoints.map((endpoint, index) => ({ endpoint, integrationId, integrationName, key: `${integrationId}:${endpoint}:${index}`, })), ), }); export function getTraefikProtocolKeys( options: Pick["options"], "showTcp" | "showUdp">, isAdvanced: boolean, ): ProtocolKey[] { const protocols: ProtocolKey[] = ["http"]; if (isAdvanced || options.showTcp) protocols.push("tcp"); if (isAdvanced || options.showUdp) protocols.push("udp"); return protocols; } export function getVisibleTraefikEntryPoints(values: string[], width: number, isAdvanced: boolean): string[] { const entryPoints = dedupe(values); return isAdvanced ? entryPoints : entryPoints.slice(0, getEntryPointLimit(width)); } export default function TraefikWidget(props: WidgetComponentProps<"traefik">) { if (props.integrationIds.length === 0) { throw new NoIntegrationDataError(); } return ; } function TraefikWidgetContent({ integrationIds, options, width, displayMode = "compact", }: WidgetComponentProps<"traefik">) { const t = useI18n("widget.traefik"); const tCommon = useI18n("common"); const dashboardQuery = clientApi.widget.traefik.getDashboard.useQuery({ integrationIds }); const data = getUsableWidgetQueryData(dashboardQuery); if (!data) return ; const successfulData = data.flatMap((item) => item.dashboard === null ? [] : [{ ...item, dashboard: item.dashboard }], ); const combined = successfulData.reduce( (acc, item) => ({ version: acc.version ?? item.dashboard.version, entryPoints: [...acc.entryPoints, ...item.dashboard.entryPoints], resources: [...acc.resources, ...item.dashboard.resources], failedEndpoints: [...acc.failedEndpoints, ...item.dashboard.failedEndpoints], 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 isAdvanced = displayMode === "advanced"; 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) + combined.failedEndpoints.length; const totalWarnings = getProtocolWarnings(combined.http) + getProtocolWarnings(combined.tcp) + getUdpWarnings(combined.udp); const protocolKeys = getTraefikProtocolKeys(options, isAdvanced); const entryPoints = getVisibleTraefikEntryPoints(combined.entryPoints, width, isAdvanced); const displayedVersion = successfulData.length === 1 ? successfulData[0]?.dashboard.version : null; const summary = (
{successfulData.length === 1 ? successfulData[0]?.integrationName : t("instances")} {displayedVersion ? `v${displayedVersion}` : successfulData.length > 1 ? successfulData.map(({ integrationName }) => integrationName).join(" · ") : t("versionUnknown")}
{isAdvanced && ( {successfulData.map(({ integrationId, integrationName, dashboard }) => ( {integrationName} · {dashboard.version ? `v${dashboard.version}` : t("versionUnknown")} ))} )}
} label={t("summary.routers")} value={totalRouters} /> } label={t("summary.entryPoints")} value={dedupe(combined.entryPoints).length} />
{protocolKeys.map((protocol) => ( ))}
{(isAdvanced || options.showEntryPoints) && entryPoints.length > 0 && (
{entryPoints.map((entryPoint) => ( {entryPoint} ))}
)}
); if (!isAdvanced) { return ( {summary} ); } const sourceDetails = getTraefikSourceDetails(successfulData); const resources = sourceDetails.resources.toSorted( (left, right) => resourceStatusOrder[left.status] - resourceStatusOrder[right.status] || left.name.localeCompare(right.name), ); const resourceList = ( {sourceDetails.failedEndpoints.map(({ endpoint, integrationName, key }) => ( {integrationName} · {t("failedEndpoint", { endpoint })} ))} {resources.map((resource) => ( {resource.name} {resource.protocol.toUpperCase()} · {t(`resourceType.${resource.type}`)} · {resource.provider ?? "—"} ·{" "} {resource.integrationName} {t(`resourceStatus.${resource.status}`)} ))} ); if (width < 900) { return ( {summary} {resourceList} ); } return ( {summary} {resourceList} ); } const resourceStatusOrder = { error: 0, warning: 1, disabled: 2, unknown: 3, enabled: 4 } as const; function ProtocolCard({ protocol, data, tCommon, }: { protocol: ProtocolKey; data: TraefikDashboardData[ProtocolKey]; tCommon: ReturnType>; }) { const t = useI18n("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 }); } const resourceLabels = { routers: t("resource.routers"), services: tCommon("services"), middlewares: t("resource.middlewares"), }; return (
{protocol.toUpperCase()}
{rows.map(({ key, icon: Icon, summary }) => (
{resourceLabels[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 = useI18n("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; }