feat: modernize dashboard widgets
This commit is contained in:
1 parent
1a5e083fd0
commit
6f2cd7fc2b
270 files changed
+10189
-3544
No files matched your search
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { Box, Button, Center, Loader, Menu, ScrollArea, Select, Stack, Text } from "@mantine/core";
|
||||
import { Box, Button, Center, Loader, Menu, ScrollArea, SegmentedControl, Select, Stack, Text } from "@mantine/core";
|
||||
import { IconQuestionMark, IconServer, IconServerOff } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
@@ -16,12 +16,15 @@ import type { WidgetComponentProps } from "../definition";
|
||||
import type { BeszelTimePeriod } from "../beszel/_shared/chart";
|
||||
import { BeszelIntegrationErrorIndicator } from "../beszel/_shared/error-indicator";
|
||||
import { BeszelStatsView } from "../beszel/_shared/stats-view";
|
||||
import { createBeszelSystemChoices, resolveBeszelSystemChoice } from "./selection";
|
||||
|
||||
export default function BeszelSystemStatsWidget({
|
||||
options,
|
||||
integrationIds,
|
||||
isEditMode,
|
||||
width,
|
||||
height,
|
||||
displayMode = "compact",
|
||||
boardId,
|
||||
itemId,
|
||||
setOptions,
|
||||
@@ -37,13 +40,11 @@ export default function BeszelSystemStatsWidget({
|
||||
error: systemsError,
|
||||
} = clientApi.widget.beszel.getSystems.useQuery({ integrationIds });
|
||||
|
||||
const systems = useMemo(
|
||||
() => systemsResult.flatMap((result) => result.systems.map((system) => ({ value: system.id, label: system.name }))),
|
||||
[systemsResult],
|
||||
);
|
||||
const selectedSystem = options.systemId || systems[0]?.value || "";
|
||||
const selectedLabel = systems.find((system) => system.value === selectedSystem)?.label;
|
||||
const systemExists = systems.some((system) => system.value === selectedSystem);
|
||||
const systems = useMemo(() => createBeszelSystemChoices(systemsResult), [systemsResult]);
|
||||
const selectedSystem = resolveBeszelSystemChoice(systems, options.systemId);
|
||||
const selectedValue = selectedSystem?.value ?? "";
|
||||
const selectedLabel = selectedSystem?.label;
|
||||
const systemExists = selectedSystem !== undefined;
|
||||
|
||||
const handleSelectSystem = useCallback(
|
||||
(value: string) => {
|
||||
@@ -55,6 +56,16 @@ export default function BeszelSystemStatsWidget({
|
||||
[setOptions, hasChangeAccess, boardId, itemId, saveItemOptions],
|
||||
);
|
||||
|
||||
const handleTimePeriod = useCallback(
|
||||
(value: string) => {
|
||||
setOptions({ newOptions: { timePeriod: value as BeszelTimePeriod } });
|
||||
if (hasChangeAccess && boardId && itemId) {
|
||||
saveItemOptions({ boardId, itemId, newOptions: { timePeriod: value } });
|
||||
}
|
||||
},
|
||||
[boardId, hasChangeAccess, itemId, saveItemOptions, setOptions],
|
||||
);
|
||||
|
||||
if (systemsError) throw systemsError;
|
||||
if (systemsPending)
|
||||
return (
|
||||
@@ -90,7 +101,7 @@ export default function BeszelSystemStatsWidget({
|
||||
<Select
|
||||
size="xs"
|
||||
data={systems}
|
||||
value={selectedSystem}
|
||||
value={selectedValue}
|
||||
placeholder={t("selectSystem")}
|
||||
onChange={(value) => value && handleSelectSystem(value)}
|
||||
/>
|
||||
@@ -127,8 +138,8 @@ export default function BeszelSystemStatsWidget({
|
||||
<Menu.Item
|
||||
key={system.value}
|
||||
fz="xs"
|
||||
fw={system.value === selectedSystem ? 600 : 400}
|
||||
c={system.value !== selectedSystem ? "dimmed" : undefined}
|
||||
fw={system.value === selectedValue ? 600 : 400}
|
||||
c={system.value !== selectedValue ? "dimmed" : undefined}
|
||||
onClick={() => handleSelectSystem(system.value)}
|
||||
>
|
||||
{system.label}
|
||||
@@ -137,20 +148,35 @@ export default function BeszelSystemStatsWidget({
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
)}
|
||||
{!isEditMode && displayMode === "advanced" && (
|
||||
<SegmentedControl
|
||||
size="xs"
|
||||
value={options.timePeriod}
|
||||
onChange={handleTimePeriod}
|
||||
data={[
|
||||
{ value: "1m", label: t("period.live") },
|
||||
{ value: "1h", label: t("period.oneHour") },
|
||||
{ value: "12h", label: t("period.twelveHours") },
|
||||
{ value: "24h", label: t("period.oneDay") },
|
||||
{ value: "1w", label: t("period.oneWeek") },
|
||||
{ value: "30d", label: t("period.thirtyDays") },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<BeszelStatsView
|
||||
integrationIds={integrationIds}
|
||||
systemId={selectedSystem}
|
||||
integrationIds={selectedSystem ? [selectedSystem.integrationId] : []}
|
||||
systemId={selectedSystem?.systemId ?? ""}
|
||||
timePeriod={options.timePeriod as BeszelTimePeriod}
|
||||
columns={width > 600 ? 2 : 1}
|
||||
columns={displayMode === "advanced" || width > 600 ? 2 : 1}
|
||||
visibility={{
|
||||
cpu: options.showCpu,
|
||||
memory: options.showMemory,
|
||||
disk: options.showDisk,
|
||||
diskIO: options.showDiskIO,
|
||||
network: options.showNetwork,
|
||||
dockerCpu: options.showDockerCpu,
|
||||
dockerMemory: options.showDockerMemory,
|
||||
dockerNetwork: options.showDockerNetwork,
|
||||
diskIO: options.showDiskIO && (displayMode === "advanced" || height >= 260),
|
||||
network: options.showNetwork && (displayMode === "advanced" || height >= 260),
|
||||
dockerCpu: options.showDockerCpu && (displayMode === "advanced" || height >= 380),
|
||||
dockerMemory: options.showDockerMemory && (displayMode === "advanced" || height >= 380),
|
||||
dockerNetwork: options.showDockerNetwork && (displayMode === "advanced" || height >= 380),
|
||||
}}
|
||||
onSwitchToHistorical={() => setOptions({ newOptions: { timePeriod: "1h" } })}
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { clientApi } from "@homarr/api/client";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
import { createBeszelSystemChoices } from "./selection";
|
||||
|
||||
const timePeriodOptions = [
|
||||
{ value: "1m", label: "Live" },
|
||||
@@ -30,7 +31,7 @@ export const { definition, componentLoader } = createWidgetDefinition("beszelSys
|
||||
isPending,
|
||||
isError,
|
||||
} = clientApi.widget.beszel.getSystems.useQuery({ integrationIds }, { enabled: integrationIds.length > 0 });
|
||||
const selectData = data.flatMap((r) => r.systems.map((s) => ({ value: s.id, label: s.name })));
|
||||
const selectData = createBeszelSystemChoices(data).map(({ value, label }) => ({ value, label }));
|
||||
return { data: selectData, isPending, isError };
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createBeszelSystemChoices, resolveBeszelSystemChoice } from "./selection";
|
||||
|
||||
const choices = createBeszelSystemChoices([
|
||||
{ integrationId: "one", integrationName: "Primary", systems: [{ id: "same", name: "Host A" }] },
|
||||
{ integrationId: "two", integrationName: "Backup", systems: [{ id: "same", name: "Host B" }] },
|
||||
]);
|
||||
|
||||
describe("Beszel system selection", () => {
|
||||
it("uses a composite integration and system identity", () => {
|
||||
expect(choices.map((choice) => choice.value)).toEqual(["one:same", "two:same"]);
|
||||
expect(resolveBeszelSystemChoice(choices, "two:same")?.integrationId).toBe("two");
|
||||
});
|
||||
|
||||
it("keeps legacy plain system ids working", () => {
|
||||
expect(resolveBeszelSystemChoice(choices, "same")?.integrationId).toBe("one");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
export interface BeszelSystemChoice {
|
||||
value: string;
|
||||
label: string;
|
||||
integrationId: string;
|
||||
systemId: string;
|
||||
}
|
||||
|
||||
interface BeszelSystemsResult {
|
||||
integrationId: string;
|
||||
integrationName: string;
|
||||
systems: { id: string; name: string }[];
|
||||
}
|
||||
|
||||
export function createBeszelSystemChoices(results: BeszelSystemsResult[]): BeszelSystemChoice[] {
|
||||
const includeIntegrationName = results.length > 1;
|
||||
return results.flatMap((result) =>
|
||||
result.systems.map((system) => ({
|
||||
value: `${result.integrationId}:${system.id}`,
|
||||
label: includeIntegrationName ? `${result.integrationName} · ${system.name}` : system.name,
|
||||
integrationId: result.integrationId,
|
||||
systemId: system.id,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveBeszelSystemChoice(choices: BeszelSystemChoice[], storedValue: string) {
|
||||
return (
|
||||
choices.find((choice) => choice.value === storedValue) ??
|
||||
choices.find((choice) => choice.systemId === storedValue) ??
|
||||
choices[0]
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user