fix: address PR review findings

- WidgetCacheAge reads from query cache on every tick (reactive)
- Guard empty timestamps array (no -Infinity edge case)
- Docker widget: add explicit refetchInterval (not under [["widget"]])
- system-resources: split integrationIds reset into separate effect
- Remove unused React import in vpn widget
This commit is contained in:
Thomas Camlong committed 2026-07-01 15:36:00 +02:00
1 parent 445a100cd7
commit 781378dc76
4 files changed
+27 -26

No files matched your search

@@ -4,6 +4,7 @@ import type { MutableRefObject, ReactNode } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Group, Menu, Switch, Text } from "@mantine/core";
import { IconCopy, IconLayoutKanban, IconRefresh, IconSettings, IconTrash } from "@tabler/icons-react";
import type { QueryClient } from "@tanstack/react-query";
import { useQueryClient } from "@tanstack/react-query";
import { clientApi } from "@homarr/api/client";
@@ -52,12 +53,6 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont
void queryClient.invalidateQueries({ queryKey: [["widget"]] });
}, [queryClient]);
const dataUpdatedAt = useMemo(() => {
const queries = queryClient.getQueryCache().findAll({ queryKey: [["widget"]] });
if (queries.length === 0) return null;
return Math.max(...queries.map((q) => q.state.dataUpdatedAt).filter(Boolean));
}, [queryClient]);
const options = useMemo(
() => reduceWidgetOptionsWithDefaultValues(item.kind, settings, item.options) as Record<string, unknown>,
[item.kind, settings, item.options],
@@ -230,11 +225,9 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont
<Menu.Item closeMenuOnClick leftSection={<IconRefresh size={16} />} onClick={handleRefetch}>
<Group justify="space-between" wrap="nowrap">
{tMenu("refresh")}
{dataUpdatedAt && (
<Text size="xs" c="dimmed">
<RelativeTimestamp timestamp={dataUpdatedAt} />
</Text>
)}
<Text size="xs" c="dimmed">
<WidgetCacheAge queryClient={queryClient} />
</Text>
</Group>
</Menu.Item>
<Menu.Item
@@ -271,15 +264,22 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont
);
};
const RelativeTimestamp = ({ timestamp }: { timestamp: number }) => {
const WidgetCacheAge = ({ queryClient }: { queryClient: QueryClient }) => {
const [, setTick] = useState(0);
useEffect(() => {
const id = setInterval(() => setTick((n) => n + 1), 1000);
return () => clearInterval(id);
}, []);
const seconds = Math.floor((Date.now() - timestamp) / 1000);
const timestamps = queryClient
.getQueryCache()
.findAll({ queryKey: [["widget"]] })
.map((q) => q.state.dataUpdatedAt)
.filter(Boolean);
if (timestamps.length === 0) return null;
const seconds = Math.floor((Date.now() - Math.max(...timestamps)) / 1000);
if (seconds < 5) return "just now";
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.floor(seconds / 60);
return `${minutes}m ago`;
return `${Math.floor(seconds / 60)}m ago`;
};
+3 -1
View File
@@ -184,7 +184,9 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
const t = useScopedI18n("docker");
const isTiny = width <= 256;
const { data } = clientApi.docker.getContainers.useQuery(undefined);
const { data } = clientApi.docker.getContainers.useQuery(undefined, {
refetchInterval: 30_000,
});
const containers = data?.containers ?? [];
const timestamp = useMemo(() => data?.timestamp ?? new Date(), [data?.timestamp]);
const relativeTime = useTimeAgo(timestamp);
@@ -43,21 +43,21 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo
{ cpu: number; memory: number; gpu: number; network: { up: number; down: number } | null }[]
>(() => (data[0] ? [toChartItem(data[0].healthInfo)] : []));
const lastUpdatedAt = useRef(dataUpdatedAt);
const prevIntegrationIds = useRef(integrationIds);
useEffect(() => {
if (prevIntegrationIds.current === integrationIds) return;
prevIntegrationIds.current = integrationIds;
setItems([]);
}, [integrationIds]);
const lastUpdatedAt = useRef(dataUpdatedAt);
useEffect(() => {
if (dataUpdatedAt === lastUpdatedAt.current) return;
lastUpdatedAt.current = dataUpdatedAt;
const firstItem = data[0];
if (!firstItem) return;
const idsChanged = prevIntegrationIds.current !== integrationIds;
prevIntegrationIds.current = integrationIds;
setItems((prev) =>
idsChanged
? [toChartItem(firstItem.healthInfo)]
: [...prev, toChartItem(firstItem.healthInfo)].slice(-MAX_QUEUE_SIZE),
);
}, [dataUpdatedAt, data, integrationIds]);
setItems((prev) => [...prev, toChartItem(firstItem.healthInfo)].slice(-MAX_QUEUE_SIZE));
}, [dataUpdatedAt, data]);
const showNetwork =
items.length === 0 || (items.every((item) => item.network !== null) && options.visibleCharts.includes("network"));
-1
View File
@@ -1,6 +1,5 @@
"use client";
import React from "react";
import { Flex, ScrollArea, Stack } from "@mantine/core";
import { clientApi } from "@homarr/api/client";