"use client"; 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"; import { formatBytesPair } from "@homarr/common"; import { useI18n } from "@homarr/translation/client"; import { WidgetEmptyState } from "../common/empty-state"; import type { WidgetComponentProps } from "../definition"; import { filterStorageVolumes } from "../filter-storage-volumes"; import { NoIntegrationDataError } from "../errors/no-data-integration"; type DisplayMode = WidgetComponentProps<"systemDisks">["options"]["displayMode"]; const getDisplayText = (item: { used: string; available: string; percentage: number }, displayMode: DisplayMode) => { switch (displayMode) { case "percentage": return `${Math.round(item.percentage)}%`; case "absolute": { const usedInBytes = Number(item.used); const availableInBytes = Number(item.available); if (Number.isFinite(usedInBytes) && Number.isFinite(availableInBytes)) { const { used, available } = formatBytesPair(usedInBytes, availableInBytes); return `${used} / ${available}`; } return `${item.used} / ${item.available}`; } case "free": return `${Math.round(100 - item.percentage)}% free`; default: return `${Math.round(item.percentage)}%`; } }; 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); const lastItem = data.at(-1); if (!lastItem) return ; const rawFileSystem = lastItem.healthInfo.fileSystem; if (rawFileSystem.length === 0) { throw new NoIntegrationDataError(); } const fileSystem = filterStorageVolumes(rawFileSystem, options.visibleStorageVolumes, lastItem.integrationId); const smart = filterStorageVolumes(lastItem.healthInfo.smart, options.visibleStorageVolumes, lastItem.integrationId); if (fileSystem.length === 0) return ; return ( {fileSystem.map((item) => { const smartItem = smart.find((smart) => smart.deviceName === item.deviceName); return ( ); })} ); }