From 777bbd89a12b8f249bfbc9c5bb45a63385d63357 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sat, 1 Aug 2026 21:45:01 +0200 Subject: [PATCH] fix: refine widget modernization --- .../advanced-focus/advanced-focus.module.css | 20 ++- .../board/advanced-focus/geometry.spec.ts | 12 +- .../board/advanced-focus/geometry.ts | 6 +- .../components/board/items/item-actions.tsx | 13 +- e2e/board-advanced-interactions.spec.ts | 9 ++ packages/api/src/router/widgets/firewall.ts | 1 - packages/boards/src/updater.spec.ts | 18 --- packages/boards/src/updater.ts | 13 +- packages/translation/src/lang/en.json | 4 + .../widgets/src/anchor-note/component.tsx | 37 +---- .../widgets/src/archive-team-warrior/index.ts | 1 - packages/widgets/src/bazarr/index.ts | 1 - packages/widgets/src/calendar/component.tsx | 16 +- packages/widgets/src/calendar/index.ts | 1 - packages/widgets/src/coolify/index.ts | 1 - packages/widgets/src/custom-api/index.ts | 1 - packages/widgets/src/downloads/index.ts | 1 - packages/widgets/src/firewall/component.tsx | 71 ++++++--- .../src/firewall/failure-state.spec.ts | 25 +++ .../widgets/src/firewall/firewall-version.tsx | 7 +- packages/widgets/src/firewall/index.ts | 1 - .../src/immich/album-carousel/component.tsx | 17 +- .../src/immich/server-stats/component.tsx | 5 +- packages/widgets/src/indexer-manager/index.ts | 1 - packages/widgets/src/media-server/index.ts | 1 - .../widgets/src/media-transcoding/index.ts | 1 - .../network-status/component.tsx | 3 +- packages/widgets/src/notebook/notebook.tsx | 95 +++++------- packages/widgets/src/paperless-ngx/index.ts | 1 - packages/widgets/src/patchmon/index.ts | 1 - packages/widgets/src/releases/index.ts | 1 - packages/widgets/src/rssFeed/index.ts | 1 - .../execute-automation/component.tsx | 145 ++++++++++-------- .../widgets/src/speedtest-tracker/index.ts | 1 - packages/widgets/src/stocks/index.ts | 1 - .../src/system-resources/component.tsx | 29 +--- .../src/system-resources/history.spec.ts | 10 +- packages/widgets/src/tracearr/index.ts | 1 - packages/widgets/src/traefik/index.ts | 1 - packages/widgets/src/umami/index.ts | 1 - packages/widgets/src/ups/index.ts | 1 - packages/widgets/src/uptime-kuma/index.ts | 1 - packages/widgets/src/vpn/index.ts | 1 - packages/widgets/src/weather/index.ts | 1 - 44 files changed, 259 insertions(+), 320 deletions(-) delete mode 100644 packages/boards/src/updater.spec.ts create mode 100644 packages/widgets/src/firewall/failure-state.spec.ts diff --git a/apps/nextjs/src/components/board/advanced-focus/advanced-focus.module.css b/apps/nextjs/src/components/board/advanced-focus/advanced-focus.module.css index d1b045802..6ff513bf5 100644 --- a/apps/nextjs/src/components/board/advanced-focus/advanced-focus.module.css +++ b/apps/nextjs/src/components/board/advanced-focus/advanced-focus.module.css @@ -22,6 +22,7 @@ overscroll-behavior: contain; animation: focus-surface-in 220ms cubic-bezier(0.16, 1, 0.3, 1); transform-origin: top left; + padding-top: 60px; } .backdropClosing { @@ -57,6 +58,21 @@ pointer-events: none; } +.surfaceControlsAdvanced { + right: 0; + bottom: auto; + height: 60px; + background: color-mix(in srgb, var(--mantine-color-body) 92%, transparent); + border-bottom: 1px solid var(--mantine-color-default-border); +} + +.surfaceTitle { + position: absolute; + top: 20px; + right: 120px; + left: 16px; +} + .closeButton { position: absolute; top: 8px; @@ -68,7 +84,7 @@ .pinButton { position: absolute; top: 8px; - right: 52px; + right: 60px; z-index: 2; pointer-events: auto; } @@ -82,7 +98,7 @@ pointer-events: auto; } -@media (pointer: coarse) { +@media (any-pointer: coarse) { .touchButton { display: inline-flex; } diff --git a/apps/nextjs/src/components/board/advanced-focus/geometry.spec.ts b/apps/nextjs/src/components/board/advanced-focus/geometry.spec.ts index 4b5ee0b89..c769e7fbc 100644 --- a/apps/nextjs/src/components/board/advanced-focus/geometry.spec.ts +++ b/apps/nextjs/src/components/board/advanced-focus/geometry.spec.ts @@ -4,12 +4,12 @@ import { getAdvancedFocusRect } from "./geometry"; describe("getAdvancedFocusRect", () => { it("expands a widget and keeps it inside the viewport", () => { - expect(getAdvancedFocusRect({ left: 10, top: 10, width: 300, height: 200 }, { width: 1200, height: 800 })).toEqual({ - left: 240, - top: 190, - width: 720, - height: 480, - }); + const rect = getAdvancedFocusRect({ left: 10, top: 10, width: 300, height: 200 }, { width: 1200, height: 800 }); + + expect(rect.left).toBe(120); + expect(rect.top).toBeCloseTo(118.6); + expect(rect.width).toBe(960); + expect(rect.height).toBeCloseTo(622.8); }); it("uses the available viewport on small screens", () => { diff --git a/apps/nextjs/src/components/board/advanced-focus/geometry.ts b/apps/nextjs/src/components/board/advanced-focus/geometry.ts index 78bdda016..086cfd622 100644 --- a/apps/nextjs/src/components/board/advanced-focus/geometry.ts +++ b/apps/nextjs/src/components/board/advanced-focus/geometry.ts @@ -11,8 +11,10 @@ export const getAdvancedFocusRect = (source: FocusRect, viewport: { width: numbe const margin = viewport.width < 640 ? 8 : 24; const availableWidth = Math.max(0, viewport.width - margin * 2); const availableHeight = Math.max(0, viewport.height - appShellHeaderHeight - margin * 2); - const width = Math.min(availableWidth, Math.max(source.width, Math.min(960, source.width * 2.4))); - const height = Math.min(availableHeight, Math.max(source.height, Math.min(720, source.height * 2.4))); + const preferredWidth = viewport.width < 640 ? availableWidth : Math.min(960, availableWidth * 0.9); + const preferredHeight = viewport.width < 640 ? availableHeight : Math.min(720, availableHeight * 0.9); + const width = Math.min(availableWidth, Math.max(source.width, preferredWidth)); + const height = Math.min(availableHeight, Math.max(source.height, preferredHeight)); return { left: Math.max(margin, (viewport.width - width) / 2), diff --git a/apps/nextjs/src/components/board/items/item-actions.tsx b/apps/nextjs/src/components/board/items/item-actions.tsx index 5344a2289..cc7b637b4 100644 --- a/apps/nextjs/src/components/board/items/item-actions.tsx +++ b/apps/nextjs/src/components/board/items/item-actions.tsx @@ -3,7 +3,6 @@ import { useCallback } from "react"; import { useUpdateBoard } from "@homarr/boards/updater"; import type { BoardItemAdvancedOptions } from "@homarr/validation/shared"; -import type { Board } from "~/app/[locale]/boards/_types"; import type { CreateItemInput } from "./actions/create-item"; import { createItemCallback } from "./actions/create-item"; import type { DuplicateItemInput } from "./actions/duplicate-item"; @@ -33,17 +32,7 @@ interface UpdateItemIntegrations { export const useItemActions = () => { const { updateBoard } = useUpdateBoard(); - const createItem = useCallback( - (input: CreateItemInput) => { - let nextBoard: Board | undefined; - updateBoard((previous) => { - nextBoard = createItemCallback(input)(previous); - return nextBoard; - }); - return nextBoard; - }, - [updateBoard], - ); + const createItem = useCallback((input: CreateItemInput) => updateBoard(createItemCallback(input)), [updateBoard]); const duplicateItem = useCallback( ({ itemId }: DuplicateItemInput) => { diff --git a/e2e/board-advanced-interactions.spec.ts b/e2e/board-advanced-interactions.spec.ts index 898c43d00..bf82259fb 100644 --- a/e2e/board-advanced-interactions.spec.ts +++ b/e2e/board-advanced-interactions.spec.ts @@ -76,6 +76,8 @@ describe("Board advanced interactions", () => { await page.keyboard.down("Shift"); await widget.hover(); await expect(advancedDialog).toBeVisible({ timeout: 2_000 }); + const advancedBounds = await advancedDialog.boundingBox(); + expect(advancedBounds?.width).toBeGreaterThanOrEqual(900); await expect(advancedDialog).toHaveAttribute("data-lifecycle-probe", "same-instance"); await expect(widget).toBeFocused(); await page.keyboard.up("Shift"); @@ -156,8 +158,15 @@ describe("Board advanced interactions", () => { if (!bounds) return; expect(bounds.x).toBeGreaterThanOrEqual(0); expect(bounds.y).toBeGreaterThanOrEqual(60); + expect(bounds.width).toBeGreaterThanOrEqual(44); + expect(bounds.height).toBeGreaterThanOrEqual(44); expect(bounds.x + bounds.width).toBeLessThanOrEqual(1366); expect(bounds.y + bounds.height).toBeLessThanOrEqual(768); + const dialogBounds = await page.getByRole("dialog", { name: "Date and time advanced view" }).boundingBox(); + const contentBounds = await page.locator(".clock-widget-container").boundingBox(); + expect(dialogBounds).not.toBeNull(); + expect(contentBounds).not.toBeNull(); + if (dialogBounds && contentBounds) expect(contentBounds.y).toBeGreaterThanOrEqual(dialogBounds.y + 60); await closeButton.click(); await expect(page.getByRole("dialog", { name: "Date and time advanced view" })).toBeHidden(); } finally { diff --git a/packages/api/src/router/widgets/firewall.ts b/packages/api/src/router/widgets/firewall.ts index fdeb77831..dbaa88b4f 100644 --- a/packages/api/src/router/widgets/firewall.ts +++ b/packages/api/src/router/widgets/firewall.ts @@ -37,7 +37,6 @@ const queryFirewall = < summary: createFallback(), error: error instanceof Error ? error.message : "Integration request failed", }), - throwOnAllFailures: true, }, ), ); diff --git a/packages/boards/src/updater.spec.ts b/packages/boards/src/updater.spec.ts deleted file mode 100644 index 1f78f7e97..000000000 --- a/packages/boards/src/updater.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { getBoardSaveScopeId, shouldRefetchAfterSaveError } from "./updater"; - -describe("board persistence coordination", () => { - it("uses one mutation scope for every save of the same board", () => { - expect(getBoardSaveScopeId("board-1")).toBe(getBoardSaveScopeId("board-1")); - expect(getBoardSaveScopeId("board-1")).not.toBe(getBoardSaveScopeId("board-2")); - }); - - it("only refetches after a failed save when no newer local board exists", () => { - const failedBoard = { id: "board-1", items: ["first"] }; - const newerBoard = { id: "board-1", items: ["first", "second"] }; - - expect(shouldRefetchAfterSaveError(failedBoard, failedBoard)).toBe(true); - expect(shouldRefetchAfterSaveError(newerBoard, failedBoard)).toBe(false); - }); -}); diff --git a/packages/boards/src/updater.ts b/packages/boards/src/updater.ts index 6763c48ce..f11c70db8 100644 --- a/packages/boards/src/updater.ts +++ b/packages/boards/src/updater.ts @@ -14,11 +14,6 @@ export const updateBoardName = (name: string | null) => { boardName = name; }; -export const getBoardSaveScopeId = (boardId: string) => `board-save:${boardId}`; - -export const shouldRefetchAfterSaveError = (currentBoard: T | undefined, failedBoard: T) => - currentBoard === failedBoard; - export const useUpdateBoard = () => { const utils = clientApi.useUtils(); @@ -51,19 +46,17 @@ export const usePersistBoard = ({ id, name }: Pick) => { const utils = clientApi.useUtils(); const { updateBoard } = useUpdateBoard(); const { mutateAsync: saveBoardAsync } = clientApi.board.saveBoard.useMutation({ - scope: { id: getBoardSaveScopeId(id) }, + scope: { id: `board-save:${id}` }, }); const updateAndPersistBoard = useCallback( (updater: UpdateCallback, options?: PersistBoardOptions) => { - const updatedBoard = updateBoard(updater); - if (!updatedBoard) return undefined; - const persistedBoard = utils.board.getBoardByName.getData({ name }); + const persistedBoard = updateBoard(updater); if (!persistedBoard) return undefined; void saveBoardAsync(persistedBoard).catch((error: unknown) => { const currentBoard = utils.board.getBoardByName.getData({ name }); - if (shouldRefetchAfterSaveError(currentBoard, persistedBoard)) { + if (currentBoard === persistedBoard) { void utils.board.getBoardByName.invalidate({ name }); } options?.onError?.(error); diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index b207e11bd..6b0676b86 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -2782,6 +2782,10 @@ "running": "Running automation", "success": "Automation completed" }, + "status": { + "running": "Running automation", + "success": "Automation completed" + }, "error": { "executeFailed": "Unable to run automation" } diff --git a/packages/widgets/src/anchor-note/component.tsx b/packages/widgets/src/anchor-note/component.tsx index 781a09930..6667189b0 100644 --- a/packages/widgets/src/anchor-note/component.tsx +++ b/packages/widgets/src/anchor-note/component.tsx @@ -133,7 +133,6 @@ export default function AnchorNoteWidget({ width, height, displayMode, - widgetStateRef, }: WidgetComponentProps<"anchorNote">) { const t = useScopedI18n("widget.anchorNote"); const noteId = options.noteId.trim(); @@ -157,20 +156,10 @@ export default function AnchorNoteWidget({ width={width} height={height} isAdvanced={displayMode === "advanced"} - widgetStateRef={widgetStateRef} /> ); } -interface AnchorNotePersistedState { - integrationId: string; - noteId: string; - isEditing: boolean; - draftTitle: string; - draftContent: string; - saveError: string | null; -} - interface AnchorNoteWidgetContentProps { options: WidgetComponentProps<"anchorNote">["options"]; integrationId: string; @@ -178,7 +167,6 @@ interface AnchorNoteWidgetContentProps { width: number; height: number; isAdvanced: boolean; - widgetStateRef?: WidgetComponentProps<"anchorNote">["widgetStateRef"]; } const AnchorNoteWidgetContent = ({ @@ -188,35 +176,18 @@ const AnchorNoteWidgetContent = ({ width, height, isAdvanced, - widgetStateRef, }: AnchorNoteWidgetContentProps) => { const t = useScopedI18n("widget.anchorNote"); - const persistedState = widgetStateRef?.current?.anchorNote as AnchorNotePersistedState | undefined; - const canHydratePersistedState = persistedState?.integrationId === integrationId && persistedState.noteId === noteId; const { data: note, refetch } = clientApi.widget.anchorNotes.getNote.useQuery({ integrationId, noteId, }); const { mutateAsync: updateNoteAsync, isPending: isUpdating } = clientApi.widget.anchorNotes.updateNote.useMutation(); - const [isEditing, setIsEditing] = useState(canHydratePersistedState ? (persistedState?.isEditing ?? false) : false); - const [draftTitle, setDraftTitle] = useState( - canHydratePersistedState ? (persistedState?.draftTitle ?? "") : (note?.title ?? ""), - ); - const [draftContent, setDraftContent] = useState( - canHydratePersistedState ? (persistedState?.draftContent ?? "") : (note?.content ?? ""), - ); - const [saveError, setSaveError] = useState( - canHydratePersistedState ? (persistedState?.saveError ?? null) : null, - ); - - useEffect(() => { - if (!widgetStateRef) return; - widgetStateRef.current = { - ...widgetStateRef.current, - anchorNote: { integrationId, noteId, isEditing, draftTitle, draftContent, saveError }, - }; - }, [draftContent, draftTitle, integrationId, isEditing, noteId, saveError, widgetStateRef]); + const [isEditing, setIsEditing] = useState(false); + const [draftTitle, setDraftTitle] = useState(note?.title ?? ""); + const [draftContent, setDraftContent] = useState(note?.content ?? ""); + const [saveError, setSaveError] = useState(null); useEffect(() => { if (isEditing || !note) return; diff --git a/packages/widgets/src/archive-team-warrior/index.ts b/packages/widgets/src/archive-team-warrior/index.ts index e639c7e9f..1eaf0a691 100644 --- a/packages/widgets/src/archive-team-warrior/index.ts +++ b/packages/widgets/src/archive-team-warrior/index.ts @@ -5,7 +5,6 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("archiveTeamWarrior", { icon: IconArchive, - queryKey: [["widget", "archiveTeamWarrior"]], maxIntegrations: 1, createOptions() { return optionsBuilder.from((factory) => ({ diff --git a/packages/widgets/src/bazarr/index.ts b/packages/widgets/src/bazarr/index.ts index 7ac60e0de..a4c10a540 100644 --- a/packages/widgets/src/bazarr/index.ts +++ b/packages/widgets/src/bazarr/index.ts @@ -5,7 +5,6 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("bazarr", { icon: IconBadgeCc, - queryKey: [["widget", "bazarr"]], supportedIntegrations: ["bazarr"], integrationsRequired: true, maxIntegrations: 1, diff --git a/packages/widgets/src/calendar/component.tsx b/packages/widgets/src/calendar/component.tsx index 275c913b4..72cb062a0 100644 --- a/packages/widgets/src/calendar/component.tsx +++ b/packages/widgets/src/calendar/component.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { useParams } from "next/navigation"; import { Box, Stack, Text, useMantineTheme } from "@mantine/core"; import { Calendar } from "@mantine/dates"; @@ -19,19 +19,7 @@ import { CalendarEventList } from "./calendar-event-list"; import classes from "./component.module.css"; export default function CalendarWidget(props: WidgetComponentProps<"calendar">) { - const persistedState = props.widgetStateRef?.current?.calendar as { monthTimestamp?: number } | undefined; - const [month, setMonth] = useState(() => { - const timestamp = persistedState?.monthTimestamp; - return typeof timestamp === "number" && Number.isFinite(timestamp) ? new Date(timestamp) : new Date(); - }); - - useEffect(() => { - if (!props.widgetStateRef) return; - props.widgetStateRef.current = { - ...props.widgetStateRef.current, - calendar: { monthTimestamp: month.getTime() }, - }; - }, [month, props.widgetStateRef]); + const [month, setMonth] = useState(() => new Date()); if (props.integrationIds.length === 0) { return ; diff --git a/packages/widgets/src/calendar/index.ts b/packages/widgets/src/calendar/index.ts index 3f0abfea0..d03b2f610 100644 --- a/packages/widgets/src/calendar/index.ts +++ b/packages/widgets/src/calendar/index.ts @@ -9,7 +9,6 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("calendar", { icon: IconCalendar, - queryKey: [["widget", "calendar"]], createOptions() { return optionsBuilder.from((factory) => ({ releaseType: factory.multiSelect({ diff --git a/packages/widgets/src/coolify/index.ts b/packages/widgets/src/coolify/index.ts index e68e4c839..cebf9de05 100644 --- a/packages/widgets/src/coolify/index.ts +++ b/packages/widgets/src/coolify/index.ts @@ -5,7 +5,6 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("coolify", { icon: IconCloud, - queryKey: [["widget", "coolify"]], createOptions() { return optionsBuilder.from((factory) => ({ showServers: factory.switch({ diff --git a/packages/widgets/src/custom-api/index.ts b/packages/widgets/src/custom-api/index.ts index 009f5fe45..fddcae2fc 100644 --- a/packages/widgets/src/custom-api/index.ts +++ b/packages/widgets/src/custom-api/index.ts @@ -6,7 +6,6 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("customApi", { icon: IconApi, - queryKey: [["widget", "customApi"]], contextActions({ options, widgetStateRef }) { return [ { diff --git a/packages/widgets/src/downloads/index.ts b/packages/widgets/src/downloads/index.ts index 3f051110b..cc8f9a5b0 100644 --- a/packages/widgets/src/downloads/index.ts +++ b/packages/widgets/src/downloads/index.ts @@ -42,7 +42,6 @@ const sortColumns = [ export const { definition, componentLoader } = createWidgetDefinition("downloads", { icon: IconDownload, - queryKey: [["widget", "downloads"]], refetchInterval: 5, createOptions() { return optionsBuilder.from( diff --git a/packages/widgets/src/firewall/component.tsx b/packages/widgets/src/firewall/component.tsx index 66a65b6d1..a3bd86064 100644 --- a/packages/widgets/src/firewall/component.tsx +++ b/packages/widgets/src/firewall/component.tsx @@ -33,6 +33,20 @@ export interface Firewall { value: string; } +interface FirewallQueryState { + isPending: boolean; + isFetching: boolean; + isError: boolean; + data?: readonly { error?: string }[]; +} + +export const hasTotalFirewallFailure = (queries: readonly FirewallQueryState[]) => + queries.every((query) => !query.isPending && !query.isFetching) && + queries.every( + (query) => + query.isError || (query.data !== undefined && query.data.length > 0 && query.data.every(({ error }) => error)), + ); + export default function FirewallWidget({ integrationIds, width, @@ -48,34 +62,44 @@ export default function FirewallWidget({ setSelectedFirewall(value ?? ""); }, []); - const { data: firewallsCpuData = [] } = clientApi.widget.firewall.getFirewallCpuStatus.useQuery({ integrationIds }); - const { data: firewallsMemoryData = [] } = clientApi.widget.firewall.getFirewallMemoryStatus.useQuery({ + const cpuQuery = clientApi.widget.firewall.getFirewallCpuStatus.useQuery({ integrationIds }); + const memoryQuery = clientApi.widget.firewall.getFirewallMemoryStatus.useQuery({ integrationIds, }); - const { data: firewallsVersionData = [] } = clientApi.widget.firewall.getFirewallVersionStatus.useQuery({ + const versionQuery = clientApi.widget.firewall.getFirewallVersionStatus.useQuery({ integrationIds, }); - const { data: firewallsInterfacesData = [] } = clientApi.widget.firewall.getFirewallInterfacesStatus.useQuery({ + const interfacesQuery = clientApi.widget.firewall.getFirewallInterfacesStatus.useQuery({ integrationIds, }); + const firewallsCpuData = cpuQuery.data ?? []; + const firewallsMemoryData = memoryQuery.data ?? []; + const firewallsVersionData = versionQuery.data ?? []; + const firewallsInterfacesData = interfacesQuery.data ?? []; + const queries = [cpuQuery, memoryQuery, versionQuery, interfacesQuery]; + const hasTotalFailure = hasTotalFirewallFailure(queries); const [accordionValue, setAccordionValue] = useLocalStorage({ key: `homarr-${itemId}-firewall`, defaultValue: "interfaces", }); - const initialSelectedFirewall = firewallsVersionData[0]?.integration.id ?? ""; - const activeFirewall = firewallsVersionData.some(({ integration }) => integration.id === selectedFirewall) - ? selectedFirewall - : initialSelectedFirewall; - const displayedFirewallIds = isAdvanced - ? firewallsVersionData.map(({ integration }) => integration.id) - : activeFirewall - ? [activeFirewall] - : []; - const dropdownItems = firewallsVersionData.map((firewall) => ({ - label: firewall.integration.name, - value: firewall.integration.id, + if (hasTotalFailure) { + throw new Error(t("widget.firewall.error.internalServerError")); + } + + const firewallMetadata = new Map( + [...firewallsVersionData, ...firewallsCpuData, ...firewallsMemoryData, ...firewallsInterfacesData].map( + ({ integration }) => [integration.id, integration] as const, + ), + ); + const firewallIds = [...firewallMetadata.keys()]; + const initialSelectedFirewall = firewallIds[0] ?? ""; + const activeFirewall = firewallMetadata.has(selectedFirewall) ? selectedFirewall : initialSelectedFirewall; + const displayedFirewallIds = isAdvanced ? firewallIds : activeFirewall ? [activeFirewall] : []; + const dropdownItems = firewallIds.map((firewallId) => ({ + label: firewallMetadata.get(firewallId)?.name ?? firewallId, + value: firewallId, })); return ( @@ -90,7 +114,7 @@ export default function FirewallWidget({ isTiny={isTiny} /> !error)} selectedFirewall={activeFirewall} isTiny={isTiny} /> @@ -104,16 +128,17 @@ export default function FirewallWidget({ const version = firewallsVersionData.find(({ integration }) => integration.id === firewallId); const interfaces = firewallsInterfacesData.find(({ integration }) => integration.id === firewallId); const hasError = Boolean(cpu?.error || memory?.error || version?.error || interfaces?.error); + const metadata = firewallMetadata.get(firewallId); return ( { + it("rejects only after every dimension fails", () => { + expect(hasTotalFirewallFailure([failedQuery, failedQuery, failedQuery, failedQuery])).toBe(true); + expect( + hasTotalFirewallFailure([failedQuery, failedQuery, failedQuery, { ...failedQuery, data: [{ error: undefined }] }]), + ).toBe(false); + }); + + it("lets an error-only persisted cache recover while it refetches", () => { + expect(hasTotalFirewallFailure([failedQuery, failedQuery, { ...failedQuery, isFetching: true }, failedQuery])).toBe( + false, + ); + }); +}); diff --git a/packages/widgets/src/firewall/firewall-version.tsx b/packages/widgets/src/firewall/firewall-version.tsx index 3fae1fa2a..df3daa2a9 100644 --- a/packages/widgets/src/firewall/firewall-version.tsx +++ b/packages/widgets/src/firewall/firewall-version.tsx @@ -21,6 +21,7 @@ export interface FirewallIntegration { export const FirewallVersion = ({ firewallsVersionData, selectedFirewall, isTiny }: FirewallVersionProps) => { const t = useI18n(); + const version = firewallsVersionData.find(({ integration }) => integration.id === selectedFirewall)?.summary.version; return ( @@ -31,11 +32,7 @@ export const FirewallVersion = ({ firewallsVersionData, selectedFirewall, isTiny size={isTiny ? "8px" : "xs"} style={{ minHeight: "24px" }} > - {firewallsVersionData - .filter(({ integration }) => integration.id === selectedFirewall) - .map(({ summary, integration }) => ( - {formatVersion(summary.version, t("widget.firewall.versionUnknown"))} - ))} + {formatVersion(version ?? "", t("widget.firewall.versionUnknown"))} ); diff --git a/packages/widgets/src/firewall/index.ts b/packages/widgets/src/firewall/index.ts index 800874812..e154fd3ac 100644 --- a/packages/widgets/src/firewall/index.ts +++ b/packages/widgets/src/firewall/index.ts @@ -7,7 +7,6 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("firewall", { icon: IconWall, - queryKey: [["widget", "firewall"]], refetchInterval: 5, createOptions() { return optionsBuilder.from(() => ({})); diff --git a/packages/widgets/src/immich/album-carousel/component.tsx b/packages/widgets/src/immich/album-carousel/component.tsx index f219e9eb1..32493288f 100644 --- a/packages/widgets/src/immich/album-carousel/component.tsx +++ b/packages/widgets/src/immich/album-carousel/component.tsx @@ -27,13 +27,8 @@ export default function ImmichAlbumCarouselWidget({ displayMode = "compact", widgetStateRef, }: WidgetComponentProps<"immich-albumCarousel">) { - const persistedState = widgetStateRef?.current?.immichAlbumCarousel as - | { currentPhotoIndex?: number; paused?: boolean } - | undefined; - const [currentPhotoIndex, setCurrentPhotoIndex] = useState(() => - typeof persistedState?.currentPhotoIndex === "number" ? persistedState.currentPhotoIndex : 0, - ); - const [paused, setPaused] = useState(persistedState?.paused === true); + const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0); + const [paused, setPaused] = useState(false); const { data: album } = clientApi.widget.immich.getAlbum.useQuery( { @@ -53,14 +48,6 @@ export default function ImmichAlbumCarouselWidget({ setCurrentPhotoIndex((current) => Math.min(current, photoAssets.length - 1)); }, [photoAssets.length]); - useEffect(() => { - if (!widgetStateRef) return; - widgetStateRef.current = { - ...widgetStateRef.current, - immichAlbumCarousel: { currentPhotoIndex, paused }, - }; - }, [currentPhotoIndex, paused, widgetStateRef]); - useEffect(() => { if (!widgetStateRef) return; const clearActions = () => { diff --git a/packages/widgets/src/immich/server-stats/component.tsx b/packages/widgets/src/immich/server-stats/component.tsx index 308e803a7..71c78657d 100644 --- a/packages/widgets/src/immich/server-stats/component.tsx +++ b/packages/widgets/src/immich/server-stats/component.tsx @@ -30,7 +30,10 @@ export default function ImmichServerStatsWidget({ if (!stats) return ; const statsContent = ( - = 320 ? 2 : 1} spacing="sm"> + = 720 ? 4 : width >= 360 ? 2 : 1) : width >= 320 ? 2 : 1} + spacing="sm" + > {options.showUsers && ( } label={t("widget.immich-serverStats.users")} value={stats.userCount} /> )} diff --git a/packages/widgets/src/indexer-manager/index.ts b/packages/widgets/src/indexer-manager/index.ts index a7fc8a0cd..b48ef301a 100644 --- a/packages/widgets/src/indexer-manager/index.ts +++ b/packages/widgets/src/indexer-manager/index.ts @@ -7,7 +7,6 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("indexerManager", { icon: IconReportSearch, - queryKey: [["widget", "indexerManager"]], contextActions: ({ widgetStateRef, context }) => [ { key: "test-all-indexers", diff --git a/packages/widgets/src/media-server/index.ts b/packages/widgets/src/media-server/index.ts index 35ad060c5..ef746e921 100644 --- a/packages/widgets/src/media-server/index.ts +++ b/packages/widgets/src/media-server/index.ts @@ -7,7 +7,6 @@ import { optionsBuilder } from "../options"; export const { componentLoader, definition } = createWidgetDefinition("mediaServer", { icon: IconVideo, - queryKey: [["widget", "mediaServer"]], refetchInterval: 5, createOptions() { return optionsBuilder.from((factory) => ({ diff --git a/packages/widgets/src/media-transcoding/index.ts b/packages/widgets/src/media-transcoding/index.ts index de92a46ca..f333f3168 100644 --- a/packages/widgets/src/media-transcoding/index.ts +++ b/packages/widgets/src/media-transcoding/index.ts @@ -11,7 +11,6 @@ export const views = ["workers", "queue", "statistics"] as const; export const { componentLoader, definition } = createWidgetDefinition("mediaTranscoding", { icon: IconTransform, - queryKey: [["widget", "mediaTranscoding"]], maxIntegrations: 1, refetchInterval: null, createOptions() { diff --git a/packages/widgets/src/network-controller/network-status/component.tsx b/packages/widgets/src/network-controller/network-status/component.tsx index 7472d5007..6bfa59407 100644 --- a/packages/widgets/src/network-controller/network-status/component.tsx +++ b/packages/widgets/src/network-controller/network-status/component.tsx @@ -21,6 +21,7 @@ export default function NetworkControllerNetworkStatusWidget({ options, integrationIds, displayMode, + width, }: WidgetComponentProps<"networkControllerStatus">) { const { data: summaries = [] } = clientApi.widget.networkController.summary.useQuery({ integrationIds, @@ -35,7 +36,7 @@ export default function NetworkControllerNetworkStatusWidget({ return ( - + = 560 ? 2 : 1} h="100%" spacing="sm"> {(isAdvanced || options.content === "wifi") && ( diff --git a/packages/widgets/src/notebook/notebook.tsx b/packages/widgets/src/notebook/notebook.tsx index e9bed344e..42c346dbc 100644 --- a/packages/widgets/src/notebook/notebook.tsx +++ b/packages/widgets/src/notebook/notebook.tsx @@ -91,14 +91,6 @@ const controlIconProps = { stroke: 1.5, }; -interface NotebookPersistedState { - itemId: string | undefined; - content: string; - previousContent: string; - isEditing: boolean; - saveError: string | null; -} - export function Notebook({ options, setOptions, @@ -107,58 +99,44 @@ export function Notebook({ itemId, displayMode, height, - widgetStateRef, }: WidgetComponentProps<"notebook">) { - const persistedState = widgetStateRef?.current?.notebook as NotebookPersistedState | undefined; - const canHydratePersistedState = persistedState?.itemId === itemId; - const initialContent = canHydratePersistedState ? (persistedState?.content ?? options.content) : options.content; - const [content, setContent] = useState(initialContent); - const previousContentRef = useRef( - canHydratePersistedState ? (persistedState?.previousContent ?? initialContent) : initialContent, - ); - const [saveError, setSaveError] = useState( - canHydratePersistedState ? (persistedState?.saveError ?? null) : null, - ); + const [content, setContent] = useState(options.content); + const previousContentRef = useRef(options.content); + const [saveError, setSaveError] = useState(null); const board = useRequiredBoard(); const { data: session } = useSession(); const { hasChangeAccess } = constructBoardPermissions(board, session); const canChange = !isEditMode && hasChangeAccess; - const [isEditing, setIsEditing] = useState(canHydratePersistedState ? (persistedState?.isEditing ?? false) : false); + const [isEditing, setIsEditing] = useState(false); const canChangeRef = useRef(canChange); const allowReadOnlyCheckRef = useRef(options.allowReadOnlyCheck); + const savingRef = useRef(false); const readOnlyCheckEventName = `homarr:notebook-read-only-check:${useId()}`; useEffect(() => { - canChangeRef.current = canChange; allowReadOnlyCheckRef.current = options.allowReadOnlyCheck; - }, [canChange, options.allowReadOnlyCheck]); - - useEffect(() => { - if (!widgetStateRef) return; - widgetStateRef.current = { - ...widgetStateRef.current, - notebook: { - itemId, - content, - previousContent: previousContentRef.current, - isEditing, - saveError, - }, - }; - }, [content, isEditing, itemId, saveError, widgetStateRef]); + }, [options.allowReadOnlyCheck]); const { primaryColor } = useMantineTheme(); const { colorScheme } = useMantineColorScheme(); - const { mutateAsync } = clientApi.widget.notebook.updateContent.useMutation(); + const { mutateAsync, isPending: isSaving } = clientApi.widget.notebook.updateContent.useMutation({ + scope: { id: `notebook-content:${boardId ?? "preview"}:${itemId ?? "preview"}` }, + }); + + useEffect(() => { + canChangeRef.current = canChange && !isSaving; + }, [canChange, isSaving]); const tControls = useScopedI18n("widget.notebook.controls"); const t = useI18n(); const handleContentUpdate = useCallback( async (contentUpdate: string) => { + savingRef.current = true; + canChangeRef.current = false; setSaveError(null); try { @@ -166,26 +144,17 @@ export function Notebook({ await mutateAsync({ boardId, itemId, content: contentUpdate }); } previousContentRef.current = contentUpdate; - if (widgetStateRef) { - widgetStateRef.current = { - ...widgetStateRef.current, - notebook: { - itemId, - content: contentUpdate, - previousContent: contentUpdate, - isEditing, - saveError: null, - }, - }; - } setOptions({ newOptions: { content: contentUpdate } }); return true; } catch (error) { setSaveError(error instanceof Error ? error.message : t("widget.notebook.saveFailed")); return false; + } finally { + savingRef.current = false; + canChangeRef.current = canChange; } }, - [boardId, isEditing, itemId, mutateAsync, setOptions, t, widgetStateRef], + [boardId, canChange, itemId, mutateAsync, setOptions, t], ); const editor = useEditor( @@ -311,6 +280,7 @@ export function Notebook({ }, [editor, isEditing, options.content]); const handleEditCancelCallback = useCallback(() => { + if (savingRef.current) return true; if (!editor) return false; editor.setEditable(false); @@ -322,6 +292,7 @@ export function Notebook({ const { openConfirmModal } = useConfirmModal(); const handleEditCancel = useCallback(() => { + if (savingRef.current) return; openConfirmModal({ title: t("widget.notebook.dismiss.title"), children: t("widget.notebook.dismiss.message"), @@ -336,7 +307,7 @@ export function Notebook({ }, [setIsEditing, handleEditCancelCallback, openConfirmModal, t]); const handleEditToggle = useCallback(async () => { - if (!editor) return; + if (!editor || savingRef.current) return; if (!isEditing) { setSaveError(null); editor.setEditable(true); @@ -344,15 +315,19 @@ export function Notebook({ return; } + editor.setEditable(false); const wasSaved = await handleContentUpdate(content); - if (!wasSaved) return; + if (!wasSaved) { + editor.setEditable(true); + return; + } editor.setEditable(false); setIsEditing(false); }, [content, editor, handleContentUpdate, isEditing]); const handleDoubleClick = useCallback( (event: ReactMouseEvent) => { - if (!canChange || isEditing) return; + if (!canChange || isEditing || savingRef.current) return; // Ignore double-clicks bubbling up from interactive controls (e.g. the // edit/save ActionIcon), which would otherwise toggle edit mode twice. if (event.target instanceof Element && event.target.closest("button, a")) { @@ -377,7 +352,9 @@ export function Notebook({ p={0} mt={0} h="100%" - onKeyDown={isEditing ? getHotkeyHandler([[hotkeys.saveNotebook, () => void handleEditToggle()]]) : undefined} + onKeyDown={ + isEditing && !isSaving ? getHotkeyHandler([[hotkeys.saveNotebook, () => void handleEditToggle()]]) : undefined + } editor={editor} styles={(theme) => ({ root: { @@ -404,7 +381,8 @@ export function Notebook({ > @@ -505,7 +483,7 @@ export function Notebook({ - {editor && ( + {editor && isEditing && !isSaving && ( @@ -557,6 +535,7 @@ export function Notebook({ <> void handleEditToggle()} > {isEditing ? : } @@ -573,6 +554,7 @@ export function Notebook({ {isEditing && ( diff --git a/packages/widgets/src/paperless-ngx/index.ts b/packages/widgets/src/paperless-ngx/index.ts index af88489ab..bd13d28ec 100644 --- a/packages/widgets/src/paperless-ngx/index.ts +++ b/packages/widgets/src/paperless-ngx/index.ts @@ -5,7 +5,6 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("paperlessNgx", { icon: IconFileText, - queryKey: [["widget", "paperlessNgx"]], refetchInterval: null, supportedIntegrations: ["paperlessNgx"], integrationsRequired: true, diff --git a/packages/widgets/src/patchmon/index.ts b/packages/widgets/src/patchmon/index.ts index 6b0503844..49fd32629 100644 --- a/packages/widgets/src/patchmon/index.ts +++ b/packages/widgets/src/patchmon/index.ts @@ -8,7 +8,6 @@ import { patchmonOptionsSuperRefine } from "./threshold-validation"; export const { definition, componentLoader } = createWidgetDefinition("patchmon", { icon: IconShieldCheck, - queryKey: [["widget", "patchmon"]], supportedIntegrations: ["patchmon"], integrationsRequired: true, maxIntegrations: 1, diff --git a/packages/widgets/src/releases/index.ts b/packages/widgets/src/releases/index.ts index 88ae80cda..62fa7ebab 100644 --- a/packages/widgets/src/releases/index.ts +++ b/packages/widgets/src/releases/index.ts @@ -14,7 +14,6 @@ const relativeDateSchema = z export const { definition, componentLoader } = createWidgetDefinition("releases", { icon: IconRocket, - queryKey: [["widget", "releases"]], refetchInterval: null, createOptions() { return optionsBuilder.from((factory) => ({ diff --git a/packages/widgets/src/rssFeed/index.ts b/packages/widgets/src/rssFeed/index.ts index ec41fd69e..eee7028cb 100644 --- a/packages/widgets/src/rssFeed/index.ts +++ b/packages/widgets/src/rssFeed/index.ts @@ -12,7 +12,6 @@ import { optionsBuilder } from "../options"; */ export const { definition, componentLoader } = createWidgetDefinition("rssFeed", { icon: IconRss, - queryKey: [["widget", "rssFeed"]], refetchInterval: null, createOptions() { return optionsBuilder.from((factory) => ({ diff --git a/packages/widgets/src/smart-home/execute-automation/component.tsx b/packages/widgets/src/smart-home/execute-automation/component.tsx index 671502fb3..5d57d2c62 100644 --- a/packages/widgets/src/smart-home/execute-automation/component.tsx +++ b/packages/widgets/src/smart-home/execute-automation/component.tsx @@ -1,7 +1,18 @@ "use client"; import React from "react"; -import { Center, LoadingOverlay, Overlay, Stack, Text, ThemeIcon, Tooltip, UnstyledButton } from "@mantine/core"; +import { + Box, + Center, + LoadingOverlay, + Overlay, + Stack, + Text, + ThemeIcon, + Tooltip, + UnstyledButton, + VisuallyHidden, +} from "@mantine/core"; import { useDisclosure, useTimeout } from "@mantine/hooks"; import { IconAutomation, IconCheck } from "@tabler/icons-react"; @@ -75,67 +86,79 @@ export default function SmartHomeTriggerAutomationWidget({ const isTiny = width < 128 || height < 96; return ( - - {isShowSuccess && ( - -
- - - -
-
+ + + {isPending + ? t("widget.smartHome-executeAutomation.status.running") + : isShowSuccess + ? t("widget.smartHome-executeAutomation.status.success") + : ""} + + {error && ( + {t("widget.smartHome-executeAutomation.error.executeFailed")} )} - - {error && displayMode !== "advanced" && ( - -
- - - {t("widget.smartHome-executeAutomation.error.executeFailed")} - - -
-
- )} -
- - - - {options.displayName} - - {displayMode === "advanced" && ( - <> - {error ? ( - - - {t("widget.smartHome-executeAutomation.error.executeFailed")} - - - ) : ( - - {lastExecutedAt - ? t("widget.smartHome-executeAutomation.advanced.lastExecuted", { - time: lastExecutedAt.toLocaleTimeString(), - }) - : t("widget.smartHome-executeAutomation.advanced.automationId", { - id: options.automationId, - })} + + {isShowSuccess && ( + +
+ + + +
+
+ )} + + {error && displayMode !== "advanced" && ( + +
+ + + {t("widget.smartHome-executeAutomation.error.executeFailed")} - )} - - )} - -
-
+ +
+ + )} +
+ + + + {options.displayName} + + {displayMode === "advanced" && ( + <> + {error ? ( + + + {t("widget.smartHome-executeAutomation.error.executeFailed")} + + + ) : ( + + {lastExecutedAt + ? t("widget.smartHome-executeAutomation.advanced.lastExecuted", { + time: lastExecutedAt.toLocaleTimeString(), + }) + : t("widget.smartHome-executeAutomation.advanced.automationId", { + id: options.automationId, + })} + + )} + + )} + +
+
+
); } diff --git a/packages/widgets/src/speedtest-tracker/index.ts b/packages/widgets/src/speedtest-tracker/index.ts index 943ef7fdb..14cc5d118 100644 --- a/packages/widgets/src/speedtest-tracker/index.ts +++ b/packages/widgets/src/speedtest-tracker/index.ts @@ -5,7 +5,6 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("speedtestTracker", { icon: IconSpeedboat, - queryKey: [["widget", "speedtestTracker"]], refetchInterval: null, createOptions() { return optionsBuilder.from( diff --git a/packages/widgets/src/stocks/index.ts b/packages/widgets/src/stocks/index.ts index 1174a9caa..ba44f481e 100644 --- a/packages/widgets/src/stocks/index.ts +++ b/packages/widgets/src/stocks/index.ts @@ -13,7 +13,6 @@ const timeIntervalOptions = stockPriceTimeFrames.interval; export const { definition, componentLoader } = createWidgetDefinition("stockPrice", { icon: IconBuildingBank, - queryKey: [["widget", "stockPrice"]], refetchInterval: null, createOptions() { return optionsBuilder.from((factory) => ({ diff --git a/packages/widgets/src/system-resources/component.tsx b/packages/widgets/src/system-resources/component.tsx index 5de42f5b3..be74352de 100644 --- a/packages/widgets/src/system-resources/component.tsx +++ b/packages/widgets/src/system-resources/component.tsx @@ -40,34 +40,18 @@ export const toChartItem = (healthInfo: { export const appendBoundedHistory = (history: ChartItem[], item: ChartItem, limit: number): ChartItem[] => [...history, item].slice(-limit); -export const boundHistoryByIntegration = ( - historyByIntegration: Record, - limit: number, -): Record => - Object.fromEntries( - Object.entries(historyByIntegration).map(([integrationId, history]) => [integrationId, history.slice(-limit)]), - ); - export default function SystemResources({ integrationIds, options, width, displayMode, - widgetStateRef, }: WidgetComponentProps<"systemResources">) { const { data = [], dataUpdatedAt } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery({ integrationIds, }); const isAdvanced = displayMode === "advanced"; const integrationKey = useMemo(() => integrationIds.join("\u0000"), [integrationIds]); - const persistedState = widgetStateRef?.current?.systemResources as - | { integrationKey?: string; historyByIntegration?: Record } - | undefined; - const [historyByIntegration, setHistoryByIntegration] = useState>(() => - persistedState?.integrationKey === integrationKey && persistedState.historyByIntegration - ? boundHistoryByIntegration(persistedState.historyByIntegration, ADVANCED_HISTORY_SIZE) - : {}, - ); + const [historyByIntegration, setHistoryByIntegration] = useState>({}); const previousIntegrationKey = useRef(integrationKey); useEffect(() => { @@ -94,17 +78,6 @@ export default function SystemResources({ ); }, [dataUpdatedAt, data]); - useEffect(() => { - if (!widgetStateRef) return; - widgetStateRef.current = { - ...widgetStateRef.current, - systemResources: { - integrationKey, - historyByIntegration: boundHistoryByIntegration(historyByIntegration, ADVANCED_HISTORY_SIZE), - }, - }; - }, [historyByIntegration, integrationKey, widgetStateRef]); - const panelColumns = isAdvanced && width >= 960 && data.length > 1 ? 2 : 1; const panelWidth = width / panelColumns; const renderCharts = (entry: (typeof data)[number], availableWidth: number) => { diff --git a/packages/widgets/src/system-resources/history.spec.ts b/packages/widgets/src/system-resources/history.spec.ts index 539bd2296..421f74035 100644 --- a/packages/widgets/src/system-resources/history.spec.ts +++ b/packages/widgets/src/system-resources/history.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "vitest"; -import { appendBoundedHistory, boundHistoryByIntegration, toChartItem } from "./component"; +import { appendBoundedHistory, toChartItem } from "./component"; describe("system resource history", () => { test("averages all GPU values", () => { @@ -18,12 +18,4 @@ describe("system resource history", () => { const item = toChartItem({ cpuUtilization: 1, memUsedInBytes: 2, gpu: [], network: null }); expect(appendBoundedHistory([item, item], item, 2)).toHaveLength(2); }); - - test("bounds restored history for every integration", () => { - const item = toChartItem({ cpuUtilization: 1, memUsedInBytes: 2, gpu: [], network: null }); - expect(boundHistoryByIntegration({ first: [item, item, item], second: [item, item] }, 2)).toEqual({ - first: [item, item], - second: [item, item], - }); - }); }); diff --git a/packages/widgets/src/tracearr/index.ts b/packages/widgets/src/tracearr/index.ts index 8567197bb..9c34eb548 100644 --- a/packages/widgets/src/tracearr/index.ts +++ b/packages/widgets/src/tracearr/index.ts @@ -5,7 +5,6 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("tracearr", { icon: IconActivityHeartbeat, - queryKey: [["widget", "tracearr"]], refetchInterval: 5, createOptions() { return optionsBuilder.from((factory) => ({ diff --git a/packages/widgets/src/traefik/index.ts b/packages/widgets/src/traefik/index.ts index a45686578..854075121 100644 --- a/packages/widgets/src/traefik/index.ts +++ b/packages/widgets/src/traefik/index.ts @@ -5,7 +5,6 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("traefik", { icon: IconRoute, - queryKey: [["widget", "traefik"]], createOptions() { return optionsBuilder.from((factory) => ({ showTcp: factory.switch({ diff --git a/packages/widgets/src/umami/index.ts b/packages/widgets/src/umami/index.ts index 90861eab1..eb844575b 100644 --- a/packages/widgets/src/umami/index.ts +++ b/packages/widgets/src/umami/index.ts @@ -8,7 +8,6 @@ export const timeFrameValues = ["today", "24h", "7d", "30d", "month", "lastMonth export const { definition, componentLoader } = createWidgetDefinition("umami", { icon: IconChartBar, - queryKey: [["widget", "umami"]], refetchInterval: null, supportedIntegrations: ["umami"], integrationsRequired: true, diff --git a/packages/widgets/src/ups/index.ts b/packages/widgets/src/ups/index.ts index c9d535dd0..681daf417 100644 --- a/packages/widgets/src/ups/index.ts +++ b/packages/widgets/src/ups/index.ts @@ -7,7 +7,6 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("ups", { icon: IconBatteryCharging, - queryKey: [["widget", "ups"]], createOptions() { return optionsBuilder.from((factory) => ({ showBattery: factory.switch({ diff --git a/packages/widgets/src/uptime-kuma/index.ts b/packages/widgets/src/uptime-kuma/index.ts index cb0d4b0ea..b9eebcd8c 100644 --- a/packages/widgets/src/uptime-kuma/index.ts +++ b/packages/widgets/src/uptime-kuma/index.ts @@ -5,7 +5,6 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("uptimeKuma", { icon: IconHeartbeat, - queryKey: [["widget", "uptimeKuma"]], createOptions() { return optionsBuilder.from((factory) => ({ showAverageUptime: factory.switch({ diff --git a/packages/widgets/src/vpn/index.ts b/packages/widgets/src/vpn/index.ts index e63b55455..b3cbe8c73 100644 --- a/packages/widgets/src/vpn/index.ts +++ b/packages/widgets/src/vpn/index.ts @@ -7,7 +7,6 @@ import { optionsBuilder } from "../options"; export const { componentLoader, definition } = createWidgetDefinition("vpn", { icon: IconShieldLock, - queryKey: [["widget", "vpn"]], refetchInterval: null, createOptions() { return optionsBuilder.from(() => ({})); diff --git a/packages/widgets/src/weather/index.ts b/packages/widgets/src/weather/index.ts index f775d15b7..6c30b6df8 100644 --- a/packages/widgets/src/weather/index.ts +++ b/packages/widgets/src/weather/index.ts @@ -7,7 +7,6 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("weather", { icon: IconCloud, - queryKey: [["widget", "weather"]], createOptions() { return optionsBuilder.from( (factory) => ({