Compare commits

...
@@ -2,10 +2,10 @@
import type { MutableRefObject, ReactNode } from "react"; import type { MutableRefObject, ReactNode } from "react";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { Group, Menu, Switch, Text } from "@mantine/core"; import { Group, Loader, Menu, Switch, Text } from "@mantine/core";
import { IconCopy, IconLayoutKanban, IconRefresh, IconSettings, IconTrash } from "@tabler/icons-react"; import { IconCopy, IconLayoutKanban, IconRefresh, IconSettings, IconTrash } from "@tabler/icons-react";
import type { QueryClient } from "@tanstack/react-query"; import type { QueryClient } from "@tanstack/react-query";
import { useQueryClient } from "@tanstack/react-query"; import { useIsFetching, useQueryClient } from "@tanstack/react-query";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client"; import { useSession } from "@homarr/auth/client";
@@ -50,8 +50,9 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const widgetQueryKey = useMemo(() => [["widget", item.kind]], [item.kind]); const widgetQueryKey = useMemo(() => [["widget", item.kind]], [item.kind]);
const isWidgetFetching = useIsFetching({ queryKey: widgetQueryKey }) > 0;
const handleRefetch = useCallback(() => { const handleRefetch = useCallback(() => {
void queryClient.invalidateQueries({ queryKey: widgetQueryKey }); void queryClient.refetchQueries({ queryKey: widgetQueryKey, type: "all" });
}, [queryClient, widgetQueryKey]); }, [queryClient, widgetQueryKey]);
const options = useMemo( const options = useMemo(
@@ -223,11 +224,15 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont
<> <>
{(toggleOptions.length > 0 || visibleWidgetActions.length > 0 || isEditMode) && <Menu.Divider />} {(toggleOptions.length > 0 || visibleWidgetActions.length > 0 || isEditMode) && <Menu.Divider />}
<Menu.Item closeMenuOnClick leftSection={<IconRefresh size={16} />} onClick={handleRefetch}> <Menu.Item
leftSection={isWidgetFetching ? <Loader size={16} /> : <IconRefresh size={16} />}
onClick={handleRefetch}
disabled={isWidgetFetching}
>
<Group justify="space-between" wrap="nowrap"> <Group justify="space-between" wrap="nowrap">
{tMenu("refresh")} {tMenu("refresh")}
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
<WidgetCacheAge queryClient={queryClient} queryKey={widgetQueryKey} /> <WidgetCacheAge queryClient={queryClient} queryKey={widgetQueryKey} isFetching={isWidgetFetching} />
</Text> </Text>
</Group> </Group>
</Menu.Item> </Menu.Item>
@@ -265,13 +270,24 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont
); );
}; };
const WidgetCacheAge = ({ queryClient, queryKey }: { queryClient: QueryClient; queryKey: unknown[] }) => { const WidgetCacheAge = ({
queryClient,
queryKey,
isFetching,
}: {
queryClient: QueryClient;
queryKey: unknown[];
isFetching: boolean;
}) => {
// 1s tick for "just now" → "1s ago" label transition; isFetching prop handles refetch reactivity
const [, setTick] = useState(0); const [, setTick] = useState(0);
useEffect(() => { useEffect(() => {
const id = setInterval(() => setTick((n) => n + 1), 1000); const id = setInterval(() => setTick((n) => n + 1), 1000);
return () => clearInterval(id); return () => clearInterval(id);
}, []); }, []);
if (isFetching) return <Loader size={12} />;
const timestamps = queryClient const timestamps = queryClient
.getQueryCache() .getQueryCache()
.findAll({ queryKey }) .findAll({ queryKey })