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:
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`;
|
||||
};
|
||||
@@ -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,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Flex, ScrollArea, Stack } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
|
||||
Reference in new issue
Block a user