import { MCPFilterSidebar } from "@/components/filters/mcpFilterSidebar"; import FullPageLoader from "@/components/fullPageLoader"; import { useColumnConfig } from "@/components/table"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Card, CardContent } from "@/components/ui/card"; import { getErrorMessage, useDeleteMCPLogsMutation, useGetMCPLogsQuery, useGetMCPLogsStatsQuery } from "@/lib/store"; import { useLazyGetMCPLogsQuery } from "@/lib/store/apis/mcpLogsApi"; import type { MCPToolLogEntry, MCPToolLogFilters, Pagination } from "@/lib/types/logs"; import { dateUtils } from "@/lib/types/logs"; import { COMPACT_NUMBER_FORMAT } from "@/lib/utils/numbers"; import { getRangeForPeriod } from "@/lib/utils/timeRange"; import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib"; import NumberFlow from "@number-flow/react"; import { useLocation } from "@tanstack/react-router"; import { AlertCircle, CheckCircle, Clock, DollarSign, Hash } from "lucide-react"; import { parseAsArrayOf, parseAsBoolean, parseAsInteger, parseAsString, useQueryStates } from "nuqs"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { createMCPColumns } from "./views/columns"; import { MCPEmptyState } from "./views/emptyState"; import { McpHeaderView } from "./views/mcpHeaderView"; import { MCPLogDetailSheet } from "./views/mcpLogDetailsSheet"; import { MCPLogsDataTable } from "./views/mcpLogsTable"; export default function MCPLogsPage() { const [error, setError] = useState(null); const [showEmptyState, setShowEmptyState] = useState(false); const hasCheckedEmptyState = useRef(false); const hasDeleteAccess = useRbac(RbacResource.Logs, RbacOperation.Delete); const [deleteLogs] = useDeleteMCPLogsMutation(); // Lazy query kept only for handleLogNavigate (fetches adjacent pages on demand) const [triggerGetLogs] = useLazyGetMCPLogsQuery(); // Track if user has manually modified the time range const userModifiedTimeRange = useRef(false); // Capture initial defaults on mount to detect shared URLs with custom time ranges const initialDefaults = useRef(dateUtils.getDefaultTimeRange()); const defaultTimeRange = useMemo(() => dateUtils.getDefaultTimeRange(), []); const getDefaultTimeRange = () => dateUtils.getDefaultTimeRange(); const { search } = useLocation(); const hasExplicitTimeRange = (search as Record)?.start_time && (search as Record)?.end_time; // URL state management const [urlState, setUrlState] = useQueryStates( { tool_names: parseAsArrayOf(parseAsString).withDefault([]), server_labels: parseAsArrayOf(parseAsString).withDefault([]), status: parseAsArrayOf(parseAsString).withDefault([]), virtual_key_ids: parseAsArrayOf(parseAsString).withDefault([]), content_search: parseAsString.withDefault(""), start_time: parseAsInteger.withDefault(defaultTimeRange.startTime), end_time: parseAsInteger.withDefault(defaultTimeRange.endTime), limit: parseAsInteger.withDefault(50), offset: parseAsInteger.withDefault(0), sort_by: parseAsString.withDefault("timestamp"), order: parseAsString.withDefault("desc"), polling: parseAsBoolean.withDefault(true).withOptions({ clearOnDefault: false }), period: parseAsString.withDefault(hasExplicitTimeRange ? "" : "1h").withOptions({ clearOnDefault: false }), selected_log: parseAsString.withDefault(""), }, { history: "push", shallow: false, }, ); const selectedLogId = urlState.selected_log || null; const polling = urlState.polling; // Refresh time range on page focus/visibility useEffect(() => { const refreshDefaultsIfStale = () => { if (!polling) return if (urlState.period) { const { from, to } = getRangeForPeriod(urlState.period); setUrlState( { start_time: Math.floor(from.getTime() / 1000), end_time: Math.floor(to.getTime() / 1000), period: urlState.period ?? "", }, { history: "replace" }, ); return; } if (userModifiedTimeRange.current) return; const startTimeDiff = Math.abs(urlState.start_time - initialDefaults.current.startTime); const endTimeDiff = Math.abs(urlState.end_time - initialDefaults.current.endTime); const tolerance = 5; if (startTimeDiff <= tolerance && endTimeDiff <= tolerance) { const defaults = getDefaultTimeRange(); const currentEndDiff = Math.abs(urlState.end_time - defaults.endTime); if (currentEndDiff > 300) { setUrlState( { start_time: defaults.startTime, end_time: defaults.endTime, period: urlState.period ?? "", }, { history: "replace" }, ); initialDefaults.current.startTime = defaults.startTime; initialDefaults.current.endTime = defaults.endTime; } } }; const handleVisibilityChange = () => { if (!document.hidden) refreshDefaultsIfStale(); }; const handleFocus = () => refreshDefaultsIfStale(); document.addEventListener("visibilitychange", handleVisibilityChange); window.addEventListener("focus", handleFocus); return () => { document.removeEventListener("visibilitychange", handleVisibilityChange); window.removeEventListener("focus", handleFocus); }; }, [urlState.period, urlState.start_time, urlState.end_time, setUrlState, polling]); // Refresh the time window every 5s while live polling is on and a relative period is active. // Updating start_time/end_time changes RTK args → triggers a refetch without needing pollingInterval. useEffect(() => { if (!polling || !urlState.period) return; const id = setInterval(() => { if (document.hidden) return; const { from, to } = getRangeForPeriod(urlState.period); setUrlState( { start_time: Math.floor(from.getTime() / 1000), end_time: Math.floor(to.getTime() / 1000), period: urlState.period ?? "", }, { history: "replace" }, ); }, 5000); return () => clearInterval(id); }, [polling, urlState.period, setUrlState]); // Convert URL state to filters and pagination for API calls const filters: MCPToolLogFilters = useMemo( () => ({ tool_names: urlState.tool_names, server_labels: urlState.server_labels, status: urlState.status, virtual_key_ids: urlState.virtual_key_ids, content_search: urlState.content_search, start_time: dateUtils.toISOString(urlState.start_time), end_time: dateUtils.toISOString(urlState.end_time), }), // eslint-disable-next-line react-hooks/exhaustive-deps [ urlState.tool_names, urlState.server_labels, urlState.status, urlState.virtual_key_ids, urlState.content_search, urlState.start_time, urlState.end_time, ], ); const pagination: Pagination = useMemo( () => ({ limit: urlState.limit, offset: urlState.offset, sort_by: urlState.sort_by as "timestamp" | "latency", order: urlState.order as "asc" | "desc", }), [urlState.limit, urlState.offset, urlState.sort_by, urlState.order], ); // Non-lazy RTK Query hooks const { data: logsData, isLoading: logsIsLoading, isFetching: logsIsFetching, error: logsError, refetch: refetchLogs, } = useGetMCPLogsQuery( { filters, pagination }, { // When a relative period is active, the setInterval above updates URL timestamps → RTK // detects arg changes and refetches automatically; no separate pollingInterval needed. pollingInterval: showEmptyState ? 3000 : polling && !urlState.period ? 5000 : 0, refetchOnMountOrArgChange: true, skipPollingIfUnfocused: true, }, ); const { data: statsData, isFetching: statsIsFetching, refetch: refetchStats, } = useGetMCPLogsStatsQuery({ filters }, { refetchOnMountOrArgChange: true }); const refreshAllData = useCallback(() => { refetchLogs(); refetchStats(); }, [refetchLogs, refetchStats]); // Derive data directly from RTK const logs = logsData?.logs ?? []; const totalItems = logsData?.stats?.total_executions ?? 0; const selectedLog = useMemo(() => (selectedLogId ? (logs.find((l) => l.id === selectedLogId) ?? null) : null), [selectedLogId, logs]); // Set showEmptyState on first response; clear it as soon as logs appear. useEffect(() => { if (!logsData) return; if (!hasCheckedEmptyState.current) { setShowEmptyState(!logsData.has_logs); hasCheckedEmptyState.current = true; } else if (showEmptyState && logsData.has_logs) { setShowEmptyState(false); } }, [logsData, showEmptyState]); // On mount: freshen period timestamps if stale useEffect(() => { if (urlState.period) { const { from, to } = getRangeForPeriod(urlState.period); const freshEnd = Math.floor(to.getTime() / 1000); if (Math.abs(urlState.end_time - freshEnd) > 60) { setUrlState( { start_time: Math.floor(from.getTime() / 1000), end_time: freshEnd, period: urlState.period ?? "", }, { history: "replace" }, ); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Helper to update filters in URL const setFilters = useCallback( (newFilters: MCPToolLogFilters) => { const timeChanged = newFilters.start_time !== undefined || newFilters.end_time !== undefined; if (timeChanged) userModifiedTimeRange.current = true; setUrlState({ ...(timeChanged && { period: "" }), tool_names: newFilters.tool_names || [], server_labels: newFilters.server_labels || [], status: newFilters.status || [], virtual_key_ids: newFilters.virtual_key_ids || [], content_search: newFilters.content_search || "", start_time: newFilters.start_time ? dateUtils.toUnixTimestamp(new Date(newFilters.start_time)) : undefined, end_time: newFilters.end_time ? dateUtils.toUnixTimestamp(new Date(newFilters.end_time)) : undefined, offset: 0, }); }, [setUrlState], ); // Helper to update pagination in URL const setPagination = useCallback( (newPagination: Pagination) => { setUrlState({ limit: newPagination.limit, offset: newPagination.offset, sort_by: newPagination.sort_by, order: newPagination.order, }); }, [setUrlState], ); const handleDelete = useCallback( async (log: MCPToolLogEntry) => { if (!hasDeleteAccess) throw new Error("No delete access"); try { await deleteLogs({ ids: [log.id] }).unwrap(); if (urlState.selected_log === log.id) { setUrlState({ selected_log: "" }); } refreshAllData(); } catch (err) { const errorMessage = getErrorMessage(err); setError(errorMessage); throw new Error(errorMessage); } }, [deleteLogs, hasDeleteAccess, urlState.selected_log, setUrlState, refreshAllData], ); const handlePeriodChange = useCallback( (p: string, from: Date, to: Date) => { setUrlState({ period: p, start_time: Math.floor(from.getTime() / 1000), end_time: Math.floor(to.getTime() / 1000), offset: 0, }); }, [setUrlState], ); const handlePollToggle = useCallback( (enabled: boolean) => { setUrlState({ polling: enabled }); if (enabled) refreshAllData(); }, [setUrlState, refreshAllData], ); const statCards = useMemo( () => [ { title: "Total Executions", value: , icon: , }, { title: "Success Rate", value: ( ), icon: , }, { title: "Avg Latency", value: ( ), icon: , }, { title: "Total Cost", value: ( ), icon: , }, ], [statsData], ); const columns = useMemo(() => createMCPColumns(handleDelete, hasDeleteAccess), [handleDelete, hasDeleteAccess]); const columnIds = useMemo( () => columns.map((col) => ("id" in col && col.id ? col.id : "accessorKey" in col ? String(col.accessorKey) : "")).filter(Boolean), [columns], ); const MCP_COLUMN_LABELS: Record = useMemo( () => ({ timestamp: "Time", tool_name: "Tool Name", server_label: "Server", latency: "Latency", cost: "Cost", }), [], ); const { entries: columnEntries, columnOrder, columnVisibility, columnPinning, toggleVisibility: toggleColumnVisibility, togglePin: toggleColumnPin, reorder: reorderColumns, reset: resetColumns, } = useColumnConfig({ columnIds, paramName: "mcp_cols", fixedColumns: { left: [], right: [] }, }); const selectedLogIndex = useMemo(() => (selectedLogId ? logs.findIndex((l) => l.id === selectedLogId) : -1), [selectedLogId, logs]); const handleLogNavigate = useCallback( (direction: "prev" | "next") => { const replaceHistory = { history: "replace" as const }; const currentLogId = selectedLogId || ""; if (direction === "prev") { if (selectedLogIndex > 0) { setUrlState({ selected_log: logs[selectedLogIndex - 1].id }, replaceHistory); } else if (pagination.offset > 0) { const newOffset = Math.max(0, pagination.offset - pagination.limit); setUrlState({ offset: newOffset, selected_log: "" }, replaceHistory); triggerGetLogs({ filters, pagination: { ...pagination, offset: newOffset }, }).then((result) => { const pageLogs = result.data?.logs; if (pageLogs?.length) { setUrlState({ selected_log: pageLogs[pageLogs.length - 1].id }, replaceHistory); } else if (result.error) { setUrlState({ offset: pagination.offset, selected_log: currentLogId }, replaceHistory); setError(getErrorMessage(result.error)); } }); } } else { if (selectedLogIndex >= 0 && selectedLogIndex < logs.length - 1) { setUrlState({ selected_log: logs[selectedLogIndex + 1].id }, replaceHistory); } else if (pagination.offset + pagination.limit < totalItems) { const newOffset = pagination.offset + pagination.limit; setUrlState({ offset: newOffset, selected_log: "" }, replaceHistory); triggerGetLogs({ filters, pagination: { ...pagination, offset: newOffset }, }).then((result) => { const pageLogs = result.data?.logs; if (pageLogs?.length) { setUrlState({ selected_log: pageLogs[0].id }, replaceHistory); } else if (result.error) { setUrlState({ offset: pagination.offset, selected_log: currentLogId }, replaceHistory); setError(getErrorMessage(result.error)); } }); } } }, [selectedLogId, selectedLogIndex, logs, pagination, totalItems, filters, setUrlState, triggerGetLogs], ); const displayError = error ?? (logsError ? getErrorMessage(logsError as Parameters[0]) : null); return (
{logsIsLoading ? ( ) : showEmptyState ? ( ) : (
{/* Sidebar Filters */} {/* Main Content */}
{/* Quick Stats */}
{statCards.map((card) => (
{card.title}
{card.value}
))}
{displayError && ( {displayError} )}
{ if (columnId === "actions") return; setUrlState({ selected_log: row.id }, { history: "replace" }); }} onRefresh={refreshAllData} polling={polling} columnEntries={columnEntries} columnOrder={columnOrder} columnVisibility={columnVisibility} columnPinning={columnPinning} onToggleColumnVisibility={toggleColumnVisibility} onTogglePin={toggleColumnPin} onReorderColumns={reorderColumns} />
{/* Log Detail Sheet */} !open && setUrlState({ selected_log: "" }, { history: "replace" })} handleDelete={handleDelete} onNavigate={handleLogNavigate} hasPrev={selectedLogIndex > 0 || (selectedLogIndex !== -1 && pagination.offset > 0)} hasNext={selectedLogIndex !== -1 && (selectedLogIndex < logs.length - 1 || pagination.offset + pagination.limit < totalItems)} />
)}
); }