Compare commits

...
Author SHA1 Message Date
ajnart 84f7ed97fc style: auto-fix lint and format 2026-06-17 14:11:52 +00:00
Thomas Camlong dc1e1891fb fix: persist Beszel server selection and improve picker visibility
The server picker was invisible (opacity: 0, hover-only) and selections
were lost on refresh because setOptions only updated the React Query cache.
Add a generic saveItemOptions mutation that persists widget option changes
directly to the database, and replace the hidden icon with a visible button.
2026-06-17 16:09:32 +02:00
4 changed files with 69 additions and 35 deletions

No files matched your search

+37 -1
View File
@@ -1,7 +1,14 @@
import { TRPCError } from "@trpc/server";
import SuperJSON from "superjson";
import { z } from "zod/v4";
import { eq } from "@homarr/db";
import { getServerSettingsAsync } from "@homarr/db/queries";
import { boards, items } from "@homarr/db/schema";
import type { WidgetOptionsSettings } from "../../../../widgets/src";
import { createTRPCRouter, publicProcedure } from "../../trpc";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
import { throwIfActionForbiddenAsync } from "../board/board-access";
export const optionsRouter = createTRPCRouter({
getWidgetOptionSettings: publicProcedure.query(async ({ ctx }): Promise<WidgetOptionsSettings> => {
@@ -16,4 +23,33 @@ export const optionsRouter = createTRPCRouter({
},
};
}),
saveItemOptions: protectedProcedure
.input(
z.object({
itemId: z.string(),
boardId: z.string(),
newOptions: z.record(z.string(), z.unknown()),
}),
)
.mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.boardId), "modify");
const item = await ctx.db.query.items.findFirst({
where: eq(items.id, input.itemId),
});
if (item?.boardId !== input.boardId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Specified item was not found",
});
}
const options = SuperJSON.parse<Record<string, unknown>>(item.options);
Object.assign(options, input.newOptions);
await ctx.db
.update(items)
.set({ options: SuperJSON.stringify(options) })
.where(eq(items.id, input.itemId));
}),
});
@@ -98,7 +98,6 @@ export type HomarrDocumentationPath =
| "/docs/tags/puid"
| "/docs/tags/redis"
| "/docs/tags/responsive"
| "/docs/tags/restore"
| "/docs/tags/roles"
| "/docs/tags/search-engines"
| "/docs/tags/search"
@@ -180,6 +179,7 @@ export type HomarrDocumentationPath =
| "/docs/integrations/github"
| "/docs/integrations/gitlab"
| "/docs/integrations/glances"
| "/docs/integrations/gotify"
| "/docs/integrations/home-assistant"
| "/docs/integrations/ical"
| "/docs/integrations/immich"
@@ -233,7 +233,6 @@ export type HomarrDocumentationPath =
| "/docs/management/search-engines"
| "/docs/management/settings"
| "/docs/management/tasks"
| "/docs/management/tools/backup"
| "/docs/management/users"
| "/docs/widgets/anchor-note"
| "/docs/widgets/app"
@@ -1,8 +1,6 @@
.beszelStatsSystemToggle {
opacity: 0;
transition: opacity 150ms ease;
}
.beszelStatsWrapper:hover .beszelStatsSystemToggle {
opacity: 1;
position: absolute;
top: 6px;
left: 6px;
z-index: 1;
}
@@ -1,10 +1,13 @@
"use client";
import { useCallback, useMemo } from "react";
import { ActionIcon, Button, Center, Menu, ScrollArea, Select, SimpleGrid, Stack, Text } from "@mantine/core";
import { Button, Center, Menu, ScrollArea, Select, SimpleGrid, Stack, Text } from "@mantine/core";
import { IconPlugConnectedX, IconServer, IconQuestionMark } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { constructBoardPermissions } from "@homarr/auth/shared";
import { useOptionalBoard } from "@homarr/boards/context";
import type { BeszelContainerStatsRecord, BeszelSystemStatsRecord } from "@homarr/integrations/types";
import { useScopedI18n } from "@homarr/translation/client";
@@ -49,9 +52,15 @@ export default function BeszelSystemStatsWidget({
integrationIds,
isEditMode,
width,
boardId,
itemId,
setOptions,
}: WidgetComponentProps<"beszelSystemStats">) {
const t = useScopedI18n("widget.beszelSystemStats");
const board = useOptionalBoard();
const { data: session } = useSession();
const hasChangeAccess = board ? constructBoardPermissions(board, session).hasChangeAccess : false;
const { mutate: saveItemOptions } = clientApi.widget.options.saveItemOptions.useMutation();
const { data: systemsResult = [], isPending: systemsPending } = clientApi.widget.beszel.getSystems.useQuery(
{ integrationIds },
{ staleTime: 30 * 1000 },
@@ -68,8 +77,13 @@ export default function BeszelSystemStatsWidget({
const systemReady = !systemsPending && (selectedSystem === "" || systemExists);
const handleSelectSystem = useCallback(
(value: string) => setOptions({ newOptions: { systemId: value } }),
[setOptions],
(value: string) => {
setOptions({ newOptions: { systemId: value } });
if (hasChangeAccess && boardId && itemId) {
saveItemOptions({ boardId, itemId, newOptions: { systemId: value } });
}
},
[setOptions, hasChangeAccess, boardId, itemId, saveItemOptions],
);
const showDocker = options.showDockerCpu || options.showDockerMemory || options.showDockerNetwork;
@@ -208,40 +222,27 @@ export default function BeszelSystemStatsWidget({
}
return (
<ScrollArea
h="100%"
className={classes.beszelStatsWrapper}
style={{ pointerEvents: (isEditMode && "none") || undefined }}
>
<ScrollArea h="100%" style={{ pointerEvents: (isEditMode && "none") || undefined }}>
<Stack gap="md" p="sm">
{!isEditMode && systems.length > 1 && (
<Menu
trigger="click-hover"
openDelay={100}
closeDelay={300}
position="bottom-start"
withArrow
shadow="md"
withinPortal
>
<Menu position="bottom-start" withArrow shadow="md" withinPortal>
<Menu.Target>
<ActionIcon
variant="subtle"
size="sm"
<Button
variant="default"
size="compact-xs"
leftSection={<IconServer size={14} />}
className={classes.beszelStatsSystemToggle}
style={{ position: "absolute", top: 6, left: 6, zIndex: 1 }}
title={selectedLabel}
>
<IconServer size={14} />
</ActionIcon>
{selectedLabel}
</Button>
</Menu.Target>
<Menu.Dropdown>
{systems.map((s) => (
<Menu.Item
key={s.value}
fz="xs"
fw={(s.value === selectedSystem && 600) || 400}
c={(s.value !== selectedSystem && "dimmed") || undefined}
fw={s.value === selectedSystem ? 600 : 400}
c={s.value !== selectedSystem ? "dimmed" : undefined}
onClick={() => handleSelectSystem(s.value)}
>
{s.label}