remove green dot (#5822)
This commit is contained in:
1 parent
9fc0e5f94c
commit
ef27d739bb
45 files changed
+179
-319
No files matched your search
@@ -0,0 +1,3 @@
|
||||
[tools]
|
||||
node = "24.16.0"
|
||||
pnpm = "10.33.4"
|
||||
@@ -6,7 +6,6 @@ import { fetchApi } from "@homarr/api/client";
|
||||
import {
|
||||
getActiveQueryCacheBoardId,
|
||||
isPersistableWidgetQueryKey,
|
||||
queryCacheMaxAgeMs,
|
||||
queryCacheStoragePrefix,
|
||||
} from "@homarr/api/query-cache";
|
||||
|
||||
@@ -39,7 +38,7 @@ const queryCacheStorage = {
|
||||
export const createWidgetQueryPersister = () =>
|
||||
experimental_createQueryPersister<string>({
|
||||
storage: typeof window === "undefined" ? undefined : queryCacheStorage,
|
||||
maxAge: queryCacheMaxAgeMs,
|
||||
maxAge: Number.POSITIVE_INFINITY,
|
||||
prefix: queryCacheStoragePrefix,
|
||||
refetchOnRestore: true,
|
||||
serialize: (query) => stringify(query),
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { SuperJSONResult } from "superjson";
|
||||
|
||||
import type { AppRouter } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { queryCacheDefaultStaleTimeMs } from "@homarr/api/query-cache";
|
||||
import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared";
|
||||
import { env } from "@homarr/common/env";
|
||||
|
||||
@@ -58,7 +59,7 @@ export function TRPCReactProvider(props: PropsWithChildren) {
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
persister: queryPersister.persisterFn,
|
||||
staleTime: 5 * 1000,
|
||||
staleTime: queryCacheDefaultStaleTimeMs,
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { DehydratedState } from "@tanstack/react-query";
|
||||
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
|
||||
import { parse } from "superjson";
|
||||
|
||||
import { queryCacheMaxAgeMs } from "@homarr/api/query-cache";
|
||||
import { getQueryClient } from "@homarr/api/server";
|
||||
import { IntegrationProvider } from "@homarr/auth/client";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
@@ -127,13 +126,11 @@ interface PersistedEntry {
|
||||
async function QueryCacheHydration({ userId, boardId }: { userId: string; boardId: string }) {
|
||||
try {
|
||||
const entries = await getAllQueryCacheAsync(userId, boardId);
|
||||
const now = Date.now();
|
||||
const queries: DehydratedState["queries"] = [];
|
||||
|
||||
for (const serialized of Object.values(entries)) {
|
||||
try {
|
||||
const persisted = parse<PersistedEntry>(serialized);
|
||||
if (now - persisted.state.dataUpdatedAt > queryCacheMaxAgeMs) continue;
|
||||
if (persisted.state.status !== "success") continue;
|
||||
|
||||
queries.push({
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef, useEffect, useState } from "react";
|
||||
import { Avatar, Box, Group, Indicator, Menu, ScrollArea, Text, Tooltip, UnstyledButton } from "@mantine/core";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
import { getIconUrl } from "@homarr/definitions";
|
||||
|
||||
interface IntegrationInfo {
|
||||
id: string;
|
||||
kind: IntegrationKind;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface QueryStatusRow {
|
||||
id: string;
|
||||
procedure: string;
|
||||
integrationIcons: { kind: IntegrationKind; name: string }[];
|
||||
status: "pending" | "error" | "success";
|
||||
fetchStatus: "fetching" | "paused" | "idle";
|
||||
dataUpdatedAt: number;
|
||||
}
|
||||
|
||||
const hiddenProcedures = new Set(["widget.app.ping"]);
|
||||
|
||||
const isTrackedWidgetQuery = (queryKey: readonly unknown[]): boolean => {
|
||||
const first = queryKey[0];
|
||||
if (!Array.isArray(first) || first.length < 2 || first[0] !== "widget") return false;
|
||||
|
||||
const path = first.join(".");
|
||||
return !hiddenProcedures.has(path);
|
||||
};
|
||||
|
||||
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
||||
|
||||
const parseWidgetQueryKey = (queryKey: readonly unknown[]): { procedure: string; integrationIds: string[] } => {
|
||||
const first = queryKey[0] as string[];
|
||||
const procedure = first.slice(1).map(capitalize).join(" — ");
|
||||
|
||||
const second = queryKey[1] as { input?: Record<string, unknown> } | undefined;
|
||||
const integrationIds = Array.isArray(second?.input?.integrationIds) ? (second.input.integrationIds as string[]) : [];
|
||||
|
||||
return { procedure, integrationIds };
|
||||
};
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
fetching: "orange",
|
||||
success: "green",
|
||||
error: "red",
|
||||
pending: "gray",
|
||||
};
|
||||
|
||||
const resolveRowColor = (fetchStatus: string, status: string) => {
|
||||
if (fetchStatus === "fetching") return statusColors.fetching!;
|
||||
return statusColors[status] ?? "gray";
|
||||
};
|
||||
|
||||
const resolveDotColor = (fetchingCount: number, errorCount: number) => {
|
||||
if (fetchingCount > 0) return statusColors.fetching!;
|
||||
if (errorCount > 0) return statusColors.error!;
|
||||
return statusColors.success!;
|
||||
};
|
||||
|
||||
interface CacheSnapshot {
|
||||
rows: QueryStatusRow[];
|
||||
fetchingCount: number;
|
||||
errorCount: number;
|
||||
}
|
||||
|
||||
const emptyCacheSnapshot: CacheSnapshot = { rows: [], fetchingCount: 0, errorCount: 0 };
|
||||
|
||||
function resolveIntegrationMap(queryClient: ReturnType<typeof useQueryClient>): Map<string, IntegrationInfo> {
|
||||
const cache = queryClient.getQueryCache();
|
||||
const integrationQuery = cache.find({ queryKey: [["integration", "all"]] });
|
||||
|
||||
const map = new Map<string, IntegrationInfo>();
|
||||
const data = integrationQuery?.state.data as IntegrationInfo[] | undefined;
|
||||
if (!data) return map;
|
||||
|
||||
for (const integration of data) {
|
||||
map.set(integration.id, integration);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function useQueryCacheStatus() {
|
||||
const queryClient = useQueryClient();
|
||||
const [snapshot, setSnapshot] = useState<CacheSnapshot>(emptyCacheSnapshot);
|
||||
|
||||
useEffect(() => {
|
||||
const update = () => {
|
||||
const cache = queryClient.getQueryCache();
|
||||
const queries = cache.findAll({ type: "active" }).filter((q) => isTrackedWidgetQuery(q.queryKey));
|
||||
const integrationMap = resolveIntegrationMap(queryClient);
|
||||
|
||||
let fetching = 0;
|
||||
let errors = 0;
|
||||
const nextRows: QueryStatusRow[] = queries.map((q) => {
|
||||
if (q.state.fetchStatus === "fetching") fetching++;
|
||||
if (q.state.status === "error") errors++;
|
||||
const { procedure, integrationIds } = parseWidgetQueryKey(q.queryKey);
|
||||
|
||||
const integrationIcons = integrationIds
|
||||
.map((id) => integrationMap.get(id))
|
||||
.filter((info): info is IntegrationInfo => info !== undefined)
|
||||
.map(({ kind, name }) => ({ kind, name }));
|
||||
|
||||
return {
|
||||
id: q.queryHash,
|
||||
procedure,
|
||||
integrationIcons,
|
||||
status: q.state.status,
|
||||
fetchStatus: q.state.fetchStatus,
|
||||
dataUpdatedAt: q.state.dataUpdatedAt,
|
||||
};
|
||||
});
|
||||
|
||||
setSnapshot({ rows: nextRows, fetchingCount: fetching, errorCount: errors });
|
||||
};
|
||||
|
||||
update();
|
||||
|
||||
let rafId: number | null = null;
|
||||
const unsubscribe = queryClient.getQueryCache().subscribe(() => {
|
||||
if (rafId !== null) return;
|
||||
rafId = requestAnimationFrame(() => {
|
||||
rafId = null;
|
||||
update();
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
if (rafId !== null) cancelAnimationFrame(rafId);
|
||||
};
|
||||
}, [queryClient]);
|
||||
|
||||
return { ...snapshot, totalCount: snapshot.rows.length };
|
||||
}
|
||||
|
||||
const StatusDot = forwardRef<HTMLButtonElement, { color: string; processing: boolean }>(
|
||||
({ color, processing, ...others }, ref) => (
|
||||
<UnstyledButton ref={ref} {...others} style={{ lineHeight: 0 }}>
|
||||
<Indicator color={color} size={10} processing={processing} withBorder>
|
||||
<Box w={0} h={0} />
|
||||
</Indicator>
|
||||
</UnstyledButton>
|
||||
),
|
||||
);
|
||||
|
||||
export const QueryRefreshIndicator = () => {
|
||||
const [isEditMode] = useEditMode();
|
||||
const { rows, fetchingCount, errorCount, totalCount } = useQueryCacheStatus();
|
||||
|
||||
if (isEditMode || totalCount === 0) return null;
|
||||
|
||||
const isFetching = fetchingCount > 0;
|
||||
const dotColor = resolveDotColor(fetchingCount, errorCount);
|
||||
|
||||
return (
|
||||
<Menu width={340} position="bottom-start" shadow="md" withArrow>
|
||||
<Menu.Target>
|
||||
<StatusDot color={dotColor} processing={isFetching} />
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<ScrollArea.Autosize mah={360}>
|
||||
{rows.map((row) => (
|
||||
<QueryMenuItem key={row.id} row={row} />
|
||||
))}
|
||||
</ScrollArea.Autosize>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const SmallDot = ({ color }: { color: string }) => (
|
||||
<Box
|
||||
w={8}
|
||||
h={8}
|
||||
miw={8}
|
||||
style={{ borderRadius: "50%", flexShrink: 0, backgroundColor: `var(--mantine-color-${color}-6)` }}
|
||||
/>
|
||||
);
|
||||
|
||||
const QueryMenuItem = ({ row }: { row: QueryStatusRow }) => {
|
||||
const rowColor = resolveRowColor(row.fetchStatus, row.status);
|
||||
const updatedLabel = row.dataUpdatedAt > 0 ? dayjs(row.dataUpdatedAt).fromNow() : "—";
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
leftSection={
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<SmallDot color={rowColor} />
|
||||
{row.integrationIcons.length > 0 && <IntegrationAvatars icons={row.integrationIcons} />}
|
||||
</Group>
|
||||
}
|
||||
rightSection={
|
||||
<Text size="xs" c="dimmed" style={{ whiteSpace: "nowrap" }}>
|
||||
{updatedLabel}
|
||||
</Text>
|
||||
}
|
||||
style={{ cursor: "default" }}
|
||||
>
|
||||
<Text size="xs" lineClamp={1}>
|
||||
{row.procedure}
|
||||
</Text>
|
||||
</Menu.Item>
|
||||
);
|
||||
};
|
||||
|
||||
const IntegrationAvatars = ({ icons }: { icons: QueryStatusRow["integrationIcons"] }) => (
|
||||
<Group gap={2} wrap="nowrap">
|
||||
{icons.slice(0, 3).map((icon, index) => (
|
||||
<Tooltip key={`${icon.kind}-${index}`} label={icon.name} withArrow>
|
||||
<Avatar size={18} radius={0} src={getIconUrl(icon.kind)} />
|
||||
</Tooltip>
|
||||
))}
|
||||
</Group>
|
||||
);
|
||||
@@ -1,10 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Group } from "@mantine/core";
|
||||
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
|
||||
import { QueryRefreshIndicator } from "../header/query-refresh-indicator";
|
||||
import { homarrLogoPath, homarrPageTitle } from "./homarr-logo";
|
||||
import type { LogoWithTitleProps } from "./logo";
|
||||
import { Logo, LogoWithTitle } from "./logo";
|
||||
@@ -36,14 +33,11 @@ export const BoardLogoWithTitle = ({ size, hideTitleOnMobile }: CommonLogoWithTi
|
||||
const board = useRequiredBoard();
|
||||
const imageOptions = useImageOptions();
|
||||
return (
|
||||
<Group gap="xs" wrap="nowrap" align="center">
|
||||
<LogoWithTitle
|
||||
size={size}
|
||||
hideTitleOnMobile={hideTitleOnMobile}
|
||||
title={board.pageTitle ?? homarrPageTitle}
|
||||
image={imageOptions}
|
||||
/>
|
||||
<QueryRefreshIndicator />
|
||||
</Group>
|
||||
<LogoWithTitle
|
||||
size={size}
|
||||
hideTitleOnMobile={hideTitleOnMobile}
|
||||
title={board.pageTitle ?? homarrPageTitle}
|
||||
image={imageOptions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { QueryKey } from "@tanstack/react-query";
|
||||
|
||||
export const queryCacheMaxAgeMs = 1000 * 60 * 60 * 24;
|
||||
export const queryCacheRetentionMs = 1000 * 60 * 60 * 24;
|
||||
export const queryCacheDefaultStaleTimeMs = 1000 * 60 * 5;
|
||||
export const queryCacheMaxValueBytes = 1024 * 1024;
|
||||
export const queryCacheStoragePrefix = "homarr-widget-query";
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { eq } from "@homarr/db";
|
||||
import { boards } from "@homarr/db/schema";
|
||||
import { createQueryCacheChannel } from "@homarr/redis";
|
||||
|
||||
import { queryCacheMaxAgeMs, queryCacheMaxValueBytes } from "../query-cache";
|
||||
import { queryCacheMaxValueBytes, queryCacheRetentionMs } from "../query-cache";
|
||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
||||
|
||||
@@ -22,7 +22,7 @@ const createChannel = (input: z.infer<typeof queryCacheKeyInput>, userId: string
|
||||
userId,
|
||||
boardId: input.boardId,
|
||||
key: input.key,
|
||||
ttlMs: queryCacheMaxAgeMs,
|
||||
ttlMs: queryCacheRetentionMs,
|
||||
maxValueBytes: queryCacheMaxValueBytes,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { defaultShouldDehydrateQuery, QueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { queryCacheDefaultStaleTimeMs } from "./query-cache";
|
||||
|
||||
/**
|
||||
* Creates a headers callback for a given source
|
||||
* It will set the x-trpc-source header and cookies if needed
|
||||
@@ -58,7 +60,7 @@ export const makeQueryClient = () => {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 5 * 1000,
|
||||
staleTime: queryCacheDefaultStaleTimeMs,
|
||||
},
|
||||
dehydrate: {
|
||||
shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === "pending",
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { createId } from "@homarr/common";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { boards, users } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
import { isPersistableWidgetQueryKey, queryCacheMaxAgeMs, queryCacheMaxValueBytes } from "@homarr/api/query-cache";
|
||||
import {
|
||||
isPersistableWidgetQueryKey,
|
||||
queryCacheDefaultStaleTimeMs,
|
||||
queryCacheMaxValueBytes,
|
||||
queryCacheRetentionMs,
|
||||
} from "@homarr/api/query-cache";
|
||||
|
||||
import { queryCacheRouter } from "../router/query-cache";
|
||||
|
||||
@@ -141,7 +147,7 @@ describe("queryCacheRouter", () => {
|
||||
userId: "anonymous",
|
||||
boardId: "board-1",
|
||||
key: "query-key",
|
||||
ttlMs: queryCacheMaxAgeMs,
|
||||
ttlMs: queryCacheRetentionMs,
|
||||
maxValueBytes: queryCacheMaxValueBytes,
|
||||
});
|
||||
});
|
||||
@@ -161,7 +167,7 @@ describe("queryCacheRouter", () => {
|
||||
expect(redisMock.calls.at(-1)).toMatchObject({
|
||||
userId: "user-1",
|
||||
boardId: "board-1",
|
||||
ttlMs: queryCacheMaxAgeMs,
|
||||
ttlMs: queryCacheRetentionMs,
|
||||
maxValueBytes: queryCacheMaxValueBytes,
|
||||
});
|
||||
});
|
||||
@@ -180,8 +186,39 @@ describe("queryCacheRouter", () => {
|
||||
await caller.setItem({ boardId: "board-1", key: "query-key", value: "cached-value" });
|
||||
await expect(caller.getItem({ boardId: "board-1", key: "query-key" })).resolves.toBe("cached-value");
|
||||
|
||||
await vi.advanceTimersByTimeAsync(queryCacheMaxAgeMs + 1);
|
||||
await vi.advanceTimersByTimeAsync(queryCacheRetentionMs + 1);
|
||||
|
||||
await expect(caller.getItem({ boardId: "board-1", key: "query-key" })).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("query cache stale handling", () => {
|
||||
test("uses TanStack Query dataUpdatedAt and staleTime to decide when cached data refetches", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-01T00:10:00.000Z"));
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
staleTime: queryCacheDefaultStaleTimeMs,
|
||||
},
|
||||
},
|
||||
});
|
||||
const queryKey = [["widget", "example"], { input: { integrationIds: ["integration-1"] }, type: "query" }];
|
||||
const queryFn = vi.fn().mockResolvedValue("fresh");
|
||||
|
||||
queryClient.setQueryData(queryKey, "cached", {
|
||||
updatedAt: Date.now() - queryCacheDefaultStaleTimeMs - 1,
|
||||
});
|
||||
|
||||
await expect(
|
||||
queryClient.fetchQuery({
|
||||
queryKey,
|
||||
queryFn,
|
||||
}),
|
||||
).resolves.toBe("fresh");
|
||||
|
||||
expect(queryFn).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -151,7 +151,7 @@ const AnchorNoteWidgetContent = ({ options, integrationId, noteId }: AnchorNoteW
|
||||
noteId,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
staleTime: 15 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
|
||||
@@ -28,7 +28,6 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
|
||||
id: options.appId,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
|
||||
@@ -40,7 +40,7 @@ const FetchCalendar = ({ month, setMonth, isEditMode, integrationIds, options }:
|
||||
showUnmonitored: options.showUnmonitored,
|
||||
};
|
||||
const { data } = clientApi.widget.calendar.findAllEvents.useQuery(input, {
|
||||
refetchOnMount: false,
|
||||
staleTime: 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
|
||||
@@ -31,7 +31,10 @@ interface CoolifyContentProps {
|
||||
}
|
||||
|
||||
function CoolifyContent({ integrationIds, options, width }: CoolifyContentProps) {
|
||||
const { data: instancesData = [] } = clientApi.widget.coolify.getInstancesInfo.useQuery({ integrationIds });
|
||||
const { data: instancesData = [] } = clientApi.widget.coolify.getInstancesInfo.useQuery(
|
||||
{ integrationIds },
|
||||
{ staleTime: 30 * 1000 },
|
||||
);
|
||||
|
||||
const utils = clientApi.useUtils();
|
||||
clientApi.widget.coolify.subscribeInstancesInfo.useSubscription(
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function DnsHoleControlsWidget({
|
||||
integrationIds,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
staleTime: 5 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function DnsHoleSummaryWidget({ options, integrationIds }: Widget
|
||||
integrationIds,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
staleTime: 5 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
|
||||
@@ -204,7 +204,9 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
|
||||
const isTiny = width <= 256;
|
||||
|
||||
const utils = clientApi.useUtils();
|
||||
const { data } = clientApi.docker.getContainers.useQuery();
|
||||
const { data } = clientApi.docker.getContainers.useQuery(undefined, {
|
||||
staleTime: 20 * 1000,
|
||||
});
|
||||
const containers = data?.containers ?? [];
|
||||
const timestamp = useMemo(() => data?.timestamp ?? new Date(), [data?.timestamp]);
|
||||
const relativeTime = useTimeAgo(timestamp);
|
||||
|
||||
@@ -93,7 +93,7 @@ export default function DownloadClientsWidget({
|
||||
limitPerIntegration: options.limitPerIntegration,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
staleTime: 5 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
|
||||
@@ -189,7 +189,7 @@ export const useUpdatingCpuStatus = (integrationIds: string[]) => {
|
||||
integrationIds,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
staleTime: 5 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
@@ -230,7 +230,7 @@ export const useUpdatingMemoryStatus = (integrationIds: string[]) => {
|
||||
integrationIds,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
staleTime: 15 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
@@ -271,7 +271,7 @@ export const useUpdatingVersionStatus = (integrationIds: string[]) => {
|
||||
integrationIds,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
staleTime: 60 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
@@ -311,7 +311,7 @@ export const useUpdatingInterfacesStatus = (integrationIds: string[]) => {
|
||||
integrationIds,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
staleTime: 30 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
|
||||
@@ -40,7 +40,7 @@ export const ClusterHealthMonitoring = ({
|
||||
integrationId,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
staleTime: 5 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
|
||||
@@ -57,7 +57,7 @@ export const SystemHealthMonitoring = ({
|
||||
integrationIds,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
staleTime: 5 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function ImmichAlbumCarouselWidget({
|
||||
integrationId: integrationIds[0] ?? "",
|
||||
albumId: options.albumId ?? "",
|
||||
},
|
||||
{ enabled: Boolean(options.albumId) },
|
||||
{ enabled: Boolean(options.albumId), staleTime: 15 * 60 * 1000 },
|
||||
);
|
||||
|
||||
if (!options.albumId) {
|
||||
|
||||
@@ -17,9 +17,12 @@ export default function ImmichServerStatsWidget({
|
||||
options,
|
||||
}: WidgetComponentProps<"immich-serverStats">) {
|
||||
const t = useI18n();
|
||||
const { data: stats } = clientApi.widget.immich.getServerStats.useQuery({
|
||||
integrationId: integrationIds[0] ?? "",
|
||||
});
|
||||
const { data: stats } = clientApi.widget.immich.getServerStats.useQuery(
|
||||
{
|
||||
integrationId: integrationIds[0] ?? "",
|
||||
},
|
||||
{ staleTime: 15 * 60 * 1000 },
|
||||
);
|
||||
|
||||
if (!stats) return <WidgetEmptyState />;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function IndexerManagerWidget({
|
||||
const { data: indexersData = [] } = clientApi.widget.indexerManager.getIndexersStatus.useQuery(
|
||||
{ integrationIds },
|
||||
{
|
||||
refetchOnMount: false,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
|
||||
@@ -18,9 +18,12 @@ import { OverflowBadge } from "@homarr/ui";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
|
||||
export default function MediaReleasesWidget({ options, integrationIds }: WidgetComponentProps<"mediaReleases">) {
|
||||
const { data: releases = [] } = clientApi.widget.mediaRelease.getMediaReleases.useQuery({
|
||||
integrationIds,
|
||||
});
|
||||
const { data: releases = [] } = clientApi.widget.mediaRelease.getMediaReleases.useQuery(
|
||||
{
|
||||
integrationIds,
|
||||
},
|
||||
{ staleTime: 5 * 60 * 1000 },
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack p="xs" gap="sm">
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function MediaServerWidget({
|
||||
integrationIds,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
staleTime: 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
},
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function MediaServerWidget({
|
||||
integrationIds,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
staleTime: 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function MediaServerWidget({
|
||||
showOnlyPlaying: options.showOnlyPlaying,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
staleTime: 5 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
},
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function MediaTranscodingWidget({
|
||||
page: queuePage,
|
||||
};
|
||||
const { data: transcodingData } = clientApi.widget.mediaTranscoding.getDataAsync.useQuery(input, {
|
||||
refetchOnMount: false,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
@@ -11,7 +11,9 @@ import { WidgetEmptyState } from "../../common/empty-state";
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
|
||||
export default function MinecraftServerStatusWidget({ options }: WidgetComponentProps<"minecraftServerStatus">) {
|
||||
const { data: result } = clientApi.widget.minecraft.getServerStatus.useQuery(options);
|
||||
const { data: result } = clientApi.widget.minecraft.getServerStatus.useQuery(options, {
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
const utils = clientApi.useUtils();
|
||||
clientApi.widget.minecraft.subscribeServerStatus.useSubscription(options, {
|
||||
onData(data) {
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function NetworkControllerNetworkStatusWidget({
|
||||
integrationIds,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function NetworkControllerSummaryWidget({
|
||||
integrationIds,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function NotificationsWidget({ options, integrationIds }: WidgetC
|
||||
integrationIds,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
|
||||
@@ -13,7 +13,7 @@ import classes from "./component.module.scss";
|
||||
|
||||
const useLiveFeedEntries = (input: RouterInputs["widget"]["rssFeed"]["getFeeds"]) => {
|
||||
const { data: feedEntries = [] } = clientApi.widget.rssFeed.getFeeds.useQuery(input, {
|
||||
refetchOnMount: false,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
|
||||
@@ -23,7 +23,9 @@ export default function SmartHomeEntityStateWidget({
|
||||
entityId: options.entityId,
|
||||
integrationId,
|
||||
};
|
||||
const { data: entityState } = clientApi.widget.smartHome.entityState.useQuery(input);
|
||||
const { data: entityState } = clientApi.widget.smartHome.entityState.useQuery(input, {
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
|
||||
const utils = clientApi.useUtils();
|
||||
|
||||
|
||||
@@ -20,7 +20,10 @@ export default function SpeedtestTrackerWidget({
|
||||
isEditMode,
|
||||
}: WidgetComponentProps<"speedtestTracker">) {
|
||||
const t = useScopedI18n("widget.speedtestTracker");
|
||||
const { data: dashboardData = [] } = clientApi.widget.speedtestTracker.getDashboard.useQuery({ integrationIds });
|
||||
const { data: dashboardData = [] } = clientApi.widget.speedtestTracker.getDashboard.useQuery(
|
||||
{ integrationIds },
|
||||
{ staleTime: 10 * 60 * 1000 },
|
||||
);
|
||||
|
||||
const utils = clientApi.useUtils();
|
||||
clientApi.widget.speedtestTracker.subscribeToDashboard.useSubscription(
|
||||
|
||||
@@ -25,7 +25,9 @@ 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: result } = clientApi.widget.stockPrice.getPriceHistory.useQuery(options);
|
||||
const { data: result } = clientApi.widget.stockPrice.getPriceHistory.useQuery(options, {
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
if (!result) return <WidgetEmptyState />;
|
||||
const { data } = result;
|
||||
|
||||
@@ -33,7 +33,9 @@ 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.useQuery(queryInput);
|
||||
const { data = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery(queryInput, {
|
||||
staleTime: 5 * 1000,
|
||||
});
|
||||
const utils = clientApi.useUtils();
|
||||
|
||||
const board = useRequiredBoard();
|
||||
|
||||
@@ -18,9 +18,14 @@ const MAX_QUEUE_SIZE = 15;
|
||||
export default function SystemResources({ integrationIds, options }: WidgetComponentProps<"systemResources">) {
|
||||
const { ref, width } = useElementSize();
|
||||
|
||||
const { data = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery({
|
||||
integrationIds,
|
||||
});
|
||||
const { data = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery(
|
||||
{
|
||||
integrationIds,
|
||||
},
|
||||
{
|
||||
staleTime: 5 * 1000,
|
||||
},
|
||||
);
|
||||
const memoryCapacityInBytes =
|
||||
(data[0]?.healthInfo.memAvailableInBytes ?? 0) + (data[0]?.healthInfo.memUsedInBytes ?? 0);
|
||||
|
||||
|
||||
@@ -26,11 +26,16 @@ interface TimetableWidgetInnerProps {
|
||||
}
|
||||
|
||||
const TimetableWidgetInner = ({ station, integrationId }: TimetableWidgetInnerProps) => {
|
||||
const { data: timetable } = clientApi.widget.timetable.getTimetable.useQuery({
|
||||
integrationId,
|
||||
stationId: station.value,
|
||||
limit: 10,
|
||||
});
|
||||
const { data: timetable } = clientApi.widget.timetable.getTimetable.useQuery(
|
||||
{
|
||||
integrationId,
|
||||
stationId: station.value,
|
||||
limit: 10,
|
||||
},
|
||||
{
|
||||
staleTime: 60 * 1000,
|
||||
},
|
||||
);
|
||||
const t = useScopedI18n("widget.timetable");
|
||||
|
||||
return (
|
||||
|
||||
@@ -35,7 +35,10 @@ interface TracearrContentProps {
|
||||
|
||||
function TracearrContent({ integrationIds, options, width, isEditMode }: TracearrContentProps) {
|
||||
const t = useScopedI18n("widget.tracearr");
|
||||
const { data: dashboardData = [] } = clientApi.widget.tracearr.getDashboard.useQuery({ integrationIds });
|
||||
const { data: dashboardData = [] } = clientApi.widget.tracearr.getDashboard.useQuery(
|
||||
{ integrationIds },
|
||||
{ staleTime: 5 * 1000 },
|
||||
);
|
||||
|
||||
const utils = clientApi.useUtils();
|
||||
clientApi.widget.tracearr.subscribeToDashboard.useSubscription(
|
||||
|
||||
@@ -40,15 +40,20 @@ export function UmamiContent({
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const tickColor = colorScheme === "dark" ? "#c1c2c5" : "#495057";
|
||||
|
||||
const { data: results = [] } = clientApi.widget.umami.getVisitorStats.useQuery({
|
||||
integrationIds,
|
||||
websiteId,
|
||||
timeFrame,
|
||||
eventName,
|
||||
});
|
||||
const { data: results = [] } = clientApi.widget.umami.getVisitorStats.useQuery(
|
||||
{
|
||||
integrationIds,
|
||||
websiteId,
|
||||
timeFrame,
|
||||
eventName,
|
||||
},
|
||||
{ staleTime: 5 * 60 * 1000 },
|
||||
);
|
||||
|
||||
const activeVisitorsInput = { integrationId: integrationIds[0] ?? "", websiteId };
|
||||
const { data: activeVisitors = 0 } = clientApi.widget.umami.getActiveVisitors.useQuery(activeVisitorsInput);
|
||||
const { data: activeVisitors = 0 } = clientApi.widget.umami.getActiveVisitors.useQuery(activeVisitorsInput, {
|
||||
staleTime: 30 * 1000,
|
||||
});
|
||||
const utils = clientApi.useUtils();
|
||||
clientApi.widget.umami.subscribeActiveVisitors.useSubscription(activeVisitorsInput, {
|
||||
onData(count) {
|
||||
@@ -58,7 +63,7 @@ export function UmamiContent({
|
||||
|
||||
const { data: multiEventSeries } = clientApi.widget.umami.getMultiEventTimeSeries.useQuery(
|
||||
{ integrationId: integrationIds[0] ?? "", websiteId, timeFrame, eventNames: [...eventNames].toSorted() },
|
||||
{ enabled: viewMode === "events" && eventNames.length > 0 },
|
||||
{ enabled: viewMode === "events" && eventNames.length > 0, staleTime: 5 * 60 * 1000 },
|
||||
);
|
||||
|
||||
const multiEventTotal = multiEventSeries
|
||||
|
||||
@@ -28,12 +28,15 @@ export function UmamiEventsContent({
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const tickColor = colorScheme === "dark" ? "#c1c2c5" : "#495057";
|
||||
|
||||
const { data: series = [] } = clientApi.widget.umami.getMultiEventTimeSeries.useQuery({
|
||||
integrationId: integrationIds[0] ?? "",
|
||||
websiteId,
|
||||
timeFrame,
|
||||
eventNames: [...eventNames].toSorted(),
|
||||
});
|
||||
const { data: series = [] } = clientApi.widget.umami.getMultiEventTimeSeries.useQuery(
|
||||
{
|
||||
integrationId: integrationIds[0] ?? "",
|
||||
websiteId,
|
||||
timeFrame,
|
||||
eventNames: [...eventNames].toSorted(),
|
||||
},
|
||||
{ staleTime: 5 * 60 * 1000 },
|
||||
);
|
||||
|
||||
if (eventNames.length === 0) {
|
||||
return (
|
||||
|
||||
@@ -15,23 +15,29 @@ interface UmamiTopListProps {
|
||||
|
||||
export function UmamiTopPagesContent({ integrationIds, websiteId, timeFrame, limit }: UmamiTopListProps) {
|
||||
const t = useScopedI18n("widget.umami");
|
||||
const { data = [] } = clientApi.widget.umami.getTopPages.useQuery({
|
||||
integrationId: integrationIds[0] ?? "",
|
||||
websiteId,
|
||||
timeFrame,
|
||||
limit,
|
||||
});
|
||||
const { data = [] } = clientApi.widget.umami.getTopPages.useQuery(
|
||||
{
|
||||
integrationId: integrationIds[0] ?? "",
|
||||
websiteId,
|
||||
timeFrame,
|
||||
limit,
|
||||
},
|
||||
{ staleTime: 5 * 60 * 1000 },
|
||||
);
|
||||
return <UmamiTopList items={data} heading={t("option.viewMode.option.topPages")} emptyLabel={t("topPages.direct")} />;
|
||||
}
|
||||
|
||||
export function UmamiTopReferrersContent({ integrationIds, websiteId, timeFrame, limit }: UmamiTopListProps) {
|
||||
const t = useScopedI18n("widget.umami");
|
||||
const { data = [] } = clientApi.widget.umami.getTopReferrers.useQuery({
|
||||
integrationId: integrationIds[0] ?? "",
|
||||
websiteId,
|
||||
timeFrame,
|
||||
limit,
|
||||
});
|
||||
const { data = [] } = clientApi.widget.umami.getTopReferrers.useQuery(
|
||||
{
|
||||
integrationId: integrationIds[0] ?? "",
|
||||
websiteId,
|
||||
timeFrame,
|
||||
limit,
|
||||
},
|
||||
{ staleTime: 5 * 60 * 1000 },
|
||||
);
|
||||
return (
|
||||
<UmamiTopList
|
||||
items={data}
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function WeatherWidget({ isEditMode, options }: WidgetComponentPr
|
||||
longitude: options.location.longitude,
|
||||
};
|
||||
const { data: weather } = clientApi.widget.weather.atLocation.useQuery(input, {
|
||||
refetchOnMount: false,
|
||||
staleTime: 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user