"use client"; import type { CSSProperties } from "react"; import { Badge, Box, Group, RingProgress, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core"; import { IconArrowDown, IconArrowUp, IconClockPause, IconServer } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; import { formatNumber } from "@homarr/common"; 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"; import { aggregateUptimeKumaDashboards } from "./aggregate"; const statVisibilityByOption = { showTotalMonitors: "totalMonitors", showUpCount: "upCount", showDownCount: "downCount", showPausedCount: "pausedCount", } as const; const statIcons = { totalMonitors: IconServer, upCount: IconArrowUp, downCount: IconArrowDown, pausedCount: IconClockPause, } as const; const statColorByKey = { totalMonitors: "gray", upCount: "green", downCount: "red", pausedCount: "yellow", } as const; const uptimeTierByThreshold = [ [99, "excellent"], [95, "good"], [0, "poor"], ] as const; const uptimeColorByTier = { excellent: "green", good: "yellow", poor: "red", } as const; const gridColsByWidth = [ { minWidth: 380, cols: 4 }, { minWidth: 260, cols: 2 }, { minWidth: 0, cols: 2 }, ] as const; const ringSizeByWidth = [ { minWidth: 400, size: 88 }, { minWidth: 320, size: 72 }, { minWidth: 220, size: 56 }, { minWidth: 0, size: 44 }, ] as const; const iconSizeByWidth = [ { minWidth: 320, size: 22 }, { minWidth: 200, size: 18 }, { minWidth: 0, size: 16 }, ] as const; const heroLayoutBySecondaryStats = { false: classes.heroExpanded, true: "", } as const; const heroVariantByRing = { true: "", false: classes.heroTextOnly, } as const; export default function UptimeKumaWidget({ integrationIds, options, width, height, displayMode = "compact", }: WidgetComponentProps<"uptimeKuma">) { if (integrationIds.length === 0) { throw new NoIntegrationDataError(); } return ( ); } interface UptimeKumaContentProps { integrationIds: string[]; options: WidgetComponentProps<"uptimeKuma">["options"]; width: number; height: number; displayMode: "compact" | "advanced"; } function UptimeKumaContent({ integrationIds, options, width, height, displayMode }: UptimeKumaContentProps) { const t = useI18n("widget.uptimeKuma"); const dashboardQuery = clientApi.widget.uptimeKuma.getDashboard.useQuery({ integrationIds }); const dashboardData = getUsableWidgetQueryData(dashboardQuery); if (!dashboardData) return ; const successfulDashboardData = dashboardData.flatMap((item) => item.dashboard === null ? [] : [{ ...item, dashboard: item.dashboard }], ); const combined = aggregateUptimeKumaDashboards(successfulDashboardData.map((item) => item.dashboard)); const monitors = successfulDashboardData.flatMap((result) => result.dashboard.monitors.map((monitor) => ({ ...monitor, key: `${result.integrationId}:${monitor.id}`, integrationName: result.integrationName, })), ); const uptimeValue = clampPercent(combined.averageUptimePercent); const uptimeColor = getUptimeColor(uptimeValue); const ringSize = getRingSize(width); const iconSize = getIconSize(width); const gridCols = getGridCols(width); const isAdvanced = displayMode === "advanced"; const enabledStatKeys = Object.entries(statVisibilityByOption) .filter(([optionKey]) => isAdvanced || options[optionKey as keyof typeof options]) .map(([, statKey]) => statKey); const showHero = isAdvanced || options.showAverageUptime; const showRing = isAdvanced || options.showUptimeRing; const visibleStatKeys = enabledStatKeys; const hasContent = showHero || visibleStatKeys.length > 0; const heroLayoutClass = heroLayoutBySecondaryStats[String(visibleStatKeys.length > 0) as keyof typeof heroLayoutBySecondaryStats]; const heroRingClass = heroVariantByRing[String(showRing) as keyof typeof heroVariantByRing]; const summaryContent = (
{showHero && (
{t("averageUptime")} {formatNumber(uptimeValue, 1)}%
{showRing && ( {formatNumber(uptimeValue, 0)}% } /> )}
)} {!hasContent && (
—
)} {visibleStatKeys.length > 0 && (
{visibleStatKeys.map((statKey) => { const Icon = statIcons[statKey]; const color = statColorByKey[statKey]; return (
{formatNumber(combined[statKey], 0)} {t(`stats.${statKey}`)}
); })}
)}
); if (displayMode === "compact") { return ( {summaryContent} ); } const monitorList = ( {monitors .toSorted((left, right) => statusOrder[left.status] - statusOrder[right.status]) .map((monitor) => ( {monitor.name} {(isAdvanced || successfulDashboardData.length > 1) && ( {monitor.integrationName} )} {t(`status.${monitor.status}`)} ·{" "} {monitor.uptimePercent24h === null ? "—" : `${formatNumber(monitor.uptimePercent24h, 1)}%`} ))} ); if (width < 760) { return ( {summaryContent} {monitorList} ); } return ( {summaryContent} {monitorList} ); } const statusOrder = { down: 0, paused: 1, up: 2 } as const; function clampPercent(value: number): number { return Math.min(100, Math.max(0, value)); } function getUptimeTier(uptime: number): keyof typeof uptimeColorByTier { const match = uptimeTierByThreshold.find(([min]) => uptime >= min); const tier = match?.[1] ?? "poor"; return tier; } function getUptimeColor(uptime: number): string { return uptimeColorByTier[getUptimeTier(uptime)]; } function getGridCols(width: number): number { const match = gridColsByWidth.find(({ minWidth }) => width >= minWidth); return match?.cols ?? 2; } function getRingSize(width: number): number { const match = ringSizeByWidth.find(({ minWidth }) => width >= minWidth); return match?.size ?? 44; } function getIconSize(width: number): number { const match = iconSizeByWidth.find(({ minWidth }) => width >= minWidth); return match?.size ?? 16; }