Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/dashboard/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -49,7 +50,7 @@ export default async function Layout({ children }: LayoutProps) {

<div className="w-full">
{workspace.enabled ? (
children
<QueryTimeProvider>{children}</QueryTimeProvider>
) : (
<div className="flex items-center justify-center w-full h-full">
<Empty>
Expand Down
Original file line number Diff line number Diff line change
@@ -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: "",
};
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<SortFields>();

//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,
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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: "",
};
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -23,6 +24,8 @@ export function useRatelimitLogsQuery({
const [realtimeLogsMap, setRealtimeLogsMap] = useState(() => new Map<string, RatelimitLog>());
const [totalCount, setTotalCount] = useState(0);

const { queryTime: timestamp } = useQueryTime();

const { filters } = useFilters();
const queryClient = trpc.useUtils();

Expand All @@ -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: [] },
Expand Down Expand Up @@ -105,7 +106,7 @@ export function useRatelimitLogsQuery({
});

return params;
}, [filters, limit, dateNow, namespaceId]);
}, [filters, limit, namespaceId, timestamp]);

// Main query for historical data
const {
Expand Down
80 changes: 80 additions & 0 deletions apps/dashboard/providers/query-time-provider.tsx
Original file line number Diff line number Diff line change
@@ -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<QueryTimeContextType | undefined>(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 (
<QueryTimeContext.Provider value={{ queryTime, refreshQueryTime }}>
{children}
</QueryTimeContext.Provider>
);
};

export const useQueryTime = () => {
const context = useContext(QueryTimeContext);
if (context === undefined) {
throw new Error("useQueryTime must be used within a QueryTimeProvider");
}
return context;
};
Loading