feat: modernize dashboard widgets

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-13 19:54:28 +02:00
1 parent 1a5e083fd0
commit 6f2cd7fc2b
270 files changed
+10189 -3544

No files matched your search

+85 -14
View File
@@ -1,6 +1,18 @@
"use client";
import { Badge, Box, Card, Group, Progress, RingProgress, ScrollArea, Stack, Text } from "@mantine/core";
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";
@@ -22,29 +34,47 @@ const statusColors: Record<UpsStatus, string> = {
type UpsLayout = "mini" | "compact" | "full";
export default function UpsWidget({ options, integrationIds, width }: WidgetComponentProps<"ups">) {
export default function UpsWidget({
options,
integrationIds,
width,
height,
displayMode = "compact",
}: WidgetComponentProps<"ups">) {
if (integrationIds.length === 0) {
throw new NoIntegrationSelectedError();
}
return <UpsContent integrationIds={integrationIds} options={options} width={width} />;
return (
<UpsContent
integrationIds={integrationIds}
options={options}
width={width}
height={height}
displayMode={displayMode}
/>
);
}
interface UpsContentProps {
integrationIds: string[];
options: WidgetComponentProps<"ups">["options"];
width: number;
height: number;
displayMode: "compact" | "advanced";
}
function UpsContent({ integrationIds, options, width }: UpsContentProps) {
function UpsContent({ integrationIds, options, width, height, displayMode }: UpsContentProps) {
const t = useScopedI18n("widget.ups");
const { data } = clientApi.widget.ups.getSummaries.useQuery({ integrationIds });
if (!data) return <WidgetEmptyState />;
const devices = data.flatMap((instance) =>
instance.summaries.map((summary) => ({ key: `${instance.integrationId}:${summary.id}`, summary })),
);
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 <WidgetEmptyState />;
@@ -55,27 +85,52 @@ function UpsContent({ integrationIds, options, width }: UpsContentProps) {
// - 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";
const layout: UpsLayout =
displayMode === "advanced" ? "full" : width < 150 || height < 110 ? "mini" : width < 256 ? "compact" : "full";
const cards = devices.map(({ key, summary }) => (
<UpsDeviceCard
key={key}
summary={summary}
options={options}
layout={layout}
advanced={displayMode === "advanced"}
t={t}
/>
));
return (
<ScrollArea h="100%">
<Stack gap="xs" p="xs">
{devices.map(({ key, summary }) => (
<UpsDeviceCard key={key} summary={summary} options={options} layout={layout} t={t} />
))}
</Stack>
{displayMode === "advanced" ? (
<SimpleGrid cols={width >= 760 ? 2 : 1} spacing="md" p="md">
{cards}
</SimpleGrid>
) : (
<Stack gap="xs" p="xs">
{cards}
</Stack>
)}
</ScrollArea>
);
}
const statusPriority: Record<UpsStatus, number> = {
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, t }: UpsDeviceCardProps) {
function UpsDeviceCard({ summary, options, layout, advanced, t }: UpsDeviceCardProps) {
const showRing = options.showBattery && summary.batteryCharge !== null;
const statusBadge = (
<Badge
@@ -95,12 +150,14 @@ function UpsDeviceCard({ summary, options, layout, t }: UpsDeviceCardProps) {
<Box
w={8}
h={8}
aria-hidden="true"
style={{
borderRadius: "50%",
backgroundColor: `var(--mantine-color-${statusColors[summary.status]}-6)`,
flexShrink: 0,
}}
/>
<VisuallyHidden>{t(`status.${summary.status}`)}</VisuallyHidden>
<Text fw={600} size="xs" truncate>
{summary.name}
</Text>
@@ -205,6 +262,20 @@ function UpsDeviceCard({ summary, options, layout, t }: UpsDeviceCardProps) {
)}
</Stack>
</Group>
{advanced && (
<SimpleGrid cols={2} spacing={4} mt="sm">
{summary.batteryVoltage !== null && (
<StatRow label={t("field.batteryVoltage")} value={`${summary.batteryVoltage} V`} />
)}
{summary.power !== null && <StatRow label={t("field.power")} value={`${summary.power} W`} />}
{summary.temperature !== null && (
<StatRow label={t("field.temperature")} value={`${summary.temperature} °C`} />
)}
{summary.manufacturer && <StatRow label={t("field.manufacturer")} value={summary.manufacturer} />}
{summary.model && <StatRow label={t("field.model")} value={summary.model} />}
{summary.serial && <StatRow label={t("field.serial")} value={summary.serial} />}
</SimpleGrid>
)}
</Card>
);
}
+1
View File
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("ups", {
icon: IconBatteryCharging,
queryKey: [["widget", "ups"]],
createOptions() {
return optionsBuilder.from((factory) => ({
showBattery: factory.switch({