diff --git a/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/logs-table.tsx b/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/logs-table.tsx index e7c5b0a59f..69d3339d49 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/logs-table.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/logs-table.tsx @@ -181,6 +181,9 @@ export const KeysOverviewLogsTable = ({ apiId, setSelectedLog, log: selectedLog selectedItem={selectedLog} keyExtractor={(log) => log.request_id} rowClassName={(log) => getRowClassName(log, selectedLog as KeysOverviewLog)} + loadMoreFooterProps={{ + hide: true, + }} emptyState={
diff --git a/apps/dashboard/app/(app)/logs/components/table/hooks/use-logs-query.ts b/apps/dashboard/app/(app)/logs/components/table/hooks/use-logs-query.ts index 972d64428a..8886a52ebd 100644 --- a/apps/dashboard/app/(app)/logs/components/table/hooks/use-logs-query.ts +++ b/apps/dashboard/app/(app)/logs/components/table/hooks/use-logs-query.ts @@ -22,6 +22,7 @@ export function useLogsQuery({ }: UseLogsQueryParams = {}) { const [historicalLogsMap, setHistoricalLogsMap] = useState(() => new Map()); const [realtimeLogsMap, setRealtimeLogsMap] = useState(() => new Map()); + const [totalCount, setTotalCount] = useState(0); const { filters } = useFilters(); const queryClient = trpc.useUtils(); @@ -213,6 +214,10 @@ export function useLogsQuery({ }); }); setHistoricalLogsMap(newMap); + + if (initialData.pages.length > 0) { + setTotalCount(initialData.pages[0].total); + } } }, [initialData]); @@ -231,6 +236,7 @@ export function useLogsQuery({ loadMore: fetchNextPage, isLoadingMore: isFetchingNextPage, isPolling: startPolling, + total: totalCount, }; } diff --git a/apps/dashboard/app/(app)/logs/components/table/logs-table.tsx b/apps/dashboard/app/(app)/logs/components/table/logs-table.tsx index e98610921c..8dd28fb992 100644 --- a/apps/dashboard/app/(app)/logs/components/table/logs-table.tsx +++ b/apps/dashboard/app/(app)/logs/components/table/logs-table.tsx @@ -116,10 +116,11 @@ const additionalColumns: Column[] = [ export const LogsTable = () => { const { displayProperties, setSelectedLog, selectedLog, isLive } = useLogsContext(); - const { realtimeLogs, historicalLogs, isLoading, isLoadingMore, loadMore } = useLogsQuery({ - startPolling: isLive, - pollIntervalMs: 2000, - }); + const { realtimeLogs, historicalLogs, isLoading, isLoadingMore, loadMore, hasMore, total } = + useLogsQuery({ + startPolling: isLive, + pollIntervalMs: 2000, + }); const getRowClassName = (log: Log) => { const style = getStatusStyle(log.response_status); @@ -246,6 +247,19 @@ export const LogsTable = () => { keyExtractor={(log) => log.request_id} rowClassName={getRowClassName} selectedClassName={getSelectedClassName} + loadMoreFooterProps={{ + hide: isLoading, + buttonText: "Load more logs", + hasMore, + countInfoText: ( +
+ Showing {historicalLogs.length} + of + {total} + requests +
+ ), + }} emptyState={
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 d5aebc695f..e0a907dfbd 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 @@ -12,6 +12,7 @@ type UseLogsQueryParams = { }; export function useRatelimitOverviewLogsQuery({ namespaceId, limit = 50 }: UseLogsQueryParams) { + const [totalCount, setTotalCount] = useState(0); const [historicalLogsMap, setHistoricalLogsMap] = useState( () => new Map(), ); @@ -109,6 +110,9 @@ export function useRatelimitOverviewLogsQuery({ namespaceId, limit = 50 }: UseLo newMap.set(log.identifier, log); }); }); + if (initialData.pages.length > 0) { + setTotalCount(initialData.pages[0].total); + } setHistoricalLogsMap(newMap); } }, [initialData]); @@ -117,6 +121,7 @@ export function useRatelimitOverviewLogsQuery({ namespaceId, limit = 50 }: UseLo historicalLogs, isLoading: isLoadingInitial, hasMore: hasNextPage, + totalCount, loadMore: fetchNextPage, isLoadingMore: isFetchingNextPage, }; diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx index 3f01f6bd5b..c281bcdb06 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx @@ -26,9 +26,10 @@ export const RatelimitOverviewLogsTable = ({ }) => { const [selectedLog, setSelectedLog] = useState(); const { getSortDirection, toggleSort } = useSort(); - const { historicalLogs, isLoading, isLoadingMore, loadMore } = useRatelimitOverviewLogsQuery({ - namespaceId, - }); + const { historicalLogs, isLoading, isLoadingMore, loadMore, hasMore, totalCount } = + useRatelimitOverviewLogsQuery({ + namespaceId, + }); const columns = (namespaceId: string): Column[] => { return [ @@ -214,6 +215,19 @@ export const RatelimitOverviewLogsTable = ({ columns={columns(namespaceId)} keyExtractor={(log) => log.identifier} rowClassName={(rowLog) => getRowClassName(rowLog, selectedLog as RatelimitOverviewLog)} + loadMoreFooterProps={{ + itemLabel: "identifiers", + buttonText: "Load more logs", + hasMore, + hide: isLoading, + countInfoText: ( +
+ Showing {historicalLogs.length} + of {totalCount} + rate limit identifiers +
+ ), + }} emptyState={
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 f80bbcf4d3..1d50c9e6c4 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 @@ -21,6 +21,7 @@ export function useRatelimitLogsQuery({ }: UseLogsQueryParams) { const [historicalLogsMap, setHistoricalLogsMap] = useState(() => new Map()); const [realtimeLogsMap, setRealtimeLogsMap] = useState(() => new Map()); + const [totalCount, setTotalCount] = useState(0); const { filters } = useFilters(); const queryClient = trpc.useUtils(); @@ -193,6 +194,9 @@ export function useRatelimitLogsQuery({ newMap.set(log.request_id, log); }); }); + if (initialData.pages.length > 0) { + setTotalCount(initialData.pages[0].total); + } setHistoricalLogsMap(newMap); } }, [initialData]); @@ -212,6 +216,7 @@ export function useRatelimitLogsQuery({ loadMore: fetchNextPage, isLoadingMore: isFetchingNextPage, isPolling: startPolling, + totalCount, }; } diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx index f1776a8352..d0d8625583 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx @@ -66,7 +66,7 @@ const getSelectedClassName = (log: RatelimitLog, isSelected: boolean) => { export const RatelimitLogsTable = () => { const { setSelectedLog, selectedLog, isLive, namespaceId } = useRatelimitLogsContext(); - const { realtimeLogs, historicalLogs, isLoading, isLoadingMore, loadMore } = + const { realtimeLogs, historicalLogs, isLoading, isLoadingMore, loadMore, totalCount, hasMore } = useRatelimitLogsQuery({ namespaceId, startPolling: isLive, @@ -218,6 +218,19 @@ export const RatelimitLogsTable = () => { keyExtractor={(log) => log.request_id} rowClassName={getRowClassName} selectedClassName={getSelectedClassName} + loadMoreFooterProps={{ + buttonText: "Load more logs", + hasMore, + hide: isLoading, + countInfoText: ( +
+ Showing {historicalLogs.length} + of + {totalCount} + ratelimit requests +
+ ), + }} emptyState={
diff --git a/apps/dashboard/components/virtual-table/components/loading-indicator.tsx b/apps/dashboard/components/virtual-table/components/loading-indicator.tsx index 469178a8b2..b4a7872c1b 100644 --- a/apps/dashboard/components/virtual-table/components/loading-indicator.tsx +++ b/apps/dashboard/components/virtual-table/components/loading-indicator.tsx @@ -1,8 +1,66 @@ -export const LoadingIndicator = () => ( -
-
-
- Loading more data +import { Button } from "@unkey/ui"; + +type LoadMoreFooterProps = { + onLoadMore?: () => void; + isFetchingNextPage?: boolean; + totalVisible: number; + totalCount: number; + className?: string; + itemLabel?: string; + buttonText?: string; + hasMore?: boolean; + hide?: boolean; + countInfoText?: React.ReactNode; +}; + +export const LoadMoreFooter = ({ + onLoadMore, + isFetchingNextPage = false, + totalVisible, + totalCount, + itemLabel = "items", + buttonText = "Load more", + hasMore = true, + countInfoText, + hide, +}: LoadMoreFooterProps) => { + const shouldShow = !!onLoadMore; + + if (hide) { + return; + } + + return ( +
+
+
+ {countInfoText &&
{countInfoText}
} + {!countInfoText && ( +
+ Viewing {totalVisible} + of + {totalCount} + {itemLabel} +
+ )} + + +
+
-
-); + ); +}; diff --git a/apps/dashboard/components/virtual-table/constants.ts b/apps/dashboard/components/virtual-table/constants.ts index bb044d355f..51d99354fc 100644 --- a/apps/dashboard/components/virtual-table/constants.ts +++ b/apps/dashboard/components/virtual-table/constants.ts @@ -4,7 +4,6 @@ export const DEFAULT_CONFIG: TableConfig = { rowHeight: 26, loadingRows: 50, overscan: 5, - tableBorder: 1, throttleDelay: 350, headerHeight: 40, } as const; diff --git a/apps/dashboard/components/virtual-table/hooks/useTableHeight.ts b/apps/dashboard/components/virtual-table/hooks/useTableHeight.ts index 08c2b8fe97..96f420f0a0 100644 --- a/apps/dashboard/components/virtual-table/hooks/useTableHeight.ts +++ b/apps/dashboard/components/virtual-table/hooks/useTableHeight.ts @@ -1,36 +1,27 @@ import { useEffect, useState } from "react"; - -export const useTableHeight = ( - containerRef: React.RefObject, - headerHeight: number, - tableBorder: number, -) => { +// Adds bottom spacing to prevent the table from extending to the edge of the viewport +const BREATHING_SPACE = 20; +export const useTableHeight = (containerRef: React.RefObject) => { const [fixedHeight, setFixedHeight] = useState(0); - useEffect(() => { const calculateHeight = () => { if (!containerRef.current) { return; } const rect = containerRef.current.getBoundingClientRect(); - const totalHeaderHeight = headerHeight + tableBorder; - const availableHeight = window.innerHeight - rect.top - totalHeaderHeight; + const availableHeight = window.innerHeight - rect.top - BREATHING_SPACE; setFixedHeight(Math.max(availableHeight, 0)); }; - calculateHeight(); const resizeObserver = new ResizeObserver(calculateHeight); window.addEventListener("resize", calculateHeight); - if (containerRef.current) { resizeObserver.observe(containerRef.current); } - return () => { resizeObserver.disconnect(); window.removeEventListener("resize", calculateHeight); }; - }, [containerRef, headerHeight, tableBorder]); - + }, [containerRef]); return fixedHeight; }; diff --git a/apps/dashboard/components/virtual-table/index.tsx b/apps/dashboard/components/virtual-table/index.tsx index d72216af0f..f34c275953 100644 --- a/apps/dashboard/components/virtual-table/index.tsx +++ b/apps/dashboard/components/virtual-table/index.tsx @@ -2,7 +2,7 @@ import { cn } from "@/lib/utils"; import { CaretDown, CaretExpandY, CaretUp, CircleCarretRight } from "@unkey/icons"; import { Fragment, type Ref, forwardRef, useImperativeHandle, useMemo, useRef } from "react"; import { EmptyState } from "./components/empty-state"; -import { LoadingIndicator } from "./components/loading-indicator"; +import { LoadMoreFooter } from "./components/loading-indicator"; import { DEFAULT_CONFIG } from "./constants"; import { useTableData } from "./hooks/useTableData"; import { useTableHeight } from "./hooks/useTableHeight"; @@ -48,6 +48,7 @@ export const VirtualTable = forwardRef>( selectedClassName, selectedItem, isFetchingNextPage, + loadMoreFooterProps, }: VirtualTableProps, ref: Ref | undefined, ) { @@ -55,7 +56,7 @@ export const VirtualTable = forwardRef>( const parentRef = useRef(null); const containerRef = useRef(null); - const fixedHeight = useTableHeight(containerRef, config.headerHeight, config.tableBorder); + const fixedHeight = useTableHeight(containerRef); const tableData = useTableData(realtimeData, historicData); const virtualizer = useVirtualData({ @@ -276,7 +277,13 @@ export const VirtualTable = forwardRef>( /> - {isFetchingNextPage && } +
); diff --git a/apps/dashboard/components/virtual-table/types.ts b/apps/dashboard/components/virtual-table/types.ts index 0f355f9bc6..eb5be620d3 100644 --- a/apps/dashboard/components/virtual-table/types.ts +++ b/apps/dashboard/components/virtual-table/types.ts @@ -25,7 +25,6 @@ export type TableConfig = { rowHeight: number; loadingRows: number; overscan: number; - tableBorder: number; throttleDelay: number; headerHeight: number; }; @@ -45,6 +44,13 @@ export type VirtualTableProps = { selectedClassName?: (item: T, isSelected: boolean) => string; selectedItem?: T | null; isFetchingNextPage?: boolean; + loadMoreFooterProps?: { + itemLabel?: string; + buttonText?: string; + countInfoText?: React.ReactNode; + hasMore?: boolean; + hide?: boolean; + }; }; export type SeparatorItem = { diff --git a/apps/dashboard/lib/trpc/routers/logs/query-logs/index.ts b/apps/dashboard/lib/trpc/routers/logs/query-logs/index.ts index 9125db88d6..c2bf141dce 100644 --- a/apps/dashboard/lib/trpc/routers/logs/query-logs/index.ts +++ b/apps/dashboard/lib/trpc/routers/logs/query-logs/index.ts @@ -10,6 +10,7 @@ import { transformFilters } from "./utils"; const LogsResponse = z.object({ logs: z.array(log), hasMore: z.boolean(), + total: z.number(), nextCursor: z .object({ time: z.number().int(), @@ -49,26 +50,29 @@ export const queryLogs = t.procedure } const transformedInputs = transformFilters(input); - const result = await clickhouse.api.logs({ + const { logsQuery, totalQuery } = await clickhouse.api.logs({ ...transformedInputs, cursorRequestId: input.cursor?.requestId ?? null, cursorTime: input.cursor?.time ?? null, workspaceId: workspace.id, }); - if (result.err) { + const [countResult, logsResult] = await Promise.all([totalQuery, logsQuery]); + + if (countResult.err || logsResult.err) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Something went wrong when fetching data from clickhouse.", }); } - const logs = result.val; + const logs = logsResult.val; // Prepare the response with pagination info const response: LogsResponse = { logs, hasMore: logs.length === input.limit, + total: countResult.val[0].total_count, nextCursor: logs.length > 0 ? { diff --git a/apps/dashboard/lib/trpc/routers/ratelimit/query-logs/index.ts b/apps/dashboard/lib/trpc/routers/ratelimit/query-logs/index.ts index f61cd67e1a..c5e2f2f229 100644 --- a/apps/dashboard/lib/trpc/routers/ratelimit/query-logs/index.ts +++ b/apps/dashboard/lib/trpc/routers/ratelimit/query-logs/index.ts @@ -10,6 +10,7 @@ import { transformFilters } from "./utils"; const RatelimitLogsResponse = z.object({ ratelimitLogs: z.array(ratelimitLogs), hasMore: z.boolean(), + total: z.number(), nextCursor: z .object({ time: z.number().int(), @@ -58,7 +59,7 @@ export const queryRatelimitLogs = t.procedure } const transformedInputs = transformFilters(input); - const result = await clickhouse.ratelimits.logs({ + const { countQuery, logsQuery } = await clickhouse.ratelimits.logs({ ...transformedInputs, cursorRequestId: input.cursor?.requestId ?? null, cursorTime: input.cursor?.time ?? null, @@ -66,17 +67,19 @@ export const queryRatelimitLogs = t.procedure namespaceId: ratelimitNamespaces[0].id, }); - if (result.err) { + const [countResult, logsResult] = await Promise.all([countQuery, logsQuery]); + + if (countResult.err || logsResult.err) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Something went wrong when fetching data from clickhouse.", }); } - const logs = result.val; - + const logs = logsResult.val; const response: RatelimitLogsResponse = { ratelimitLogs: logs, + total: countResult.val[0].total_count, hasMore: logs.length === input.limit, nextCursor: logs.length > 0 diff --git a/apps/dashboard/lib/trpc/routers/ratelimit/query-overview-logs/index.ts b/apps/dashboard/lib/trpc/routers/ratelimit/query-overview-logs/index.ts index af8bf1ab15..ebacd801d4 100644 --- a/apps/dashboard/lib/trpc/routers/ratelimit/query-overview-logs/index.ts +++ b/apps/dashboard/lib/trpc/routers/ratelimit/query-overview-logs/index.ts @@ -10,6 +10,7 @@ import { transformFilters } from "./utils"; const RatelimitOverviewLogsResponse = z.object({ ratelimitOverviewLogs: z.array(ratelimitOverviewLogs), hasMore: z.boolean(), + total: z.number(), nextCursor: z .object({ time: z.number().int(), @@ -58,7 +59,7 @@ export const queryRatelimitOverviewLogs = t.procedure } const transformedInputs = transformFilters(input); - const result = await clickhouse.ratelimits.overview.logs({ + const { countQuery, logsQuery } = await clickhouse.ratelimits.overview.logs({ ...transformedInputs, cursorRequestId: input.cursor?.requestId ?? null, cursorTime: input.cursor?.time ?? null, @@ -66,17 +67,20 @@ export const queryRatelimitOverviewLogs = t.procedure namespaceId: ratelimitNamespaces[0].id, }); - if (result.err) { + const [countResult, logsResult] = await Promise.all([countQuery, logsQuery]); + + if (countResult.err || logsResult.err) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Something went wrong when fetching data from clickhouse.", }); } - const logsWithOverrides = await checkIfIdentifierHasOverride(result.val); + const logsWithOverrides = await checkIfIdentifierHasOverride(logsResult.val); const response: RatelimitOverviewLogsResponse = { ratelimitOverviewLogs: logsWithOverrides, + total: countResult.val[0].total_count, hasMore: logsWithOverrides.length === input.limit, nextCursor: logsWithOverrides.length === input.limit diff --git a/internal/clickhouse/src/logs.ts b/internal/clickhouse/src/logs.ts index 4a7b5baf69..826031dc3a 100644 --- a/internal/clickhouse/src/logs.ts +++ b/internal/clickhouse/src/logs.ts @@ -72,80 +72,77 @@ export function getLogs(ch: Querier) { const extendedParamsSchema = getLogsClickhousePayload.extend(paramSchemaExtension); - const query = ch.query({ - query: ` - WITH filtered_requests AS ( - SELECT * - FROM metrics.raw_api_requests_v1 - WHERE workspace_id = {workspaceId: String} - AND time BETWEEN {startTime: UInt64} AND {endTime: UInt64} - - ---------- Apply request ID filter if present (highest priority) - AND ( - CASE - WHEN length({requestIds: Array(String)}) > 0 THEN - request_id IN {requestIds: Array(String)} - ELSE TRUE - END - ) - - ---------- Apply host filter - AND ( - CASE - WHEN length({hosts: Array(String)}) > 0 THEN - host IN {hosts: Array(String)} - ELSE TRUE - END - ) - - ---------- Apply method filter - AND ( - CASE - WHEN length({methods: Array(String)}) > 0 THEN - method IN {methods: Array(String)} - ELSE TRUE - END - ) - - ---------- Apply path filter using pre-generated conditions - AND (${pathConditions}) - - ---------- Apply status code filter - AND ( - CASE - WHEN length({statusCodes: Array(UInt16)}) > 0 THEN - response_status IN ( - SELECT status - FROM ( - SELECT multiIf( - code = 200, arrayJoin(range(200, 300)), - code = 400, arrayJoin(range(400, 500)), - code = 500, arrayJoin(range(500, 600)), - code - ) as status - FROM ( - SELECT arrayJoin({statusCodes: Array(UInt16)}) as code - ) - ) - ) - ELSE TRUE - END - ) - - -- Apply cursor pagination last - AND ( - CASE - WHEN {cursorTime: Nullable(UInt64)} IS NOT NULL - AND {cursorRequestId: Nullable(String)} IS NOT NULL - THEN (time, request_id) < ( - {cursorTime: Nullable(UInt64)}, - {cursorRequestId: Nullable(String)} + const filterConditions = ` + workspace_id = {workspaceId: String} + AND time BETWEEN {startTime: UInt64} AND {endTime: UInt64} + + ---------- Apply request ID filter if present (highest priority) + AND ( + CASE + WHEN length({requestIds: Array(String)}) > 0 THEN + request_id IN {requestIds: Array(String)} + ELSE TRUE + END + ) + + ---------- Apply host filter + AND ( + CASE + WHEN length({hosts: Array(String)}) > 0 THEN + host IN {hosts: Array(String)} + ELSE TRUE + END + ) + + ---------- Apply method filter + AND ( + CASE + WHEN length({methods: Array(String)}) > 0 THEN + method IN {methods: Array(String)} + ELSE TRUE + END + ) + + ---------- Apply path filter using pre-generated conditions + AND (${pathConditions}) + + ---------- Apply status code filter + AND ( + CASE + WHEN length({statusCodes: Array(UInt16)}) > 0 THEN + response_status IN ( + SELECT status + FROM ( + SELECT multiIf( + code = 200, arrayJoin(range(200, 300)), + code = 400, arrayJoin(range(400, 500)), + code = 500, arrayJoin(range(500, 600)), + code + ) as status + FROM ( + SELECT arrayJoin({statusCodes: Array(UInt16)}) as code ) - ELSE TRUE - END + ) ) - ) - + ELSE TRUE + END + ) + `; + + const totalQuery = ch.query({ + query: ` + SELECT + count(request_id) as total_count + FROM metrics.raw_api_requests_v1 + WHERE ${filterConditions}`, + params: extendedParamsSchema, + schema: z.object({ + total_count: z.number().int(), + }), + }); + + const logsQuery = ch.query({ + query: ` SELECT request_id, time, @@ -160,14 +157,30 @@ export function getLogs(ch: Querier) { response_body, error, service_latency - FROM filtered_requests + FROM metrics.raw_api_requests_v1 + WHERE ${filterConditions} + -- Apply cursor pagination last + AND ( + CASE + WHEN {cursorTime: Nullable(UInt64)} IS NOT NULL + AND {cursorRequestId: Nullable(String)} IS NOT NULL + THEN (time, request_id) < ( + {cursorTime: Nullable(UInt64)}, + {cursorRequestId: Nullable(String)} + ) + ELSE TRUE + END + ) ORDER BY time DESC, request_id DESC LIMIT {limit: Int}`, params: extendedParamsSchema, schema: log, }); - return query(parameters); + return { + logsQuery: logsQuery(parameters), + totalQuery: totalQuery(parameters), + }; }; } diff --git a/internal/clickhouse/src/ratelimits.ts b/internal/clickhouse/src/ratelimits.ts index e9a844af78..e30ee52d9b 100644 --- a/internal/clickhouse/src/ratelimits.ts +++ b/internal/clickhouse/src/ratelimits.ts @@ -338,7 +338,7 @@ export function getRatelimitLogs(ch: Querier) { const extendedParamsSchema = ratelimitLogsParams.extend(paramSchemaExtension); - const query = ch.query({ + const logsQuery = ch.query({ query: ` WITH filtered_ratelimits AS ( SELECT @@ -401,11 +401,30 @@ LIMIT {limit: Int}`, schema: ratelimitLogs, }); - return query(parameters); + const countQuery = ch.query({ + query: ` +SELECT + count(*) as total_count +FROM ratelimits.raw_ratelimits_v1 r +WHERE workspace_id = {workspaceId: String} + AND namespace_id = {namespaceId: String} + AND time BETWEEN {startTime: UInt64} AND {endTime: UInt64} + ${hasRequestIds ? "AND request_id IN {requestIds: Array(String)}" : ""} + AND (${identifierConditions}) + AND (${statusCondition})`, + params: extendedParamsSchema, + schema: z.object({ + total_count: z.number().int(), + }), + }); + + return { + logsQuery: logsQuery(parameters), + countQuery: countQuery(parameters), + }; }; } -// ## OVERVIEWS export const ratelimitOverviewLogsParams = z.object({ workspaceId: z.string(), namespaceId: z.string(), @@ -629,7 +648,26 @@ LIMIT {limit: Int}`, schema: ratelimitOverviewLogs, }); - return query(parameters); + const countQuery = ch.query({ + query: ` +SELECT + count(DISTINCT identifier) as total_count +FROM ratelimits.raw_ratelimits_v1 +WHERE workspace_id = {workspaceId: String} + AND namespace_id = {namespaceId: String} + AND time BETWEEN {startTime: UInt64} AND {endTime: UInt64} + AND (${identifierConditions}) + AND (${statusCondition})`, + params: extendedParamsSchema, + schema: z.object({ + total_count: z.number().int(), + }), + }); + + return { + logsQuery: query(parameters), + countQuery: countQuery(parameters), + }; }; } diff --git a/internal/ui/tailwind.config.js b/internal/ui/tailwind.config.js index f7fc4ba6f0..bba7dd4227 100644 --- a/internal/ui/tailwind.config.js +++ b/internal/ui/tailwind.config.js @@ -65,7 +65,7 @@ const getColor = (colorVar, { opacityVariable, opacityValue }) => { function generateRadixColors() { const colorNames = [ "gray", - "grayA", // Also labeled as "brand" in Figma colors + "grayA", "info", "success", "successA", // Added tealA @@ -75,7 +75,7 @@ function generateRadixColors() { "error", "errorA", // Added tomatoA "feature", - "accent", + "accent", // Also labeled as "brand" in Figma colors "base", ];