fix(system-health): improve responsive styles (#2566)
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
]}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user