diff --git a/packages/widgets/src/system-disks/component.tsx b/packages/widgets/src/system-disks/component.tsx index 5c6271c09..efe2fe73d 100644 --- a/packages/widgets/src/system-disks/component.tsx +++ b/packages/widgets/src/system-disks/component.tsx @@ -1,6 +1,7 @@ "use client"; -import { Box, Card, Group, Stack, useMantineColorScheme } from "@mantine/core"; +import { useEffect, useRef, useState } from "react"; +import { Box, Card, Group, Stack, Tooltip, useMantineColorScheme } from "@mantine/core"; import { clientApi } from "@homarr/api/client"; import { useRequiredBoard } from "@homarr/boards/context"; @@ -31,6 +32,90 @@ const getDisplayText = (item: { used: string; available: string; percentage: num } }; +interface SystemDiskCardProps { + deviceName: string; + percentage: number; + displayText: string; + temperature: number | null | undefined; + healthy: boolean; + showBackgroundBar: boolean; +} + +const SystemDiskCard = ({ + deviceName, + percentage, + displayText, + temperature, + healthy, + showBackgroundBar, +}: SystemDiskCardProps) => { + const board = useRequiredBoard(); + const scheme = useMantineColorScheme(); + const t = useI18n(); + const valueRef = useRef(null); + const [valueFits, setValueFits] = useState(true); + + // When the card is squeezed (small widget), the value line is clipped by the card's `overflow: hidden`. + // Detect that and hide it, surfacing the value in a tooltip instead. The value keeps its layout space + // (visibility, not display) so toggling it does not change the measurement and flap. The ref lives on + // the value (not the Card) so the Tooltip can attach its own ref to the Card. + useEffect(() => { + const value = valueRef.current; + const card = value?.offsetParent as HTMLElement | null; // the position:relative Card + if (!value || !card) return; + const measure = () => { + setValueFits(value.getBoundingClientRect().bottom <= card.getBoundingClientRect().bottom + 1); + }; + measure(); + const observer = new ResizeObserver(measure); + observer.observe(card); + return () => observer.disconnect(); + }, [displayText, healthy]); + + const unhealthyLabel = t("widget.systemDisks.status.unhealthy"); + + return ( + + + +
+

+ {deviceName} +

+

+ {displayText} + {!healthy && {unhealthyLabel}} +

+
+
{temperature ?

{temperature}°C

: null}
+
+ +
+
+ ); +}; + export default function SystemResources({ integrationIds, options }: WidgetComponentProps<"systemDisks">) { const queryInput = { integrationIds }; const { data = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery(queryInput, { @@ -38,10 +123,6 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo }); const utils = clientApi.useUtils(); - const board = useRequiredBoard(); - const scheme = useMantineColorScheme(); - const t = useI18n(); - clientApi.widget.healthMonitoring.subscribeSystemHealthStatus.useSubscription(queryInput, { onData(data) { utils.widget.healthMonitoring.getSystemHealthStatus.setData(queryInput, (oldData) => @@ -63,45 +144,17 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo {fileSystem.map((item) => { const smartItem = smart.find((smart) => smart.deviceName === item.deviceName); - const healthy = smartItem?.healthy ?? true; // fall back to healthy if no information is available return ( - - -
-

- {item.deviceName} -

-

- {getDisplayText(item, options.displayMode)} - {!healthy && {t("widget.systemDisks.status.unhealthy")}} -

-
-
- {smartItem?.temperature && options.showTemperatureIfAvailable ? ( -

{smartItem.temperature}°C

- ) : null} -
-
- -
+ deviceName={item.deviceName} + percentage={item.percentage} + displayText={getDisplayText(item, options.displayMode)} + temperature={options.showTemperatureIfAvailable ? smartItem?.temperature : undefined} + healthy={smartItem?.healthy ?? true} // fall back to healthy if no information is available + showBackgroundBar={options.showBackgroundBar} + /> ); })}