feat: adopt Mantine 9.6 components (#6758)

This commit is contained in:
Thomas authored and GitHub committed 2026-09-03 18:21:51 +02:00
1 parent b3aa6155cc
commit 35155b18b9
28 files changed
+1192 -567

No files matched your search

@@ -1,17 +1,32 @@
import { Center, RingProgress, Text } from "@mantine/core";
import { GaugeChart } from "@mantine/charts";
import { Center, Text } from "@mantine/core";
import { IconCpu } from "@tabler/icons-react";
import { zoomCompensatedSize } from "@homarr/ui";
import { progressColor } from "../system-health";
export const CpuRing = ({ cpuUtilization, isTiny }: { cpuUtilization: number; isTiny: boolean }) => {
export const CpuRing = ({
cpuUtilization,
isTiny,
ariaLabel,
}: {
cpuUtilization: number;
isTiny: boolean;
ariaLabel: string;
}) => {
return (
<RingProgress
<GaugeChart
className="health-monitoring-cpu"
aria-label={ariaLabel}
roundCaps
size={isTiny ? 50 : 100}
thickness={isTiny ? 4 : 8}
startAngle={0}
endAngle={360}
value={Number(cpuUtilization.toFixed(2))}
valueFormatter={(value) => `${value.toFixed(2)}%`}
filledColor={progressColor(Number(cpuUtilization.toFixed(2)))}
label={
<Center style={{ flexDirection: "column" }}>
<Text
@@ -21,12 +36,6 @@ export const CpuRing = ({ cpuUtilization, isTiny }: { cpuUtilization: number; is
<IconCpu className="health-monitoring-cpu-utilization-icon" style={zoomCompensatedSize(isTiny ? 8 : 16)} />
</Center>
}
sections={[
{
value: Number(cpuUtilization.toFixed(2)),
color: progressColor(Number(cpuUtilization.toFixed(2))),
},
]}
/>
);
};
@@ -0,0 +1,66 @@
// @vitest-environment jsdom
import { act } from "react";
import { createRoot } from "react-dom/client";
import type { Root } from "react-dom/client";
import { MantineProvider } from "@mantine/core";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { CpuTempRing } from "./cpu-temp-ring";
describe("CpuTempRing", () => {
let host: HTMLDivElement;
let root: Root;
beforeEach(() => {
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
vi.stubGlobal("matchMedia", () => ({
matches: false,
media: "",
onchange: null,
addListener: () => undefined,
removeListener: () => undefined,
addEventListener: () => undefined,
removeEventListener: () => undefined,
dispatchEvent: () => false,
}));
host = document.createElement("div");
document.body.append(host);
root = createRoot(host);
});
afterEach(async () => {
await act(() => root.unmount());
host.remove();
vi.unstubAllGlobals();
});
const renderRing = async (cpuTemp: number, fahrenheit = false) => {
await act(() =>
root.render(
<MantineProvider env="test">
<CpuTempRing cpuTemp={cpuTemp} fahrenheit={fahrenheit} isTiny={false} ariaLabel="CPU temperature" />
</MantineProvider>,
),
);
return host.querySelector('[role="meter"]');
};
test("renders a zero temperature reading", async () => {
const meter = await renderRing(0);
expect(meter).not.toBeNull();
expect(meter?.getAttribute("aria-valuenow")).toBe("0");
expect(meter?.getAttribute("aria-valuetext")).toBe("0.0°C");
});
test("clamps readings above the configured maximum consistently", async () => {
const meter = await renderRing(150, true);
expect(meter?.getAttribute("aria-valuenow")).toBe("100");
expect(meter?.getAttribute("aria-valuemax")).toBe("100");
expect(meter?.getAttribute("aria-valuetext")).toBe("212.0°F");
expect(meter?.textContent).toContain("212.0°F");
});
});
@@ -1,43 +1,56 @@
import { Center, RingProgress, Text } from "@mantine/core";
import { GaugeChart } from "@mantine/charts";
import { Center, Text } from "@mantine/core";
import { IconCpu } from "@tabler/icons-react";
import { zoomCompensatedSize } from "@homarr/ui";
import { progressColor } from "../system-health";
const CPU_TEMPERATURE_MIN_CELSIUS = 0;
const CPU_TEMPERATURE_MAX_CELSIUS = 100;
export const CpuTempRing = ({
fahrenheit,
cpuTemp,
isTiny,
ariaLabel,
}: {
fahrenheit: boolean;
cpuTemp: number | undefined;
isTiny: boolean;
ariaLabel: string;
}) => {
if (!cpuTemp) {
if (cpuTemp === undefined) {
return null;
}
const normalizedCpuTemp = Math.max(CPU_TEMPERATURE_MIN_CELSIUS, Math.min(CPU_TEMPERATURE_MAX_CELSIUS, cpuTemp));
const formatTemperature = (value: number) =>
fahrenheit ? `${(value * 1.8 + 32).toFixed(1)}°F` : `${value.toFixed(1)}°C`;
const temperatureDisplay = formatTemperature(normalizedCpuTemp);
return (
<RingProgress
<GaugeChart
className="health-monitoring-cpu-temperature"
aria-label={ariaLabel}
roundCaps
size={isTiny ? 50 : 100}
thickness={isTiny ? 4 : 8}
startAngle={0}
endAngle={360}
min={CPU_TEMPERATURE_MIN_CELSIUS}
max={CPU_TEMPERATURE_MAX_CELSIUS}
value={normalizedCpuTemp}
valueFormatter={formatTemperature}
filledColor={progressColor(normalizedCpuTemp)}
label={
<Center style={{ flexDirection: "column" }}>
<Text className="health-monitoring-cpu-temp-value" size={isTiny ? "8px" : "xs"}>
{fahrenheit ? `${(cpuTemp * 1.8 + 32).toFixed(1)}°F` : `${cpuTemp.toFixed(1)}°C`}
{temperatureDisplay}
</Text>
<IconCpu className="health-monitoring-cpu-temp-icon" style={zoomCompensatedSize(isTiny ? 8 : 16)} />
</Center>
}
sections={[
{
value: cpuTemp,
color: progressColor(cpuTemp),
},
]}
/>
);
};
@@ -1,4 +1,5 @@
import { Center, RingProgress, Stack, Text, Tooltip } from "@mantine/core";
import { GaugeChart } from "@mantine/charts";
import { Center, Stack, Text, Tooltip } from "@mantine/core";
import { IconDeviceDesktop } from "@tabler/icons-react";
import { zoomCompensatedSize } from "@homarr/ui";
@@ -16,9 +17,10 @@ interface GpuRingProps {
};
isTiny: boolean;
fahrenheit: boolean;
ariaLabel: string;
}
export const GpuRing = ({ gpu, isTiny, fahrenheit }: GpuRingProps) => {
export const GpuRing = ({ gpu, isTiny, fahrenheit, ariaLabel }: GpuRingProps) => {
const tempDisplay =
gpu.temperature != null
? fahrenheit
@@ -40,11 +42,17 @@ export const GpuRing = ({ gpu, isTiny, fahrenheit }: GpuRingProps) => {
}
multiline
>
<RingProgress
<GaugeChart
className={`health-monitoring-gpu health-monitoring-gpu-${gpu.gpuId}`}
aria-label={ariaLabel}
roundCaps
size={isTiny ? 50 : 100}
thickness={isTiny ? 4 : 8}
startAngle={0}
endAngle={360}
value={Number(gpu.processorUtilization.toFixed(2))}
valueFormatter={(value) => `${value.toFixed(0)}%`}
filledColor={progressColor(Number(gpu.processorUtilization.toFixed(2)))}
label={
<Center style={{ flexDirection: "column" }}>
<Text
@@ -57,12 +65,6 @@ export const GpuRing = ({ gpu, isTiny, fahrenheit }: GpuRingProps) => {
/>
</Center>
}
sections={[
{
value: Number(gpu.processorUtilization.toFixed(2)),
color: progressColor(Number(gpu.processorUtilization.toFixed(2))),
},
]}
/>
</Tooltip>
);
@@ -1,35 +1,47 @@
import { Center, RingProgress, Text } from "@mantine/core";
import { GaugeChart } from "@mantine/charts";
import { Center, Text, Tooltip } from "@mantine/core";
import { IconBrain } from "@tabler/icons-react";
import { zoomCompensatedSize } from "@homarr/ui";
import { progressColor } from "../system-health";
export const MemoryRing = ({ available, used, isTiny }: { available: number; used: number; isTiny: boolean }) => {
export const MemoryRing = ({
available,
used,
isTiny,
ariaLabel,
}: {
available: number;
used: number;
isTiny: boolean;
ariaLabel: string;
}) => {
const memoryUsage = formatMemoryUsage(available, used);
return (
<RingProgress
className="health-monitoring-memory"
roundCaps
size={isTiny ? 50 : 100}
thickness={isTiny ? 4 : 8}
label={
<Center style={{ flexDirection: "column" }}>
<Text className="health-monitoring-memory-value" size={isTiny ? "8px" : "xs"}>
{memoryUsage.memUsed.GB}GiB
</Text>
<IconBrain className="health-monitoring-memory-icon" style={zoomCompensatedSize(isTiny ? 8 : 16)} />
</Center>
}
sections={[
{
value: Number(memoryUsage.memUsed.percent),
color: progressColor(Number(memoryUsage.memUsed.percent)),
tooltip: `${memoryUsage.memUsed.percent}%`,
},
]}
/>
<Tooltip label={`${memoryUsage.memUsed.percent}%`}>
<GaugeChart
className="health-monitoring-memory"
aria-label={ariaLabel}
roundCaps
size={isTiny ? 50 : 100}
thickness={isTiny ? 4 : 8}
startAngle={0}
endAngle={360}
value={Number(memoryUsage.memUsed.percent)}
valueFormatter={(value) => `${value}%`}
filledColor={progressColor(Number(memoryUsage.memUsed.percent))}
label={
<Center style={{ flexDirection: "column" }}>
<Text className="health-monitoring-memory-value" size={isTiny ? "8px" : "xs"}>
{memoryUsage.memUsed.GB}GiB
</Text>
<IconBrain className="health-monitoring-memory-icon" style={zoomCompensatedSize(isTiny ? 8 : 16)} />
</Center>
}
/>
</Tooltip>
);
};
@@ -157,10 +157,7 @@ export const SystemHealthMonitoring = ({
onClick={() => setOpenedIntegrationId(integrationId)}
aria-label={t("popover.information")}
>
<IconInfoCircle
className="health-monitoring-information-icon"
style={zoomCompensatedSize(30)}
/>
<IconInfoCircle className="health-monitoring-information-icon" style={zoomCompensatedSize(30)} />
</ActionIcon>
</Indicator>
<Modal
@@ -177,20 +174,34 @@ export const SystemHealthMonitoring = ({
</Modal>
</Box>
<Flex className="health-monitoring-information-card-elements" justify="center" align="center" wrap="wrap">
{showCpu && <CpuRing cpuUtilization={healthInfo.cpuUtilization} isTiny={isTiny} />}
{showCpu && (
<CpuTempRing fahrenheit={options.fahrenheit} cpuTemp={healthInfo.cpuTemp} isTiny={isTiny} />
<CpuRing cpuUtilization={healthInfo.cpuUtilization} isTiny={isTiny} ariaLabel={t("gauge.cpuUsage")} />
)}
{showCpu && (
<CpuTempRing
fahrenheit={options.fahrenheit}
cpuTemp={healthInfo.cpuTemp}
isTiny={isTiny}
ariaLabel={t("gauge.cpuTemperature")}
/>
)}
{showMemory && (
<MemoryRing
available={healthInfo.memAvailableInBytes}
used={healthInfo.memUsedInBytes}
isTiny={isTiny}
ariaLabel={t("gauge.memoryUsage")}
/>
)}
{showGpu &&
healthInfo.gpu.map((gpu) => (
<GpuRing key={gpu.gpuId} gpu={gpu} isTiny={isTiny} fahrenheit={options.fahrenheit} />
<GpuRing
key={gpu.gpuId}
gpu={gpu}
isTiny={isTiny}
fahrenheit={options.fahrenheit}
ariaLabel={t("gauge.gpuUsage", { name: gpu.name })}
/>
))}
</Flex>
{isAdvanced && (
+40 -191
View File
@@ -9,7 +9,6 @@ import {
ColorPicker,
ColorSwatch,
Group,
NumberInput,
Popover,
ScrollArea,
Stack,
@@ -24,9 +23,6 @@ import { Link, RichTextEditor, useRichTextEditorContext } from "@mantine/tiptap"
import {
IconCheck,
IconCircleOff,
IconColumnInsertLeft,
IconColumnInsertRight,
IconColumnRemove,
IconDeviceFloppy,
IconEdit,
IconHighlight,
@@ -36,16 +32,12 @@ import {
IconLetterA,
IconListCheck,
IconPhoto,
IconRowInsertBottom,
IconRowInsertTop,
IconRowRemove,
IconTableOff,
IconTablePlus,
IconTextDirectionLtr,
IconTextDirectionRtl,
IconX,
} from "@tabler/icons-react";
import { Color } from "@tiptap/extension-color";
import { Details, DetailsContent, DetailsSummary } from "@tiptap/extension-details";
import { Highlight } from "@tiptap/extension-highlight";
import { Image } from "@tiptap/extension-image";
import { Placeholder } from "@tiptap/extension-placeholder";
@@ -56,14 +48,12 @@ import { TableRow } from "@tiptap/extension-table-row";
import { TaskList } from "@tiptap/extension-task-list";
import { TextAlign } from "@tiptap/extension-text-align";
import { TextStyle } from "@tiptap/extension-text-style";
import type { Editor } from "@tiptap/react";
import { useEditor } from "@tiptap/react";
import { BubbleMenu } from "@tiptap/react/menus";
import { StarterKit } from "@tiptap/starter-kit";
import { clientApi } from "@homarr/api/client";
import { useForm } from "@homarr/form";
import type { TranslationObject } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import type { TablerIcon } from "@homarr/ui";
import { InlineConfirmActionIcon } from "@homarr/ui";
@@ -164,6 +154,13 @@ export function Notebook({
placeholder: `${t("placeholder")}…`,
}),
Color,
Details.configure({
renderToggleButton: ({ element, isOpen }) => {
element.setAttribute("aria-label", tControls(isOpen ? "collapseDetails" : "expandDetails"));
},
}),
DetailsSummary,
DetailsContent,
Highlight.configure({ multicolor: true }),
Image.extend({
addAttributes() {
@@ -347,6 +344,23 @@ export function Notebook({
isEditing && !isSaving ? getHotkeyHandler([[hotkeys.saveNotebook, () => void handleEditToggle()]]) : undefined
}
editor={editor}
labels={{
detailsControlLabel: tControls("details"),
tableInsertControlLabel: tControls("insertTable"),
tableInsertLabel: (columns, rows) =>
tControls("insertTableSize", { columns: columns.toString(), rows: rows.toString() }),
tableDeleteControlLabel: tControls("deleteTable"),
tableColumnBeforeControlLabel: tControls("addColumnLeft"),
tableColumnAfterControlLabel: tControls("addColumnRight"),
tableColumnDeleteControlLabel: tControls("deleteColumn"),
tableRowBeforeControlLabel: tControls("addRowTop"),
tableRowAfterControlLabel: tControls("addRowBelow"),
tableRowDeleteControlLabel: tControls("deleteRow"),
tableToggleHeaderRowControlLabel: tControls("toggleHeaderRow"),
tableToggleHeaderColumnControlLabel: tControls("toggleHeaderColumn"),
tableMergeCellsControlLabel: tControls("mergeCells"),
tableSplitCellControlLabel: tControls("splitCell"),
}}
styles={() => ({
root: {
backgroundColor: "transparent",
@@ -433,6 +447,7 @@ export function Notebook({
<RichTextEditor.ControlsGroup className="homarr-notebook-toolbar-group">
<RichTextEditor.Blockquote title={tControls("blockquote")} />
<RichTextEditor.Hr title={tControls("horizontalLine")} />
<RichTextEditor.Details title={tControls("details")} />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup className="homarr-notebook-toolbar-group">
@@ -456,18 +471,23 @@ export function Notebook({
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup className="homarr-notebook-toolbar-group">
<TableToggle />
{editor?.isActive("table") && (
{editor?.isActive("table") ? (
<>
<RichTextEditor.TableDelete title={tControls("deleteTable")} />
<ColorCellControl />
<TableToggleMerge />
<TableAddColumnBefore />
<TableAddColumnAfter />
<TableRemoveColumn />
<TableAddRowBefore />
<TableAddRowAfter />
<TableRemoveRow />
<RichTextEditor.TableColumnBefore title={tControls("addColumnLeft")} />
<RichTextEditor.TableColumnAfter title={tControls("addColumnRight")} />
<RichTextEditor.TableColumnDelete title={tControls("deleteColumn")} />
<RichTextEditor.TableRowBefore title={tControls("addRowTop")} />
<RichTextEditor.TableRowAfter title={tControls("addRowBelow")} />
<RichTextEditor.TableRowDelete title={tControls("deleteRow")} />
<RichTextEditor.TableToggleHeaderRow title={tControls("toggleHeaderRow")} />
<RichTextEditor.TableToggleHeaderColumn title={tControls("toggleHeaderColumn")} />
<RichTextEditor.TableMergeCells title={tControls("mergeCells")} />
<RichTextEditor.TableSplitCell title={tControls("splitCell")} />
</>
) : (
<RichTextEditor.TableInsert title={tControls("insertTable")} withHeaderRow={false} />
)}
</RichTextEditor.ControlsGroup>
@@ -916,174 +936,3 @@ function ListIndentDecrease() {
</RichTextEditor.Control>
);
}
const handleAddColumnBefore = (editor: Editor) => {
editor.commands.addColumnBefore();
};
const TableAddColumnBefore = () => (
<TableControl title="addColumnLeft" onClick={handleAddColumnBefore} icon={IconColumnInsertLeft} />
);
const handleAddColumnAfter = (editor: Editor) => {
editor.commands.addColumnAfter();
};
const TableAddColumnAfter = () => (
<TableControl title="addColumnRight" onClick={handleAddColumnAfter} icon={IconColumnInsertRight} />
);
const handleRemoveColumn = (editor: Editor) => {
editor.commands.deleteColumn();
};
const TableRemoveColumn = () => (
<TableControl title="deleteColumn" onClick={handleRemoveColumn} icon={IconColumnRemove} />
);
const handleAddRowBefore = (editor: Editor) => {
editor.commands.addRowBefore();
};
const TableAddRowBefore = () => <TableControl title="addRowTop" onClick={handleAddRowBefore} icon={IconRowInsertTop} />;
const handleAddRowAfter = (editor: Editor) => {
editor.commands.addRowAfter();
};
const TableAddRowAfter = () => (
<TableControl title="addRowBelow" onClick={handleAddRowAfter} icon={IconRowInsertBottom} />
);
const handleRemoveRow = (editor: Editor) => {
editor.commands.deleteRow();
};
const TableRemoveRow = () => <TableControl title="deleteRow" onClick={handleRemoveRow} icon={IconRowRemove} />;
interface TableControlProps {
title: Exclude<keyof TranslationObject["widget"]["notebook"]["controls"], "align" | "heading">;
onClick: (editor: Editor) => void;
icon: TablerIcon;
}
const TableControl = ({ title, onClick, icon: Icon }: TableControlProps) => {
const { editor } = useRichTextEditorContext();
const tControls = useI18n("widget.notebook.controls");
const handleControlClick = useCallback(() => {
if (!editor) return;
onClick(editor);
}, [editor, onClick]);
return (
<RichTextEditor.Control title={tControls(title)} onClick={handleControlClick}>
<Icon {...controlIconProps} />
</RichTextEditor.Control>
);
};
function TableToggleMerge() {
const { editor } = useRichTextEditorContext();
const tControls = useI18n("widget.notebook.controls");
const handleToggleMerge = useCallback(() => {
editor?.commands.mergeOrSplit();
}, [editor]);
return (
<RichTextEditor.Control
title={tControls("mergeCell")}
onClick={handleToggleMerge}
active={editor?.getAttributes("tableCell").colspan > 1}
>
<svg
height="1.25rem"
width="1.25rem"
strokeWidth="0.1"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
{/* No existing icon from tabler, taken from https://icon-sets.iconify.design/fluent/table-cells-merge-24-regular/ */}
<path
fill="currentColor"
d="M15.58 11.25H8.42l.89-1.002a.75.75 0 0 0-1.12-.996l-2 2.25a.75.75 0 0 0 0 .996l2 2.25a.75.75 0 1 0 1.12-.996l-.89-1.002h7.16l-.89 1.002a.75.75 0 0 0 1.12.996l2-2.25l.011-.012a.746.746 0 0 0-.013-.987l-1.997-2.247a.75.75 0 0 0-1.121.996l.89 1.002ZM6.25 3A3.25 3.25 0 0 0 3 6.25v11.5A3.25 3.25 0 0 0 6.25 21h11.5A3.25 3.25 0 0 0 21 17.75V6.25A3.25 3.25 0 0 0 17.75 3H6.25ZM4.5 6.25c0-.966.784-1.75 1.75-1.75h11.5c.966 0 1.75.784 1.75 1.75v.25h-15v-.25ZM4.5 8h15v8h-15V8Zm15 9.5v.25a1.75 1.75 0 0 1-1.75 1.75H6.25a1.75 1.75 0 0 1-1.75-1.75v-.25h15Z"
/>
</svg>
</RichTextEditor.Control>
);
}
function TableToggle() {
const { editor } = useRichTextEditorContext();
const isActive = editor?.isActive("table");
const { colors, white } = useMantineTheme();
const { colorScheme } = useMantineColorScheme();
const [opened, { open, close, toggle }] = useDisclosure(false);
const t = useI18n("widget.notebook");
const tCommon = useI18n("common");
const tControls = useI18n("widget.notebook.controls");
const form = useForm({
initialValues: {
cols: 3,
rows: 3,
},
});
const handleOpen = useCallback(() => {
form.reset();
open();
}, [form, open]);
const handleSubmit = useCallback(
(values: { rows: number; cols: number }) => {
editor?.commands.insertTable({ ...values, withHeaderRow: false });
close();
},
[editor, close],
);
const handleControlClick = useCallback(() => {
if (isActive) {
editor?.commands.deleteTable();
} else {
toggle();
}
}, [isActive, editor, toggle]);
return (
<Popover
opened={opened}
onOpen={handleOpen}
onClose={close}
styles={{
dropdown: {
backgroundColor: colorScheme === "dark" ? colors.dark[7] : white,
},
}}
trapFocus
>
<Popover.Target>
<RichTextEditor.Control
title={tControls(isActive ? "deleteTable" : "addTable")}
active={isActive}
onClick={handleControlClick}
>
{isActive ? <IconTableOff stroke={1.5} size="1rem" /> : <IconTablePlus stroke={1.5} size="1rem" />}
</RichTextEditor.Control>
</Popover.Target>
<Popover.Dropdown>
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap={5}>
<NumberInput label={t("popover.columns")} min={1} {...form.getInputProps("cols")} />
<NumberInput label={t("popover.rows")} min={1} {...form.getInputProps("rows")} />
<Button type="submit" variant="default" mt={10} mb={5}>
{tCommon("action.insert")}
</Button>
</Stack>
</form>
</Popover.Dropdown>
</Popover>
);
}
@@ -4,7 +4,7 @@ import { useMemo } from "react";
import { AreaChart, ChartTooltip } from "@mantine/charts";
import { Stack, useMantineTheme } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import { ReferenceLine, XAxis } from "recharts";
import { XAxis } from "recharts";
import { useRequiredBoard } from "@homarr/boards/context";
import type { SpeedtestTrackerResult } from "@homarr/integrations/types";
@@ -237,6 +237,13 @@ function SpeedHistoryChart({ results, height }: { results: SpeedtestTrackerResul
width: 50,
tick: { fontSize: 10 },
}}
referenceLines={xTicks.map((tickTs) => ({
x: tickTs,
yAxisId: "left",
stroke: "var(--mantine-color-dimmed)",
strokeDasharray: "3 3",
strokeOpacity: 0.35,
}))}
tooltipProps={{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
content: (props: any) => {
@@ -269,16 +276,6 @@ function SpeedHistoryChart({ results, height }: { results: SpeedtestTrackerResul
tickLine={false}
interval={0}
/>
{xTicks.map((tickTs) => (
<ReferenceLine
key={tickTs}
x={tickTs}
yAxisId="left"
stroke="var(--mantine-color-dimmed)"
strokeDasharray="3 3"
strokeOpacity={0.35}
/>
))}
</AreaChart>
);
}
@@ -349,6 +346,13 @@ function PingHistoryChart({ results, height }: { results: SpeedtestTrackerResult
width: 50,
tick: { fontSize: 10 },
}}
referenceLines={xTicks.map((tickTs) => ({
x: tickTs,
yAxisId: "left",
stroke: "var(--mantine-color-dimmed)",
strokeDasharray: "3 3",
strokeOpacity: 0.35,
}))}
tooltipProps={{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
content: (props: any) => {
@@ -367,17 +371,6 @@ function PingHistoryChart({ results, height }: { results: SpeedtestTrackerResult
);
},
}}
>
{xTicks.map((tickTs) => (
<ReferenceLine
key={tickTs}
x={tickTs}
yAxisId="left"
stroke="var(--mantine-color-dimmed)"
strokeDasharray="3 3"
strokeOpacity={0.35}
/>
))}
</AreaChart>
/>
);
}