feat(system-resources-widget): add label display mode option (#4086)

This commit is contained in:
Meier Lukas
2025-09-19 16:19:58 +02:00
committed by GitHub
parent 2a67d2f9da
commit 312e084e2c
8 changed files with 108 additions and 14 deletions

View File

@@ -2511,6 +2511,15 @@
"memory": "Memory", "memory": "Memory",
"network": "Network" "network": "Network"
} }
},
"labelDisplayMode": {
"label": "Label display mode",
"option": {
"textWithIcon": "Show text with icon",
"text": "Show only text",
"icon": "Show only icon",
"hidden": "Hide label"
}
} }
}, },
"card": { "card": {

View File

@@ -1,17 +1,21 @@
import { Box, Group, Paper, Stack, Text } from "@mantine/core"; import { Box, Group, Paper, Stack, Text } from "@mantine/core";
import { IconNetwork } from "@tabler/icons-react";
import { humanFileSize } from "@homarr/common"; import { humanFileSize } from "@homarr/common";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import type { LabelDisplayModeOption } from "..";
import { CommonChart } from "./common-chart"; import { CommonChart } from "./common-chart";
export const CombinedNetworkTrafficChart = ({ export const CombinedNetworkTrafficChart = ({
usageOverTime, usageOverTime,
labelDisplayMode,
}: { }: {
usageOverTime: { usageOverTime: {
up: number; up: number;
down: number; down: number;
}[]; }[];
labelDisplayMode: LabelDisplayModeOption;
}) => { }) => {
const chartData = usageOverTime.map((usage, index) => ({ index, up: usage.up, down: usage.down })); const chartData = usageOverTime.map((usage, index) => ({ index, up: usage.up, down: usage.down }));
const t = useScopedI18n("widget.systemResources.card"); const t = useScopedI18n("widget.systemResources.card");
@@ -25,7 +29,9 @@ export const CombinedNetworkTrafficChart = ({
{ name: "down", color: "yellow.5" }, { name: "down", color: "yellow.5" },
]} ]}
title={t("network")} title={t("network")}
icon={IconNetwork}
yAxisProps={{ domain: [0, "dataMax"] }} yAxisProps={{ domain: [0, "dataMax"] }}
labelDisplayMode={labelDisplayMode}
tooltipProps={{ tooltipProps={{
content: ({ payload }) => { content: ({ payload }) => {
if (!payload) { if (!payload) {

View File

@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import type { ReactNode } from "react";
import type { LineChartSeries } from "@mantine/charts"; import type { LineChartSeries } from "@mantine/charts";
import { LineChart } from "@mantine/charts"; import { LineChart } from "@mantine/charts";
import { Card, Center, Group, Loader, Stack, Text, useMantineColorScheme, useMantineTheme } from "@mantine/core"; import { Card, Center, Group, Loader, Stack, Text, useMantineColorScheme, useMantineTheme } from "@mantine/core";
@@ -6,12 +7,17 @@ import { useElementSize, useHover, useMergedRef } from "@mantine/hooks";
import type { TooltipProps, YAxisProps } from "recharts"; import type { TooltipProps, YAxisProps } from "recharts";
import { useRequiredBoard } from "@homarr/boards/context"; import { useRequiredBoard } from "@homarr/boards/context";
import type { TablerIcon } from "@homarr/ui";
import type { LabelDisplayModeOption } from "..";
export const CommonChart = ({ export const CommonChart = ({
data, data,
dataKey, dataKey,
series, series,
title, title,
icon: Icon,
labelDisplayMode,
tooltipProps, tooltipProps,
yAxisProps, yAxisProps,
lastValue, lastValue,
@@ -19,7 +25,9 @@ export const CommonChart = ({
data: Record<string, any>[]; data: Record<string, any>[];
dataKey: string; dataKey: string;
series: LineChartSeries[]; series: LineChartSeries[];
title: string; title: ReactNode;
icon: TablerIcon;
labelDisplayMode: LabelDisplayModeOption;
tooltipProps?: TooltipProps<number, any>; tooltipProps?: TooltipProps<number, any>;
yAxisProps?: Omit<YAxisProps, "ref">; yAxisProps?: Omit<YAxisProps, "ref">;
lastValue?: string; lastValue?: string;
@@ -35,6 +43,9 @@ export const CommonChart = ({
const backgroundColor = const backgroundColor =
scheme.colorScheme === "dark" ? `rgba(57, 57, 57, ${opacity})` : `rgba(246, 247, 248, ${opacity})`; scheme.colorScheme === "dark" ? `rgba(57, 57, 57, ${opacity})` : `rgba(246, 247, 248, ${opacity})`;
const showIcon = labelDisplayMode === "icon" || labelDisplayMode === "textWithIcon";
const showText = labelDisplayMode === "text" || labelDisplayMode === "textWithIcon";
return ( return (
<Card <Card
ref={ref} ref={ref}
@@ -55,10 +66,14 @@ export const CommonChart = ({
gap={5} gap={5}
wrap={"nowrap"} wrap={"nowrap"}
style={{ zIndex: 2, pointerEvents: "none" }} style={{ zIndex: 2, pointerEvents: "none" }}
align="center"
> >
<Text c={"dimmed"} size={height > 100 ? "md" : "xs"} fw={"bold"}> {showIcon && <Icon color={"var(--mantine-color-dimmed)"} size={height > 100 ? 20 : 14} stroke={1.5} />}
{title} {showText && (
</Text> <Text c={"dimmed"} size={height > 100 ? "md" : "xs"} fw={"bold"}>
{title}
</Text>
)}
{lastValue && ( {lastValue && (
<Text c={"dimmed"} size={height > 100 ? "md" : "xs"} lineClamp={1}> <Text c={"dimmed"} size={height > 100 ? "md" : "xs"} lineClamp={1}>
{lastValue} {lastValue}

View File

@@ -1,10 +1,18 @@
import { Paper, Text } from "@mantine/core"; import { Paper, Text } from "@mantine/core";
import { IconCpu } from "@tabler/icons-react";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { LabelDisplayModeOption } from "..";
import { CommonChart } from "./common-chart"; import { CommonChart } from "./common-chart";
export const SystemResourceCPUChart = ({ cpuUsageOverTime }: { cpuUsageOverTime: number[] }) => { export const SystemResourceCPUChart = ({
cpuUsageOverTime,
labelDisplayMode,
}: {
cpuUsageOverTime: number[];
labelDisplayMode: LabelDisplayModeOption;
}) => {
const chartData = cpuUsageOverTime.map((usage, index) => ({ index, usage })); const chartData = cpuUsageOverTime.map((usage, index) => ({ index, usage }));
const t = useScopedI18n("widget.systemResources.card"); const t = useScopedI18n("widget.systemResources.card");
@@ -14,11 +22,13 @@ export const SystemResourceCPUChart = ({ cpuUsageOverTime }: { cpuUsageOverTime:
dataKey={"index"} dataKey={"index"}
series={[{ name: "usage", color: "blue.5" }]} series={[{ name: "usage", color: "blue.5" }]}
title={t("cpu")} title={t("cpu")}
icon={IconCpu}
lastValue={ lastValue={
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
cpuUsageOverTime.length > 0 ? `${Math.round(cpuUsageOverTime[cpuUsageOverTime.length - 1]!)}%` : undefined cpuUsageOverTime.length > 0 ? `${Math.round(cpuUsageOverTime[cpuUsageOverTime.length - 1]!)}%` : undefined
} }
yAxisProps={{ domain: [0, 100] }} yAxisProps={{ domain: [0, 100] }}
labelDisplayMode={labelDisplayMode}
tooltipProps={{ tooltipProps={{
content: ({ payload }) => { content: ({ payload }) => {
if (!payload) { if (!payload) {

View File

@@ -1,16 +1,20 @@
import { Paper, Text } from "@mantine/core"; import { Paper, Text } from "@mantine/core";
import { IconBrain } from "@tabler/icons-react";
import { humanFileSize } from "@homarr/common"; import { humanFileSize } from "@homarr/common";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import type { LabelDisplayModeOption } from "..";
import { CommonChart } from "./common-chart"; import { CommonChart } from "./common-chart";
export const SystemResourceMemoryChart = ({ export const SystemResourceMemoryChart = ({
memoryUsageOverTime, memoryUsageOverTime,
totalCapacityInBytes, totalCapacityInBytes,
labelDisplayMode,
}: { }: {
memoryUsageOverTime: number[]; memoryUsageOverTime: number[];
totalCapacityInBytes: number; totalCapacityInBytes: number;
labelDisplayMode: LabelDisplayModeOption;
}) => { }) => {
const chartData = memoryUsageOverTime.map((usage, index) => ({ index, usage })); const chartData = memoryUsageOverTime.map((usage, index) => ({ index, usage }));
const t = useScopedI18n("widget.systemResources.card"); const t = useScopedI18n("widget.systemResources.card");
@@ -27,6 +31,8 @@ export const SystemResourceMemoryChart = ({
dataKey={"index"} dataKey={"index"}
series={[{ name: "usage", color: "red.6" }]} series={[{ name: "usage", color: "red.6" }]}
title={t("memory")} title={t("memory")}
icon={IconBrain}
labelDisplayMode={labelDisplayMode}
yAxisProps={{ domain: [0, totalCapacityInBytes] }} yAxisProps={{ domain: [0, totalCapacityInBytes] }}
lastValue={percentageUsed !== undefined ? `${Math.round(percentageUsed * 100)}%` : undefined} lastValue={percentageUsed !== undefined ? `${Math.round(percentageUsed * 100)}%` : undefined}
tooltipProps={{ tooltipProps={{

View File

@@ -1,11 +1,21 @@
import { Paper, Text } from "@mantine/core"; import { Paper, Text } from "@mantine/core";
import { IconArrowDown, IconArrowUp } from "@tabler/icons-react";
import { humanFileSize } from "@homarr/common"; import { humanFileSize } from "@homarr/common";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import type { LabelDisplayModeOption } from "..";
import { CommonChart } from "./common-chart"; import { CommonChart } from "./common-chart";
export const NetworkTrafficChart = ({ usageOverTime, isUp }: { usageOverTime: number[]; isUp: boolean }) => { export const NetworkTrafficChart = ({
usageOverTime,
isUp,
labelDisplayMode,
}: {
usageOverTime: number[];
isUp: boolean;
labelDisplayMode: LabelDisplayModeOption;
}) => {
const chartData = usageOverTime.map((usage, index) => ({ index, usage })); const chartData = usageOverTime.map((usage, index) => ({ index, usage }));
const t = useScopedI18n("widget.systemResources.card"); const t = useScopedI18n("widget.systemResources.card");
@@ -18,8 +28,10 @@ export const NetworkTrafficChart = ({ usageOverTime, isUp }: { usageOverTime: nu
dataKey={"index"} dataKey={"index"}
series={[{ name: "usage", color: "yellow.5" }]} series={[{ name: "usage", color: "yellow.5" }]}
title={isUp ? t("up") : t("down")} title={isUp ? t("up") : t("down")}
icon={isUp ? IconArrowUp : IconArrowDown}
yAxisProps={{ domain: [0, upperBound] }} yAxisProps={{ domain: [0, upperBound] }}
lastValue={`${humanFileSize(Math.round(max))}/s`} lastValue={`${humanFileSize(Math.round(max))}/s`}
labelDisplayMode={labelDisplayMode}
tooltipProps={{ tooltipProps={{
content: ({ payload }) => { content: ({ payload }) => {
if (!payload) { if (!payload) {

View File

@@ -57,7 +57,10 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo
<Stack gap="xs" p="xs" ref={ref} h="100%"> <Stack gap="xs" p="xs" ref={ref} h="100%">
{options.visibleCharts.includes("cpu") && ( {options.visibleCharts.includes("cpu") && (
<Box h={rowHeight}> <Box h={rowHeight}>
<SystemResourceCPUChart cpuUsageOverTime={items.map((item) => item.cpu)} /> <SystemResourceCPUChart
cpuUsageOverTime={items.map((item) => item.cpu)}
labelDisplayMode={options.labelDisplayMode}
/>
</Box> </Box>
)} )}
{options.visibleCharts.includes("memory") && ( {options.visibleCharts.includes("memory") && (
@@ -65,22 +68,34 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo
<SystemResourceMemoryChart <SystemResourceMemoryChart
memoryUsageOverTime={items.map((item) => item.memory)} memoryUsageOverTime={items.map((item) => item.memory)}
totalCapacityInBytes={memoryCapacityInBytes} totalCapacityInBytes={memoryCapacityInBytes}
labelDisplayMode={options.labelDisplayMode}
/> />
</Box> </Box>
)} )}
{showNetwork && {showNetwork &&
(width > 256 ? ( (width > 256 ? (
<Group h={rowHeight} gap="xs" grow> <Group h={rowHeight} gap="xs" grow>
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} <NetworkTrafficChart
<NetworkTrafficChart usageOverTime={items.map((item) => item.network!.down)} isUp={false} /> // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
usageOverTime={items.map((item) => item.network!.down)}
isUp={false}
labelDisplayMode={options.labelDisplayMode}
/>
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} <NetworkTrafficChart
<NetworkTrafficChart usageOverTime={items.map((item) => item.network!.up)} isUp /> // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
usageOverTime={items.map((item) => item.network!.up)}
isUp
labelDisplayMode={options.labelDisplayMode}
/>
</Group> </Group>
) : ( ) : (
<Box h={rowHeight}> <Box h={rowHeight}>
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} <CombinedNetworkTrafficChart
<CombinedNetworkTrafficChart usageOverTime={items.map((item) => item.network!)} /> // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
usageOverTime={items.map((item) => item.network!)}
labelDisplayMode={options.labelDisplayMode}
/>
</Box> </Box>
))} ))}
</Stack> </Stack>

View File

@@ -1,8 +1,17 @@
import { IconGraphFilled } from "@tabler/icons-react"; import { IconAlignLeft, IconEyeOff, IconGraphFilled, IconListDetails, IconPhoto } from "@tabler/icons-react";
import { objectEntries } from "@homarr/common";
import { createWidgetDefinition } from "../definition"; import { createWidgetDefinition } from "../definition";
import { optionsBuilder } from "../options"; import { optionsBuilder } from "../options";
const labelDisplayModeOptions = {
textWithIcon: IconListDetails,
text: IconAlignLeft,
icon: IconPhoto,
hidden: IconEyeOff,
} as const;
export const { definition, componentLoader } = createWidgetDefinition("systemResources", { export const { definition, componentLoader } = createWidgetDefinition("systemResources", {
icon: IconGraphFilled, icon: IconGraphFilled,
supportedIntegrations: ["dashDot", "openmediavault", "truenas"], supportedIntegrations: ["dashDot", "openmediavault", "truenas"],
@@ -16,6 +25,18 @@ export const { definition, componentLoader } = createWidgetDefinition("systemRes
defaultValue: ["cpu", "memory", "network"], defaultValue: ["cpu", "memory", "network"],
withDescription: true, withDescription: true,
}), }),
labelDisplayMode: factory.select({
options: objectEntries(labelDisplayModeOptions).map(([key, icon]) => ({
value: key,
label: (t) => t(`widget.systemResources.option.labelDisplayMode.option.${key}`),
icon,
})),
defaultValue: "textWithIcon",
}),
})); }));
}, },
}).withDynamicImport(() => import("./component")); }).withDynamicImport(() => import("./component"));
export type LabelDisplayModeOption = ReturnType<
(typeof definition)["createOptions"]
>["labelDisplayMode"]["options"][number]["value"];