feat(system-resources-widget): add label display mode option (#4086)
This commit is contained in:
@@ -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": {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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={{
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"];
|
||||||
|
|||||||
Reference in New Issue
Block a user