"use client"; import { Badge, Box, Card, Group, Progress, RingProgress, ScrollArea, Stack, Text } 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 { NoIntegrationDataError } from "../errors/no-data-integration"; 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 }: WidgetComponentProps<"ups">) { if (integrationIds.length === 0) { throw new NoIntegrationSelectedError(); } return ; } interface UpsContentProps { integrationIds: string[]; options: WidgetComponentProps<"ups">["options"]; width: number; } function UpsContent({ integrationIds, options, width }: UpsContentProps) { const t = useScopedI18n("widget.ups"); const { data } = clientApi.widget.ups.getSummaries.useQuery({ integrationIds }); if (!data) return ; const devices = data.flatMap((instance) => instance.summaries.map((summary) => ({ key: `${instance.integrationId}:${summary.id}`, summary })), ); if (devices.length === 0) { throw new NoIntegrationDataError(); } // 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 = width < 150 ? "mini" : width < 256 ? "compact" : "full"; return ( {devices.map(({ key, summary }) => ( ))} ); } interface UpsDeviceCardProps { summary: UpsSummary; options: WidgetComponentProps<"ups">["options"]; layout: UpsLayout; t: ScopedTranslationFunction<"widget.ups">; } function UpsDeviceCard({ summary, options, layout, t }: UpsDeviceCardProps) { const showRing = options.showBattery && summary.batteryCharge !== null; const statusBadge = ( {t(`status.${summary.status}`)} ); if (layout === "mini") { return ( {summary.name} {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 && ( )} )} ); } 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"; }