diff --git a/apps/dashboard/app/(app)/layout.tsx b/apps/dashboard/app/(app)/layout.tsx index 91d36a224d..c90af8707f 100644 --- a/apps/dashboard/app/(app)/layout.tsx +++ b/apps/dashboard/app/(app)/layout.tsx @@ -6,6 +6,7 @@ import { db } from "@/lib/db"; import { Empty } from "@unkey/ui"; import Link from "next/link"; import { redirect } from "next/navigation"; +import { QueryTimeProvider } from "../../providers/query-time-provider"; interface LayoutProps { children: React.ReactNode; @@ -49,7 +50,7 @@ export default async function Layout({ children }: LayoutProps) {
{workspace.enabled ? ( - children + {children} ) : (
diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/charts/bar-chart/hooks/use-fetch-timeseries.ts b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/charts/bar-chart/hooks/use-fetch-timeseries.ts index d834c0054d..381e63a0da 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/charts/bar-chart/hooks/use-fetch-timeseries.ts +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/charts/bar-chart/hooks/use-fetch-timeseries.ts @@ -1,19 +1,20 @@ import { formatTimestampForChart } from "@/components/logs/chart/utils/format-timestamp"; import { TIMESERIES_DATA_WINDOW } from "@/components/logs/constants"; import { trpc } from "@/lib/trpc/client"; +import { useQueryTime } from "@/providers/query-time-provider"; import { useMemo } from "react"; import { useFilters } from "../../../../hooks/use-filters"; import type { RatelimitOverviewQueryTimeseriesPayload } from "../query-timeseries.schema"; export const useFetchRatelimitOverviewTimeseries = (namespaceId: string) => { const { filters } = useFilters(); - const dateNow = useMemo(() => Date.now(), []); + const { queryTime: timestamp } = useQueryTime(); const queryParams = useMemo(() => { const params: RatelimitOverviewQueryTimeseriesPayload = { namespaceId, - startTime: dateNow - TIMESERIES_DATA_WINDOW, - endTime: dateNow, + startTime: timestamp - TIMESERIES_DATA_WINDOW, + endTime: timestamp, identifiers: { filters: [] }, since: "", }; @@ -52,7 +53,7 @@ export const useFetchRatelimitOverviewTimeseries = (namespaceId: string) => { }); return params; - }, [filters, dateNow, namespaceId]); + }, [filters, timestamp, namespaceId]); const { data, isLoading, isError } = trpc.ratelimit.logs.queryRatelimitTimeseries.useQuery( queryParams, diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/controls/components/logs-refresh.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/controls/components/logs-refresh.tsx index 25e6f6dcdc..8ab9cd22b3 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/controls/components/logs-refresh.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/controls/components/logs-refresh.tsx @@ -1,13 +1,17 @@ import { RefreshButton } from "@/components/logs/refresh-button"; import { trpc } from "@/lib/trpc/client"; +import { useQueryTime } from "@/providers/query-time-provider"; import { useFilters } from "../../../hooks/use-filters"; export const LogsRefresh = () => { const { filters } = useFilters(); + const { refreshQueryTime } = useQueryTime(); + const { ratelimit } = trpc.useUtils(); const hasRelativeFilter = filters.find((f) => f.field === "since"); const handleRefresh = () => { + refreshQueryTime(); ratelimit.overview.logs.query.invalidate(); ratelimit.overview.logs.queryRatelimitLatencyTimeseries.invalidate(); ratelimit.logs.queryRatelimitTimeseries.invalidate(); diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/hooks/use-logs-query.ts b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/hooks/use-logs-query.ts index 256ee3ec1c..bd9db15a39 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/hooks/use-logs-query.ts +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/hooks/use-logs-query.ts @@ -1,5 +1,6 @@ import { HISTORICAL_DATA_WINDOW } from "@/components/logs/constants"; import { trpc } from "@/lib/trpc/client"; +import { useQueryTime } from "@/providers/query-time-provider"; import type { RatelimitOverviewLog } from "@unkey/clickhouse/src/ratelimits"; import { useEffect, useMemo, useState } from "react"; import { useSort } from "../../../../../../../../components/logs/hooks/use-sort"; @@ -18,18 +19,19 @@ export function useRatelimitOverviewLogsQuery({ namespaceId, limit = 50 }: UseLo ); const { filters } = useFilters(); + const { queryTime: timestamp } = useQueryTime(); const historicalLogs = useMemo(() => Array.from(historicalLogsMap.values()), [historicalLogsMap]); const { sorts } = useSort(); //Required for preventing double trpc call during initial render - const dateNow = useMemo(() => Date.now(), []); + const queryParams = useMemo(() => { const params: RatelimitQueryOverviewLogsPayload = { limit, - startTime: dateNow - HISTORICAL_DATA_WINDOW, - endTime: dateNow, + startTime: timestamp - HISTORICAL_DATA_WINDOW, + endTime: timestamp, identifiers: { filters: [] }, status: { filters: [] }, namespaceId, @@ -84,7 +86,7 @@ export function useRatelimitOverviewLogsQuery({ namespaceId, limit = 50 }: UseLo }); return params; - }, [filters, limit, dateNow, namespaceId, sorts]); + }, [filters, limit, timestamp, namespaceId, sorts]); // Main query for historical data const { diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/charts/hooks/use-fetch-timeseries.ts b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/charts/hooks/use-fetch-timeseries.ts index 33602ca29a..a5205f2324 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/charts/hooks/use-fetch-timeseries.ts +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/charts/hooks/use-fetch-timeseries.ts @@ -1,19 +1,20 @@ import { formatTimestampForChart } from "@/components/logs/chart/utils/format-timestamp"; import { TIMESERIES_DATA_WINDOW } from "@/components/logs/constants"; import { trpc } from "@/lib/trpc/client"; +import { useQueryTime } from "@/providers/query-time-provider"; import { useMemo } from "react"; import { useFilters } from "../../../hooks/use-filters"; import type { RatelimitQueryTimeseriesPayload } from "../query-timeseries.schema"; export const useFetchRatelimitTimeseries = (namespaceId: string) => { const { filters } = useFilters(); - const dateNow = useMemo(() => Date.now(), []); + const { queryTime: timestamp } = useQueryTime(); const queryParams = useMemo(() => { const params: RatelimitQueryTimeseriesPayload = { namespaceId, - startTime: dateNow - TIMESERIES_DATA_WINDOW, - endTime: dateNow, + startTime: timestamp - TIMESERIES_DATA_WINDOW, + endTime: timestamp, identifiers: { filters: [] }, since: "", }; @@ -52,7 +53,7 @@ export const useFetchRatelimitTimeseries = (namespaceId: string) => { }); return params; - }, [filters, dateNow, namespaceId]); + }, [filters, namespaceId, timestamp]); const { data, isLoading, isError } = trpc.ratelimit.logs.queryRatelimitTimeseries.useQuery( queryParams, diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-refresh.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-refresh.tsx index ba114836ad..3bf0b5cf5b 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-refresh.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-refresh.tsx @@ -1,15 +1,18 @@ import { RefreshButton } from "@/components/logs/refresh-button"; import { trpc } from "@/lib/trpc/client"; +import { useQueryTime } from "@/providers/query-time-provider"; import { useRatelimitLogsContext } from "../../../context/logs"; import { useFilters } from "../../../hooks/use-filters"; export const LogsRefresh = () => { const { toggleLive, isLive } = useRatelimitLogsContext(); + const { refreshQueryTime } = useQueryTime(); const { filters } = useFilters(); const { ratelimit } = trpc.useUtils(); const hasRelativeFilter = filters.find((f) => f.field === "since"); const handleRefresh = () => { + refreshQueryTime(); ratelimit.logs.query.invalidate(); ratelimit.logs.queryRatelimitTimeseries.invalidate(); }; diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts index 73ead945b5..b2f9c6ba09 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts @@ -1,5 +1,6 @@ import { HISTORICAL_DATA_WINDOW } from "@/components/logs/constants"; import { trpc } from "@/lib/trpc/client"; +import { useQueryTime } from "@/providers/query-time-provider"; import type { RatelimitLog } from "@unkey/clickhouse/src/ratelimits"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useFilters } from "../../../hooks/use-filters"; @@ -23,6 +24,8 @@ export function useRatelimitLogsQuery({ const [realtimeLogsMap, setRealtimeLogsMap] = useState(() => new Map()); const [totalCount, setTotalCount] = useState(0); + const { queryTime: timestamp } = useQueryTime(); + const { filters } = useFilters(); const queryClient = trpc.useUtils(); @@ -32,13 +35,11 @@ export function useRatelimitLogsQuery({ const historicalLogs = useMemo(() => Array.from(historicalLogsMap.values()), [historicalLogsMap]); - //Required for preventing double trpc call during initial render - const dateNow = useMemo(() => Date.now(), []); const queryParams = useMemo(() => { const params: RatelimitQueryLogsPayload = { limit, - startTime: dateNow - HISTORICAL_DATA_WINDOW, - endTime: dateNow, + startTime: timestamp - HISTORICAL_DATA_WINDOW, + endTime: timestamp, requestIds: { filters: [] }, identifiers: { filters: [] }, status: { filters: [] }, @@ -105,7 +106,7 @@ export function useRatelimitLogsQuery({ }); return params; - }, [filters, limit, dateNow, namespaceId]); + }, [filters, limit, namespaceId, timestamp]); // Main query for historical data const { diff --git a/apps/dashboard/providers/query-time-provider.tsx b/apps/dashboard/providers/query-time-provider.tsx new file mode 100644 index 0000000000..2aa760ef0b --- /dev/null +++ b/apps/dashboard/providers/query-time-provider.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { type ReactNode, createContext, useContext, useEffect, useState } from "react"; + +let queryTimestamp = Date.now(); +let subscribers: ((timestamp: number) => void)[] = []; + +export const refreshQueryTime = () => { + queryTimestamp = Date.now(); + subscribers.forEach((callback) => callback(queryTimestamp)); + return queryTimestamp; +}; + +type QueryTimeContextType = { + queryTime: number; + refreshQueryTime: () => number; +}; + +const QueryTimeContext = createContext(undefined); + +/** + * Provides a shared timestamp reference for data fetching operations. + * + * Prevents React Query and other data fetching mechanisms from triggering + * unnecessary refetches due to components generating new timestamps on each render. + * + * Without this utility, each component would: + * - Create its own `Date.now()` when mounted or re-rendered + * - Cause React Query to treat it as a new dependency and refetch + * - Result in different components showing data from inconsistent time windows + * + * + * ```tsx + * // In a data fetching hook + * const { queryTime } = useQueryTime(); + * + * // Use in query parameters + * const params = { + * startTime: queryTime - TIME_WINDOW, + * endTime: queryTime + * }; + * + * // Only refetches when explicitly refreshed + * const { data } = useQuery(['data', params], fetchData); + * ``` + * + * When you need to refresh all queries simultaneously: + * ```tsx + * const { refreshQueryTime } = useQueryTime(); + * + * // Updates timestamp for ALL components + * const handleRefresh = () => refreshQueryTime(); + * ``` + */ +export const QueryTimeProvider = ({ children }: { children: ReactNode }) => { + const [queryTime, setQueryTime] = useState(queryTimestamp); + + useEffect(() => { + const callback = (newTimestamp: number) => setQueryTime(newTimestamp); + subscribers.push(callback); + + return () => { + subscribers = subscribers.filter((cb) => cb !== callback); + }; + }, []); + + return ( + + {children} + + ); +}; + +export const useQueryTime = () => { + const context = useContext(QueryTimeContext); + if (context === undefined) { + throw new Error("useQueryTime must be used within a QueryTimeProvider"); + } + return context; +};