[Refactor] UI - Spend Logs: consolidate filter state, extract components, remove dead code

- Lift filter state into index.tsx and pass to hook (removes selectedX vars + sync useEffect)
- Move main useQuery into useLogFilterLogic hook (removes isMainQueryEnabled toggle)
- Delete dead RequestViewer component (300 lines, replaced by LogDetailsDrawer)
- Extract LogsTableToolbar component (search, date range, pagination, live tail)
- Extract filter options config to filter_options.ts
- Remove dead code: handleRefresh, handleSelectLog, handleCloseDrawer, formatTimeUnit,
  showFilters/showColumnDropdown state, dropdownRef/filtersRef
This commit is contained in:
Ryan Crabbe 2026-04-16 00:45:48 -07:00
parent 72a461ba4a
commit 62100a6860
No known key found for this signature in database
5 changed files with 487 additions and 919 deletions

View file

@ -0,0 +1,246 @@
import moment from "moment";
import { useEffect, useRef, useState } from "react";
import { SyncOutlined } from "@ant-design/icons";
import { Switch } from "@tremor/react";
import { Button } from "antd";
import { QUICK_SELECT_OPTIONS } from "./constants";
import { getTimeRangeDisplay } from "./logs_utils";
import type { PaginatedResponse } from ".";
interface LogsTableToolbarProps {
searchTerm: string;
onSearchChange: (value: string) => void;
startTime: string;
onStartTimeChange: (value: string) => void;
endTime: string;
onEndTimeChange: (value: string) => void;
isCustomDate: boolean;
onIsCustomDateChange: (value: boolean) => void;
selectedTimeInterval: { value: number; unit: string };
onSelectedTimeIntervalChange: (value: { value: number; unit: string }) => void;
isLiveTail: boolean;
onIsLiveTailChange: (value: boolean) => void;
currentPage: number;
onCurrentPageChange: (updater: number | ((prev: number) => number)) => void;
pageSize: number;
isLoading: boolean;
isButtonLoading: boolean;
onRefetch: () => void;
filteredLogs: PaginatedResponse;
hasBackendFilters: boolean;
}
export function LogsTableToolbar({
searchTerm,
onSearchChange,
startTime,
onStartTimeChange,
endTime,
onEndTimeChange,
isCustomDate,
onIsCustomDateChange,
selectedTimeInterval,
onSelectedTimeIntervalChange,
isLiveTail,
onIsLiveTailChange,
currentPage,
onCurrentPageChange,
pageSize,
isLoading,
isButtonLoading,
onRefetch,
filteredLogs,
hasBackendFilters,
}: LogsTableToolbarProps) {
const [quickSelectOpen, setQuickSelectOpen] = useState(false);
const quickSelectRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (quickSelectRef.current && !quickSelectRef.current.contains(event.target as Node)) {
setQuickSelectOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const selectedOption = QUICK_SELECT_OPTIONS.find(
(option) => option.value === selectedTimeInterval.value && option.unit === selectedTimeInterval.unit,
);
const displayLabel = isCustomDate ? getTimeRangeDisplay(isCustomDate, startTime, endTime) : selectedOption?.label;
return (
<>
<div className="border-b px-6 py-4 w-full max-w-full box-border">
<div className="flex flex-col md:flex-row items-start md:items-center justify-between space-y-4 md:space-y-0 w-full max-w-full box-border">
<div className="flex flex-wrap items-center gap-3 w-full max-w-full box-border">
<div className="relative w-64 min-w-0 flex-shrink-0">
<input
type="text"
placeholder="Search by Request ID"
className="w-full px-3 py-2 pl-8 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
/>
<svg
className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
<div className="flex items-center gap-2 min-w-0 flex-shrink">
<div className="relative z-50" ref={quickSelectRef}>
<button
onClick={() => setQuickSelectOpen(!quickSelectOpen)}
className="px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
{displayLabel}
</button>
{quickSelectOpen && (
<div className="absolute right-0 mt-2 w-64 bg-white rounded-lg shadow-lg border p-2 z-50">
<div className="space-y-1">
{QUICK_SELECT_OPTIONS.map((option) => (
<button
key={option.label}
className={`w-full px-3 py-2 text-left text-sm hover:bg-gray-50 rounded-md ${displayLabel === option.label ? "bg-blue-50 text-blue-600" : ""}`}
onClick={() => {
onCurrentPageChange(1);
onEndTimeChange(moment().format("YYYY-MM-DDTHH:mm"));
onStartTimeChange(
moment()
.subtract(option.value, option.unit as any)
.format("YYYY-MM-DDTHH:mm"),
);
onSelectedTimeIntervalChange({ value: option.value, unit: option.unit });
onIsCustomDateChange(false);
setQuickSelectOpen(false);
}}
>
{option.label}
</button>
))}
<div className="border-t my-2" />
<button
className={`w-full px-3 py-2 text-left text-sm hover:bg-gray-50 rounded-md ${isCustomDate ? "bg-blue-50 text-blue-600" : ""}`}
onClick={() => onIsCustomDateChange(!isCustomDate)}
>
Custom Range
</button>
</div>
</div>
)}
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900">Live Tail</span>
<Switch color="green" checked={isLiveTail} defaultChecked={true} onChange={onIsLiveTailChange} />
</div>
<Button
type="default"
icon={<SyncOutlined spin={isButtonLoading} />}
onClick={onRefetch}
disabled={isButtonLoading}
title="Fetch data"
>
{isButtonLoading ? "Fetching" : "Fetch"}
</Button>
</div>
{isCustomDate && (
<div className="flex items-center gap-2">
<div>
<input
type="datetime-local"
value={startTime}
onChange={(e) => {
onStartTimeChange(e.target.value);
onCurrentPageChange(1);
}}
className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<span className="text-gray-500">to</span>
<div>
<input
type="datetime-local"
value={endTime}
onChange={(e) => {
onEndTimeChange(e.target.value);
onCurrentPageChange(1);
}}
className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
)}
</div>
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-700 whitespace-nowrap">
Showing {isLoading ? "..." : filteredLogs ? (currentPage - 1) * pageSize + 1 : 0} -{" "}
{isLoading
? "..."
: filteredLogs
? Math.min(currentPage * pageSize, filteredLogs.total)
: 0}{" "}
of {isLoading ? "..." : filteredLogs ? filteredLogs.total : 0} results
</span>
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-700 min-w-[90px]">
Page {isLoading ? "..." : currentPage} of{" "}
{isLoading ? "..." : filteredLogs ? filteredLogs.total_pages : 1}
</span>
<button
onClick={() => onCurrentPageChange((p: number) => Math.max(1, p - 1))}
disabled={isLoading || currentPage === 1}
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
<button
onClick={() => onCurrentPageChange((p: number) => Math.min(filteredLogs.total_pages || 1, p + 1))}
disabled={isLoading || currentPage === (filteredLogs.total_pages || 1)}
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
</div>
</div>
{isLiveTail && currentPage === 1 && !hasBackendFilters && (
<div className="mb-4 px-4 py-2 bg-green-50 border border-greem-200 rounded-md flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-green-700">Auto-refreshing every 15 seconds</span>
</div>
<button
onClick={() => onIsLiveTailChange(false)}
className="text-sm text-green-600 hover:text-green-800"
>
Stop
</button>
</div>
)}
</>
);
}

View file

@ -0,0 +1,71 @@
import FilterTeamDropdown from "../common_components/FilterTeamDropdown";
import { PaginatedKeyAliasSelect } from "../KeyAliasSelect/PaginatedKeyAliasSelect/PaginatedKeyAliasSelect";
import { PaginatedModelSelect } from "../ModelSelect/PaginatedModelSelect/PaginatedModelSelect";
import { FilterOption } from "../molecules/filter";
import { allEndUsersCall } from "../networking";
import { ERROR_CODE_OPTIONS } from "./constants";
export function getLogFilterOptions(accessToken: string): FilterOption[] {
return [
{
name: "Team ID",
label: "Team ID",
customComponent: FilterTeamDropdown,
},
{
name: "Status",
label: "Status",
isSearchable: false,
options: [
{ label: "Success", value: "success" },
{ label: "Failure", value: "failure" },
],
},
{
name: "Model",
label: "Model",
customComponent: PaginatedModelSelect,
},
{
name: "Key Alias",
label: "Key Alias",
customComponent: PaginatedKeyAliasSelect,
},
{
name: "End User",
label: "End User",
isSearchable: true,
searchFn: async (searchText: string) => {
const data = await allEndUsersCall(accessToken);
const users = data?.map((u: any) => u.user_id) || [];
const filtered = users.filter((u: string) => u.toLowerCase().includes(searchText.toLowerCase()));
return filtered.map((u: string) => ({ label: u, value: u }));
},
},
{
name: "Error Code",
label: "Error Code",
isSearchable: true,
searchFn: async (searchText: string) => {
if (!searchText) return ERROR_CODE_OPTIONS;
const lower = searchText.toLowerCase();
const filtered = ERROR_CODE_OPTIONS.filter((opt) => opt.label.toLowerCase().includes(lower));
const isExactValue = ERROR_CODE_OPTIONS.some((opt) => opt.value === searchText.trim());
if (!isExactValue && searchText.trim()) {
filtered.push({ label: `Use custom code: ${searchText.trim()}`, value: searchText.trim() });
}
return filtered;
},
},
{
name: "Key Hash",
label: "Key Hash",
isSearchable: false,
},
{
name: "Error Message",
label: "Error Message",
isSearchable: false,
},
];
}

View file

@ -1,20 +1,30 @@
import { render, screen, waitFor } from "@testing-library/react";
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import SpendLogsTable, { RequestViewer } from "./index";
import type { LogEntry } from "./columns";
import type { Row } from "@tanstack/react-table";
import SpendLogsTable from "./index";
import { renderWithProviders } from "../../../tests/test-utils";
const mockHandleFilterResetFromHook = vi.fn();
vi.mock("./log_filter_logic", () => ({
useLogFilterLogic: vi.fn(() => ({
filters: {},
logsQuery: { isLoading: false, isFetching: false, refetch: vi.fn() },
filteredLogs: { data: [], total: 0, page: 1, page_size: 50, total_pages: 1 },
allTeams: [],
handleFilterChange: vi.fn(),
handleFilterReset: mockHandleFilterResetFromHook,
})),
defaultFilters: {
"Team ID": "",
"Key Hash": "",
"Request ID": "",
"Model": "",
"User ID": "",
"End User": "",
"Status": "",
"Key Alias": "",
"Error Code": "",
"Error Message": "",
},
}));
vi.mock("../networking", async (importOriginal) => {
@ -38,139 +48,6 @@ vi.mock("../key_team_helpers/filter_helpers", () => ({
fetchAllTeams: vi.fn().mockResolvedValue([]),
}));
const baseLogEntry: LogEntry = {
request_id: "chatcmpl-test-id",
api_key: "api-key",
team_id: "team-id",
model: "gpt-4",
model_id: "gpt-4",
call_type: "chat",
spend: 0,
total_tokens: 0,
prompt_tokens: 0,
completion_tokens: 0,
startTime: "2025-11-14T00:00:00Z",
endTime: "2025-11-14T00:00:00Z",
cache_hit: "miss",
request_duration_ms: 1000,
messages: [{ role: "user", content: "hello" }],
response: { status: "ok" },
metadata: {
status: "success",
additional_usage_values: {
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
},
request_tags: {},
custom_llm_provider: "openai",
api_base: "https://api.example.com",
};
const createRow = (overrides: Partial<LogEntry> = {}): Row<LogEntry> =>
({
original: {
...baseLogEntry,
...overrides,
},
}) as unknown as Row<LogEntry>;
describe("Request Viewer", () => {
it("renders the request details heading", () => {
render(<RequestViewer row={createRow()} />);
expect(screen.getByText("Request Details")).toBeInTheDocument();
});
it("should truncate the request id if it is longer than 64 characters", () => {
const LONG_REQUEST_ID = "a".repeat(128);
const TRUNCATED_REQUEST_ID = `${"a".repeat(64)}...`;
render(
<RequestViewer
row={createRow({
request_id: LONG_REQUEST_ID,
})}
/>,
);
expect(screen.getByText(TRUNCATED_REQUEST_ID)).toBeInTheDocument();
});
it("should display LiteLLM Overhead when litellm_overhead_time_ms is present in metadata", () => {
render(
<RequestViewer
row={createRow({
metadata: {
status: "success",
litellm_overhead_time_ms: 150,
additional_usage_values: {
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
},
})}
/>,
);
expect(screen.getByText("LiteLLM Overhead:")).toBeInTheDocument();
expect(screen.getByText("150 ms")).toBeInTheDocument();
});
it("should not display LiteLLM Overhead when litellm_overhead_time_ms is not present in metadata", () => {
render(<RequestViewer row={createRow()} />);
expect(screen.queryByText("LiteLLM Overhead:")).not.toBeInTheDocument();
});
it("should display retry count when attempted_retries > 0 in metadata", () => {
render(
<RequestViewer
row={createRow({
metadata: {
status: "success",
attempted_retries: 2,
max_retries: 3,
additional_usage_values: {
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
},
})}
/>,
);
expect(screen.getByText("Retries:")).toBeInTheDocument();
expect(screen.getByText("2 / 3")).toBeInTheDocument();
});
it("should display green 'None' tag when attempted_retries is 0", () => {
render(
<RequestViewer
row={createRow({
metadata: {
status: "success",
attempted_retries: 0,
max_retries: 3,
additional_usage_values: {
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
},
})}
/>,
);
expect(screen.getByText("Retries:")).toBeInTheDocument();
expect(screen.getByText("None")).toBeInTheDocument();
});
it("should display '-' for Retries when attempted_retries is not present in metadata", () => {
render(<RequestViewer row={createRow()} />);
expect(screen.getByText("Retries:")).toBeInTheDocument();
expect(screen.getByText("-")).toBeInTheDocument();
});
});
describe("SpendLogsTable", () => {
const defaultProps = {
accessToken: "test-token",

View file

@ -1,36 +1,24 @@
import { keepPreviousData, useQuery, useQueryClient } from "@tanstack/react-query";
import moment from "moment";
import { useCallback, useDeferredValue, useEffect, useRef, useState } from "react";
import GuardrailViewer from "@/components/view_logs/GuardrailViewer/GuardrailViewer";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { truncateString } from "@/utils/textUtils";
import { SettingOutlined, SyncOutlined } from "@ant-design/icons";
import { Row } from "@tanstack/react-table";
import { Switch, Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
import { Button, Tag, Tooltip } from "antd";
import { useCallback, useDeferredValue, useEffect, useState } from "react";
import { SettingOutlined } from "@ant-design/icons";
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
import { Button } from "antd";
import { internalUserRoles } from "../../utils/roles";
import DeletedKeysPage from "../DeletedKeysPage/DeletedKeysPage";
import DeletedTeamsPage from "../DeletedTeamsPage/DeletedTeamsPage";
import FilterTeamDropdown from "../common_components/FilterTeamDropdown";
import { KeyResponse } from "../key_team_helpers/key_list";
import { PaginatedKeyAliasSelect } from "../KeyAliasSelect/PaginatedKeyAliasSelect/PaginatedKeyAliasSelect";
import { PaginatedModelSelect } from "../ModelSelect/PaginatedModelSelect/PaginatedModelSelect";
import FilterComponent, { FilterOption } from "../molecules/filter";
import { allEndUsersCall, keyInfoV1Call, uiSpendLogsCall } from "../networking";
import FilterComponent from "../molecules/filter";
import { keyInfoV1Call } from "../networking";
import KeyInfoView from "../templates/key_info_view";
import AuditLogs from "./audit_logs";
import { createColumns, LogEntry, type LogsSortField } from "./columns";
import { ConfigInfoMessage } from "./ConfigInfoMessage";
import { AGENT_CALL_TYPES, ERROR_CODE_OPTIONS, MCP_CALL_TYPES, QUICK_SELECT_OPTIONS } from "./constants";
import { CostBreakdownViewer } from "./CostBreakdownViewer";
import { ErrorViewer } from "./ErrorViewer";
import { useLogFilterLogic } from "./log_filter_logic";
import { AGENT_CALL_TYPES, MCP_CALL_TYPES } from "./constants";
import { getLogFilterOptions } from "./filter_options";
import { useLogFilterLogic, defaultFilters, type LogFilterState } from "./log_filter_logic";
import { LogDetailsDrawer } from "./LogDetailsDrawer";
import { getTimeRangeDisplay } from "./logs_utils";
import { RequestResponsePanel } from "./RequestResponsePanel";
import { LogsTableToolbar } from "./LogsTableToolbar";
import SpendLogsSettingsModal from "./SpendLogsSettingsModal/SpendLogsSettingsModal";
import { DataTable } from "./table";
import { VectorStoreViewer } from "./VectorStoreViewer";
interface SpendLogsTableProps {
accessToken: string | null;
@ -56,29 +44,17 @@ export default function SpendLogsTable({
premiumUser,
}: SpendLogsTableProps) {
const [searchTerm, setSearchTerm] = useState("");
const [showFilters, setShowFilters] = useState(false);
const [showColumnDropdown, setShowColumnDropdown] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize] = useState(50);
const dropdownRef = useRef<HTMLDivElement>(null);
const filtersRef = useRef<HTMLDivElement>(null);
const quickSelectRef = useRef<HTMLDivElement>(null);
// New state variables for Start and End Time
const [startTime, setStartTime] = useState<string>(moment().subtract(24, "hours").format("YYYY-MM-DDTHH:mm"));
const [endTime, setEndTime] = useState<string>(moment().format("YYYY-MM-DDTHH:mm"));
const [isCustomDate, setIsCustomDate] = useState(false);
const [quickSelectOpen, setQuickSelectOpen] = useState(false);
const [tempTeamId, setTempTeamId] = useState("");
const [tempKeyHash, setTempKeyHash] = useState("");
const [selectedTeamId, setSelectedTeamId] = useState("");
const [selectedKeyHash, setSelectedKeyHash] = useState("");
const [selectedModelId, setSelectedModelId] = useState("");
const [filters, setFilters] = useState<LogFilterState>(defaultFilters);
const [selectedKeyInfo, setSelectedKeyInfo] = useState<KeyResponse | null>(null);
const [selectedKeyIdInfoView, setSelectedKeyIdInfoView] = useState<string | null>(null);
const [selectedStatus, setSelectedStatus] = useState("");
const [selectedEndUser, setSelectedEndUser] = useState("");
const [filterByCurrentUser, setFilterByCurrentUser] = useState(userRole && internalUserRoles.includes(userRole));
const [activeTab, setActiveTab] = useState("request logs");
@ -90,12 +66,6 @@ export default function SpendLogsTable({
const [sortBy, setSortBy] = useState<LogsSortField>("startTime");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
// Tracks whether any filter that uses performSearch (backend) is active.
// Used to disable the main query so it doesn't fire redundant unfiltered requests
// when time range / sort / page changes while a backend filter is in effect.
const [isMainQueryEnabled, setIsMainQueryEnabled] = useState(true);
const queryClient = useQueryClient();
const [isLiveTail, setIsLiveTail] = useState<boolean>(() => {
const storedValue = sessionStorage.getItem("isLiveTail");
@ -128,23 +98,6 @@ export default function SpendLogsTable({
fetchKeyInfo();
}, [selectedKeyIdInfoView, accessToken]);
// Close dropdown when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setShowColumnDropdown(false);
}
if (filtersRef.current && !filtersRef.current.contains(event.target as Node)) {
setShowFilters(false);
}
if (quickSelectRef.current && !quickSelectRef.current.contains(event.target as Node)) {
setQuickSelectOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
useEffect(() => {
if (userRole && internalUserRoles.includes(userRole)) {
@ -152,112 +105,39 @@ export default function SpendLogsTable({
}
}, [userRole]);
const LiveTailControls = () => {
return (
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900">Live Tail</span>
<Switch color="green" checked={isLiveTail} defaultChecked={true} onChange={setIsLiveTail} />
</div>
);
};
const logs = useQuery<PaginatedResponse>({
queryKey: [
"logs",
"table",
currentPage,
pageSize,
startTime,
endTime,
selectedTeamId,
selectedKeyHash,
filterByCurrentUser ? userID : null,
selectedStatus,
selectedModelId,
sortBy,
sortOrder,
],
queryFn: async () => {
if (!accessToken || !token || !userRole || !userID) {
return {
data: [],
total: 0,
page: 1,
page_size: pageSize,
total_pages: 0,
};
}
const formattedStartTime = moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss");
const formattedEndTime = isCustomDate
? moment(endTime).utc().format("YYYY-MM-DD HH:mm:ss")
: moment().utc().format("YYYY-MM-DD HH:mm:ss");
// Get base response from API
// NOTE: We only fetch the list of logs here (lightweight).
// Log details (messages/response) are fetched on-demand when user clicks a row.
const response = await uiSpendLogsCall({
accessToken,
start_date: formattedStartTime,
end_date: formattedEndTime,
page: currentPage,
page_size: pageSize,
params: {
api_key: selectedKeyHash || undefined,
team_id: selectedTeamId || undefined,
user_id: filterByCurrentUser ? userID ?? undefined : undefined,
end_user: selectedEndUser || undefined,
status_filter: selectedStatus || undefined,
model_id: selectedModelId || undefined,
sort_by: sortBy,
sort_order: sortOrder,
},
});
return response;
},
enabled: !!accessToken && !!token && !!userRole && !!userID && activeTab === "request logs" && isMainQueryEnabled,
refetchInterval: isLiveTail && currentPage === 1 ? 15000 : false,
placeholderData: keepPreviousData,
refetchIntervalInBackground: true,
});
// Defer the transition from "Fetching" to "Fetch" so the button stays loading until
// the table has rendered with the new data (avoids the visual gap where the button
// exits loading state before the table updates)
const isFetchingDeferred = useDeferredValue(logs.isFetching);
const isButtonLoading = logs.isFetching || isFetchingDeferred;
const logsData = logs.data || {
data: [],
total: 0,
page: 1,
page_size: pageSize || 10,
total_pages: 1,
};
const {
filters,
logsQuery,
filteredLogs,
hasBackendFilters,
allTeams,
handleFilterChange,
handleFilterReset: handleFilterResetFromHook,
} = useLogFilterLogic({
logs: logsData,
accessToken,
token,
userRole,
userID,
filters,
setFilters,
filterByCurrentUser: !!filterByCurrentUser,
activeTab,
isLiveTail,
startTime,
endTime,
pageSize,
isCustomDate,
setCurrentPage,
userID,
userRole,
sortBy,
sortOrder,
currentPage,
});
// Defer the transition from "Fetching" to "Fetch" so the button stays loading until
// the table has rendered with the new data (avoids the visual gap where the button
// exits loading state before the table updates)
const isFetchingDeferred = useDeferredValue(logsQuery.isFetching);
const isButtonLoading = logsQuery.isFetching || isFetchingDeferred;
const handleFilterReset = useCallback(() => {
handleFilterResetFromHook();
// Reset custom time range to default (last 24 hours)
@ -268,30 +148,6 @@ export default function SpendLogsTable({
setCurrentPage(1);
}, [handleFilterResetFromHook]);
// Disable the main query whenever backend filters are active so it doesn't fire
// redundant unfiltered requests when time range / sort / page changes.
useEffect(() => {
setIsMainQueryEnabled(!hasBackendFilters);
}, [hasBackendFilters]);
// Sync filter state into the individual selectedX state variables used by the main query
useEffect(() => {
if (!accessToken) return;
if (filters["Team ID"]) {
setSelectedTeamId(filters["Team ID"]);
} else {
setSelectedTeamId("");
}
setSelectedStatus(filters["Status"] || "");
setSelectedModelId(filters["Model"] || "");
setSelectedEndUser(filters["End User"] || "");
// Key Alias filtering is handled server-side by performSearch via the key_alias param.
// We intentionally do not translate the alias to a hash here to avoid firing a
// redundant main-query request (api_key=hash) alongside performSearch's key_alias request.
setSelectedKeyHash(filters["Key Hash"] || "");
}, [filters, accessToken]);
if (!accessToken || !token || !userRole || !userID) {
return null;
@ -361,13 +217,8 @@ export default function SpendLogsTable({
return sessionRepresentativeMap.get(log.session_id)?.requestId === log.request_id;
}) || [];
// Add this function to handle manual refresh
const handleRefresh = () => {
logs.refetch();
};
const handleRowClick = (log: LogEntry) => {
// Multi-call session row: open in the same right-side drawer (session mode)
// Multi-call session row: open in the same right-side drawer (session mode)can
if (log.session_id && (log.session_total_count || 1) > 1) {
setSelectedSessionId(log.session_id);
setSelectedLog(log);
@ -380,95 +231,6 @@ export default function SpendLogsTable({
setIsDrawerOpen(true);
};
const handleCloseDrawer = () => {
setIsDrawerOpen(false);
setSelectedSessionId(null);
};
const handleSelectLog = (log: LogEntry) => {
setSelectedLog(log);
};
const logFilterOptions: FilterOption[] = [
{
name: "Team ID",
label: "Team ID",
customComponent: FilterTeamDropdown,
},
{
name: "Status",
label: "Status",
isSearchable: false,
options: [
{ label: "Success", value: "success" },
{ label: "Failure", value: "failure" },
],
},
{
name: "Model",
label: "Model",
customComponent: PaginatedModelSelect,
},
{
name: "Key Alias",
label: "Key Alias",
customComponent: PaginatedKeyAliasSelect,
},
{
name: "End User",
label: "End User",
isSearchable: true,
searchFn: async (searchText: string) => {
if (!accessToken) return [];
const data = await allEndUsersCall(accessToken);
// data if set, is a list of objects, with key = user_id
const users = data?.map((u: any) => u.user_id) || [];
const filtered = users.filter((u: string) => u.toLowerCase().includes(searchText.toLowerCase()));
return filtered.map((u: string) => ({ label: u, value: u }));
},
},
{
name: "Error Code",
label: "Error Code",
isSearchable: true,
searchFn: async (searchText: string) => {
if (!searchText) return ERROR_CODE_OPTIONS;
const lower = searchText.toLowerCase();
const filtered = ERROR_CODE_OPTIONS.filter((opt) => opt.label.toLowerCase().includes(lower));
const isExactValue = ERROR_CODE_OPTIONS.some((opt) => opt.value === searchText.trim());
if (!isExactValue && searchText.trim()) {
filtered.push({ label: `Use custom code: ${searchText.trim()}`, value: searchText.trim() });
}
return filtered;
},
},
{
name: "Key Hash",
label: "Key Hash",
isSearchable: false,
},
{
name: "Error Message",
label: "Error Message",
isSearchable: false,
},
];
const formatTimeUnit = (value: number, unit: string) => {
if (value === 1) {
if (unit === "minutes") return "minute";
if (unit === "hours") return "hour";
if (unit === "days") return "day";
}
return unit;
};
const selectedOption = QUICK_SELECT_OPTIONS.find(
(option) => option.value === selectedTimeInterval.value && option.unit === selectedTimeInterval.unit,
);
const displayLabel = isCustomDate ? getTimeRangeDisplay(isCustomDate, startTime, endTime) : selectedOption?.label;
return (
<div className="w-full max-w-screen p-6 overflow-x-hidden box-border">
<TabGroup defaultIndex={0} onIndexChange={(index) => setActiveTab(index === 0 ? "request logs" : "audit logs")}>
@ -499,7 +261,7 @@ export default function SpendLogsTable({
) : (
<>
<FilterComponent
options={logFilterOptions}
options={getLogFilterOptions(accessToken)}
onApplyFilters={handleFilterChange}
onResetFilters={handleFilterReset}
/>
@ -509,174 +271,28 @@ export default function SpendLogsTable({
onSuccess={() => setIsSpendLogsSettingsModalVisible(false)}
/>
<div className="bg-white rounded-lg shadow w-full max-w-full box-border">
<div className="border-b px-6 py-4 w-full max-w-full box-border">
<div className="flex flex-col md:flex-row items-start md:items-center justify-between space-y-4 md:space-y-0 w-full max-w-full box-border">
<div className="flex flex-wrap items-center gap-3 w-full max-w-full box-border">
<div className="relative w-64 min-w-0 flex-shrink-0">
<input
type="text"
placeholder="Search by Request ID"
className="w-full px-3 py-2 pl-8 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<svg
className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
<div className="flex items-center gap-2 min-w-0 flex-shrink">
<div className="relative z-50" ref={quickSelectRef}>
<button
onClick={() => setQuickSelectOpen(!quickSelectOpen)}
className="px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
{displayLabel}
</button>
{quickSelectOpen && (
<div className="absolute right-0 mt-2 w-64 bg-white rounded-lg shadow-lg border p-2 z-50">
<div className="space-y-1">
{QUICK_SELECT_OPTIONS.map((option) => (
<button
key={option.label}
className={`w-full px-3 py-2 text-left text-sm hover:bg-gray-50 rounded-md ${displayLabel === option.label ? "bg-blue-50 text-blue-600" : ""
}`}
onClick={() => {
setCurrentPage(1);
setEndTime(moment().format("YYYY-MM-DDTHH:mm"));
setStartTime(
moment()
.subtract(option.value, option.unit as any)
.format("YYYY-MM-DDTHH:mm"),
);
setSelectedTimeInterval({ value: option.value, unit: option.unit });
setIsCustomDate(false);
setQuickSelectOpen(false);
}}
>
{option.label}
</button>
))}
<div className="border-t my-2" />
<button
className={`w-full px-3 py-2 text-left text-sm hover:bg-gray-50 rounded-md ${isCustomDate ? "bg-blue-50 text-blue-600" : ""
}`}
onClick={() => setIsCustomDate(!isCustomDate)}
>
Custom Range
</button>
</div>
</div>
)}
</div>
<LiveTailControls />
<Button
type="default"
icon={<SyncOutlined spin={isButtonLoading} />}
onClick={handleRefresh}
disabled={isButtonLoading}
title="Fetch data"
>
{isButtonLoading ? "Fetching" : "Fetch"}
</Button>
</div>
{isCustomDate && (
<div className="flex items-center gap-2">
<div>
<input
type="datetime-local"
value={startTime}
onChange={(e) => {
setStartTime(e.target.value);
setCurrentPage(1);
}}
className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<span className="text-gray-500">to</span>
<div>
<input
type="datetime-local"
value={endTime}
onChange={(e) => {
setEndTime(e.target.value);
setCurrentPage(1);
}}
className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
)}
</div>
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-700 whitespace-nowrap">
Showing {logs.isLoading ? "..." : filteredLogs ? (currentPage - 1) * pageSize + 1 : 0} -{" "}
{logs.isLoading
? "..."
: filteredLogs
? Math.min(currentPage * pageSize, filteredLogs.total)
: 0}{" "}
of {logs.isLoading ? "..." : filteredLogs ? filteredLogs.total : 0} results
</span>
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-700 min-w-[90px]">
Page {logs.isLoading ? "..." : currentPage} of{" "}
{logs.isLoading ? "..." : filteredLogs ? filteredLogs.total_pages : 1}
</span>
<button
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={logs.isLoading || currentPage === 1}
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
<button
onClick={() => setCurrentPage((p) => Math.min(filteredLogs.total_pages || 1, p + 1))}
disabled={logs.isLoading || currentPage === (filteredLogs.total_pages || 1)}
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
</div>
</div>
{isLiveTail && currentPage === 1 && isMainQueryEnabled && (
<div className="mb-4 px-4 py-2 bg-green-50 border border-greem-200 rounded-md flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-green-700">Auto-refreshing every 15 seconds</span>
</div>
<button
onClick={() => setIsLiveTail(false)}
className="text-sm text-green-600 hover:text-green-800"
>
Stop
</button>
</div>
)}
<LogsTableToolbar
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
startTime={startTime}
onStartTimeChange={setStartTime}
endTime={endTime}
onEndTimeChange={setEndTime}
isCustomDate={isCustomDate}
onIsCustomDateChange={setIsCustomDate}
selectedTimeInterval={selectedTimeInterval}
onSelectedTimeIntervalChange={setSelectedTimeInterval}
isLiveTail={isLiveTail}
onIsLiveTailChange={setIsLiveTail}
currentPage={currentPage}
onCurrentPageChange={setCurrentPage}
pageSize={pageSize}
isLoading={logsQuery.isLoading}
isButtonLoading={isButtonLoading}
onRefetch={() => logsQuery.refetch()}
filteredLogs={filteredLogs}
hasBackendFilters={hasBackendFilters}
/>
<DataTable
columns={createColumns({
sortBy,
@ -689,7 +305,7 @@ export default function SpendLogsTable({
})}
data={filteredData}
onRowClick={handleRowClick}
isLoading={logs.isLoading}
isLoading={logsQuery.isLoading}
/>
</div>
</>
@ -713,323 +329,16 @@ export default function SpendLogsTable({
{/* Log Details Drawer */}
<LogDetailsDrawer
open={isDrawerOpen}
onClose={handleCloseDrawer}
onClose={() => { setIsDrawerOpen(false); setSelectedSessionId(null); }}
logEntry={selectedLog}
sessionId={selectedSessionId}
accessToken={accessToken}
onOpenSettings={() => setIsSpendLogsSettingsModalVisible(true)}
allLogs={filteredData}
onSelectLog={handleSelectLog}
onSelectLog={setSelectedLog}
startTime={moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss")}
/>
</div>
);
}
export function RequestViewer({ row, onOpenSettings }: { row: Row<LogEntry>; onOpenSettings?: () => void }) {
// Helper function to clean metadata by removing specific fields
const formatData = (input: any) => {
if (typeof input === "string") {
try {
return JSON.parse(input);
} catch {
return input;
}
}
return input;
};
// New helper function to get raw request
const getRawRequest = () => {
// First check if proxy_server_request exists in metadata
if (row.original?.proxy_server_request) {
return formatData(row.original.proxy_server_request);
}
// Fall back to messages if proxy_server_request is empty
return formatData(row.original.messages);
};
// Extract error information from metadata if available
const metadata = row.original.metadata || {};
const hasError = metadata.status === "failure";
const errorInfo = hasError ? metadata.error_information : null;
// Check if request/response data is missing
const hasMessages =
row.original.messages &&
(Array.isArray(row.original.messages)
? row.original.messages.length > 0
: Object.keys(row.original.messages).length > 0);
const hasResponse = row.original.response && Object.keys(formatData(row.original.response)).length > 0;
const missingData = !hasMessages && !hasResponse && !hasError;
// Format the response with error details if present
const formattedResponse = () => {
if (hasError && errorInfo) {
return {
error: {
message: errorInfo.error_message || "An error occurred",
type: errorInfo.error_class || "error",
code: errorInfo.error_code || "unknown",
param: null,
},
};
}
return formatData(row.original.response);
};
// Extract vector store request metadata if available
const hasVectorStoreData =
metadata.vector_store_request_metadata &&
Array.isArray(metadata.vector_store_request_metadata) &&
metadata.vector_store_request_metadata.length > 0;
// Extract guardrail information from metadata if available
const guardrailInfo = row.original.metadata?.guardrail_information;
const guardrailEntries = Array.isArray(guardrailInfo) ? guardrailInfo : guardrailInfo ? [guardrailInfo] : [];
const hasGuardrailData = guardrailEntries.length > 0;
// Calculate total masked entities if guardrail data exists
const totalMaskedEntities = guardrailEntries.reduce((sum, entry) => {
const maskedCounts = entry?.masked_entity_count;
if (!maskedCounts) {
return sum;
}
return (
sum +
Object.values(maskedCounts).reduce<number>((acc, count) => (typeof count === "number" ? acc + count : acc), 0)
);
}, 0);
const primaryGuardrailLabel =
guardrailEntries.length === 1
? guardrailEntries[0]?.guardrail_name ?? "-"
: guardrailEntries.length > 1
? `${guardrailEntries.length} guardrails`
: "-";
const truncatedRequestId = truncateString(row.original.request_id, 64);
return (
<div className="p-6 bg-gray-50 space-y-6 w-full max-w-full overflow-hidden box-border">
{/* Combined Info Card */}
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden">
<div className="p-4 border-b">
<h3 className="text-lg font-medium">Request Details</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 w-full max-w-full overflow-hidden">
<div className="space-y-2">
<div className="flex">
<span className="font-medium w-1/3">Request ID:</span>
{row.original.request_id.length > 64 ? (
<Tooltip title={row.original.request_id}>
<span className="font-mono text-sm">{truncatedRequestId}</span>
</Tooltip>
) : (
<span className="font-mono text-sm">{row.original.request_id}</span>
)}
</div>
<div className="flex">
<span className="font-medium w-1/3">Model:</span>
<span>{row.original.model}</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Model ID:</span>
<span>{row.original.model_id}</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Call Type:</span>
<span>{row.original.call_type}</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Provider:</span>
<span>{row.original.custom_llm_provider || "-"}</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">API Base:</span>
<Tooltip title={row.original.api_base || "-"}>
<span className="max-w-[15ch] truncate block">{row.original.api_base || "-"}</span>
</Tooltip>
</div>
{row?.original?.requester_ip_address && (
<div className="flex">
<span className="font-medium w-1/3">IP Address:</span>
<span>{row?.original?.requester_ip_address}</span>
</div>
)}
{hasGuardrailData && (
<div className="flex">
<span className="font-medium w-1/3">Guardrail:</span>
<div>
<span className="font-mono">{primaryGuardrailLabel}</span>
{totalMaskedEntities > 0 && (
<span className="ml-2 px-2 py-0.5 bg-blue-50 text-blue-700 rounded-md text-xs font-medium">
{totalMaskedEntities} masked
</span>
)}
</div>
</div>
)}
</div>
<div className="space-y-2">
<div className="flex">
<span className="font-medium w-1/3">Tokens:</span>
<span>
{row.original.total_tokens} ({row.original.prompt_tokens} prompt tokens +{" "}
{row.original.completion_tokens} completion tokens)
</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Cache Read Tokens:</span>
<span>
{formatNumberWithCommas(row.original.metadata?.additional_usage_values?.cache_read_input_tokens || 0)}
</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Cache Creation Tokens:</span>
<span>
{formatNumberWithCommas(row.original.metadata?.additional_usage_values.cache_creation_input_tokens)}
</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Cost:</span>
<span>${formatNumberWithCommas(row.original.spend || 0, 6)}</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Cache Hit:</span>
<span>{row.original.cache_hit}</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Status:</span>
<span
className={`px-2 py-1 rounded-md text-xs font-medium inline-block text-center w-16 ${(row.original.metadata?.status || "Success").toLowerCase() !== "failure"
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}
>
{(row.original.metadata?.status || "Success").toLowerCase() !== "failure" ? "Success" : "Failure"}
</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Start Time:</span>
<span>{row.original.startTime}</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">End Time:</span>
<span>{row.original.endTime}</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Duration:</span>
<span>{row.original.request_duration_ms != null ? (row.original.request_duration_ms / 1000).toFixed(3) : "-"} s.</span>
</div>
{row.original.metadata?.litellm_overhead_time_ms !== undefined && (
<div className="flex">
<span className="font-medium w-1/3">LiteLLM Overhead:</span>
<span>{row.original.metadata.litellm_overhead_time_ms} ms</span>
</div>
)}
<div className="flex">
<span className="font-medium w-1/3">Retries:</span>
<span>
{row.original.metadata?.attempted_retries !== undefined && row.original.metadata?.attempted_retries !== null
? row.original.metadata.attempted_retries > 0
? `${row.original.metadata.attempted_retries}${row.original.metadata.max_retries !== undefined && row.original.metadata.max_retries !== null ? ` / ${row.original.metadata.max_retries}` : ''}`
: <Tag color="green">None</Tag>
: '-'}
</span>
</div>
</div>
</div>
</div>
{/* Cost Breakdown - Show if cost breakdown data is available */}
<CostBreakdownViewer
costBreakdown={row.original.metadata?.cost_breakdown}
totalSpend={row.original.spend ?? 0}
promptTokens={row.original.prompt_tokens}
completionTokens={row.original.completion_tokens}
cacheHit={row.original.cache_hit}
/>
{/* Configuration Info Message - Show when data is missing */}
<ConfigInfoMessage show={missingData} onOpenSettings={onOpenSettings} />
{/* Request/Response Panel */}
<div className="w-full max-w-full overflow-hidden">
<RequestResponsePanel
row={row}
hasMessages={hasMessages}
hasResponse={hasResponse}
hasError={hasError}
errorInfo={errorInfo}
getRawRequest={getRawRequest}
formattedResponse={formattedResponse}
/>
</div>
{/* Guardrail Data - Show only if present */}
{hasGuardrailData && <GuardrailViewer data={guardrailInfo} />}
{/* Vector Store Request Data - Show only if present */}
{hasVectorStoreData && <VectorStoreViewer data={metadata.vector_store_request_metadata} />}
{/* Error Card - Only show for failures */}
{hasError && errorInfo && <ErrorViewer errorInfo={errorInfo} />}
{/* Tags Card - Only show if there are tags */}
{row.original.request_tags && Object.keys(row.original.request_tags).length > 0 && (
<div className="bg-white rounded-lg shadow">
<div className="flex justify-between items-center p-4 border-b">
<h3 className="text-lg font-medium">Request Tags</h3>
</div>
<div className="p-4">
<div className="flex flex-wrap gap-2">
{Object.entries(row.original.request_tags).map(([key, value]) => (
<span key={key} className="px-2 py-1 bg-gray-100 rounded-full text-xs">
{key}: {String(value)}
</span>
))}
</div>
</div>
</div>
)}
{/* Metadata Card - Only show if there's metadata */}
{row.original.metadata && Object.keys(row.original.metadata).length > 0 && (
<div className="bg-white rounded-lg shadow">
<div className="flex justify-between items-center p-4 border-b">
<h3 className="text-lg font-medium">Metadata</h3>
<button
onClick={() => {
navigator.clipboard.writeText(JSON.stringify(row.original.metadata, null, 2));
}}
className="p-1 hover:bg-gray-200 rounded"
title="Copy metadata"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
</div>
<div className="p-4 overflow-auto max-h-64">
<pre className="text-xs font-mono whitespace-pre-wrap break-all">
{JSON.stringify(row.original.metadata, null, 2)}
</pre>
</div>
</div>
)}
</div>
);
}

View file

@ -2,14 +2,14 @@ import moment from "moment";
import { useCallback, useEffect, useState, useRef, useMemo } from "react";
import { uiSpendLogsCall } from "../networking";
import { Team } from "../key_team_helpers/key_list";
import { useQuery } from "@tanstack/react-query";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { fetchAllTeams } from "../../components/key_team_helpers/filter_helpers";
import { debounce } from "lodash";
import { defaultPageSize } from "../constants";
import { PaginatedResponse } from ".";
import type { LogsSortField } from "./columns";
const FILTER_KEYS = {
export const FILTER_KEYS = {
TEAM_ID: "Team ID",
KEY_HASH: "Key Hash",
REQUEST_ID: "Request ID",
@ -25,50 +25,56 @@ const FILTER_KEYS = {
export type FilterKey = keyof typeof FILTER_KEYS;
export type LogFilterState = Record<(typeof FILTER_KEYS)[FilterKey], string>;
export const defaultFilters: LogFilterState = {
[FILTER_KEYS.TEAM_ID]: "",
[FILTER_KEYS.KEY_HASH]: "",
[FILTER_KEYS.REQUEST_ID]: "",
[FILTER_KEYS.MODEL]: "",
[FILTER_KEYS.USER_ID]: "",
[FILTER_KEYS.END_USER]: "",
[FILTER_KEYS.STATUS]: "",
[FILTER_KEYS.KEY_ALIAS]: "",
[FILTER_KEYS.ERROR_CODE]: "",
[FILTER_KEYS.ERROR_MESSAGE]: "",
};
export function useLogFilterLogic({
logs,
accessToken,
startTime, // Receive from SpendLogsTable
endTime, // Receive from SpendLogsTable
token,
userRole,
userID,
filters,
setFilters,
filterByCurrentUser,
activeTab,
isLiveTail,
startTime,
endTime,
pageSize = defaultPageSize,
isCustomDate,
setCurrentPage,
userID,
userRole,
sortBy = "startTime",
sortOrder = "desc",
currentPage = 1,
}: {
logs: PaginatedResponse;
accessToken: string | null;
token: string | null;
userRole: string | null;
userID: string | null;
filters: LogFilterState;
setFilters: React.Dispatch<React.SetStateAction<LogFilterState>>;
filterByCurrentUser: boolean | null;
activeTab: string;
isLiveTail: boolean;
startTime: string;
endTime: string;
pageSize?: number;
isCustomDate: boolean;
setCurrentPage: (page: number) => void;
userID: string | null;
userRole: string | null;
sortBy?: LogsSortField;
sortOrder?: "asc" | "desc";
currentPage?: number;
}) {
const defaultFilters = useMemo<LogFilterState>(
() => ({
[FILTER_KEYS.TEAM_ID]: "",
[FILTER_KEYS.KEY_HASH]: "",
[FILTER_KEYS.REQUEST_ID]: "",
[FILTER_KEYS.MODEL]: "",
[FILTER_KEYS.USER_ID]: "",
[FILTER_KEYS.END_USER]: "",
[FILTER_KEYS.STATUS]: "",
[FILTER_KEYS.KEY_ALIAS]: "",
[FILTER_KEYS.ERROR_CODE]: "",
[FILTER_KEYS.ERROR_MESSAGE]: "",
}),
[],
);
const [filters, setFilters] = useState<LogFilterState>(defaultFilters);
const [backendFilteredLogs, setBackendFilteredLogs] = useState<PaginatedResponse | null>(null);
const lastSearchTimestamp = useRef(0);
const performSearch = useCallback(
@ -152,6 +158,64 @@ export function useLogFilterLogic({
[filters],
);
const logsQuery = useQuery<PaginatedResponse>({
queryKey: [
"logs",
"table",
currentPage,
pageSize,
startTime,
endTime,
filters[FILTER_KEYS.TEAM_ID],
filters[FILTER_KEYS.KEY_HASH],
filterByCurrentUser ? userID : null,
filters[FILTER_KEYS.STATUS],
filters[FILTER_KEYS.MODEL],
sortBy,
sortOrder,
],
queryFn: async () => {
if (!accessToken || !token || !userRole || !userID) {
return {
data: [],
total: 0,
page: 1,
page_size: pageSize,
total_pages: 0,
};
}
const formattedStartTime = moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss");
const formattedEndTime = isCustomDate
? moment(endTime).utc().format("YYYY-MM-DD HH:mm:ss")
: moment().utc().format("YYYY-MM-DD HH:mm:ss");
const response = await uiSpendLogsCall({
accessToken,
start_date: formattedStartTime,
end_date: formattedEndTime,
page: currentPage,
page_size: pageSize,
params: {
api_key: filters[FILTER_KEYS.KEY_HASH] || undefined,
team_id: filters[FILTER_KEYS.TEAM_ID] || undefined,
user_id: filterByCurrentUser ? userID ?? undefined : undefined,
end_user: filters[FILTER_KEYS.END_USER] || undefined,
status_filter: filters[FILTER_KEYS.STATUS] || undefined,
model_id: filters[FILTER_KEYS.MODEL] || undefined,
sort_by: sortBy,
sort_order: sortOrder,
},
});
return response;
},
enabled: !!accessToken && !!token && !!userRole && !!userID && activeTab === "request logs" && !hasBackendFilters,
refetchInterval: isLiveTail && currentPage === 1 ? 15000 : false,
placeholderData: keepPreviousData,
refetchIntervalInBackground: true,
});
// Refetch when sort, page, or time range changes (backend filters use their own fetch, not the main query)
useEffect(() => {
if (hasBackendFilters && accessToken) {
@ -167,9 +231,10 @@ export function useLogFilterLogic({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sortBy, sortOrder, currentPage, startTime, endTime, isCustomDate]);
// Compute client-side filtered logs directly from incoming logs and filters
// Compute client-side filtered logs directly from query data and filters
const spendLogsData = logsQuery.data;
const clientDerivedFilteredLogs: PaginatedResponse = useMemo(() => {
if (!logs || !logs.data) {
if (!spendLogsData) {
return {
data: [],
total: 0,
@ -181,10 +246,10 @@ export function useLogFilterLogic({
// If backend filters are on, don't perform client-side filtering here
if (hasBackendFilters) {
return logs;
return spendLogsData;
}
let filteredData = [...logs.data];
let filteredData = [...spendLogsData.data];
if (filters[FILTER_KEYS.TEAM_ID]) {
filteredData = filteredData.filter((log) => log.team_id === filters[FILTER_KEYS.TEAM_ID]);
@ -221,12 +286,12 @@ export function useLogFilterLogic({
return {
data: filteredData,
total: logs.total,
page: logs.page,
page_size: logs.page_size,
total_pages: logs.total_pages,
total: spendLogsData.total,
page: spendLogsData.page,
page_size: spendLogsData.page_size,
total_pages: spendLogsData.total_pages,
};
}, [logs, filters, hasBackendFilters]);
}, [spendLogsData, filters, hasBackendFilters]);
// Choose which filtered logs to expose: backend result when active, otherwise client-derived
const filteredLogs: PaginatedResponse = useMemo(() => {
@ -300,7 +365,7 @@ export function useLogFilterLogic({
};
return {
filters,
logsQuery,
filteredLogs,
hasBackendFilters,
allTeams,