mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
[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:
parent
72a461ba4a
commit
62100a6860
5 changed files with 487 additions and 919 deletions
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue