fix(widgets): hide system disk usage value when it overflows and show it in a tooltip (#6022)
This commit is contained in:
1 parent
1fb97ee9c6
commit
a19c9d88ca
1 file changed
+94
-41
@@ -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<HTMLParagraphElement>(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 (
|
||||
<Tooltip
|
||||
label={healthy ? displayText : `${displayText} (${unhealthyLabel})`}
|
||||
disabled={valueFits}
|
||||
position="top"
|
||||
withinPortal
|
||||
>
|
||||
<Card
|
||||
radius={board.itemRadius}
|
||||
py={"xs"}
|
||||
bg={scheme.colorScheme === "dark" ? "dark.7" : "gray.1"}
|
||||
style={{ overflow: "hidden", position: "relative" }}
|
||||
>
|
||||
<Group justify="space-between" style={{ zIndex: 1 }}>
|
||||
<div>
|
||||
<p style={{ margin: 0 }}>
|
||||
<b>{deviceName}</b>
|
||||
</p>
|
||||
<p ref={valueRef} style={{ margin: 0, visibility: valueFits ? "visible" : "hidden" }}>
|
||||
<span>{displayText}</span>
|
||||
{!healthy && <span style={{ marginLeft: 5 }}>{unhealthyLabel}</span>}
|
||||
</p>
|
||||
</div>
|
||||
<div>{temperature ? <p style={{ margin: 0 }}>{temperature}°C</p> : null}</div>
|
||||
</Group>
|
||||
<Box
|
||||
bg={healthy ? "green" : "red"}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: `${percentage}%`,
|
||||
height: "100%",
|
||||
zIndex: 0,
|
||||
display: showBackgroundBar ? "block" : "none",
|
||||
}}
|
||||
></Box>
|
||||
</Card>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
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
|
||||
<Stack gap="xs" p="xs" h="100%">
|
||||
{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 (
|
||||
<Card
|
||||
radius={board.itemRadius}
|
||||
py={"xs"}
|
||||
bg={scheme.colorScheme === "dark" ? "dark.7" : "gray.1"}
|
||||
<SystemDiskCard
|
||||
key={`disk-${item.deviceName}`}
|
||||
style={{ overflow: "hidden", position: "relative" }}
|
||||
>
|
||||
<Group justify="space-between" style={{ zIndex: 1 }}>
|
||||
<div>
|
||||
<p style={{ margin: 0 }}>
|
||||
<b>{item.deviceName}</b>
|
||||
</p>
|
||||
<p style={{ margin: 0 }}>
|
||||
<span>{getDisplayText(item, options.displayMode)}</span>
|
||||
{!healthy && <span style={{ marginLeft: 5 }}>{t("widget.systemDisks.status.unhealthy")}</span>}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
{smartItem?.temperature && options.showTemperatureIfAvailable ? (
|
||||
<p style={{ margin: 0 }}>{smartItem.temperature}°C</p>
|
||||
) : null}
|
||||
</div>
|
||||
</Group>
|
||||
<Box
|
||||
bg={healthy ? "green" : "red"}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: `${item.percentage}%`,
|
||||
height: "100%",
|
||||
zIndex: 0,
|
||||
display: options.showBackgroundBar ? "block" : "none",
|
||||
}}
|
||||
></Box>
|
||||
</Card>
|
||||
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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
|
||||
Reference in new issue
Block a user