901 lines
35 KiB
TypeScript
901 lines
35 KiB
TypeScript
import { LogsFilterSidebar } from "@/components/filters/logsFilterSidebar";
|
|
import { DateTimePickerWithRange } from "@/components/ui/datePickerWithRange";
|
|
import { ScrollArea } from "@/components/ui/scrollArea";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import {
|
|
useGetMCPAvailableFilterDataQuery,
|
|
useLazyGetLogsCostHistogramQuery,
|
|
useLazyGetLogsHistogramQuery,
|
|
useLazyGetLogsLatencyHistogramQuery,
|
|
useLazyGetLogsModelHistogramQuery,
|
|
useLazyGetLogsProviderCostHistogramQuery,
|
|
useLazyGetLogsProviderLatencyHistogramQuery,
|
|
useLazyGetLogsProviderTokenHistogramQuery,
|
|
useLazyGetLogsTokenHistogramQuery,
|
|
useLazyGetMCPCostHistogramQuery,
|
|
useLazyGetMCPHistogramQuery,
|
|
useLazyGetMCPTopToolsQuery,
|
|
useLazyGetModelRankingsQuery,
|
|
} from "@/lib/store";
|
|
import type {
|
|
CostHistogramResponse,
|
|
LatencyHistogramResponse,
|
|
LogFilters,
|
|
LogsHistogramResponse,
|
|
MCPCostHistogramResponse,
|
|
MCPHistogramResponse,
|
|
MCPToolLogFilters,
|
|
MCPTopToolsResponse,
|
|
ModelHistogramResponse,
|
|
ModelRankingsResponse,
|
|
ProviderCostHistogramResponse,
|
|
ProviderLatencyHistogramResponse,
|
|
ProviderTokenHistogramResponse,
|
|
TokenHistogramResponse,
|
|
} from "@/lib/types/logs";
|
|
import { dateUtils } from "@/lib/types/logs";
|
|
import { getRangeForPeriod, TIME_PERIODS } from "@/lib/utils/timeRange";
|
|
import UserRankingsTab from "@enterprise/components/user-rankings/userRankingsTab";
|
|
import { useLocation } from "@tanstack/react-router";
|
|
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
import { type ChartType } from "./components/charts/chartTypeToggle";
|
|
import { ModelFilterSelect } from "./components/charts/modelFilterSelect";
|
|
import { ExportPopover } from "./components/exportPopover";
|
|
import { MCPTab } from "./components/mcpTab";
|
|
import { ModelRankingsTab } from "./components/modelRankingsTab";
|
|
import { OverviewTab } from "./components/overviewTab";
|
|
import { ProviderUsageTab } from "./components/providerUsageTab";
|
|
|
|
// Type-safe parser for chart type URL state
|
|
const toChartType = (value: string): ChartType => (value === "line" ? "line" : "bar");
|
|
|
|
const parseCsvParam = (value: string): string[] => (value ? value.split(",").filter(Boolean) : []);
|
|
const sanitizeSeriesLabels = (values?: string[]): string[] => {
|
|
if (!values) return [];
|
|
const trimmedValues = values.map((value) => value.trim()).filter((value) => value.length > 0);
|
|
|
|
return [...new Set(trimmedValues)];
|
|
};
|
|
|
|
export default function DashboardPage() {
|
|
// Data states - Overview
|
|
const [histogramData, setHistogramData] = useState<LogsHistogramResponse | null>(null);
|
|
const [tokenData, setTokenData] = useState<TokenHistogramResponse | null>(null);
|
|
const [costData, setCostData] = useState<CostHistogramResponse | null>(null);
|
|
const [modelData, setModelData] = useState<ModelHistogramResponse | null>(null);
|
|
const [latencyData, setLatencyData] = useState<LatencyHistogramResponse | null>(null);
|
|
const [providerCostData, setProviderCostData] = useState<ProviderCostHistogramResponse | null>(null);
|
|
const [providerTokenData, setProviderTokenData] = useState<ProviderTokenHistogramResponse | null>(null);
|
|
const [providerLatencyData, setProviderLatencyData] = useState<ProviderLatencyHistogramResponse | null>(null);
|
|
|
|
// Data states - MCP
|
|
const [mcpHistogramData, setMcpHistogramData] = useState<MCPHistogramResponse | null>(null);
|
|
const [mcpCostData, setMcpCostData] = useState<MCPCostHistogramResponse | null>(null);
|
|
const [mcpTopToolsData, setMcpTopToolsData] = useState<MCPTopToolsResponse | null>(null);
|
|
|
|
// Data states - Rankings
|
|
const [rankingsData, setRankingsData] = useState<ModelRankingsResponse | null>(null);
|
|
|
|
// Loading states - Overview
|
|
const [loadingHistogram, setLoadingHistogram] = useState(true);
|
|
const [loadingTokens, setLoadingTokens] = useState(true);
|
|
const [loadingCost, setLoadingCost] = useState(true);
|
|
const [loadingModels, setLoadingModels] = useState(true);
|
|
const [loadingLatency, setLoadingLatency] = useState(true);
|
|
const [loadingProviderCost, setLoadingProviderCost] = useState(true);
|
|
const [loadingProviderTokens, setLoadingProviderTokens] = useState(true);
|
|
const [loadingProviderLatency, setLoadingProviderLatency] = useState(true);
|
|
|
|
// Loading states - MCP
|
|
const [loadingMcpHistogram, setLoadingMcpHistogram] = useState(true);
|
|
const [loadingMcpCost, setLoadingMcpCost] = useState(true);
|
|
const [loadingMcpTopTools, setLoadingMcpTopTools] = useState(true);
|
|
|
|
// Loading states - Rankings
|
|
const [loadingRankings, setLoadingRankings] = useState(true);
|
|
|
|
// RTK Query lazy hooks - Overview
|
|
const [triggerHistogram] = useLazyGetLogsHistogramQuery({});
|
|
const [triggerTokens] = useLazyGetLogsTokenHistogramQuery();
|
|
const [triggerCost] = useLazyGetLogsCostHistogramQuery();
|
|
const [triggerModels] = useLazyGetLogsModelHistogramQuery();
|
|
const [triggerLatency] = useLazyGetLogsLatencyHistogramQuery();
|
|
const [triggerProviderCost] = useLazyGetLogsProviderCostHistogramQuery();
|
|
const [triggerProviderTokens] = useLazyGetLogsProviderTokenHistogramQuery();
|
|
const [triggerProviderLatency] = useLazyGetLogsProviderLatencyHistogramQuery();
|
|
|
|
// RTK Query lazy hooks - MCP
|
|
const [triggerMcpHistogram] = useLazyGetMCPHistogramQuery();
|
|
const [triggerMcpCost] = useLazyGetMCPCostHistogramQuery();
|
|
const [triggerMcpTopTools] = useLazyGetMCPTopToolsQuery();
|
|
|
|
// RTK Query lazy hooks - Rankings
|
|
const [triggerRankings] = useLazyGetModelRankingsQuery();
|
|
|
|
// MCP filter data
|
|
const { data: mcpFilterData } = useGetMCPAvailableFilterDataQuery();
|
|
|
|
// Memoize default time range to prevent recalculation on every render
|
|
// This is crucial to avoid triggering refetches when the sheet opens/closes
|
|
const defaultTimeRange = useMemo(() => dateUtils.getDefaultTimeRange(), []);
|
|
|
|
const { search } = useLocation();
|
|
const hasExplicitTimeRange = (search as Record<string, unknown>)?.start_time && (search as Record<string, unknown>)?.end_time;
|
|
// URL state management
|
|
const [urlState, setUrlState] = useQueryStates(
|
|
{
|
|
period: parseAsString.withDefault(hasExplicitTimeRange ? "" : "1h").withOptions({ clearOnDefault: false }),
|
|
start_time: parseAsInteger.withDefault(defaultTimeRange.startTime),
|
|
end_time: parseAsInteger.withDefault(defaultTimeRange.endTime),
|
|
tab: parseAsString.withDefault("overview"),
|
|
virtual_key_ids: parseAsString.withDefault(""),
|
|
providers: parseAsString.withDefault(""),
|
|
models: parseAsString.withDefault(""),
|
|
selected_key_ids: parseAsString.withDefault(""),
|
|
objects: parseAsString.withDefault(""),
|
|
status: parseAsString.withDefault(""),
|
|
routing_rule_ids: parseAsString.withDefault(""),
|
|
routing_engine_used: parseAsString.withDefault(""),
|
|
missing_cost_only: parseAsString.withDefault("false"),
|
|
metadata_filters: parseAsString.withDefault(""),
|
|
volume_chart: parseAsString.withDefault("bar"),
|
|
token_chart: parseAsString.withDefault("bar"),
|
|
cost_chart: parseAsString.withDefault("bar"),
|
|
model_chart: parseAsString.withDefault("bar"),
|
|
latency_chart: parseAsString.withDefault("bar"),
|
|
cost_model: parseAsString.withDefault("all"),
|
|
usage_model: parseAsString.withDefault("all"),
|
|
provider_cost_chart: parseAsString.withDefault("bar"),
|
|
provider_token_chart: parseAsString.withDefault("bar"),
|
|
provider_latency_chart: parseAsString.withDefault("bar"),
|
|
provider_cost_provider: parseAsString.withDefault("all"),
|
|
provider_token_provider: parseAsString.withDefault("all"),
|
|
provider_latency_provider: parseAsString.withDefault("all"),
|
|
mcp_volume_chart: parseAsString.withDefault("bar"),
|
|
mcp_cost_chart: parseAsString.withDefault("bar"),
|
|
mcp_tool_names: parseAsString.withDefault(""),
|
|
mcp_server_labels: parseAsString.withDefault(""),
|
|
},
|
|
{
|
|
history: "push",
|
|
shallow: false,
|
|
},
|
|
);
|
|
|
|
// Parse filter arrays from URL state
|
|
const selectedProviders = useMemo(() => parseCsvParam(urlState.providers), [urlState.providers]);
|
|
const selectedModels = useMemo(() => parseCsvParam(urlState.models), [urlState.models]);
|
|
const selectedKeyIds = useMemo(() => parseCsvParam(urlState.selected_key_ids), [urlState.selected_key_ids]);
|
|
const selectedVirtualKeyIds = useMemo(() => parseCsvParam(urlState.virtual_key_ids), [urlState.virtual_key_ids]);
|
|
const selectedTypes = useMemo(() => parseCsvParam(urlState.objects), [urlState.objects]);
|
|
const selectedStatuses = useMemo(() => parseCsvParam(urlState.status), [urlState.status]);
|
|
const selectedRoutingRuleIds = useMemo(() => parseCsvParam(urlState.routing_rule_ids), [urlState.routing_rule_ids]);
|
|
const selectedRoutingEngines = useMemo(() => parseCsvParam(urlState.routing_engine_used), [urlState.routing_engine_used]);
|
|
const missingCostOnly = useMemo(() => urlState.missing_cost_only === "true", [urlState.missing_cost_only]);
|
|
const metadataFilters = useMemo(() => {
|
|
if (!urlState.metadata_filters) return undefined;
|
|
try {
|
|
return JSON.parse(urlState.metadata_filters) as Record<string, string>;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}, [urlState.metadata_filters]);
|
|
|
|
// MCP filter arrays
|
|
const selectedMcpToolNames = useMemo(() => parseCsvParam(urlState.mcp_tool_names), [urlState.mcp_tool_names]);
|
|
const selectedMcpServerLabels = useMemo(() => parseCsvParam(urlState.mcp_server_labels), [urlState.mcp_server_labels]);
|
|
|
|
// Derived filter for API calls
|
|
const filters: LogFilters = useMemo(
|
|
() => ({
|
|
start_time: dateUtils.toISOString(urlState.start_time),
|
|
end_time: dateUtils.toISOString(urlState.end_time),
|
|
...(selectedProviders.length > 0 && { providers: selectedProviders }),
|
|
...(selectedModels.length > 0 && { models: selectedModels }),
|
|
...(selectedKeyIds.length > 0 && { selected_key_ids: selectedKeyIds }),
|
|
...(selectedVirtualKeyIds.length > 0 && {
|
|
virtual_key_ids: selectedVirtualKeyIds,
|
|
}),
|
|
...(selectedTypes.length > 0 && { objects: selectedTypes }),
|
|
...(selectedStatuses.length > 0 && { status: selectedStatuses }),
|
|
...(selectedRoutingRuleIds.length > 0 && {
|
|
routing_rule_ids: selectedRoutingRuleIds,
|
|
}),
|
|
...(selectedRoutingEngines.length > 0 && {
|
|
routing_engine_used: selectedRoutingEngines,
|
|
}),
|
|
...(missingCostOnly && { missing_cost_only: true }),
|
|
...(metadataFilters &&
|
|
Object.keys(metadataFilters).length > 0 && {
|
|
metadata_filters: metadataFilters,
|
|
}),
|
|
}),
|
|
[
|
|
urlState.start_time,
|
|
urlState.end_time,
|
|
selectedProviders,
|
|
selectedModels,
|
|
selectedKeyIds,
|
|
selectedVirtualKeyIds,
|
|
selectedTypes,
|
|
selectedStatuses,
|
|
selectedRoutingRuleIds,
|
|
selectedRoutingEngines,
|
|
missingCostOnly,
|
|
metadataFilters,
|
|
],
|
|
);
|
|
|
|
// MCP filters
|
|
const mcpFilters: MCPToolLogFilters = useMemo(
|
|
() => ({
|
|
start_time: dateUtils.toISOString(urlState.start_time),
|
|
end_time: dateUtils.toISOString(urlState.end_time),
|
|
...(selectedMcpToolNames.length > 0 && {
|
|
tool_names: selectedMcpToolNames,
|
|
}),
|
|
...(selectedMcpServerLabels.length > 0 && {
|
|
server_labels: selectedMcpServerLabels,
|
|
}),
|
|
}),
|
|
[urlState.start_time, urlState.end_time, selectedMcpToolNames, selectedMcpServerLabels],
|
|
);
|
|
|
|
// Model lists for each chart's legend (must match what the chart component actually renders)
|
|
const costModels = useMemo(() => sanitizeSeriesLabels(costData?.models), [costData?.models]);
|
|
const usageModels = useMemo(() => sanitizeSeriesLabels(modelData?.models), [modelData?.models]);
|
|
|
|
// Available models for filter dropdowns (union of both sources)
|
|
const availableModels = useMemo(() => {
|
|
return sanitizeSeriesLabels([...(costData?.models ?? []), ...(modelData?.models ?? [])]);
|
|
}, [costData?.models, modelData?.models]);
|
|
|
|
// Available providers for provider chart filter dropdowns
|
|
const availableProviders = useMemo(() => {
|
|
return sanitizeSeriesLabels([
|
|
...(providerCostData?.providers ?? []),
|
|
...(providerTokenData?.providers ?? []),
|
|
...(providerLatencyData?.providers ?? []),
|
|
]);
|
|
}, [providerCostData?.providers, providerTokenData?.providers, providerLatencyData?.providers]);
|
|
|
|
// Provider lists for each chart's legend
|
|
const providerCostProviders = useMemo(() => sanitizeSeriesLabels(providerCostData?.providers), [providerCostData?.providers]);
|
|
const providerTokenProviders = useMemo(() => sanitizeSeriesLabels(providerTokenData?.providers), [providerTokenData?.providers]);
|
|
const providerLatencyProviders = useMemo(() => sanitizeSeriesLabels(providerLatencyData?.providers), [providerLatencyData?.providers]);
|
|
|
|
// Fetch Overview tab data (5 calls)
|
|
const fetchOverviewData = useCallback(async () => {
|
|
setLoadingHistogram(true);
|
|
setLoadingTokens(true);
|
|
setLoadingCost(true);
|
|
setLoadingModels(true);
|
|
setLoadingLatency(true);
|
|
|
|
const fetchFilters = { filters };
|
|
|
|
const [histogramResult, tokenResult, costResult, modelResult, latencyResult] = await Promise.all([
|
|
triggerHistogram(fetchFilters, false),
|
|
triggerTokens(fetchFilters, false),
|
|
triggerCost(fetchFilters, false),
|
|
triggerModels(fetchFilters, false),
|
|
triggerLatency(fetchFilters, false),
|
|
]);
|
|
|
|
setHistogramData(histogramResult.data ?? null);
|
|
setLoadingHistogram(false);
|
|
setTokenData(tokenResult.data ?? null);
|
|
setLoadingTokens(false);
|
|
setCostData(costResult.data ?? null);
|
|
setLoadingCost(false);
|
|
setModelData(modelResult.data ?? null);
|
|
setLoadingModels(false);
|
|
setLatencyData(latencyResult.data ?? null);
|
|
setLoadingLatency(false);
|
|
}, [filters, triggerHistogram, triggerTokens, triggerCost, triggerModels, triggerLatency]);
|
|
|
|
// Fetch Provider Usage tab data (3 calls)
|
|
const fetchProviderData = useCallback(async () => {
|
|
setLoadingProviderCost(true);
|
|
setLoadingProviderTokens(true);
|
|
setLoadingProviderLatency(true);
|
|
|
|
const fetchFilters = { filters };
|
|
|
|
const [providerCostResult, providerTokenResult, providerLatencyResult] = await Promise.all([
|
|
triggerProviderCost(fetchFilters, false),
|
|
triggerProviderTokens(fetchFilters, false),
|
|
triggerProviderLatency(fetchFilters, false),
|
|
]);
|
|
|
|
setProviderCostData(providerCostResult.data ?? null);
|
|
setLoadingProviderCost(false);
|
|
setProviderTokenData(providerTokenResult.data ?? null);
|
|
setLoadingProviderTokens(false);
|
|
setProviderLatencyData(providerLatencyResult.data ?? null);
|
|
setLoadingProviderLatency(false);
|
|
}, [filters, triggerProviderCost, triggerProviderTokens, triggerProviderLatency]);
|
|
|
|
// Fetch MCP data
|
|
const fetchMcpData = useCallback(async () => {
|
|
setLoadingMcpHistogram(true);
|
|
setLoadingMcpCost(true);
|
|
setLoadingMcpTopTools(true);
|
|
|
|
const fetchFilters = { filters: mcpFilters };
|
|
|
|
const [mcpHistResult, mcpCostResult, mcpTopToolsResult] = await Promise.all([
|
|
triggerMcpHistogram(fetchFilters, false),
|
|
triggerMcpCost(fetchFilters, false),
|
|
triggerMcpTopTools(fetchFilters, false),
|
|
]);
|
|
|
|
setMcpHistogramData(mcpHistResult.data ?? null);
|
|
setLoadingMcpHistogram(false);
|
|
setMcpCostData(mcpCostResult.data ?? null);
|
|
setLoadingMcpCost(false);
|
|
setMcpTopToolsData(mcpTopToolsResult.data ?? null);
|
|
setLoadingMcpTopTools(false);
|
|
}, [mcpFilters, triggerMcpHistogram, triggerMcpCost, triggerMcpTopTools]);
|
|
|
|
// Fetch Rankings data
|
|
const fetchRankingsData = useCallback(async () => {
|
|
setLoadingRankings(true);
|
|
const result = await triggerRankings({ filters }, false);
|
|
setRankingsData(result.data ?? null);
|
|
setLoadingRankings(false);
|
|
}, [filters, triggerRankings]);
|
|
|
|
// --- Lazy-load refs: each tab fetches only once per filter change ---
|
|
const overviewFetchedRef = useRef(false);
|
|
const overviewLoadingRef = useRef(false);
|
|
const overviewGenRef = useRef(0);
|
|
const overviewPromiseRef = useRef<Promise<void> | null>(null);
|
|
|
|
const providerFetchedRef = useRef(false);
|
|
const providerLoadingRef = useRef(false);
|
|
const providerGenRef = useRef(0);
|
|
const providerPromiseRef = useRef<Promise<void> | null>(null);
|
|
|
|
const mcpFetchedRef = useRef(false);
|
|
const mcpLoadingRef = useRef(false);
|
|
const mcpGenRef = useRef(0);
|
|
const mcpPromiseRef = useRef<Promise<void> | null>(null);
|
|
|
|
const rankingsFetchedRef = useRef(false);
|
|
const rankingsLoadingRef = useRef(false);
|
|
const rankingsGenRef = useRef(0);
|
|
const rankingsPromiseRef = useRef<Promise<void> | null>(null);
|
|
|
|
const ensureOverviewDataLoaded = useCallback(async () => {
|
|
if (overviewFetchedRef.current) return;
|
|
if (overviewLoadingRef.current) return overviewPromiseRef.current ?? undefined;
|
|
const gen = overviewGenRef.current;
|
|
overviewLoadingRef.current = true;
|
|
const promise = fetchOverviewData()
|
|
.then(() => {
|
|
if (gen === overviewGenRef.current) overviewFetchedRef.current = true;
|
|
})
|
|
.finally(() => {
|
|
if (gen === overviewGenRef.current) {
|
|
overviewLoadingRef.current = false;
|
|
overviewPromiseRef.current = null;
|
|
}
|
|
});
|
|
overviewPromiseRef.current = promise;
|
|
return promise;
|
|
}, [fetchOverviewData]);
|
|
|
|
const ensureProviderDataLoaded = useCallback(async () => {
|
|
if (providerFetchedRef.current) return;
|
|
if (providerLoadingRef.current) return providerPromiseRef.current ?? undefined;
|
|
const gen = providerGenRef.current;
|
|
providerLoadingRef.current = true;
|
|
const promise = fetchProviderData()
|
|
.then(() => {
|
|
if (gen === providerGenRef.current) providerFetchedRef.current = true;
|
|
})
|
|
.finally(() => {
|
|
if (gen === providerGenRef.current) {
|
|
providerLoadingRef.current = false;
|
|
providerPromiseRef.current = null;
|
|
}
|
|
});
|
|
providerPromiseRef.current = promise;
|
|
return promise;
|
|
}, [fetchProviderData]);
|
|
|
|
const ensureMcpDataLoaded = useCallback(async () => {
|
|
if (mcpFetchedRef.current) return;
|
|
if (mcpLoadingRef.current) return mcpPromiseRef.current ?? undefined;
|
|
const gen = mcpGenRef.current;
|
|
mcpLoadingRef.current = true;
|
|
const promise = fetchMcpData()
|
|
.then(() => {
|
|
if (gen === mcpGenRef.current) mcpFetchedRef.current = true;
|
|
})
|
|
.finally(() => {
|
|
if (gen === mcpGenRef.current) {
|
|
mcpLoadingRef.current = false;
|
|
mcpPromiseRef.current = null;
|
|
}
|
|
});
|
|
mcpPromiseRef.current = promise;
|
|
return promise;
|
|
}, [fetchMcpData]);
|
|
|
|
const ensureRankingsDataLoaded = useCallback(async () => {
|
|
if (rankingsFetchedRef.current) return;
|
|
if (rankingsLoadingRef.current) return rankingsPromiseRef.current ?? undefined;
|
|
const gen = rankingsGenRef.current;
|
|
rankingsLoadingRef.current = true;
|
|
const promise = fetchRankingsData()
|
|
.then(() => {
|
|
if (gen === rankingsGenRef.current) rankingsFetchedRef.current = true;
|
|
})
|
|
.finally(() => {
|
|
if (gen === rankingsGenRef.current) {
|
|
rankingsLoadingRef.current = false;
|
|
rankingsPromiseRef.current = null;
|
|
}
|
|
});
|
|
rankingsPromiseRef.current = promise;
|
|
return promise;
|
|
}, [fetchRankingsData]);
|
|
|
|
// Reset all lazy-load flags when filters change (not on tab switch)
|
|
useEffect(() => {
|
|
overviewFetchedRef.current = false;
|
|
overviewLoadingRef.current = false;
|
|
overviewGenRef.current += 1;
|
|
providerFetchedRef.current = false;
|
|
providerLoadingRef.current = false;
|
|
providerGenRef.current += 1;
|
|
rankingsFetchedRef.current = false;
|
|
rankingsLoadingRef.current = false;
|
|
rankingsGenRef.current += 1;
|
|
}, [filters]);
|
|
|
|
useEffect(() => {
|
|
mcpFetchedRef.current = false;
|
|
mcpLoadingRef.current = false;
|
|
mcpGenRef.current += 1;
|
|
}, [mcpFilters]);
|
|
|
|
// Fetch current tab's data when filters change or tab switches
|
|
// The ensure* functions are no-ops if data is already loaded for the current filters
|
|
useEffect(() => {
|
|
const tab = urlState.tab || "overview";
|
|
if (tab === "overview") void ensureOverviewDataLoaded();
|
|
else if (tab === "provider-usage") void ensureProviderDataLoaded();
|
|
else if (tab === "rankings") void ensureRankingsDataLoaded();
|
|
else if (tab === "mcp") void ensureMcpDataLoaded();
|
|
}, [urlState.tab, ensureOverviewDataLoaded, ensureProviderDataLoaded, ensureRankingsDataLoaded, ensureMcpDataLoaded]);
|
|
|
|
// Warm other tabs in the background after 150ms
|
|
useEffect(() => {
|
|
const tab = urlState.tab || "overview";
|
|
const timeoutId = window.setTimeout(() => {
|
|
if (tab !== "overview") void ensureOverviewDataLoaded();
|
|
if (tab !== "provider-usage") void ensureProviderDataLoaded();
|
|
if (tab !== "mcp") void ensureMcpDataLoaded();
|
|
if (tab !== "rankings") void ensureRankingsDataLoaded();
|
|
}, 150);
|
|
return () => window.clearTimeout(timeoutId);
|
|
}, [urlState.tab, ensureOverviewDataLoaded, ensureProviderDataLoaded, ensureMcpDataLoaded, ensureRankingsDataLoaded]);
|
|
|
|
// Tab change handler
|
|
const handleTabChange = useCallback(
|
|
(tab: string) => {
|
|
setUrlState({ tab });
|
|
},
|
|
[setUrlState],
|
|
);
|
|
|
|
// Chart type toggles
|
|
const handleVolumeChartToggle = useCallback((type: ChartType) => setUrlState({ volume_chart: type }), [setUrlState]);
|
|
const handleTokenChartToggle = useCallback((type: ChartType) => setUrlState({ token_chart: type }), [setUrlState]);
|
|
const handleCostChartToggle = useCallback((type: ChartType) => setUrlState({ cost_chart: type }), [setUrlState]);
|
|
const handleModelChartToggle = useCallback((type: ChartType) => setUrlState({ model_chart: type }), [setUrlState]);
|
|
const handleLatencyChartToggle = useCallback((type: ChartType) => setUrlState({ latency_chart: type }), [setUrlState]);
|
|
|
|
// Adapter: converts a full LogFilters object to dashboard's CSV-based URL state
|
|
const setFilters = useCallback(
|
|
(newFilters: LogFilters) => {
|
|
const newStartTime = newFilters.start_time ? dateUtils.toUnixTimestamp(new Date(newFilters.start_time)) : undefined;
|
|
const newEndTime = newFilters.end_time ? dateUtils.toUnixTimestamp(new Date(newFilters.end_time)) : undefined;
|
|
const timeChanged = newStartTime !== urlState.start_time || newEndTime !== urlState.end_time;
|
|
setUrlState({
|
|
...(timeChanged && { period: "" }),
|
|
start_time: newStartTime,
|
|
end_time: newEndTime,
|
|
providers: (newFilters.providers || []).join(","),
|
|
models: (newFilters.models || []).join(","),
|
|
selected_key_ids: (newFilters.selected_key_ids || []).join(","),
|
|
virtual_key_ids: (newFilters.virtual_key_ids || []).join(","),
|
|
objects: (newFilters.objects || []).join(","),
|
|
status: (newFilters.status || []).join(","),
|
|
routing_rule_ids: (newFilters.routing_rule_ids || []).join(","),
|
|
routing_engine_used: (newFilters.routing_engine_used || []).join(","),
|
|
missing_cost_only: String(newFilters.missing_cost_only ?? false),
|
|
metadata_filters:
|
|
newFilters.metadata_filters && Object.keys(newFilters.metadata_filters).length > 0
|
|
? JSON.stringify(newFilters.metadata_filters)
|
|
: "",
|
|
});
|
|
},
|
|
[setUrlState, urlState.start_time, urlState.end_time],
|
|
);
|
|
|
|
// Date range for picker
|
|
const dateRange = useMemo(
|
|
() => ({
|
|
from: dateUtils.fromUnixTimestamp(urlState.start_time),
|
|
to: dateUtils.fromUnixTimestamp(urlState.end_time),
|
|
}),
|
|
[urlState.start_time, urlState.end_time],
|
|
);
|
|
|
|
const handlePeriodChange = useCallback(
|
|
(period: string | undefined) => {
|
|
if (!period) return;
|
|
const { from, to } = getRangeForPeriod(period);
|
|
setUrlState({
|
|
period,
|
|
start_time: Math.floor(from.getTime() / 1000),
|
|
end_time: Math.floor(to.getTime() / 1000),
|
|
});
|
|
},
|
|
[setUrlState],
|
|
);
|
|
|
|
const handleDateRangeChange = useCallback(
|
|
(range: { from?: Date; to?: Date }) => {
|
|
if (!range.from || !range.to) return;
|
|
setUrlState({
|
|
period: "",
|
|
start_time: dateUtils.toUnixTimestamp(range.from),
|
|
end_time: dateUtils.toUnixTimestamp(range.to),
|
|
});
|
|
},
|
|
[setUrlState],
|
|
);
|
|
|
|
const handleProviderCostChartToggle = useCallback((type: ChartType) => setUrlState({ provider_cost_chart: type }), [setUrlState]);
|
|
const handleProviderTokenChartToggle = useCallback((type: ChartType) => setUrlState({ provider_token_chart: type }), [setUrlState]);
|
|
const handleProviderLatencyChartToggle = useCallback((type: ChartType) => setUrlState({ provider_latency_chart: type }), [setUrlState]);
|
|
|
|
// MCP chart type toggles
|
|
const handleMcpVolumeChartToggle = useCallback((type: ChartType) => setUrlState({ mcp_volume_chart: type }), [setUrlState]);
|
|
const handleMcpCostChartToggle = useCallback((type: ChartType) => setUrlState({ mcp_cost_chart: type }), [setUrlState]);
|
|
|
|
// Model filter changes
|
|
const handleCostModelChange = useCallback((model: string) => setUrlState({ cost_model: model }), [setUrlState]);
|
|
const handleUsageModelChange = useCallback((model: string) => setUrlState({ usage_model: model }), [setUrlState]);
|
|
|
|
// Provider filter changes
|
|
const handleProviderCostProviderChange = useCallback(
|
|
(provider: string) => setUrlState({ provider_cost_provider: provider }),
|
|
[setUrlState],
|
|
);
|
|
const handleProviderTokenProviderChange = useCallback(
|
|
(provider: string) => setUrlState({ provider_token_provider: provider }),
|
|
[setUrlState],
|
|
);
|
|
const handleProviderLatencyProviderChange = useCallback(
|
|
(provider: string) => setUrlState({ provider_latency_provider: provider }),
|
|
[setUrlState],
|
|
);
|
|
|
|
// Aggregate data object for export
|
|
const dashboardData = useMemo(
|
|
() => ({
|
|
histogramData,
|
|
tokenData,
|
|
costData,
|
|
modelData,
|
|
latencyData,
|
|
providerCostData,
|
|
providerTokenData,
|
|
providerLatencyData,
|
|
rankingsData,
|
|
mcpHistogramData,
|
|
mcpCostData,
|
|
mcpTopToolsData,
|
|
}),
|
|
[
|
|
histogramData,
|
|
tokenData,
|
|
costData,
|
|
modelData,
|
|
latencyData,
|
|
providerCostData,
|
|
providerTokenData,
|
|
providerLatencyData,
|
|
rankingsData,
|
|
mcpHistogramData,
|
|
mcpCostData,
|
|
mcpTopToolsData,
|
|
],
|
|
);
|
|
|
|
// Keep a ref in sync so export callbacks always read the latest data
|
|
const dashboardDataRef = useRef(dashboardData);
|
|
dashboardDataRef.current = dashboardData;
|
|
const getDashboardData = useCallback(() => dashboardDataRef.current, []);
|
|
|
|
// Preload all tab data (used by CSV and PDF export)
|
|
const handlePreloadData = useCallback(async () => {
|
|
await Promise.all([ensureOverviewDataLoaded(), ensureProviderDataLoaded(), ensureRankingsDataLoaded(), ensureMcpDataLoaded()]);
|
|
}, [ensureOverviewDataLoaded, ensureProviderDataLoaded, ensureRankingsDataLoaded, ensureMcpDataLoaded]);
|
|
|
|
// PDF export mode — when true, all TabsContent are force-mounted so
|
|
// html2canvas can capture every tab.
|
|
const [pdfMode, setPdfMode] = useState(false);
|
|
const dashboardMinHeightRef = useRef<string>("");
|
|
const hiddenTabsRef = useRef<HTMLElement[]>([]);
|
|
|
|
// Called by ExportPopover. Loads all data, force-mounts all tabs,
|
|
// unhides inactive tabs so html2canvas can capture them, then returns
|
|
// the 4 section DOM elements. Caller must invoke the returned cleanup
|
|
// function when done capturing.
|
|
const handlePdfExport = useCallback(async (): Promise<HTMLElement[]> => {
|
|
// Ensure every tab's data is loaded
|
|
await handlePreloadData();
|
|
|
|
setPdfMode(true);
|
|
|
|
// Wait for React to render the force-mounted tabs
|
|
await new Promise<void>((resolve) => {
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => resolve());
|
|
});
|
|
});
|
|
|
|
// Radix sets `hidden` on inactive force-mounted TabsContent.
|
|
// Temporarily remove it so html2canvas can capture them.
|
|
const hiddenTabs = document.querySelectorAll<HTMLElement>('[data-slot="tabs-content"][hidden]');
|
|
hiddenTabsRef.current = Array.from(hiddenTabs);
|
|
for (const tab of hiddenTabs) {
|
|
tab.removeAttribute("hidden");
|
|
tab.style.display = "block";
|
|
}
|
|
|
|
// Collapse min-height on the dashboard container so captured
|
|
// sections wrap tightly around their content (no extra whitespace).
|
|
const dashboardEl = document.getElementById("dashboard-root");
|
|
if (dashboardEl) {
|
|
dashboardMinHeightRef.current = dashboardEl.style.minHeight;
|
|
dashboardEl.style.minHeight = "0";
|
|
}
|
|
|
|
// Let ResizeObserver-based charts (meter gauge) re-measure
|
|
window.dispatchEvent(new Event("resize"));
|
|
await new Promise<void>((resolve) => {
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => resolve());
|
|
});
|
|
});
|
|
|
|
const ids = ["dashboard-section-overview", "dashboard-section-provider-usage", "dashboard-section-rankings", "dashboard-section-mcp"];
|
|
return ids.map((id) => document.getElementById(id)).filter(Boolean) as HTMLElement[];
|
|
}, [handlePreloadData]);
|
|
|
|
// Cleanup after PDF capture is complete
|
|
const handlePdfExportDone = useCallback(() => {
|
|
// Restore minHeight on dashboard container
|
|
const dashboardEl = document.getElementById("dashboard-root");
|
|
if (dashboardEl) {
|
|
dashboardEl.style.minHeight = dashboardMinHeightRef.current;
|
|
}
|
|
|
|
// Re-hide tabs that were temporarily shown for capture
|
|
for (const tab of hiddenTabsRef.current) {
|
|
tab.setAttribute("hidden", "");
|
|
tab.style.display = "";
|
|
}
|
|
hiddenTabsRef.current = [];
|
|
|
|
setPdfMode(false);
|
|
}, []);
|
|
|
|
return (
|
|
<div id="dashboard-root" className="no-padding-parent no-border-parent bg-background flex h-[calc(100vh_-_16px)] w-full gap-3">
|
|
{/* Sidebar Filters */}
|
|
<LogsFilterSidebar filters={filters} onFiltersChange={setFilters} />
|
|
|
|
{/* Main Content */}
|
|
<ScrollArea className="bg-card flex min-w-0 flex-1 flex-col gap-4 rounded-l-md">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between p-4">
|
|
<div className="flex items-center gap-2">
|
|
<h1 className="text-lg font-semibold">Dashboard</h1>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<ExportPopover
|
|
getData={getDashboardData}
|
|
onPreloadData={handlePreloadData}
|
|
onPdfExport={handlePdfExport}
|
|
onPdfExportDone={handlePdfExportDone}
|
|
/>
|
|
{urlState.tab === "mcp" && mcpFilterData && (
|
|
<div className="flex items-center gap-1">
|
|
{mcpFilterData.tool_names?.length > 0 && (
|
|
<ModelFilterSelect
|
|
models={mcpFilterData.tool_names}
|
|
selectedModel={selectedMcpToolNames.length === 1 ? selectedMcpToolNames[0] : "all"}
|
|
onModelChange={(value) => {
|
|
if (value === "all") {
|
|
setUrlState({ mcp_tool_names: "" });
|
|
} else {
|
|
setUrlState({ mcp_tool_names: value });
|
|
}
|
|
}}
|
|
placeholder="All Tools"
|
|
data-testid="dashboard-mcp-tool-filter"
|
|
/>
|
|
)}
|
|
{mcpFilterData.server_labels?.length > 0 && (
|
|
<ModelFilterSelect
|
|
models={mcpFilterData.server_labels}
|
|
selectedModel={selectedMcpServerLabels.length === 1 ? selectedMcpServerLabels[0] : "all"}
|
|
onModelChange={(value) => {
|
|
if (value === "all") {
|
|
setUrlState({ mcp_server_labels: "" });
|
|
} else {
|
|
setUrlState({ mcp_server_labels: value });
|
|
}
|
|
}}
|
|
placeholder="All Servers"
|
|
data-testid="dashboard-mcp-server-filter"
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
<DateTimePickerWithRange
|
|
dateTime={dateRange}
|
|
onDateTimeUpdate={handleDateRangeChange}
|
|
preDefinedPeriods={TIME_PERIODS}
|
|
predefinedPeriod={urlState.period || undefined}
|
|
onPredefinedPeriodChange={handlePeriodChange}
|
|
triggerTestId="dashboard-filter-daterange"
|
|
popupAlignment="end"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-4">
|
|
{/* Tabs */}
|
|
<Tabs value={urlState.tab} onValueChange={handleTabChange}>
|
|
<TabsList className="mb-2">
|
|
<TabsTrigger value="overview" data-testid="dashboard-tab-overview">
|
|
Overview
|
|
</TabsTrigger>
|
|
<TabsTrigger value="provider-usage" data-testid="dashboard-tab-provider-usage">
|
|
Provider Usage
|
|
</TabsTrigger>
|
|
<TabsTrigger value="rankings" data-testid="dashboard-tab-rankings">
|
|
Model Rankings
|
|
</TabsTrigger>
|
|
<TabsTrigger value="mcp" data-testid="dashboard-tab-mcp">
|
|
MCP usage
|
|
</TabsTrigger>
|
|
<TabsTrigger value="user-rankings" data-testid="dashboard-tab-user-rankings">
|
|
User Rankings
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* Overview Tab */}
|
|
<TabsContent value="overview" {...(pdfMode && { forceMount: true })}>
|
|
<div id="dashboard-section-overview">
|
|
<OverviewTab
|
|
histogramData={histogramData}
|
|
tokenData={tokenData}
|
|
costData={costData}
|
|
modelData={modelData}
|
|
latencyData={latencyData}
|
|
loadingHistogram={loadingHistogram}
|
|
loadingTokens={loadingTokens}
|
|
loadingCost={loadingCost}
|
|
loadingModels={loadingModels}
|
|
loadingLatency={loadingLatency}
|
|
startTime={urlState.start_time}
|
|
endTime={urlState.end_time}
|
|
volumeChartType={toChartType(urlState.volume_chart)}
|
|
tokenChartType={toChartType(urlState.token_chart)}
|
|
costChartType={toChartType(urlState.cost_chart)}
|
|
modelChartType={toChartType(urlState.model_chart)}
|
|
latencyChartType={toChartType(urlState.latency_chart)}
|
|
costModel={urlState.cost_model}
|
|
usageModel={urlState.usage_model}
|
|
costModels={costModels}
|
|
usageModels={usageModels}
|
|
availableModels={availableModels}
|
|
onVolumeChartToggle={handleVolumeChartToggle}
|
|
onTokenChartToggle={handleTokenChartToggle}
|
|
onCostChartToggle={handleCostChartToggle}
|
|
onModelChartToggle={handleModelChartToggle}
|
|
onLatencyChartToggle={handleLatencyChartToggle}
|
|
onCostModelChange={handleCostModelChange}
|
|
onUsageModelChange={handleUsageModelChange}
|
|
/>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* Provider Usage Tab */}
|
|
<TabsContent value="provider-usage" {...(pdfMode && { forceMount: true })}>
|
|
<div id="dashboard-section-provider-usage">
|
|
<ProviderUsageTab
|
|
providerCostData={providerCostData}
|
|
providerTokenData={providerTokenData}
|
|
providerLatencyData={providerLatencyData}
|
|
loadingProviderCost={loadingProviderCost}
|
|
loadingProviderTokens={loadingProviderTokens}
|
|
loadingProviderLatency={loadingProviderLatency}
|
|
startTime={urlState.start_time}
|
|
endTime={urlState.end_time}
|
|
providerCostChartType={toChartType(urlState.provider_cost_chart)}
|
|
providerTokenChartType={toChartType(urlState.provider_token_chart)}
|
|
providerLatencyChartType={toChartType(urlState.provider_latency_chart)}
|
|
providerCostProvider={urlState.provider_cost_provider}
|
|
providerTokenProvider={urlState.provider_token_provider}
|
|
providerLatencyProvider={urlState.provider_latency_provider}
|
|
availableProviders={availableProviders}
|
|
providerCostProviders={providerCostProviders}
|
|
providerTokenProviders={providerTokenProviders}
|
|
providerLatencyProviders={providerLatencyProviders}
|
|
onProviderCostChartToggle={handleProviderCostChartToggle}
|
|
onProviderTokenChartToggle={handleProviderTokenChartToggle}
|
|
onProviderLatencyChartToggle={handleProviderLatencyChartToggle}
|
|
onProviderCostProviderChange={handleProviderCostProviderChange}
|
|
onProviderTokenProviderChange={handleProviderTokenProviderChange}
|
|
onProviderLatencyProviderChange={handleProviderLatencyProviderChange}
|
|
/>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* Model Rankings Tab */}
|
|
<TabsContent value="rankings" {...(pdfMode && { forceMount: true })}>
|
|
<div id="dashboard-section-rankings">
|
|
<ModelRankingsTab
|
|
rankingsData={rankingsData}
|
|
loading={loadingRankings}
|
|
modelData={modelData}
|
|
loadingModels={loadingModels}
|
|
startTime={urlState.start_time}
|
|
endTime={urlState.end_time}
|
|
/>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* MCP Tab */}
|
|
<TabsContent value="mcp" {...(pdfMode && { forceMount: true })}>
|
|
<div id="dashboard-section-mcp">
|
|
<MCPTab
|
|
mcpHistogramData={mcpHistogramData}
|
|
mcpCostData={mcpCostData}
|
|
mcpTopToolsData={mcpTopToolsData}
|
|
loadingMcpHistogram={loadingMcpHistogram}
|
|
loadingMcpCost={loadingMcpCost}
|
|
loadingMcpTopTools={loadingMcpTopTools}
|
|
startTime={urlState.start_time}
|
|
endTime={urlState.end_time}
|
|
mcpVolumeChartType={toChartType(urlState.mcp_volume_chart)}
|
|
mcpCostChartType={toChartType(urlState.mcp_cost_chart)}
|
|
onMcpVolumeChartToggle={handleMcpVolumeChartToggle}
|
|
onMcpCostChartToggle={handleMcpCostChartToggle}
|
|
/>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* User Rankings Tab (Enterprise) */}
|
|
<TabsContent value="user-rankings">
|
|
<UserRankingsTab />
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
);
|
|
} |