From 394debbea13a6195f1344be932c60f7e7e3fbaba Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Thu, 28 May 2026 17:20:44 +0200 Subject: [PATCH] refactor(widgets): migrate all widgets from useSuspenseQuery to useQuery Non-blocking data fetching for all 38 widget components. Widgets now render immediately and show data when available, with loading state delegated to the global refresh indicator in the board header. --- .../widgets/src/anchor-note/component.tsx | 34 +++++++++++-------- packages/widgets/src/app/component.tsx | 9 ++--- .../widgets/src/app/ping/ping-indicator.tsx | 15 ++++---- packages/widgets/src/bookmarks/component.tsx | 2 +- packages/widgets/src/coolify/component.tsx | 2 +- .../src/dns-hole/controls/component.tsx | 2 +- .../src/dns-hole/summary/component.tsx | 2 +- packages/widgets/src/docker/component.tsx | 5 +-- packages/widgets/src/downloads/component.tsx | 2 +- packages/widgets/src/firewall/component.tsx | 8 ++--- .../cluster/cluster-health.tsx | 4 ++- .../src/health-monitoring/component.tsx | 2 +- .../src/health-monitoring/system-health.tsx | 2 +- .../src/immich/album-carousel/component.tsx | 4 ++- .../src/immich/server-stats/component.tsx | 4 ++- .../widgets/src/indexer-manager/component.tsx | 2 +- .../widgets/src/media-releases/component.tsx | 2 +- .../src/media-requests/list/component.tsx | 2 +- .../src/media-requests/stats/component.tsx | 3 +- .../widgets/src/media-server/component.tsx | 2 +- .../src/media-transcoding/component.tsx | 5 ++- .../src/minecraft/server-status/component.tsx | 5 ++- .../network-status/component.tsx | 2 +- .../network-controller/summary/component.tsx | 2 +- .../widgets/src/notifications/component.tsx | 2 +- packages/widgets/src/releases/component.tsx | 4 ++- packages/widgets/src/rssFeed/component.tsx | 2 +- .../src/smart-home/entity-state/component.tsx | 2 +- .../src/speedtest-tracker/component.tsx | 2 +- packages/widgets/src/stocks/component.tsx | 5 ++- .../widgets/src/system-disks/component.tsx | 2 +- .../src/system-resources/component.tsx | 2 +- packages/widgets/src/timetable/component.tsx | 4 +-- packages/widgets/src/tracearr/component.tsx | 2 +- packages/widgets/src/umami/umami-content.tsx | 4 +-- .../src/umami/umami-events-content.tsx | 2 +- packages/widgets/src/umami/umami-top-list.tsx | 4 +-- packages/widgets/src/weather/component.tsx | 4 ++- 38 files changed, 96 insertions(+), 67 deletions(-) diff --git a/packages/widgets/src/anchor-note/component.tsx b/packages/widgets/src/anchor-note/component.tsx index f3fde1f97..220c2983a 100644 --- a/packages/widgets/src/anchor-note/component.tsx +++ b/packages/widgets/src/anchor-note/component.tsx @@ -144,7 +144,7 @@ interface AnchorNoteWidgetContentProps { const AnchorNoteWidgetContent = ({ options, integrationId, noteId }: AnchorNoteWidgetContentProps) => { const t = useScopedI18n("widget.anchorNote"); - const [note, { refetch }] = clientApi.widget.anchorNotes.getNote.useSuspenseQuery( + const { data: note, refetch } = clientApi.widget.anchorNotes.getNote.useQuery( { integrationId, noteId, @@ -159,31 +159,32 @@ const AnchorNoteWidgetContent = ({ options, integrationId, noteId }: AnchorNoteW const { mutateAsync: updateNoteAsync, isPending: isUpdating } = clientApi.widget.anchorNotes.updateNote.useMutation(); const [isEditing, setIsEditing] = useState(false); - const [draftTitle, setDraftTitle] = useState(note.title); - const [draftContent, setDraftContent] = useState(note.content ?? ""); + const [draftTitle, setDraftTitle] = useState(note?.title ?? ""); + const [draftContent, setDraftContent] = useState(note?.content ?? ""); const [saveError, setSaveError] = useState(null); useEffect(() => { - if (isEditing) return; + if (isEditing || !note) return; startTransition(() => { setDraftTitle(note.title); setDraftContent(note.content ?? ""); }); - }, [isEditing, note.content, note.title]); + }, [isEditing, note]); - const canEdit = canEditPermission(note.permission); - const isViewer = note.permission === "viewer"; - const updatedAt = useMemo(() => new Date(note.updatedAt), [note.updatedAt]); + const canEdit = canEditPermission(note?.permission ?? "viewer"); + const isViewer = !note || note.permission === "viewer"; + const updatedAt = useMemo(() => (note ? new Date(note.updatedAt) : new Date()), [note]); const updatedAtRelative = useTimeAgo(updatedAt, 30000); const hasChanges = useMemo(() => { + if (!note) return false; const normalizedTitle = draftTitle.trim() || note.title; return normalizedTitle !== note.title || draftContent !== (note.content ?? ""); - }, [draftContent, draftTitle, note.content, note.title]); + }, [draftContent, draftTitle, note]); const editorValue = useMemo(() => parseStoredContent(draftContent), [draftContent]); - const readOnlyValue = useMemo(() => parseStoredContent(note.content), [note.content]); + const readOnlyValue = useMemo(() => parseStoredContent(note?.content), [note?.content]); const handleEditorChange = useCallback( (_html, _delta, source, editor) => { if (!isEditing || source !== "user") return; @@ -194,26 +195,27 @@ const AnchorNoteWidgetContent = ({ options, integrationId, noteId }: AnchorNoteW ); const handleEdit = useCallback(() => { - if (!canEdit) return; + if (!canEdit || !note) return; startTransition(() => { setDraftTitle(note.title); setDraftContent(note.content ?? ""); setSaveError(null); setIsEditing(true); }); - }, [canEdit, note.content, note.title]); + }, [canEdit, note]); const handleCancel = useCallback(() => { + if (!note) return; startTransition(() => { setDraftTitle(note.title); setDraftContent(note.content ?? ""); setSaveError(null); setIsEditing(false); }); - }, [note.content, note.title]); + }, [note]); const handleSave = useCallback(async () => { - if (!canEdit) return; + if (!canEdit || !note) return; const normalizedTitle = draftTitle.trim() || note.title || t("untitled"); @@ -260,7 +262,9 @@ const AnchorNoteWidgetContent = ({ options, integrationId, noteId }: AnchorNoteW }, }, ); - }, [canEdit, draftContent, draftTitle, hasChanges, integrationId, note.title, noteId, refetch, t, updateNoteAsync]); + }, [canEdit, draftContent, draftTitle, hasChanges, integrationId, note, noteId, refetch, t, updateNoteAsync]); + + if (!note) return null; return ( diff --git a/packages/widgets/src/app/component.tsx b/packages/widgets/src/app/component.tsx index 1caa1ce56..b2654b6eb 100644 --- a/packages/widgets/src/app/component.tsx +++ b/packages/widgets/src/app/component.tsx @@ -22,7 +22,7 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget const t = useI18n(); const settings = useSettings(); const board = useRequiredBoard(); - const [app] = clientApi.app.byId.useSuspenseQuery( + const { data: app } = clientApi.app.byId.useQuery( { id: options.appId, }, @@ -34,8 +34,8 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget }, ); useRegisterSpotlightContextResults( - `app-${app.id}`, - app.href + `app-${app?.id ?? options.appId}`, + app?.href ? [ { id: app.id, @@ -44,7 +44,6 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget interaction() { return { type: "link", - // We checked above that app.href is defined // eslint-disable-next-line @typescript-eslint/no-non-null-assertion href: app.href!, newTab: options.openInNewTab, @@ -56,6 +55,8 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget [app, options.openInNewTab], ); + if (!app) return null; + const isTiny = height < 100 || width < 100; const isColumnLayout = options.layout.startsWith("column"); diff --git a/packages/widgets/src/app/ping/ping-indicator.tsx b/packages/widgets/src/app/ping/ping-indicator.tsx index a6299ed95..6ca1aaa2d 100644 --- a/packages/widgets/src/app/ping/ping-indicator.tsx +++ b/packages/widgets/src/app/ping/ping-indicator.tsx @@ -11,7 +11,7 @@ interface PingIndicatorProps { } export const PingIndicator = ({ appId }: PingIndicatorProps) => { - const [ping] = clientApi.widget.app.ping.useSuspenseQuery( + const { data: ping } = clientApi.widget.app.ping.useQuery( { id: appId, }, @@ -21,7 +21,7 @@ export const PingIndicator = ({ appId }: PingIndicatorProps) => { }, ); - const [pingResult, setPingResult] = useState(ping); + const [pingResult, setPingResult] = useState(ping); clientApi.widget.app.updatedPing.useSubscription( { id: appId }, @@ -32,16 +32,19 @@ export const PingIndicator = ({ appId }: PingIndicatorProps) => { }, ); - const isError = "error" in pingResult || pingResult.statusCode >= 500; + const current = pingResult ?? ping; + if (!current) return null; + + const isError = "error" in current || current.statusCode >= 500; return ( ); diff --git a/packages/widgets/src/bookmarks/component.tsx b/packages/widgets/src/bookmarks/component.tsx index 09066f90d..a6b8853ad 100644 --- a/packages/widgets/src/bookmarks/component.tsx +++ b/packages/widgets/src/bookmarks/component.tsx @@ -14,7 +14,7 @@ import classes from "./bookmark.module.css"; export default function BookmarksWidget({ options, itemId }: WidgetComponentProps<"bookmarks">) { const board = useRequiredBoard(); - const [data] = clientApi.app.byIds.useSuspenseQuery(options.items, { + const { data = [] } = clientApi.app.byIds.useQuery(options.items, { select(data) { return data.sort((appA, appB) => options.items.indexOf(appA.id) - options.items.indexOf(appB.id)); }, diff --git a/packages/widgets/src/coolify/component.tsx b/packages/widgets/src/coolify/component.tsx index a62b4f3cd..3dde4bcfb 100644 --- a/packages/widgets/src/coolify/component.tsx +++ b/packages/widgets/src/coolify/component.tsx @@ -31,7 +31,7 @@ interface CoolifyContentProps { } function CoolifyContent({ integrationIds, options, width }: CoolifyContentProps) { - const [instancesData] = clientApi.widget.coolify.getInstancesInfo.useSuspenseQuery({ integrationIds }); + const { data: instancesData = [] } = clientApi.widget.coolify.getInstancesInfo.useQuery({ integrationIds }); const utils = clientApi.useUtils(); clientApi.widget.coolify.subscribeInstancesInfo.useSubscription( diff --git a/packages/widgets/src/dns-hole/controls/component.tsx b/packages/widgets/src/dns-hole/controls/component.tsx index 8ce9251a4..460e86d04 100644 --- a/packages/widgets/src/dns-hole/controls/component.tsx +++ b/packages/widgets/src/dns-hole/controls/component.tsx @@ -51,7 +51,7 @@ export default function DnsHoleControlsWidget({ .map(({ id }) => id) .filter((id) => integrationIds.includes(id)); - const [summaries] = clientApi.widget.dnsHole.summary.useSuspenseQuery( + const { data: summaries = [] } = clientApi.widget.dnsHole.summary.useQuery( { integrationIds, }, diff --git a/packages/widgets/src/dns-hole/summary/component.tsx b/packages/widgets/src/dns-hole/summary/component.tsx index b0636ff44..a72960d03 100644 --- a/packages/widgets/src/dns-hole/summary/component.tsx +++ b/packages/widgets/src/dns-hole/summary/component.tsx @@ -20,7 +20,7 @@ import type { widgetKind } from "."; import type { WidgetComponentProps, WidgetProps } from "../../definition"; export default function DnsHoleSummaryWidget({ options, integrationIds }: WidgetComponentProps) { - const [summaries] = clientApi.widget.dnsHole.summary.useSuspenseQuery( + const { data: summaries = [] } = clientApi.widget.dnsHole.summary.useQuery( { integrationIds, }, diff --git a/packages/widgets/src/docker/component.tsx b/packages/widgets/src/docker/component.tsx index e47c77f33..4a95c3989 100644 --- a/packages/widgets/src/docker/component.tsx +++ b/packages/widgets/src/docker/component.tsx @@ -204,8 +204,9 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo const isTiny = width <= 256; const utils = clientApi.useUtils(); - const [{ containers, timestamp }] = clientApi.docker.getContainers.useSuspenseQuery(); - const relativeTime = useTimeAgo(timestamp); + const { data } = clientApi.docker.getContainers.useQuery(); + const containers = data?.containers ?? []; + const relativeTime = useTimeAgo(data?.timestamp ?? new Date()); clientApi.docker.subscribeContainers.useSubscription(undefined, { onData(data) { diff --git a/packages/widgets/src/downloads/component.tsx b/packages/widgets/src/downloads/component.tsx index 96e63a106..54a602507 100644 --- a/packages/widgets/src/downloads/component.tsx +++ b/packages/widgets/src/downloads/component.tsx @@ -87,7 +87,7 @@ export default function DownloadClientsWidget({ integrationIds.includes(id) ? [id] : [], ); - const [currentItems] = clientApi.widget.downloads.getJobsAndStatuses.useSuspenseQuery( + const { data: currentItems = [] } = clientApi.widget.downloads.getJobsAndStatuses.useQuery( { integrationIds, limitPerIntegration: options.limitPerIntegration, diff --git a/packages/widgets/src/firewall/component.tsx b/packages/widgets/src/firewall/component.tsx index cf5ac2b0e..80f1812eb 100644 --- a/packages/widgets/src/firewall/component.tsx +++ b/packages/widgets/src/firewall/component.tsx @@ -184,7 +184,7 @@ export default function FirewallWidget({ integrationIds, width, itemId }: Widget export const useUpdatingCpuStatus = (integrationIds: string[]) => { const utils = clientApi.useUtils(); - const [firewallsCpuData] = clientApi.widget.firewall.getFirewallCpuStatus.useSuspenseQuery( + const { data: firewallsCpuData = [] } = clientApi.widget.firewall.getFirewallCpuStatus.useQuery( { integrationIds, }, @@ -225,7 +225,7 @@ export const useUpdatingCpuStatus = (integrationIds: string[]) => { export const useUpdatingMemoryStatus = (integrationIds: string[]) => { const utils = clientApi.useUtils(); - const [firewallsMemoryData] = clientApi.widget.firewall.getFirewallMemoryStatus.useSuspenseQuery( + const { data: firewallsMemoryData = [] } = clientApi.widget.firewall.getFirewallMemoryStatus.useQuery( { integrationIds, }, @@ -266,7 +266,7 @@ export const useUpdatingMemoryStatus = (integrationIds: string[]) => { export const useUpdatingVersionStatus = (integrationIds: string[]) => { const utils = clientApi.useUtils(); - const [firewallsVersionData] = clientApi.widget.firewall.getFirewallVersionStatus.useSuspenseQuery( + const { data: firewallsVersionData = [] } = clientApi.widget.firewall.getFirewallVersionStatus.useQuery( { integrationIds, }, @@ -306,7 +306,7 @@ export const useUpdatingVersionStatus = (integrationIds: string[]) => { export const useUpdatingInterfacesStatus = (integrationIds: string[]) => { const utils = clientApi.useUtils(); - const [firewallsInterfacesData] = clientApi.widget.firewall.getFirewallInterfacesStatus.useSuspenseQuery( + const { data: firewallsInterfacesData = [] } = clientApi.widget.firewall.getFirewallInterfacesStatus.useQuery( { integrationIds, }, diff --git a/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx b/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx index 3ffa99bdc..9f9d2a1bd 100644 --- a/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx +++ b/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx @@ -34,7 +34,7 @@ export const ClusterHealthMonitoring = ({ width, }: WidgetComponentProps<"healthMonitoring"> & { integrationId: string }) => { const t = useI18n(); - const [healthData] = clientApi.widget.healthMonitoring.getClusterHealthStatus.useSuspenseQuery( + const { data: healthData } = clientApi.widget.healthMonitoring.getClusterHealthStatus.useQuery( { integrationId, }, @@ -56,6 +56,8 @@ export const ClusterHealthMonitoring = ({ }, ); + if (!healthData) return null; + const activeNodes = healthData.nodes.reduce(running, 0); const activeVMs = healthData.vms.reduce(running, 0); const activeLXCs = healthData.lxcs.reduce(running, 0); diff --git a/packages/widgets/src/health-monitoring/component.tsx b/packages/widgets/src/health-monitoring/component.tsx index baa958fe9..ead4fef92 100644 --- a/packages/widgets/src/health-monitoring/component.tsx +++ b/packages/widgets/src/health-monitoring/component.tsx @@ -18,7 +18,7 @@ const isClusterIntegration = (integration: { kind: IntegrationKind }) => integration.kind === "proxmox" || integration.kind === "mock"; export default function HealthMonitoringWidget(props: WidgetComponentProps<"healthMonitoring">) { - const [integrations] = clientApi.integration.byIds.useSuspenseQuery(props.integrationIds); + const { data: integrations = [] } = clientApi.integration.byIds.useQuery(props.integrationIds); const t = useI18n(); const clusterIntegrationId = integrations.find(isClusterIntegration)?.id; diff --git a/packages/widgets/src/health-monitoring/system-health.tsx b/packages/widgets/src/health-monitoring/system-health.tsx index 6e7d2965c..4a66b61ac 100644 --- a/packages/widgets/src/health-monitoring/system-health.tsx +++ b/packages/widgets/src/health-monitoring/system-health.tsx @@ -51,7 +51,7 @@ export const SystemHealthMonitoring = ({ width, }: WidgetComponentProps<"healthMonitoring">) => { const t = useI18n(); - const [healthData] = clientApi.widget.healthMonitoring.getSystemHealthStatus.useSuspenseQuery( + const { data: healthData = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery( { integrationIds, }, diff --git a/packages/widgets/src/immich/album-carousel/component.tsx b/packages/widgets/src/immich/album-carousel/component.tsx index a8a920d5d..c84ed7ae7 100644 --- a/packages/widgets/src/immich/album-carousel/component.tsx +++ b/packages/widgets/src/immich/album-carousel/component.tsx @@ -21,11 +21,13 @@ export default function ImmichAlbumCarouselWidget({ return ; } - const [album] = clientApi.widget.immich.getAlbum.useSuspenseQuery({ + const { data: album } = clientApi.widget.immich.getAlbum.useQuery({ integrationId: integrationIds[0] ?? "", albumId: options.albumId, }); + if (!album) return null; + if (album.assets.length === 0) { return ; } diff --git a/packages/widgets/src/immich/server-stats/component.tsx b/packages/widgets/src/immich/server-stats/component.tsx index 0240a27c4..22346d376 100644 --- a/packages/widgets/src/immich/server-stats/component.tsx +++ b/packages/widgets/src/immich/server-stats/component.tsx @@ -16,10 +16,12 @@ export default function ImmichServerStatsWidget({ options, }: WidgetComponentProps<"immich-serverStats">) { const t = useI18n(); - const [stats] = clientApi.widget.immich.getServerStats.useSuspenseQuery({ + const { data: stats } = clientApi.widget.immich.getServerStats.useQuery({ integrationId: integrationIds[0] ?? "", }); + if (!stats) return null; + return ( {options.showUsers && ( diff --git a/packages/widgets/src/indexer-manager/component.tsx b/packages/widgets/src/indexer-manager/component.tsx index e406ffa8a..ce0e89403 100644 --- a/packages/widgets/src/indexer-manager/component.tsx +++ b/packages/widgets/src/indexer-manager/component.tsx @@ -18,7 +18,7 @@ export default function IndexerManagerWidget({ height, }: WidgetComponentProps<"indexerManager">) { const t = useI18n(); - const [indexersData] = clientApi.widget.indexerManager.getIndexersStatus.useSuspenseQuery( + const { data: indexersData = [] } = clientApi.widget.indexerManager.getIndexersStatus.useQuery( { integrationIds }, { refetchOnMount: false, diff --git a/packages/widgets/src/media-releases/component.tsx b/packages/widgets/src/media-releases/component.tsx index cd2432ddb..26fe367a9 100644 --- a/packages/widgets/src/media-releases/component.tsx +++ b/packages/widgets/src/media-releases/component.tsx @@ -18,7 +18,7 @@ import { OverflowBadge } from "@homarr/ui"; import type { WidgetComponentProps } from "../definition"; export default function MediaReleasesWidget({ options, integrationIds }: WidgetComponentProps<"mediaReleases">) { - const [releases] = clientApi.widget.mediaRelease.getMediaReleases.useSuspenseQuery({ + const { data: releases = [] } = clientApi.widget.mediaRelease.getMediaReleases.useQuery({ integrationIds, }); diff --git a/packages/widgets/src/media-requests/list/component.tsx b/packages/widgets/src/media-requests/list/component.tsx index 5d86c40a1..b2320f426 100644 --- a/packages/widgets/src/media-requests/list/component.tsx +++ b/packages/widgets/src/media-requests/list/component.tsx @@ -19,7 +19,7 @@ export default function MediaServerWidget({ options, width, }: WidgetComponentProps<"mediaRequests-requestList">) { - const [mediaRequests] = clientApi.widget.mediaRequests.getLatestRequests.useSuspenseQuery( + const { data: mediaRequests = [] } = clientApi.widget.mediaRequests.getLatestRequests.useQuery( { integrationIds, }, diff --git a/packages/widgets/src/media-requests/stats/component.tsx b/packages/widgets/src/media-requests/stats/component.tsx index 4a1b6ed09..2119457c4 100644 --- a/packages/widgets/src/media-requests/stats/component.tsx +++ b/packages/widgets/src/media-requests/stats/component.tsx @@ -32,7 +32,7 @@ export default function MediaServerWidget({ width, }: WidgetComponentProps<"mediaRequests-requestStats">) { const t = useScopedI18n("widget.mediaRequests-requestStats"); - const [requestStats] = clientApi.widget.mediaRequests.getStats.useSuspenseQuery( + const { data: requestStats } = clientApi.widget.mediaRequests.getStats.useQuery( { integrationIds, }, @@ -45,6 +45,7 @@ export default function MediaServerWidget({ const board = useRequiredBoard(); + if (!requestStats) return null; if (requestStats.users.length === 0 && requestStats.stats.length === 0) throw new NoIntegrationDataError(); const data = [ diff --git a/packages/widgets/src/media-server/component.tsx b/packages/widgets/src/media-server/component.tsx index 9b540339e..138728e57 100644 --- a/packages/widgets/src/media-server/component.tsx +++ b/packages/widgets/src/media-server/component.tsx @@ -23,7 +23,7 @@ export default function MediaServerWidget({ integrationIds, isEditMode, }: WidgetComponentProps<"mediaServer">) { - const [currentStreams] = clientApi.widget.mediaServer.getCurrentStreams.useSuspenseQuery( + const { data: currentStreams = [] } = clientApi.widget.mediaServer.getCurrentStreams.useQuery( { integrationIds, showOnlyPlaying: options.showOnlyPlaying, diff --git a/packages/widgets/src/media-transcoding/component.tsx b/packages/widgets/src/media-transcoding/component.tsx index 8967210b3..8910de1ab 100644 --- a/packages/widgets/src/media-transcoding/component.tsx +++ b/packages/widgets/src/media-transcoding/component.tsx @@ -37,7 +37,7 @@ export default function MediaTranscodingWidget({ pageSize: queuePageSize, page: queuePage, }; - const [transcodingData] = clientApi.widget.mediaTranscoding.getDataAsync.useSuspenseQuery(input, { + const { data: transcodingData } = clientApi.widget.mediaTranscoding.getDataAsync.useQuery(input, { refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, @@ -51,6 +51,9 @@ export default function MediaTranscodingWidget({ }); const [view, setView] = useState(options.defaultView); + + if (!transcodingData) return null; + const totalQueuePages = Math.ceil((transcodingData.data.queue.totalCount || 1) / queuePageSize); const t = useI18n("widget.mediaTranscoding"); diff --git a/packages/widgets/src/minecraft/server-status/component.tsx b/packages/widgets/src/minecraft/server-status/component.tsx index 00556be85..630fb049f 100644 --- a/packages/widgets/src/minecraft/server-status/component.tsx +++ b/packages/widgets/src/minecraft/server-status/component.tsx @@ -10,7 +10,7 @@ import { useScopedI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../../definition"; export default function MinecraftServerStatusWidget({ options }: WidgetComponentProps<"minecraftServerStatus">) { - const [{ data }] = clientApi.widget.minecraft.getServerStatus.useSuspenseQuery(options); + const { data: result } = clientApi.widget.minecraft.getServerStatus.useQuery(options); const utils = clientApi.useUtils(); clientApi.widget.minecraft.subscribeServerStatus.useSubscription(options, { onData(data) { @@ -22,6 +22,9 @@ export default function MinecraftServerStatusWidget({ options }: WidgetComponent }); const tStatus = useScopedI18n("widget.minecraftServerStatus.status"); + if (!result) return null; + const { data } = result; + const title = options.title.trim().length > 0 ? options.title : options.domain; return ( diff --git a/packages/widgets/src/network-controller/network-status/component.tsx b/packages/widgets/src/network-controller/network-status/component.tsx index f87973252..e0ce57dfb 100644 --- a/packages/widgets/src/network-controller/network-status/component.tsx +++ b/packages/widgets/src/network-controller/network-status/component.tsx @@ -21,7 +21,7 @@ export default function NetworkControllerNetworkStatusWidget({ options, integrationIds, }: WidgetComponentProps<"networkControllerStatus">) { - const [summaries] = clientApi.widget.networkController.summary.useSuspenseQuery( + const { data: summaries = [] } = clientApi.widget.networkController.summary.useQuery( { integrationIds, }, diff --git a/packages/widgets/src/network-controller/summary/component.tsx b/packages/widgets/src/network-controller/summary/component.tsx index bbd9775cd..040b53eca 100644 --- a/packages/widgets/src/network-controller/summary/component.tsx +++ b/packages/widgets/src/network-controller/summary/component.tsx @@ -20,7 +20,7 @@ dayjs.extend(duration); export default function NetworkControllerSummaryWidget({ integrationIds, }: WidgetComponentProps<"networkControllerSummary">) { - const [summaries] = clientApi.widget.networkController.summary.useSuspenseQuery( + const { data: summaries = [] } = clientApi.widget.networkController.summary.useQuery( { integrationIds, }, diff --git a/packages/widgets/src/notifications/component.tsx b/packages/widgets/src/notifications/component.tsx index 72a662956..4334836b9 100644 --- a/packages/widgets/src/notifications/component.tsx +++ b/packages/widgets/src/notifications/component.tsx @@ -12,7 +12,7 @@ import { useScopedI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../definition"; export default function NotificationsWidget({ options, integrationIds }: WidgetComponentProps<"notifications">) { - const [notificationIntegrations] = clientApi.widget.notifications.getNotifications.useSuspenseQuery( + const { data: notificationIntegrations = [] } = clientApi.widget.notifications.getNotifications.useQuery( { ...options, integrationIds, diff --git a/packages/widgets/src/releases/component.tsx b/packages/widgets/src/releases/component.tsx index fdd9f1f0b..302c4b6c9 100644 --- a/packages/widgets/src/releases/component.tsx +++ b/packages/widgets/src/releases/component.tsx @@ -81,7 +81,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas ); }, [groupedRepositories]); - const [results] = clientApi.useSuspenseQueries((t) => + const queryResults = clientApi.useQueries((t) => batchedRepositories.flatMap(({ integrationId, repositories }) => t.widget.releases.getLatest({ integrationId, @@ -94,6 +94,8 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas ), ); + const results = queryResults.map((q) => q.data).filter(Boolean); + const repositories = useMemo(() => { const formattedResults = options.repositories .map((repository) => { diff --git a/packages/widgets/src/rssFeed/component.tsx b/packages/widgets/src/rssFeed/component.tsx index 843168b88..008e06c7d 100644 --- a/packages/widgets/src/rssFeed/component.tsx +++ b/packages/widgets/src/rssFeed/component.tsx @@ -12,7 +12,7 @@ import type { WidgetComponentProps } from "../definition"; import classes from "./component.module.scss"; const useLiveFeedEntries = (input: RouterInputs["widget"]["rssFeed"]["getFeeds"]) => { - const [feedEntries] = clientApi.widget.rssFeed.getFeeds.useSuspenseQuery(input, { + const { data: feedEntries = [] } = clientApi.widget.rssFeed.getFeeds.useQuery(input, { refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, diff --git a/packages/widgets/src/smart-home/entity-state/component.tsx b/packages/widgets/src/smart-home/entity-state/component.tsx index f11cfa844..f796f3261 100644 --- a/packages/widgets/src/smart-home/entity-state/component.tsx +++ b/packages/widgets/src/smart-home/entity-state/component.tsx @@ -23,7 +23,7 @@ export default function SmartHomeEntityStateWidget({ entityId: options.entityId, integrationId, }; - const [entityState] = clientApi.widget.smartHome.entityState.useSuspenseQuery(input); + const { data: entityState } = clientApi.widget.smartHome.entityState.useQuery(input); const utils = clientApi.useUtils(); diff --git a/packages/widgets/src/speedtest-tracker/component.tsx b/packages/widgets/src/speedtest-tracker/component.tsx index 699b354a9..4c9a0e22e 100644 --- a/packages/widgets/src/speedtest-tracker/component.tsx +++ b/packages/widgets/src/speedtest-tracker/component.tsx @@ -19,7 +19,7 @@ export default function SpeedtestTrackerWidget({ isEditMode, }: WidgetComponentProps<"speedtestTracker">) { const t = useScopedI18n("widget.speedtestTracker"); - const [dashboardData] = clientApi.widget.speedtestTracker.getDashboard.useSuspenseQuery({ integrationIds }); + const { data: dashboardData = [] } = clientApi.widget.speedtestTracker.getDashboard.useQuery({ integrationIds }); const utils = clientApi.useUtils(); clientApi.widget.speedtestTracker.subscribeToDashboard.useSubscription( diff --git a/packages/widgets/src/stocks/component.tsx b/packages/widgets/src/stocks/component.tsx index e9d7b98ad..f5e64259b 100644 --- a/packages/widgets/src/stocks/component.tsx +++ b/packages/widgets/src/stocks/component.tsx @@ -24,7 +24,10 @@ function calculateChangePercentage(currentPrice: number, previousClose: number) export default function StockPriceWidget({ options, width, height }: WidgetComponentProps<"stockPrice">) { const t = useScopedI18n("widget.stockPrice"); const theme = useMantineTheme(); - const [{ data }] = clientApi.widget.stockPrice.getPriceHistory.useSuspenseQuery(options); + const { data: result } = clientApi.widget.stockPrice.getPriceHistory.useQuery(options); + + if (!result) return null; + const { data } = result; const stockValuesChange = round(calculateChange(data.priceHistory.at(-1) ?? 0, data.previousClose)); const stockValuesChangePercentage = round( diff --git a/packages/widgets/src/system-disks/component.tsx b/packages/widgets/src/system-disks/component.tsx index ba2d49c25..3e254fdce 100644 --- a/packages/widgets/src/system-disks/component.tsx +++ b/packages/widgets/src/system-disks/component.tsx @@ -32,7 +32,7 @@ const getDisplayText = (item: { used: string; available: string; percentage: num export default function SystemResources({ integrationIds, options }: WidgetComponentProps<"systemDisks">) { const queryInput = { integrationIds }; - const [data] = clientApi.widget.healthMonitoring.getSystemHealthStatus.useSuspenseQuery(queryInput); + const { data = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery(queryInput); const utils = clientApi.useUtils(); const board = useRequiredBoard(); diff --git a/packages/widgets/src/system-resources/component.tsx b/packages/widgets/src/system-resources/component.tsx index d1d54ff85..d2c5380ad 100644 --- a/packages/widgets/src/system-resources/component.tsx +++ b/packages/widgets/src/system-resources/component.tsx @@ -18,7 +18,7 @@ const MAX_QUEUE_SIZE = 15; export default function SystemResources({ integrationIds, options }: WidgetComponentProps<"systemResources">) { const { ref, width } = useElementSize(); - const [data] = clientApi.widget.healthMonitoring.getSystemHealthStatus.useSuspenseQuery({ + const { data = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery({ integrationIds, }); const memoryCapacityInBytes = diff --git a/packages/widgets/src/timetable/component.tsx b/packages/widgets/src/timetable/component.tsx index ceaf9e41d..2f1ad982b 100644 --- a/packages/widgets/src/timetable/component.tsx +++ b/packages/widgets/src/timetable/component.tsx @@ -26,7 +26,7 @@ interface TimetableWidgetInnerProps { } const TimetableWidgetInner = ({ station, integrationId }: TimetableWidgetInnerProps) => { - const [timetable] = clientApi.widget.timetable.getTimetable.useSuspenseQuery({ + const { data: timetable } = clientApi.widget.timetable.getTimetable.useQuery({ integrationId, stationId: station.value, limit: 10, @@ -36,7 +36,7 @@ const TimetableWidgetInner = ({ station, integrationId }: TimetableWidgetInnerPr return ( {t("title", { station: station.label })} - {timetable.entries.map((entry) => ( + {(timetable?.entries ?? []).map((entry) => ( {entry.line && ( diff --git a/packages/widgets/src/tracearr/component.tsx b/packages/widgets/src/tracearr/component.tsx index 1b26b6c67..4a54779ee 100644 --- a/packages/widgets/src/tracearr/component.tsx +++ b/packages/widgets/src/tracearr/component.tsx @@ -35,7 +35,7 @@ interface TracearrContentProps { function TracearrContent({ integrationIds, options, width, isEditMode }: TracearrContentProps) { const t = useScopedI18n("widget.tracearr"); - const [dashboardData] = clientApi.widget.tracearr.getDashboard.useSuspenseQuery({ integrationIds }); + const { data: dashboardData = [] } = clientApi.widget.tracearr.getDashboard.useQuery({ integrationIds }); const utils = clientApi.useUtils(); clientApi.widget.tracearr.subscribeToDashboard.useSubscription( diff --git a/packages/widgets/src/umami/umami-content.tsx b/packages/widgets/src/umami/umami-content.tsx index da9494519..9121609ed 100644 --- a/packages/widgets/src/umami/umami-content.tsx +++ b/packages/widgets/src/umami/umami-content.tsx @@ -40,7 +40,7 @@ export function UmamiContent({ const { colorScheme } = useMantineColorScheme(); const tickColor = colorScheme === "dark" ? "#c1c2c5" : "#495057"; - const [results] = clientApi.widget.umami.getVisitorStats.useSuspenseQuery({ + const { data: results = [] } = clientApi.widget.umami.getVisitorStats.useQuery({ integrationIds, websiteId, timeFrame, @@ -48,7 +48,7 @@ export function UmamiContent({ }); const activeVisitorsInput = { integrationId: integrationIds[0] ?? "", websiteId }; - const [activeVisitors] = clientApi.widget.umami.getActiveVisitors.useSuspenseQuery(activeVisitorsInput); + const { data: activeVisitors = 0 } = clientApi.widget.umami.getActiveVisitors.useQuery(activeVisitorsInput); const utils = clientApi.useUtils(); clientApi.widget.umami.subscribeActiveVisitors.useSubscription(activeVisitorsInput, { onData(count) { diff --git a/packages/widgets/src/umami/umami-events-content.tsx b/packages/widgets/src/umami/umami-events-content.tsx index 0962d3681..c6d1acedd 100644 --- a/packages/widgets/src/umami/umami-events-content.tsx +++ b/packages/widgets/src/umami/umami-events-content.tsx @@ -28,7 +28,7 @@ export function UmamiEventsContent({ const { colorScheme } = useMantineColorScheme(); const tickColor = colorScheme === "dark" ? "#c1c2c5" : "#495057"; - const [series] = clientApi.widget.umami.getMultiEventTimeSeries.useSuspenseQuery({ + const { data: series = [] } = clientApi.widget.umami.getMultiEventTimeSeries.useQuery({ integrationId: integrationIds[0] ?? "", websiteId, timeFrame, diff --git a/packages/widgets/src/umami/umami-top-list.tsx b/packages/widgets/src/umami/umami-top-list.tsx index 1d9a87988..699df9b74 100644 --- a/packages/widgets/src/umami/umami-top-list.tsx +++ b/packages/widgets/src/umami/umami-top-list.tsx @@ -15,7 +15,7 @@ interface UmamiTopListProps { export function UmamiTopPagesContent({ integrationIds, websiteId, timeFrame, limit }: UmamiTopListProps) { const t = useScopedI18n("widget.umami"); - const [data] = clientApi.widget.umami.getTopPages.useSuspenseQuery({ + const { data = [] } = clientApi.widget.umami.getTopPages.useQuery({ integrationId: integrationIds[0] ?? "", websiteId, timeFrame, @@ -26,7 +26,7 @@ export function UmamiTopPagesContent({ integrationIds, websiteId, timeFrame, lim export function UmamiTopReferrersContent({ integrationIds, websiteId, timeFrame, limit }: UmamiTopListProps) { const t = useScopedI18n("widget.umami"); - const [data] = clientApi.widget.umami.getTopReferrers.useSuspenseQuery({ + const { data = [] } = clientApi.widget.umami.getTopReferrers.useQuery({ integrationId: integrationIds[0] ?? "", websiteId, timeFrame, diff --git a/packages/widgets/src/weather/component.tsx b/packages/widgets/src/weather/component.tsx index f81736c27..60de018f4 100644 --- a/packages/widgets/src/weather/component.tsx +++ b/packages/widgets/src/weather/component.tsx @@ -18,7 +18,7 @@ export default function WeatherWidget({ isEditMode, options }: WidgetComponentPr latitude: options.location.latitude, longitude: options.location.longitude, }; - const [weather] = clientApi.widget.weather.atLocation.useSuspenseQuery(input, { + const { data: weather } = clientApi.widget.weather.atLocation.useQuery(input, { refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, @@ -29,6 +29,8 @@ export default function WeatherWidget({ isEditMode, options }: WidgetComponentPr onData: (data) => utils.widget.weather.atLocation.setData(input, data), }); + if (!weather) return null; + return (