"use client"; import { Badge, Box, Card, Group, Progress, RingProgress, ScrollArea, SimpleGrid, Stack, Text, VisuallyHidden, } from "@mantine/core"; import { clientApi } from "@homarr/api/client"; import { formatDuration } from "@homarr/common"; import type { UpsStatus, UpsSummary } from "@homarr/integrations/types"; import type { ScopedTranslationFunction } from "@homarr/translation"; import { useScopedI18n } from "@homarr/translation/client"; import { WidgetEmptyState } from "../common/empty-state"; import type { WidgetComponentProps } from "../definition"; import { getUsableWidgetQueryData } from "../common/query-state"; import { NoIntegrationSelectedError } from "../errors/no-integration-selected"; const statusColors: Record = { online: "green", charging: "blue", onBattery: "yellow", lowBattery: "red", unknown: "gray", }; type UpsLayout = "mini" | "compact" | "full"; export default function UpsWidget({ options, integrationIds, width, height, displayMode = "compact", }: WidgetComponentProps<"ups">) { if (integrationIds.length === 0) { throw new NoIntegrationSelectedError(); } return ( ); } interface UpsContentProps { integrationIds: string[]; options: WidgetComponentProps<"ups">["options"]; width: number; height: number; displayMode: "compact" | "advanced"; } function UpsContent({ integrationIds, options, width, height, displayMode }: UpsContentProps) { const t = useScopedI18n("widget.ups"); const data = getUsableWidgetQueryData(clientApi.widget.ups.getSummaries.useQuery({ integrationIds })); if (!data) return ; const devices = data .flatMap((instance) => instance.summaries.map((summary) => ({ key: `${instance.integrationId}:${summary.id}`, summary })), ) .toSorted((left, right) => statusPriority[left.summary.status] - statusPriority[right.summary.status]); if (devices.length === 0) { return ; } // Pick a layout from the available width so the widget stays useful at any size. The surrounding // ScrollArea keeps every device reachable at any height (e.g. a tall, narrow 1x3 widget scrolls). // - full: ring beside a column of stats (runtime, load, voltage) // - compact (~2 columns): ring beside the name and a status badge // - mini (~1 column): a text badge no longer fits, so status becomes a colour dot next to the name const layout: UpsLayout = displayMode === "advanced" ? "full" : width < 150 || height < 110 ? "mini" : width < 256 ? "compact" : "full"; const cards = devices.map(({ key, summary }) => ( )); return ( {displayMode === "advanced" ? ( = 760 ? 2 : 1} spacing="md" p="md"> {cards} ) : ( {cards} )} ); } const statusPriority: Record = { lowBattery: 0, onBattery: 1, charging: 2, unknown: 3, online: 4, }; interface UpsDeviceCardProps { summary: UpsSummary; options: WidgetComponentProps<"ups">["options"]; layout: UpsLayout; advanced: boolean; t: ScopedTranslationFunction<"widget.ups">; } function UpsDeviceCard({ summary, options, layout, advanced, t }: UpsDeviceCardProps) { const showRing = options.showBattery && summary.batteryCharge !== null; const statusBadge = ( {t(`status.${summary.status}`)} ); if (layout === "mini") { return ( {showRing && summary.batteryCharge !== null && ( {Math.round(summary.batteryCharge)}% } /> )} ); } if (layout === "compact") { return ( {showRing && summary.batteryCharge !== null && ( {Math.round(summary.batteryCharge)}% } /> )} {summary.name} {statusBadge} ); } return ( {summary.name} {statusBadge} {showRing && summary.batteryCharge !== null && ( {Math.round(summary.batteryCharge)}% } /> )} {options.showBattery && summary.batteryRuntime !== null && ( )} {options.showLoad && summary.load !== null && ( {t("field.load")} {Math.round(summary.load)}% )} {options.showVoltage && (summary.inputVoltage !== null || summary.outputVoltage !== null) && ( {summary.inputVoltage !== null && ( )} {summary.outputVoltage !== null && ( )} )} {advanced && ( {summary.batteryVoltage !== null && ( )} {summary.power !== null && } {summary.temperature !== null && ( )} {summary.manufacturer && } {summary.model && } {summary.serial && } )} ); } function StatRow({ label, value }: { label: string; value: string }) { return ( {label} {value} ); } function clampPercent(value: number): number { return Math.min(100, Math.max(0, value)); } function getChargeColor(charge: number): string { if (charge > 50) return "green"; if (charge > 20) return "yellow"; return "red"; } function getLoadColor(load: number): string { if (load > 90) return "red"; if (load > 70) return "yellow"; return "blue"; }