diff --git a/packages/api/src/router/widgets/options.ts b/packages/api/src/router/widgets/options.ts index aa7c5e5c6..e94ee31a4 100644 --- a/packages/api/src/router/widgets/options.ts +++ b/packages/api/src/router/widgets/options.ts @@ -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 => { @@ -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>(item.options); + Object.assign(options, input.newOptions); + await ctx.db + .update(items) + .set({ options: SuperJSON.stringify(options) }) + .where(eq(items.id, input.itemId)); + }), }); diff --git a/packages/widgets/src/beszel-system-stats/component.module.css b/packages/widgets/src/beszel-system-stats/component.module.css index a4a7c56a6..ca1613eef 100644 --- a/packages/widgets/src/beszel-system-stats/component.module.css +++ b/packages/widgets/src/beszel-system-stats/component.module.css @@ -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; } diff --git a/packages/widgets/src/beszel-system-stats/component.tsx b/packages/widgets/src/beszel-system-stats/component.tsx index d7669a93c..9708e2f41 100644 --- a/packages/widgets/src/beszel-system-stats/component.tsx +++ b/packages/widgets/src/beszel-system-stats/component.tsx @@ -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; @@ -210,38 +224,28 @@ export default function BeszelSystemStatsWidget({ return ( {!isEditMode && systems.length > 1 && ( - + - } className={classes.beszelStatsSystemToggle} - style={{ position: "absolute", top: 6, left: 6, zIndex: 1 }} - title={selectedLabel} > - - + {selectedLabel} + {systems.map((s) => ( handleSelectSystem(s.value)} > {s.label}