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.
This commit is contained in:
Thomas Camlong committed 2026-05-28 18:07:03 +02:00
1 parent cb5ae65f8a
commit 394debbea1
38 files changed
+96 -67

No files matched your search

+19 -15
View File
@@ -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<string | null>(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<ReactQuillOnChange>(
(_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 (
<Stack h="100%" gap="xs" p="sm">
+5 -4
View File
@@ -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");
@@ -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<RouterOutputs["widget"]["app"]["ping"]>(ping);
const [pingResult, setPingResult] = useState<RouterOutputs["widget"]["app"]["ping"] | undefined>(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 (
<PingDot
icon={isError ? IconX : IconCheck}
color={isError ? "red" : "green"}
tooltip={
"statusCode" in pingResult
? `${pingResult.statusCode} - ${pingResult.durationMs.toFixed(0)}ms`
: pingResult.error
"statusCode" in current
? `${current.statusCode} - ${current.durationMs.toFixed(0)}ms`
: current.error
}
/>
);
+1 -1
View File
@@ -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));
},
+1 -1
View File
@@ -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(
@@ -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,
},
@@ -20,7 +20,7 @@ import type { widgetKind } from ".";
import type { WidgetComponentProps, WidgetProps } from "../../definition";
export default function DnsHoleSummaryWidget({ options, integrationIds }: WidgetComponentProps<typeof widgetKind>) {
const [summaries] = clientApi.widget.dnsHole.summary.useSuspenseQuery(
const { data: summaries = [] } = clientApi.widget.dnsHole.summary.useQuery(
{
integrationIds,
},
+3 -2
View File
@@ -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) {
+1 -1
View File
@@ -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,
+4 -4
View File
@@ -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,
},
@@ -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);
@@ -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;
@@ -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,
},
@@ -21,11 +21,13 @@ export default function ImmichAlbumCarouselWidget({
return <NoAlbumSelected />;
}
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 <NoPhotosInAlbum />;
}
@@ -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 (
<Stack gap="md" h="100%" p="md">
{options.showUsers && (
@@ -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,
@@ -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,
});
@@ -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,
},
@@ -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 = [
@@ -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,
@@ -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<View>(options.defaultView);
if (!transcodingData) return null;
const totalQueuePages = Math.ceil((transcodingData.data.queue.totalCount || 1) / queuePageSize);
const t = useI18n("widget.mediaTranscoding");
@@ -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 (
@@ -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,
},
@@ -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,
},
@@ -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,
+3 -1
View File
@@ -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) => {
+1 -1
View File
@@ -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,
@@ -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();
@@ -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(
+4 -1
View File
@@ -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(
@@ -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();
@@ -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 =
+2 -2
View File
@@ -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 (
<Stack w="100%" gap="xs" p="sm">
<Text fw="bold">{t("title", { station: station.label })}</Text>
{timetable.entries.map((entry) => (
{(timetable?.entries ?? []).map((entry) => (
<Group key={`${entry.timestamp.toISOString()}-${entry.location}`} justify="space-between" w="100%">
<Group gap="sm">
{entry.line && (
+1 -1
View File
@@ -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(
+2 -2
View File
@@ -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) {
@@ -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,
@@ -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,
+3 -1
View File
@@ -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 (
<Stack
align="center"