feat: remove cqmin system (#2407)

* feat: remove cqmin system

* fix: improve weather widget

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
Manuel
2025-03-07 17:46:01 +00:00
committed by GitHub
parent 9881577f47
commit 37d471457a
26 changed files with 576 additions and 555 deletions
@@ -0,0 +1,33 @@
import { Box, Center, RingProgress, Text } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import { IconCpu } from "@tabler/icons-react";
import { progressColor } from "../system-health";
export const CpuRing = ({ cpuUtilization }: { cpuUtilization: number }) => {
const { width, ref } = useElementSize();
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
return (
<Box ref={ref} w="100%" h="100%" className="health-monitoring-cpu">
<RingProgress
className="health-monitoring-cpu-utilization"
roundCaps
size={fallbackWidth * 0.95}
thickness={fallbackWidth / 10}
label={
<Center style={{ flexDirection: "column" }}>
<Text className="health-monitoring-cpu-utilization-value" size="sm">{`${cpuUtilization.toFixed(2)}%`}</Text>
<IconCpu className="health-monitoring-cpu-utilization-icon" size={30} />
</Center>
}
sections={[
{
value: Number(cpuUtilization.toFixed(2)),
color: progressColor(Number(cpuUtilization.toFixed(2))),
},
]}
/>
</Box>
);
};
@@ -0,0 +1,39 @@
import { Box, Center, RingProgress, Text } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import { IconCpu } from "@tabler/icons-react";
import { progressColor } from "../system-health";
export const CpuTempRing = ({ fahrenheit, cpuTemp }: { fahrenheit: boolean; cpuTemp: number | undefined }) => {
const { width, ref } = useElementSize();
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
if (!cpuTemp) {
return null;
}
return (
<Box ref={ref} w="100%" h="100%" className="health-monitoring-cpu-temperature">
<RingProgress
className="health-monitoring-cpu-temp"
roundCaps
size={fallbackWidth * 0.95}
thickness={fallbackWidth / 10}
label={
<Center style={{ flexDirection: "column" }}>
<Text className="health-monitoring-cpu-temp-value" size="sm">
{fahrenheit ? `${(cpuTemp * 1.8 + 32).toFixed(1)}°F` : `${cpuTemp.toFixed(1)}°C`}
</Text>
<IconCpu className="health-monitoring-cpu-temp-icon" size={30} />
</Center>
}
sections={[
{
value: cpuTemp,
color: progressColor(cpuTemp),
},
]}
/>
</Box>
);
};
@@ -0,0 +1,54 @@
import { Box, Center, RingProgress, Text } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import { IconBrain } from "@tabler/icons-react";
import { progressColor } from "../system-health";
export const MemoryRing = ({ available, used }: { available: string; used: string }) => {
const { width, ref } = useElementSize();
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
const memoryUsage = formatMemoryUsage(available, used);
return (
<Box ref={ref} w="100%" h="100%" className="health-monitoring-memory">
<RingProgress
className="health-monitoring-memory-use"
roundCaps
size={fallbackWidth * 0.95}
thickness={fallbackWidth / 10}
label={
<Center style={{ flexDirection: "column" }}>
<Text className="health-monitoring-memory-value" size="sm">
{memoryUsage.memUsed.GB}GiB
</Text>
<IconBrain className="health-monitoring-memory-icon" size={30} />
</Center>
}
sections={[
{
value: Number(memoryUsage.memUsed.percent),
color: progressColor(Number(memoryUsage.memUsed.percent)),
tooltip: `${memoryUsage.memUsed.percent}%`,
},
]}
/>
</Box>
);
};
export const formatMemoryUsage = (memFree: string, memUsed: string) => {
const memFreeBytes = Number(memFree);
const memUsedBytes = Number(memUsed);
const totalMemory = memFreeBytes + memUsedBytes;
const memFreeGB = (memFreeBytes / 1024 ** 3).toFixed(2);
const memUsedGB = (memUsedBytes / 1024 ** 3).toFixed(2);
const memFreePercent = Math.round((memFreeBytes / totalMemory) * 100);
const memUsedPercent = Math.round((memUsedBytes / totalMemory) * 100);
const memTotalGB = (totalMemory / 1024 ** 3).toFixed(2);
return {
memFree: { percent: memFreePercent, GB: memFreeGB },
memUsed: { percent: memUsedPercent, GB: memUsedGB },
memTotal: { GB: memTotalGB },
};
};
@@ -0,0 +1,7 @@
[data-mantine-color-scheme="light"] .card {
background-color: var(--mantine-color-gray-1);
}
[data-mantine-color-scheme="dark"] .card {
background-color: var(--mantine-color-dark-7);
}
@@ -1,10 +1,9 @@
"use client";
import {
Avatar,
ActionIcon,
Box,
Card,
Center,
Divider,
Flex,
Group,
@@ -12,12 +11,11 @@ import {
List,
Modal,
Progress,
RingProgress,
Stack,
Text,
Tooltip,
} from "@mantine/core";
import { useDisclosure, useElementSize } from "@mantine/hooks";
import { useDisclosure } from "@mantine/hooks";
import {
IconBrain,
IconClock,
@@ -29,14 +27,20 @@ import {
IconTemperature,
IconVersions,
} from "@tabler/icons-react";
import combineClasses from "clsx";
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import type { TranslationFunction } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition";
import { CpuRing } from "./rings/cpu-ring";
import { CpuTempRing } from "./rings/cpu-temp-ring";
import { formatMemoryUsage, MemoryRing } from "./rings/memory-ring";
import classes from "./system-health.module.css";
dayjs.extend(duration);
@@ -55,6 +59,7 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
);
const [opened, { open, close }] = useDisclosure(false);
const utils = clientApi.useUtils();
const board = useRequiredBoard();
clientApi.widget.healthMonitoring.subscribeSystemHealthStatus.useSubscription(
{ integrationIds },
@@ -75,23 +80,21 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
);
return (
<Stack h="100%" gap="2.5cqmin" className="health-monitoring">
<Stack h="100%" gap="sm" className="health-monitoring">
{healthData.map(({ integrationId, integrationName, healthInfo, updatedAt }) => {
const disksData = matchFileSystemAndSmart(healthInfo.fileSystem, healthInfo.smart);
const memoryUsage = formatMemoryUsage(healthInfo.memAvailable, healthInfo.memUsed);
return (
<Stack
gap="2.5cqmin"
gap="sm"
key={integrationId}
h="100%"
className={`health-monitoring-information health-monitoring-${integrationName}`}
p="2.5cqmin"
p="sm"
>
<Card className="health-monitoring-information-card" p="2.5cqmin" withBorder>
<Box className="health-monitoring-information-card" p="sm">
<Flex
className="health-monitoring-information-card-elements"
h="100%"
w="100%"
justify="space-between"
align="center"
key={integrationId}
@@ -103,13 +106,19 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
processing
color={healthInfo.rebootRequired ? "red" : healthInfo.availablePkgUpdates > 0 ? "blue" : "gray"}
position="top-end"
size="4cqmin"
size="md"
label={healthInfo.availablePkgUpdates > 0 ? healthInfo.availablePkgUpdates : undefined}
disabled={!healthInfo.rebootRequired && healthInfo.availablePkgUpdates === 0}
>
<Avatar className="health-monitoring-information-icon-avatar" size="10cqmin" radius="sm">
<IconInfoCircle className="health-monitoring-information-icon" size="8cqmin" onClick={open} />
</Avatar>
<ActionIcon
className="health-monitoring-information-icon-avatar"
variant={"light"}
color="var(--mantine-color-text)"
size={40}
radius={board.itemRadius}
>
<IconInfoCircle className="health-monitoring-information-icon" size={30} onClick={open} />
</ActionIcon>
</Indicator>
<Modal
opened={opened}
@@ -120,49 +129,31 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
>
<Stack gap="10px" className="health-monitoring-modal-stack">
<Divider />
<List className="health-monitoring-information-list" center spacing="0.5cqmin">
<List.Item
className="health-monitoring-information-processor"
icon={<IconCpu2 size="1.5cqmin" />}
>
<List className="health-monitoring-information-list" center spacing="xs">
<List.Item className="health-monitoring-information-processor" icon={<IconCpu2 size={30} />}>
{t("widget.healthMonitoring.popover.processor", { cpuModelName: healthInfo.cpuModelName })}
</List.Item>
<List.Item
className="health-monitoring-information-memory"
icon={<IconBrain size="1.5cqmin" />}
>
<List.Item className="health-monitoring-information-memory" icon={<IconBrain size={30} />}>
{t("widget.healthMonitoring.popover.memory", { memory: memoryUsage.memTotal.GB })}
</List.Item>
<List.Item
className="health-monitoring-information-memory"
icon={<IconBrain size="1.5cqmin" />}
>
<List.Item className="health-monitoring-information-memory" icon={<IconBrain size={30} />}>
{t("widget.healthMonitoring.popover.memoryAvailable", {
memoryAvailable: memoryUsage.memFree.GB,
percent: memoryUsage.memFree.percent,
})}
</List.Item>
<List.Item
className="health-monitoring-information-version"
icon={<IconVersions size="1.5cqmin" />}
>
<List.Item className="health-monitoring-information-version" icon={<IconVersions size={30} />}>
{t("widget.healthMonitoring.popover.version", {
version: healthInfo.version,
})}
</List.Item>
<List.Item
className="health-monitoring-information-uptime"
icon={<IconClock size="1.5cqmin" />}
>
<List.Item className="health-monitoring-information-uptime" icon={<IconClock size={30} />}>
{formatUptime(healthInfo.uptime, t)}
</List.Item>
<List.Item
className="health-monitoring-information-load-average"
icon={<IconCpu size="1.5cqmin" />}
>
<List.Item className="health-monitoring-information-load-average" icon={<IconCpu size={30} />}>
{t("widget.healthMonitoring.popover.loadAverage")}
</List.Item>
<List m="0.5cqmin" withPadding center spacing="0.5cqmin" icon={<IconCpu size="1cqmin" />}>
<List m="xs" withPadding center spacing="xs" icon={<IconCpu size={30} />}>
<List.Item className="health-monitoring-information-load-average-1min">
{t("widget.healthMonitoring.popover.minute")} {healthInfo.loadAverage["1min"]}%
</List.Item>
@@ -184,56 +175,53 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
{options.memory && <MemoryRing available={healthInfo.memAvailable} used={healthInfo.memUsed} />}
</Flex>
{
<Text
className="health-monitoring-status-update-time"
c="dimmed"
size="3.5cqmin"
ta="center"
mb="2.5cqmin"
>
<Text className="health-monitoring-status-update-time" c="dimmed" size="sm" ta="center">
{t("widget.healthMonitoring.popover.lastSeen", { lastSeen: dayjs(updatedAt).fromNow() })}
</Text>
}
</Card>
</Box>
{options.fileSystem &&
disksData.map((disk) => {
return (
<Card
className={`health-monitoring-disk-card health-monitoring-disk-card-${integrationName}`}
className={combineClasses(
`health-monitoring-disk-card health-monitoring-disk-card-${integrationName}`,
classes.card,
)}
key={disk.deviceName}
p="2.5cqmin"
withBorder
radius={board.itemRadius}
p="sm"
>
<Flex className="health-monitoring-disk-status" justify="space-between" align="center" m="1.5cqmin">
<Group gap="1cqmin">
<IconServer className="health-monitoring-disk-icon" size="5cqmin" />
<Text className="dihealth-monitoring-disk-name" size="4cqmin">
<Flex className="health-monitoring-disk-status" justify="space-between" align="center" mb="sm">
<Group gap="xs">
<IconServer className="health-monitoring-disk-icon" size="1rem" />
<Text className="dihealth-monitoring-disk-name" size={"md"}>
{disk.deviceName}
</Text>
</Group>
<Group gap="1cqmin">
<IconTemperature className="health-monitoring-disk-temperature-icon" size="5cqmin" />
<Text className="health-monitoring-disk-temperature-value" size="4cqmin">
<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="1cqmin">
<IconFileReport className="health-monitoring-disk-status-icon" size="5cqmin" />
<Text className="health-monitoring-disk-status-value" size="4cqmin">
<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" h="6cqmin">
<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="2.5cqmin">
<Progress.Label className="health-monitoring-disk-use-value" fz="xs">
{t("widget.healthMonitoring.popover.used")}
</Progress.Label>
</Progress.Section>
@@ -251,7 +239,7 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
value={100 - disk.percentage}
color="default"
>
<Progress.Label className="health-monitoring-disk-available-value" fz="2.5cqmin">
<Progress.Label className="health-monitoring-disk-available-value" fz="xs">
{t("widget.healthMonitoring.popover.available")}
</Progress.Label>
</Progress.Section>
@@ -314,117 +302,3 @@ export const matchFileSystemAndSmart = (fileSystems: FileSystem[], smartData: Sm
})
.sort((fileSystemA, fileSystemB) => fileSystemA.deviceName.localeCompare(fileSystemB.deviceName));
};
const CpuRing = ({ cpuUtilization }: { cpuUtilization: number }) => {
const { width, ref } = useElementSize();
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
return (
<Box ref={ref} w="100%" h="100%" className="health-monitoring-cpu">
<RingProgress
className="health-monitoring-cpu-utilization"
roundCaps
size={fallbackWidth * 0.95}
thickness={fallbackWidth / 10}
label={
<Center style={{ flexDirection: "column" }}>
<Text
className="health-monitoring-cpu-utilization-value"
size="3cqmin"
>{`${cpuUtilization.toFixed(2)}%`}</Text>
<IconCpu className="health-monitoring-cpu-utilization-icon" size="7cqmin" />
</Center>
}
sections={[
{
value: Number(cpuUtilization.toFixed(2)),
color: progressColor(Number(cpuUtilization.toFixed(2))),
},
]}
/>
</Box>
);
};
const CpuTempRing = ({ fahrenheit, cpuTemp }: { fahrenheit: boolean; cpuTemp: number | undefined }) => {
const { width, ref } = useElementSize();
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
if (!cpuTemp) {
return null;
}
return (
<Box ref={ref} w="100%" h="100%" className="health-monitoring-cpu-temperature">
<RingProgress
className="health-monitoring-cpu-temp"
roundCaps
size={fallbackWidth * 0.95}
thickness={fallbackWidth / 10}
label={
<Center style={{ flexDirection: "column" }}>
<Text className="health-monitoring-cpu-temp-value" size="3cqmin">
{fahrenheit ? `${(cpuTemp * 1.8 + 32).toFixed(1)}°F` : `${cpuTemp.toFixed(1)}°C`}
</Text>
<IconCpu className="health-monitoring-cpu-temp-icon" size="7cqmin" />
</Center>
}
sections={[
{
value: cpuTemp,
color: progressColor(cpuTemp),
},
]}
/>
</Box>
);
};
const MemoryRing = ({ available, used }: { available: string; used: string }) => {
const { width, ref } = useElementSize();
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
const memoryUsage = formatMemoryUsage(available, used);
return (
<Box ref={ref} w="100%" h="100%" className="health-monitoring-memory">
<RingProgress
className="health-monitoring-memory-use"
roundCaps
size={fallbackWidth * 0.95}
thickness={fallbackWidth / 10}
label={
<Center style={{ flexDirection: "column" }}>
<Text className="health-monitoring-memory-value" size="3cqmin">
{memoryUsage.memUsed.GB}GiB
</Text>
<IconBrain className="health-monitoring-memory-icon" size="7cqmin" />
</Center>
}
sections={[
{
value: Number(memoryUsage.memUsed.percent),
color: progressColor(Number(memoryUsage.memUsed.percent)),
tooltip: `${memoryUsage.memUsed.percent}%`,
},
]}
/>
</Box>
);
};
export const formatMemoryUsage = (memFree: string, memUsed: string) => {
const memFreeBytes = Number(memFree);
const memUsedBytes = Number(memUsed);
const totalMemory = memFreeBytes + memUsedBytes;
const memFreeGB = (memFreeBytes / 1024 ** 3).toFixed(2);
const memUsedGB = (memUsedBytes / 1024 ** 3).toFixed(2);
const memFreePercent = Math.round((memFreeBytes / totalMemory) * 100);
const memUsedPercent = Math.round((memUsedBytes / totalMemory) * 100);
const memTotalGB = (totalMemory / 1024 ** 3).toFixed(2);
return {
memFree: { percent: memFreePercent, GB: memFreeGB },
memUsed: { percent: memUsedPercent, GB: memUsedGB },
memTotal: { GB: memTotalGB },
};
};