fix(system-health): improve responsive styles (#2566)

This commit is contained in:
Meier Lukas
2025-03-11 22:15:01 +01:00
committed by GitHub
parent a53e7aaee5
commit 268daee4a6
8 changed files with 297 additions and 277 deletions

View File

@@ -31,6 +31,7 @@ const running = (total: number, current: Resource) => {
export const ClusterHealthMonitoring = ({ export const ClusterHealthMonitoring = ({
integrationId, integrationId,
options, options,
width,
}: WidgetComponentProps<"healthMonitoring"> & { integrationId: string }) => { }: WidgetComponentProps<"healthMonitoring"> & { integrationId: string }) => {
const t = useI18n(); const t = useI18n();
const [healthData] = clientApi.widget.healthMonitoring.getClusterHealthStatus.useSuspenseQuery( const [healthData] = clientApi.widget.healthMonitoring.getClusterHealthStatus.useSuspenseQuery(
@@ -72,14 +73,15 @@ export const ClusterHealthMonitoring = ({
const cpuPercent = maxCpu ? (usedCpu / maxCpu) * 100 : 0; const cpuPercent = maxCpu ? (usedCpu / maxCpu) * 100 : 0;
const memPercent = maxMem ? (usedMem / maxMem) * 100 : 0; const memPercent = maxMem ? (usedMem / maxMem) * 100 : 0;
const isTiny = width < 256;
return ( return (
<Stack h="100%"> <Stack h="100%" p="xs" gap={isTiny ? "xs" : "md"}>
<Group justify="center" wrap="nowrap" pt="md"> <Group justify="center" wrap="nowrap">
<Text fz="md" tt="uppercase" fw={700} c="dimmed" ta="center"> <Text fz={isTiny ? 8 : "xs"} tt="uppercase" fw={700} c="dimmed" ta="center">
{formatUptime(uptime, t)} {formatUptime(uptime, t)}
</Text> </Text>
</Group> </Group>
<SummaryHeader cpu={cpuPercent} memory={memPercent} /> <SummaryHeader cpu={cpuPercent} memory={memPercent} isTiny={isTiny} />
<Accordion variant="contained" chevronPosition="right" multiple defaultValue={["node"]}> <Accordion variant="contained" chevronPosition="right" multiple defaultValue={["node"]}>
<ResourceAccordionItem <ResourceAccordionItem
value="node" value="node"
@@ -90,8 +92,9 @@ export const ClusterHealthMonitoring = ({
totalCount: healthData.nodes.length, totalCount: healthData.nodes.length,
sectionIndicatorRequirement: options.sectionIndicatorRequirement, sectionIndicatorRequirement: options.sectionIndicatorRequirement,
})} })}
isTiny={isTiny}
> >
<ResourceTable type="node" data={healthData.nodes} /> <ResourceTable type="node" data={healthData.nodes} isTiny={isTiny} />
</ResourceAccordionItem> </ResourceAccordionItem>
<ResourceAccordionItem <ResourceAccordionItem
@@ -103,8 +106,9 @@ export const ClusterHealthMonitoring = ({
totalCount: healthData.vms.length, totalCount: healthData.vms.length,
sectionIndicatorRequirement: options.sectionIndicatorRequirement, sectionIndicatorRequirement: options.sectionIndicatorRequirement,
})} })}
isTiny={isTiny}
> >
<ResourceTable type="qemu" data={healthData.vms} /> <ResourceTable type="qemu" data={healthData.vms} isTiny={isTiny} />
</ResourceAccordionItem> </ResourceAccordionItem>
<ResourceAccordionItem <ResourceAccordionItem
@@ -116,8 +120,9 @@ export const ClusterHealthMonitoring = ({
totalCount: healthData.lxcs.length, totalCount: healthData.lxcs.length,
sectionIndicatorRequirement: options.sectionIndicatorRequirement, sectionIndicatorRequirement: options.sectionIndicatorRequirement,
})} })}
isTiny={isTiny}
> >
<ResourceTable type="lxc" data={healthData.lxcs} /> <ResourceTable type="lxc" data={healthData.lxcs} isTiny={isTiny} />
</ResourceAccordionItem> </ResourceAccordionItem>
<ResourceAccordionItem <ResourceAccordionItem
@@ -129,8 +134,9 @@ export const ClusterHealthMonitoring = ({
totalCount: healthData.storages.length, totalCount: healthData.storages.length,
sectionIndicatorRequirement: options.sectionIndicatorRequirement, sectionIndicatorRequirement: options.sectionIndicatorRequirement,
})} })}
isTiny={isTiny}
> >
<ResourceTable type="storage" data={healthData.storages} /> <ResourceTable type="storage" data={healthData.storages} isTiny={isTiny} />
</ResourceAccordionItem> </ResourceAccordionItem>
</Accordion> </Accordion>
</Stack> </Stack>
@@ -140,45 +146,50 @@ export const ClusterHealthMonitoring = ({
interface SummaryHeaderProps { interface SummaryHeaderProps {
cpu: number; cpu: number;
memory: number; memory: number;
isTiny: boolean;
} }
const SummaryHeader = ({ cpu, memory }: SummaryHeaderProps) => { const SummaryHeader = ({ cpu, memory, isTiny }: SummaryHeaderProps) => {
const t = useI18n(); const t = useI18n();
return ( return (
<Center> <Center>
<Group wrap="nowrap"> <Group wrap="wrap" justify="center" gap="xs">
<Flex direction="row"> <Flex direction="row">
<RingProgress <RingProgress
roundCaps roundCaps
size={60} size={isTiny ? 32 : 48}
thickness={6} thickness={isTiny ? 2 : 4}
label={ label={
<Center> <Center>
<IconCpu /> <IconCpu size={isTiny ? 12 : 20} />
</Center> </Center>
} }
sections={[{ value: cpu, color: cpu > 75 ? "orange" : "green" }]} sections={[{ value: cpu, color: cpu > 75 ? "orange" : "green" }]}
/> />
<Stack align="center" justify="center" gap={0}> <Stack align="center" justify="center" gap={0}>
<Text fw={500}>{t("widget.healthMonitoring.cluster.summary.cpu")}</Text> <Text fw={500} size={isTiny ? "xs" : "sm"}>
<Text>{cpu.toFixed(1)}%</Text> {t("widget.healthMonitoring.cluster.summary.cpu")}
</Text>
<Text size={isTiny ? "8px" : "xs"}>{cpu.toFixed(1)}%</Text>
</Stack> </Stack>
</Flex> </Flex>
<Flex> <Flex>
<RingProgress <RingProgress
roundCaps roundCaps
size={60} size={isTiny ? 32 : 48}
thickness={6} thickness={isTiny ? 2 : 4}
label={ label={
<Center> <Center>
<IconBrain /> <IconBrain size={isTiny ? 12 : 20} />
</Center> </Center>
} }
sections={[{ value: memory, color: memory > 75 ? "orange" : "green" }]} sections={[{ value: memory, color: memory > 75 ? "orange" : "green" }]}
/> />
<Stack align="center" justify="center" gap={0}> <Stack align="center" justify="center" gap={0}>
<Text fw={500}>{t("widget.healthMonitoring.cluster.summary.memory")}</Text> <Text size={isTiny ? "xs" : "sm"} fw={500}>
<Text>{memory.toFixed(1)}%</Text> {t("widget.healthMonitoring.cluster.summary.memory")}
</Text>
<Text size={isTiny ? "8px" : "xs"}>{memory.toFixed(1)}%</Text>
</Stack> </Stack>
</Flex> </Flex>
</Group> </Group>

View File

@@ -13,6 +13,7 @@ interface ResourceAccordionItemProps {
activeCount: number; activeCount: number;
totalCount: number; totalCount: number;
}; };
isTiny: boolean;
} }
export const ResourceAccordionItem = ({ export const ResourceAccordionItem = ({
@@ -21,13 +22,14 @@ export const ResourceAccordionItem = ({
icon: Icon, icon: Icon,
badge, badge,
children, children,
isTiny,
}: PropsWithChildren<ResourceAccordionItemProps>) => { }: PropsWithChildren<ResourceAccordionItemProps>) => {
return ( return (
<Accordion.Item value={value}> <Accordion.Item value={value}>
<Accordion.Control icon={<Icon />}> <Accordion.Control icon={isTiny ? null : <Icon size={16} />}>
<Group style={{ rowGap: "0" }}> <Group style={{ rowGap: "0" }} gap="xs">
<Text>{title}</Text> <Text size="xs">{title}</Text>
<Badge variant="dot" color={badge.color} size="lg"> <Badge variant="dot" color={badge.color} size="xs">
{badge.activeCount} / {badge.totalCount} {badge.activeCount} / {badge.totalCount}
</Badge> </Badge>
</Group> </Group>

View File

@@ -1,4 +1,4 @@
import { Group, Indicator, Popover, Table, Text } from "@mantine/core"; import { Group, Indicator, Popover, Table, TableTbody, TableThead, TableTr, Text } from "@mantine/core";
import type { Resource } from "@homarr/integrations/types"; import type { Resource } from "@homarr/integrations/types";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
@@ -8,36 +8,47 @@ import { ResourcePopover } from "./resource-popover";
interface ResourceTableProps { interface ResourceTableProps {
type: Resource["type"]; type: Resource["type"];
data: Resource[]; data: Resource[];
isTiny: boolean;
} }
export const ResourceTable = ({ type, data }: ResourceTableProps) => { export const ResourceTable = ({ type, data, isTiny }: ResourceTableProps) => {
const t = useI18n(); const t = useI18n();
return ( return (
<Table highlightOnHover> <Table highlightOnHover>
<thead> <TableThead>
<tr> <TableTr fz={isTiny ? "8px" : "xs"}>
<Table.Th ta="start">{t("widget.healthMonitoring.cluster.table.header.name")}</Table.Th> <Table.Th ta="start" p={0}>
{t("widget.healthMonitoring.cluster.table.header.name")}
</Table.Th>
{type !== "storage" ? ( {type !== "storage" ? (
<Table.Th ta="start">{t("widget.healthMonitoring.cluster.table.header.cpu")}</Table.Th> <Table.Th ta="start" p={0}>
{t("widget.healthMonitoring.cluster.table.header.cpu")}
</Table.Th>
) : null} ) : null}
{type !== "storage" ? ( {type !== "storage" ? (
<Table.Th ta="start">{t("widget.healthMonitoring.cluster.table.header.memory")}</Table.Th> <Table.Th ta="start" p={0}>
{t("widget.healthMonitoring.cluster.table.header.memory")}
</Table.Th>
) : null} ) : null}
{type === "storage" ? ( {type === "storage" ? (
<Table.Th ta="start">{t("widget.healthMonitoring.cluster.table.header.node")}</Table.Th> <Table.Th ta="start" p={0}>
{t("widget.healthMonitoring.cluster.table.header.node")}
</Table.Th>
) : null} ) : null}
</tr> </TableTr>
</thead> </TableThead>
<tbody> <TableTbody>
{data.map((item) => { {data.map((item) => {
return ( return (
<ResourcePopover key={item.name} item={item}> <ResourcePopover key={item.name} item={item}>
<Popover.Target> <Popover.Target>
<tr> <TableTr fz={isTiny ? "8px" : "xs"}>
<td> <td>
<Group wrap="nowrap"> <Group wrap="nowrap" gap={isTiny ? 8 : "xs"}>
<Indicator size={14} children={null} color={item.isRunning ? "green" : "yellow"} /> <Indicator size={isTiny ? 4 : 8} children={null} color={item.isRunning ? "green" : "yellow"} />
<Text lineClamp={1}>{item.name}</Text> <Text lineClamp={1} fz={isTiny ? "8px" : "xs"}>
{item.name}
</Text>
</Group> </Group>
</td> </td>
{item.type === "storage" ? ( {item.type === "storage" ? (
@@ -50,12 +61,12 @@ export const ResourceTable = ({ type, data }: ResourceTableProps) => {
</td> </td>
</> </>
)} )}
</tr> </TableTr>
</Popover.Target> </Popover.Target>
</ResourcePopover> </ResourcePopover>
); );
})} })}
</tbody> </TableTbody>
</Table> </Table>
); );
}; };

View File

@@ -31,30 +31,20 @@ export default function HealthMonitoringWidget(props: WidgetComponentProps<"heal
} }
return ( return (
<ScrollArea <ScrollArea h="100%">
h="100%"
styles={{
viewport: {
'& div[style="min-width: 100%"]': {
display: "flex !important",
height: "100%",
},
},
}}
>
<Tabs defaultValue={props.options.defaultTab} variant="outline"> <Tabs defaultValue={props.options.defaultTab} variant="outline">
<Tabs.List grow> <Tabs.List grow>
<Tabs.Tab value="system"> <Tabs.Tab value="system" fz="xs">
<b>{t("widget.healthMonitoring.tab.system")}</b> <b>{t("widget.healthMonitoring.tab.system")}</b>
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab value="cluster"> <Tabs.Tab value="cluster" fz="xs">
<b>{t("widget.healthMonitoring.tab.cluster")}</b> <b>{t("widget.healthMonitoring.tab.cluster")}</b>
</Tabs.Tab> </Tabs.Tab>
</Tabs.List> </Tabs.List>
<Tabs.Panel mt="lg" value="system"> <Tabs.Panel value="system">
<SystemHealthMonitoring {...props} integrationIds={otherIntegrationIds} /> <SystemHealthMonitoring {...props} integrationIds={otherIntegrationIds} />
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel mt="lg" value="cluster"> <Tabs.Panel value="cluster">
<ClusterHealthMonitoring integrationId={proxmoxIntegrationId} {...props} /> <ClusterHealthMonitoring integrationId={proxmoxIntegrationId} {...props} />
</Tabs.Panel> </Tabs.Panel>
</Tabs> </Tabs>

View File

@@ -1,33 +1,30 @@
import { Box, Center, RingProgress, Text } from "@mantine/core"; import { Center, RingProgress, Text } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import { IconCpu } from "@tabler/icons-react"; import { IconCpu } from "@tabler/icons-react";
import { progressColor } from "../system-health"; import { progressColor } from "../system-health";
export const CpuRing = ({ cpuUtilization }: { cpuUtilization: number }) => { export const CpuRing = ({ cpuUtilization, isTiny }: { cpuUtilization: number; isTiny: boolean }) => {
const { width, ref } = useElementSize();
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
return ( return (
<Box ref={ref} w="100%" h="100%" className="health-monitoring-cpu"> <RingProgress
<RingProgress className="health-monitoring-cpu"
className="health-monitoring-cpu-utilization" roundCaps
roundCaps size={isTiny ? 50 : 100}
size={fallbackWidth * 0.95} thickness={isTiny ? 4 : 8}
thickness={fallbackWidth / 10} label={
label={ <Center style={{ flexDirection: "column" }}>
<Center style={{ flexDirection: "column" }}> <Text
<Text className="health-monitoring-cpu-utilization-value" size="sm">{`${cpuUtilization.toFixed(2)}%`}</Text> className="health-monitoring-cpu-utilization-value"
<IconCpu className="health-monitoring-cpu-utilization-icon" size={30} /> size={isTiny ? "8px" : "xs"}
</Center> >{`${cpuUtilization.toFixed(2)}%`}</Text>
} <IconCpu className="health-monitoring-cpu-utilization-icon" size={isTiny ? 8 : 16} />
sections={[ </Center>
{ }
value: Number(cpuUtilization.toFixed(2)), sections={[
color: progressColor(Number(cpuUtilization.toFixed(2))), {
}, value: Number(cpuUtilization.toFixed(2)),
]} color: progressColor(Number(cpuUtilization.toFixed(2))),
/> },
</Box> ]}
/>
); );
}; };

View File

@@ -1,39 +1,41 @@
import { Box, Center, RingProgress, Text } from "@mantine/core"; import { Center, RingProgress, Text } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import { IconCpu } from "@tabler/icons-react"; import { IconCpu } from "@tabler/icons-react";
import { progressColor } from "../system-health"; import { progressColor } from "../system-health";
export const CpuTempRing = ({ fahrenheit, cpuTemp }: { fahrenheit: boolean; cpuTemp: number | undefined }) => { export const CpuTempRing = ({
const { width, ref } = useElementSize(); fahrenheit,
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196 cpuTemp,
isTiny,
}: {
fahrenheit: boolean;
cpuTemp: number | undefined;
isTiny: boolean;
}) => {
if (!cpuTemp) { if (!cpuTemp) {
return null; return null;
} }
return ( return (
<Box ref={ref} w="100%" h="100%" className="health-monitoring-cpu-temperature"> <RingProgress
<RingProgress className="health-monitoring-cpu-temperature"
className="health-monitoring-cpu-temp" roundCaps
roundCaps size={isTiny ? 50 : 100}
size={fallbackWidth * 0.95} thickness={isTiny ? 4 : 8}
thickness={fallbackWidth / 10} label={
label={ <Center style={{ flexDirection: "column" }}>
<Center style={{ flexDirection: "column" }}> <Text className="health-monitoring-cpu-temp-value" size={isTiny ? "8px" : "xs"}>
<Text className="health-monitoring-cpu-temp-value" size="sm"> {fahrenheit ? `${(cpuTemp * 1.8 + 32).toFixed(1)}°F` : `${cpuTemp.toFixed(1)}°C`}
{fahrenheit ? `${(cpuTemp * 1.8 + 32).toFixed(1)}°F` : `${cpuTemp.toFixed(1)}°C`} </Text>
</Text> <IconCpu className="health-monitoring-cpu-temp-icon" size={isTiny ? 8 : 16} />
<IconCpu className="health-monitoring-cpu-temp-icon" size={30} /> </Center>
</Center> }
} sections={[
sections={[ {
{ value: cpuTemp,
value: cpuTemp, color: progressColor(cpuTemp),
color: progressColor(cpuTemp), },
}, ]}
]} />
/>
</Box>
); );
}; };

View File

@@ -1,38 +1,33 @@
import { Box, Center, RingProgress, Text } from "@mantine/core"; import { Center, RingProgress, Text } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import { IconBrain } from "@tabler/icons-react"; import { IconBrain } from "@tabler/icons-react";
import { progressColor } from "../system-health"; import { progressColor } from "../system-health";
export const MemoryRing = ({ available, used }: { available: string; used: string }) => { export const MemoryRing = ({ available, used, isTiny }: { available: string; used: string; isTiny: boolean }) => {
const { width, ref } = useElementSize();
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
const memoryUsage = formatMemoryUsage(available, used); const memoryUsage = formatMemoryUsage(available, used);
return ( return (
<Box ref={ref} w="100%" h="100%" className="health-monitoring-memory"> <RingProgress
<RingProgress className="health-monitoring-memory"
className="health-monitoring-memory-use" roundCaps
roundCaps size={isTiny ? 50 : 100}
size={fallbackWidth * 0.95} thickness={isTiny ? 4 : 8}
thickness={fallbackWidth / 10} label={
label={ <Center style={{ flexDirection: "column" }}>
<Center style={{ flexDirection: "column" }}> <Text className="health-monitoring-memory-value" size={isTiny ? "8px" : "xs"}>
<Text className="health-monitoring-memory-value" size="sm"> {memoryUsage.memUsed.GB}GiB
{memoryUsage.memUsed.GB}GiB </Text>
</Text> <IconBrain className="health-monitoring-memory-icon" size={isTiny ? 8 : 16} />
<IconBrain className="health-monitoring-memory-icon" size={30} /> </Center>
</Center> }
} sections={[
sections={[ {
{ value: Number(memoryUsage.memUsed.percent),
value: Number(memoryUsage.memUsed.percent), color: progressColor(Number(memoryUsage.memUsed.percent)),
color: progressColor(Number(memoryUsage.memUsed.percent)), tooltip: `${memoryUsage.memUsed.percent}%`,
tooltip: `${memoryUsage.memUsed.percent}%`, },
}, ]}
]} />
/>
</Box>
); );
}; };

View File

@@ -44,7 +44,11 @@ import classes from "./system-health.module.css";
dayjs.extend(duration); dayjs.extend(duration);
export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetComponentProps<"healthMonitoring">) => { export const SystemHealthMonitoring = ({
options,
integrationIds,
width,
}: WidgetComponentProps<"healthMonitoring">) => {
const t = useI18n(); const t = useI18n();
const [healthData] = clientApi.widget.healthMonitoring.getSystemHealthStatus.useSuspenseQuery( const [healthData] = clientApi.widget.healthMonitoring.getSystemHealthStatus.useSuspenseQuery(
{ {
@@ -79,6 +83,8 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
}, },
); );
const isTiny = width < 256;
return ( return (
<Stack h="100%" gap="sm" className="health-monitoring"> <Stack h="100%" gap="sm" className="health-monitoring">
{healthData.map(({ integrationId, integrationName, healthInfo, updatedAt }) => { {healthData.map(({ integrationId, integrationName, healthInfo, updatedAt }) => {
@@ -91,95 +97,92 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
h="100%" h="100%"
className={`health-monitoring-information health-monitoring-${integrationName}`} className={`health-monitoring-information health-monitoring-${integrationName}`}
p="sm" p="sm"
pos="relative"
> >
<Box className="health-monitoring-information-card" p="sm"> <Box className="health-monitoring-information-card-section" pos="absolute" top={8} right={8}>
<Flex <Indicator
className="health-monitoring-information-card-elements" className="health-monitoring-updates-reboot-indicator"
justify="space-between" inline
align="center" processing
key={integrationId} styles={{ indicator: { pointerEvents: "none" } }}
color={healthInfo.rebootRequired ? "red" : healthInfo.availablePkgUpdates > 0 ? "blue" : "gray"}
position="top-end"
size={16}
label={healthInfo.availablePkgUpdates > 0 ? healthInfo.availablePkgUpdates : undefined}
disabled={!healthInfo.rebootRequired && healthInfo.availablePkgUpdates === 0}
> >
<Box className="health-monitoring-information-card-section"> <ActionIcon
<Indicator className="health-monitoring-information-icon-avatar"
className="health-monitoring-updates-reboot-indicator" variant={"light"}
inline color="var(--mantine-color-text)"
processing size="sm"
color={healthInfo.rebootRequired ? "red" : healthInfo.availablePkgUpdates > 0 ? "blue" : "gray"} radius={board.itemRadius}
position="top-end" >
size="md" <IconInfoCircle className="health-monitoring-information-icon" size={30} onClick={open} />
label={healthInfo.availablePkgUpdates > 0 ? healthInfo.availablePkgUpdates : undefined} </ActionIcon>
disabled={!healthInfo.rebootRequired && healthInfo.availablePkgUpdates === 0} </Indicator>
> <Modal
<ActionIcon opened={opened}
className="health-monitoring-information-icon-avatar" onClose={close}
variant={"light"} size="auto"
color="var(--mantine-color-text)" title={t("widget.healthMonitoring.popover.information")}
size={40} centered
radius={board.itemRadius} >
> <Stack gap="10px" className="health-monitoring-modal-stack">
<IconInfoCircle className="health-monitoring-information-icon" size={30} onClick={open} /> <Divider />
</ActionIcon> <List className="health-monitoring-information-list" center spacing="xs">
</Indicator> <List.Item className="health-monitoring-information-processor" icon={<IconCpu2 size={30} />}>
<Modal {t("widget.healthMonitoring.popover.processor", { cpuModelName: healthInfo.cpuModelName })}
opened={opened} </List.Item>
onClose={close} <List.Item className="health-monitoring-information-memory" icon={<IconBrain size={30} />}>
size="auto" {t("widget.healthMonitoring.popover.memory", { memory: memoryUsage.memTotal.GB })}
title={t("widget.healthMonitoring.popover.information")} </List.Item>
centered <List.Item className="health-monitoring-information-memory" icon={<IconBrain size={30} />}>
> {t("widget.healthMonitoring.popover.memoryAvailable", {
<Stack gap="10px" className="health-monitoring-modal-stack"> memoryAvailable: memoryUsage.memFree.GB,
<Divider /> percent: memoryUsage.memFree.percent,
<List className="health-monitoring-information-list" center spacing="xs"> })}
<List.Item className="health-monitoring-information-processor" icon={<IconCpu2 size={30} />}> </List.Item>
{t("widget.healthMonitoring.popover.processor", { cpuModelName: healthInfo.cpuModelName })} <List.Item className="health-monitoring-information-version" icon={<IconVersions size={30} />}>
</List.Item> {t("widget.healthMonitoring.popover.version", {
<List.Item className="health-monitoring-information-memory" icon={<IconBrain size={30} />}> version: healthInfo.version,
{t("widget.healthMonitoring.popover.memory", { memory: memoryUsage.memTotal.GB })} })}
</List.Item> </List.Item>
<List.Item className="health-monitoring-information-memory" icon={<IconBrain size={30} />}> <List.Item className="health-monitoring-information-uptime" icon={<IconClock size={30} />}>
{t("widget.healthMonitoring.popover.memoryAvailable", { {formatUptime(healthInfo.uptime, t)}
memoryAvailable: memoryUsage.memFree.GB, </List.Item>
percent: memoryUsage.memFree.percent, <List.Item className="health-monitoring-information-load-average" icon={<IconCpu size={30} />}>
})} {t("widget.healthMonitoring.popover.loadAverage")}
</List.Item> </List.Item>
<List.Item className="health-monitoring-information-version" icon={<IconVersions size={30} />}> <List m="xs" withPadding center spacing="xs" icon={<IconCpu size={30} />}>
{t("widget.healthMonitoring.popover.version", { <List.Item className="health-monitoring-information-load-average-1min">
version: healthInfo.version, {t("widget.healthMonitoring.popover.minute")} {healthInfo.loadAverage["1min"]}%
})} </List.Item>
</List.Item> <List.Item className="health-monitoring-information-load-average-5min">
<List.Item className="health-monitoring-information-uptime" icon={<IconClock size={30} />}> {t("widget.healthMonitoring.popover.minutes", { count: 5 })} {healthInfo.loadAverage["5min"]}%
{formatUptime(healthInfo.uptime, t)} </List.Item>
</List.Item> <List.Item className="health-monitoring-information-load-average-15min">
<List.Item className="health-monitoring-information-load-average" icon={<IconCpu size={30} />}> {t("widget.healthMonitoring.popover.minutes", { count: 15 })} {healthInfo.loadAverage["15min"]}%
{t("widget.healthMonitoring.popover.loadAverage")} </List.Item>
</List.Item> </List>
<List m="xs" withPadding center spacing="xs" icon={<IconCpu size={30} />}> </List>
<List.Item className="health-monitoring-information-load-average-1min"> </Stack>
{t("widget.healthMonitoring.popover.minute")} {healthInfo.loadAverage["1min"]}% </Modal>
</List.Item>
<List.Item className="health-monitoring-information-load-average-5min">
{t("widget.healthMonitoring.popover.minutes", { count: 5 })}{" "}
{healthInfo.loadAverage["5min"]}%
</List.Item>
<List.Item className="health-monitoring-information-load-average-15min">
{t("widget.healthMonitoring.popover.minutes", { count: 15 })}{" "}
{healthInfo.loadAverage["15min"]}%
</List.Item>
</List>
</List>
</Stack>
</Modal>
</Box>
{options.cpu && <CpuRing cpuUtilization={healthInfo.cpuUtilization} />}
{options.cpu && <CpuTempRing fahrenheit={options.fahrenheit} cpuTemp={healthInfo.cpuTemp} />}
{options.memory && <MemoryRing available={healthInfo.memAvailable} used={healthInfo.memUsed} />}
</Flex>
{
<Text className="health-monitoring-status-update-time" c="dimmed" size="sm" ta="center">
{t("widget.healthMonitoring.popover.lastSeen", { lastSeen: dayjs(updatedAt).fromNow() })}
</Text>
}
</Box> </Box>
<Flex className="health-monitoring-information-card-elements" justify="center" align="center" wrap="wrap">
{options.cpu && <CpuRing cpuUtilization={healthInfo.cpuUtilization} isTiny={isTiny} />}
{options.cpu && (
<CpuTempRing fahrenheit={options.fahrenheit} cpuTemp={healthInfo.cpuTemp} isTiny={isTiny} />
)}
{options.memory && (
<MemoryRing available={healthInfo.memAvailable} used={healthInfo.memUsed} isTiny={isTiny} />
)}
</Flex>
{
<Text className="health-monitoring-status-update-time" c="dimmed" size="xs" ta="center">
{t("widget.healthMonitoring.popover.lastSeen", { lastSeen: dayjs(updatedAt).fromNow() })}
</Text>
}
{options.fileSystem && {options.fileSystem &&
disksData.map((disk) => { disksData.map((disk) => {
return ( return (
@@ -188,63 +191,72 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
`health-monitoring-disk-card health-monitoring-disk-card-${integrationName}`, `health-monitoring-disk-card health-monitoring-disk-card-${integrationName}`,
classes.card, classes.card,
)} )}
style={{ overflow: "visible" }}
key={disk.deviceName} key={disk.deviceName}
radius={board.itemRadius} radius={board.itemRadius}
p="sm" p="xs"
> >
<Flex className="health-monitoring-disk-status" justify="space-between" align="center" mb="sm"> <Stack gap="sm">
<Group gap="xs"> <Group
<IconServer className="health-monitoring-disk-icon" size="1rem" /> className="health-monitoring-disk-status"
<Text className="dihealth-monitoring-disk-name" size={"md"}> justify="space-between"
{disk.deviceName} align="center"
</Text> wrap="wrap"
</Group> gap={8}
<Group gap="xs">
<IconTemperature className="health-monitoring-disk-temperature-icon" size="1rem" />
<Text className="health-monitoring-disk-temperature-value" size="md">
{options.fahrenheit
? `${(disk.temperature * 1.8 + 32).toFixed(1)}°F`
: `${disk.temperature}°C`}
</Text>
</Group>
<Group gap="xs">
<IconFileReport className="health-monitoring-disk-status-icon" size="1rem" />
<Text className="health-monitoring-disk-status-value" size="md">
{disk.overallStatus ? disk.overallStatus : "N/A"}
</Text>
</Group>
</Flex>
<Progress.Root className="health-monitoring-disk-use" radius={board.itemRadius} h="md">
<Tooltip label={disk.used}>
<Progress.Section
value={disk.percentage}
color={progressColor(disk.percentage)}
className="health-monitoring-disk-use-percentage"
>
<Progress.Label className="health-monitoring-disk-use-value" fz="xs">
{t("widget.healthMonitoring.popover.used")}
</Progress.Label>
</Progress.Section>
</Tooltip>
<Tooltip
label={
Number(disk.available) / 1024 ** 4 >= 1
? `${(Number(disk.available) / 1024 ** 4).toFixed(2)} TiB`
: `${(Number(disk.available) / 1024 ** 3).toFixed(2)} GiB`
}
> >
<Progress.Section <Group gap={4} wrap="nowrap">
className="health-monitoring-disk-available-percentage" <IconServer className="health-monitoring-disk-icon" size="1rem" />
value={100 - disk.percentage} <Text className="dihealth-monitoring-disk-name" size="xs">
color="default" {disk.deviceName}
</Text>
</Group>
<Group gap={4} wrap="nowrap">
<IconTemperature className="health-monitoring-disk-temperature-icon" size="1rem" />
<Text className="health-monitoring-disk-temperature-value" size="xs">
{options.fahrenheit
? `${(disk.temperature * 1.8 + 32).toFixed(1)}°F`
: `${disk.temperature}°C`}
</Text>
</Group>
<Group gap={4} wrap="nowrap">
<IconFileReport className="health-monitoring-disk-status-icon" size="1rem" />
<Text className="health-monitoring-disk-status-value" size="xs">
{disk.overallStatus ? disk.overallStatus : "N/A"}
</Text>
</Group>
</Group>
<Progress.Root className="health-monitoring-disk-use" radius={board.itemRadius} h="md">
<Tooltip label={disk.used}>
<Progress.Section
value={disk.percentage}
color={progressColor(disk.percentage)}
className="health-monitoring-disk-use-percentage"
>
<Progress.Label className="health-monitoring-disk-use-value" fz="xs">
{t("widget.healthMonitoring.popover.used")}
</Progress.Label>
</Progress.Section>
</Tooltip>
<Tooltip
label={
Number(disk.available) / 1024 ** 4 >= 1
? `${(Number(disk.available) / 1024 ** 4).toFixed(2)} TiB`
: `${(Number(disk.available) / 1024 ** 3).toFixed(2)} GiB`
}
> >
<Progress.Label className="health-monitoring-disk-available-value" fz="xs"> <Progress.Section
{t("widget.healthMonitoring.popover.available")} className="health-monitoring-disk-available-percentage"
</Progress.Label> value={100 - disk.percentage}
</Progress.Section> color="default"
</Tooltip> >
</Progress.Root> <Progress.Label className="health-monitoring-disk-available-value" fz="xs">
{t("widget.healthMonitoring.popover.available")}
</Progress.Label>
</Progress.Section>
</Tooltip>
</Progress.Root>
</Stack>
</Card> </Card>
); );
})} })}