diff --git a/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx b/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx index aa690787f66..81759c80ab6 100644 --- a/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx @@ -2,7 +2,7 @@ import { Drawer, Tag, Typography } from "antd"; import { CloseOutlined, CopyOutlined, CheckOutlined } from "@ant-design/icons"; import { useState, useCallback } from "react"; import moment from "moment"; -import { AuditLogEntry } from "../columns"; +import { AuditLogEntry } from "../AuditLogsTableColumns"; import DefaultProxyAdminTag from "../../common_components/DefaultProxyAdminTag"; const { Text } = Typography; diff --git a/ui/litellm-dashboard/src/components/view_logs/AuditLogsPanel.tsx b/ui/litellm-dashboard/src/components/view_logs/AuditLogsPanel.tsx new file mode 100644 index 00000000000..81bd4a19f76 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/AuditLogsPanel.tsx @@ -0,0 +1,138 @@ +import { useCallback, useState } from "react"; +import { useQuery, keepPreviousData } from "@tanstack/react-query"; +import { ColumnFiltersState, OnChangeFn, PaginationState } from "@tanstack/react-table"; +import { resolveLogoSrc } from "@/lib/assetPaths"; +import { uiAuditLogsCall } from "../networking"; +import { AuditLogEntry } from "./AuditLogsTableColumns"; +import { AuditLogsTable } from "./AuditLogsTable"; +import { AuditLogDrawer } from "./AuditLogDrawer/AuditLogDrawer"; + +interface AuditLogsProps { + accessToken: string | null; + token: string | null; + userRole: string | null; + userID: string | null; + isActive: boolean; + premiumUser: boolean; +} + +const asset_logos_folder = "/ui/assets/"; +const auditLogsPreviewImg = `${asset_logos_folder}audit-logs-preview.png`; + +const PAGE_SIZE = 50; + +interface AuditLogsResponse { + audit_logs: AuditLogEntry[]; + total: number; + page: number; + page_size: number; + total_pages: number; +} + +export default function AuditLogsPanel({ + userID, + userRole, + token, + accessToken, + isActive, + premiumUser, +}: AuditLogsProps) { + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: PAGE_SIZE }); + const [columnFilters, setColumnFilters] = useState([]); + const [selectedLog, setSelectedLog] = useState(null); + const [drawerOpen, setDrawerOpen] = useState(false); + + const getFilterValue = (columnId: string): string | undefined => { + const entry = columnFilters.find((filter) => filter.id === columnId); + return typeof entry?.value === "string" && entry.value.trim() ? entry.value.trim() : undefined; + }; + + const canQueryAuditLogs = !!accessToken && !!token && !!userRole && !!userID && isActive && premiumUser; + + const query = useQuery({ + queryKey: ["audit_logs", pagination.pageIndex, pagination.pageSize, columnFilters], + queryFn: async () => { + if (!accessToken) { + return { audit_logs: [], total: 0, page: 1, page_size: pagination.pageSize, total_pages: 0 }; + } + return uiAuditLogsCall({ + accessToken, + page: pagination.pageIndex + 1, + page_size: pagination.pageSize, + params: { + object_id: getFilterValue("object_id"), + changed_by: getFilterValue("changed_by"), + object_key_hash: getFilterValue("key_hash"), + object_team_id: getFilterValue("team_id"), + action: getFilterValue("action"), + table_name: getFilterValue("table_name"), + sort_by: "updated_at", + sort_order: "desc", + }, + }); + }, + enabled: canQueryAuditLogs, + placeholderData: keepPreviousData, + }); + + const handleColumnFiltersChange = useCallback>((updaterOrValue) => { + setColumnFilters(updaterOrValue); + setPagination((prev) => ({ ...prev, pageIndex: 0 })); + }, []); + + const handleViewLog = useCallback((log: AuditLogEntry) => { + setSelectedLog(log); + setDrawerOpen(true); + }, []); + + if (!premiumUser) { + return ( +
+

✨ Enterprise Feature.

+

+ This is a LiteLLM Enterprise feature, and requires a valid key to use. +

+

+ Here's a preview of what Audit Logs offer: +

+ Audit Logs Preview { + (e.target as HTMLImageElement).style.display = "none"; + }} + /> +
+ ); + } + + return ( + <> +
+

Audit Logs

+
+ + query.refetch()} + onViewLog={handleViewLog} + /> + + setDrawerOpen(false)} log={selectedLog} /> + + ); +} diff --git a/ui/litellm-dashboard/src/components/view_logs/AuditLogsTable.test.tsx b/ui/litellm-dashboard/src/components/view_logs/AuditLogsTable.test.tsx new file mode 100644 index 00000000000..dbb0a39e2ee --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/AuditLogsTable.test.tsx @@ -0,0 +1,146 @@ +import type { ColumnFiltersState, PaginationState } from "@tanstack/react-table"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; + +import { AuditLogsTable } from "./AuditLogsTable"; +import type { AuditLogEntry } from "./AuditLogsTableColumns"; + +const ROWS: AuditLogEntry[] = [ + { + id: "log-1", + updated_at: "2026-07-20T12:00:00Z", + changed_by: "default_user_id", + changed_by_api_key: "sk-hash-abc", + action: "created", + table_name: "LiteLLM_TeamTable", + object_id: "team-obj-123", + before_value: {}, + updated_values: { foo: "bar" }, + }, + { + id: "log-2", + updated_at: "2026-07-20T11:00:00Z", + changed_by: "user-42", + changed_by_api_key: "sk-hash-def", + action: "deleted", + table_name: "LiteLLM_UserTable", + object_id: "user-obj-456", + before_value: { a: 1 }, + updated_values: {}, + }, +]; + +const FIRST_PAGE: PaginationState = { pageIndex: 0, pageSize: 50 }; + +function renderTable(overrides: Partial> = {}) { + const props: React.ComponentProps = { + data: ROWS, + rowCount: ROWS.length, + isLoading: false, + isRefreshing: false, + pagination: FIRST_PAGE, + onPaginationChange: vi.fn(), + columnFilters: [], + onColumnFiltersChange: vi.fn(), + onRefresh: vi.fn(), + onViewLog: vi.fn(), + ...overrides, + }; + render(); + return props; +} + +describe("AuditLogsTable", () => { + it("renders each audit column with the migrated shared cells", () => { + renderTable(); + + // Action -> StatusBadge with a capitalized label + expect(screen.getByText("Created")).toBeInTheDocument(); + expect(screen.getByText("Deleted")).toBeInTheDocument(); + // Table name -> display mapping + expect(screen.getByText("Teams")).toBeInTheDocument(); + expect(screen.getByText("Users")).toBeInTheDocument(); + // Changed By -> DefaultProxyAdminTag (default_user_id becomes a labeled tag; other ids stay raw) + expect(screen.getByText("Default Proxy Admin")).toBeInTheDocument(); + expect(screen.getByText("user-42")).toBeInTheDocument(); + // Object ID + API key hash + expect(screen.getByText("team-obj-123")).toBeInTheDocument(); + expect(screen.getByText("sk-hash-abc")).toBeInTheDocument(); + }); + + it("opens the detail drawer from the Object ID identity cell with the full row", async () => { + const user = userEvent.setup(); + const props = renderTable(); + + await user.click(screen.getByText("team-obj-123")); + + expect(props.onViewLog).toHaveBeenCalledTimes(1); + expect(props.onViewLog).toHaveBeenCalledWith(ROWS[0]); + }); + + it("drives the shared footer from the server rowCount and reports page changes", async () => { + const user = userEvent.setup(); + const onPaginationChange = vi.fn(); + renderTable({ rowCount: 120, onPaginationChange }); + + // ceil(120 / 50) = 3 pages, proving rowCount (not data length) feeds the footer + expect(screen.getByTestId("pagination-page")).toHaveTextContent("Page 1 of 3"); + + await user.click(screen.getByTestId("pagination-next")); + expect(onPaginationChange).toHaveBeenCalledTimes(1); + }); + + it("shows skeleton rows while loading and no data rows", () => { + renderTable({ isLoading: true, data: [] }); + + expect(screen.getAllByTestId("skeleton-row").length).toBeGreaterThan(0); + expect(screen.queryByText("No audit logs yet")).toBeNull(); + }); + + it("uses a distinct empty state for unfiltered vs filtered-empty results", () => { + const { unmount } = render( + , + ); + expect(screen.getByText("No audit logs yet")).toBeInTheDocument(); + unmount(); + + renderTable({ data: [], rowCount: 0, columnFilters: [{ id: "action", value: "created" }] }); + expect(screen.getByText("No matching audit logs")).toBeInTheDocument(); + }); + + it("renders active filter chips with human-readable labels", () => { + const filters: ColumnFiltersState = [{ id: "action", value: "created" }]; + renderTable({ columnFilters: filters }); + + const chip = screen.getByTestId("filter-chip-action"); + expect(chip).toHaveTextContent("Action:"); + expect(chip).toHaveTextContent("Created"); + }); + + it("commits a text filter through the filter drawer and reports it to the parent", async () => { + const user = userEvent.setup(); + const onColumnFiltersChange = vi.fn(); + renderTable({ onColumnFiltersChange }); + + await user.click(screen.getByTestId("datatable-filters-trigger")); + await user.type(await screen.findByPlaceholderText("Enter object ID…"), "obj-9"); + await user.click(screen.getByTestId("filter-drawer-apply")); + + expect(onColumnFiltersChange).toHaveBeenCalledTimes(1); + const arg = onColumnFiltersChange.mock.calls[0][0]; + const committed = typeof arg === "function" ? arg([]) : arg; + expect(committed).toEqual([{ id: "object_id", value: "obj-9" }]); + }); +}); diff --git a/ui/litellm-dashboard/src/components/view_logs/AuditLogsTable.tsx b/ui/litellm-dashboard/src/components/view_logs/AuditLogsTable.tsx new file mode 100644 index 00000000000..bcdce12fce8 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/AuditLogsTable.tsx @@ -0,0 +1,208 @@ +"use client"; + +import { ColumnFiltersState, OnChangeFn, PaginationState } from "@tanstack/react-table"; +import { ScrollText } from "lucide-react"; +import { useMemo, useState } from "react"; + +import { + DataTable, + DataTableFilterDrawer, + DataTableFilterField, + DataTableToolbar, +} from "@/components/shared/DataTable"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; + +import { AUDIT_TABLE_NAME_DISPLAY, AuditLogEntry, getAuditLogsTableColumns } from "./AuditLogsTableColumns"; + +interface AuditLogsTableProps { + data: AuditLogEntry[]; + rowCount: number; + isLoading: boolean; + isRefreshing: boolean; + pagination: PaginationState; + onPaginationChange: OnChangeFn; + columnFilters: ColumnFiltersState; + onColumnFiltersChange: OnChangeFn; + onRefresh: () => void; + onViewLog: (log: AuditLogEntry) => void; +} + +const ALL_VALUE = "all"; + +const ACTION_OPTIONS = [ + { label: "Created", value: "created" }, + { label: "Updated", value: "updated" }, + { label: "Deleted", value: "deleted" }, + { label: "Rotated", value: "rotated" }, +] as const; + +const TABLE_OPTIONS = [ + { label: "Keys", value: "LiteLLM_VerificationToken" }, + { label: "Teams", value: "LiteLLM_TeamTable" }, + { label: "Users", value: "LiteLLM_UserTable" }, + { label: "Organizations", value: "LiteLLM_OrganizationTable" }, + { label: "Models", value: "LiteLLM_ProxyModelTable" }, +] as const; + +const FILTER_LABELS: Record = { + object_id: "Object ID", + changed_by: "Changed By", + team_id: "Team ID", + key_hash: "Key Hash", + action: "Action", + table_name: "Table", +}; + +const formatFilterValue = (columnId: string, value: unknown): string => { + const raw = String(value); + if (columnId === "action") { + return ACTION_OPTIONS.find((option) => option.value === raw)?.label ?? raw; + } + if (columnId === "table_name") { + return AUDIT_TABLE_NAME_DISPLAY[raw] ?? raw; + } + return raw; +}; + +function AuditLogsEmptyState({ filtered }: { filtered: boolean }) { + return ( +
+
+ +
+
+ {filtered ? "No matching audit logs" : "No audit logs yet"} +
+
+ {filtered + ? "No audit log entries match your filters." + : "Administrative changes to keys, teams, users, and models will appear here."} +
+
+ ); +} + +export function AuditLogsTable({ + data, + rowCount, + isLoading, + isRefreshing, + pagination, + onPaginationChange, + columnFilters, + onColumnFiltersChange, + onRefresh, + onViewLog, +}: AuditLogsTableProps) { + const [filtersOpen, setFiltersOpen] = useState(false); + const columns = useMemo(() => getAuditLogsTableColumns({ onViewLog }), [onViewLog]); + + return ( + row.id} + paginationMode="server" + pagination={pagination} + onPaginationChange={onPaginationChange} + rowCount={rowCount} + filterMode="server" + columnFilters={columnFilters} + onColumnFiltersChange={onColumnFiltersChange} + isLoading={isLoading} + loadingMessage="Loading audit logs…" + noDataMessage={ 0} />} + size="compact" + toolbar={(table) => ( + <> + setFiltersOpen(true)} + filterLabels={FILTER_LABELS} + formatFilterValue={formatFilterValue} + showViewOptions={false} + /> + + {({ get, set }) => ( + <> + + set("object_id", event.target.value)} + placeholder="Enter object ID…" + /> + + + set("changed_by", event.target.value)} + placeholder="Enter user ID…" + /> + + + set("team_id", event.target.value)} + placeholder="Enter team ID…" + /> + + + set("key_hash", event.target.value)} + placeholder="Enter key hash…" + /> + + + + + + + + + )} + + + )} + /> + ); +} diff --git a/ui/litellm-dashboard/src/components/view_logs/AuditLogsTableColumns.tsx b/ui/litellm-dashboard/src/components/view_logs/AuditLogsTableColumns.tsx new file mode 100644 index 00000000000..6910ca1c2f7 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/AuditLogsTableColumns.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; + +import { DateCell, IdCell, IdentityCell, StatusBadge, type StatusTone } from "@/components/shared/table_cells"; + +import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag"; + +export type AuditLogEntry = { + id: string; + updated_at: string; + changed_by: string; + changed_by_api_key: string; + action: string; + table_name: string; + object_id: string; + before_value: Record; + updated_values: Record; +}; + +export const AUDIT_TABLE_NAME_DISPLAY: Record = { + LiteLLM_VerificationToken: "Keys", + LiteLLM_TeamTable: "Teams", + LiteLLM_UserTable: "Users", + LiteLLM_OrganizationTable: "Organizations", + LiteLLM_ProxyModelTable: "Models", +}; + +const ACTION_TONE: Record = { + created: "success", + updated: "info", + deleted: "error", + rotated: "warning", +}; + +const capitalize = (value: string): string => (value ? value.charAt(0).toUpperCase() + value.slice(1) : value); + +interface AuditLogsTableColumnsDeps { + onViewLog: (log: AuditLogEntry) => void; +} + +export const getAuditLogsTableColumns = ({ onViewLog }: AuditLogsTableColumnsDeps): ColumnDef[] => [ + { + id: "updated_at", + accessorKey: "updated_at", + header: "Timestamp", + size: 200, + enableSorting: false, + cell: ({ row }) => , + }, + { + id: "action", + accessorKey: "action", + header: "Action", + size: 110, + enableSorting: false, + cell: ({ row }) => ( + + ), + }, + { + id: "table_name", + accessorKey: "table_name", + header: "Table", + size: 130, + enableSorting: false, + cell: ({ row }) => ( + {AUDIT_TABLE_NAME_DISPLAY[row.original.table_name] ?? row.original.table_name} + ), + }, + { + id: "object_id", + accessorKey: "object_id", + header: "Object ID", + minSize: 220, + enableSorting: false, + cell: ({ row }) => ( + onViewLog(row.original)} + /> + ), + }, + { + id: "changed_by", + accessorKey: "changed_by", + header: "Changed By", + size: 200, + enableSorting: false, + cell: ({ row }) => , + }, + { + id: "changed_by_api_key", + accessorKey: "changed_by_api_key", + header: "API Key (Hash)", + size: 160, + enableSorting: false, + cell: ({ row }) => , + }, +]; diff --git a/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx b/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx deleted file mode 100644 index d811d3b9402..00000000000 --- a/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx +++ /dev/null @@ -1,315 +0,0 @@ -import { useState } from "react"; -import { useQuery, keepPreviousData } from "@tanstack/react-query"; -import { Table, Tag, Input, Select, Button, Pagination, Spin } from "antd"; -import { ReloadOutlined, LoadingOutlined } from "@ant-design/icons"; -import type { ColumnsType } from "antd/es/table"; -import { resolveLogoSrc } from "@/lib/assetPaths"; -import { DateCell, IdCell } from "@/components/shared/table_cells"; -import { uiAuditLogsCall } from "../networking"; -import { AuditLogEntry } from "./columns"; -import { AuditLogDrawer } from "./AuditLogDrawer/AuditLogDrawer"; -import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag"; - -const { Search } = Input; - -interface AuditLogsProps { - accessToken: string | null; - token: string | null; - userRole: string | null; - userID: string | null; - isActive: boolean; - premiumUser: boolean; -} - -const asset_logos_folder = "/ui/assets/"; -export const auditLogsPreviewImg = `${asset_logos_folder}audit-logs-preview.png`; - -const TABLE_NAME_DISPLAY: Record = { - LiteLLM_VerificationToken: "Keys", - LiteLLM_TeamTable: "Teams", - LiteLLM_UserTable: "Users", - LiteLLM_OrganizationTable: "Organizations", - LiteLLM_ProxyModelTable: "Models", -}; - -const ACTION_COLOR: Record = { - created: "green", - updated: "blue", - deleted: "red", - rotated: "orange", -}; - -const PAGE_SIZE = 50; - -export default function AuditLogs({ userID, userRole, token, accessToken, isActive, premiumUser }: AuditLogsProps) { - const [page, setPage] = useState(1); - - // Filter state - const [objectId, setObjectId] = useState(""); - const [changedBy, setChangedBy] = useState(""); - const [keyHash, setKeyHash] = useState(""); - const [teamId, setTeamId] = useState(""); - const [action, setAction] = useState(undefined); - const [tableName, setTableName] = useState(undefined); - - // Drawer state - const [selectedLog, setSelectedLog] = useState(null); - const [drawerOpen, setDrawerOpen] = useState(false); - - const query = useQuery({ - queryKey: ["audit_logs", page, PAGE_SIZE, objectId, changedBy, keyHash, teamId, action, tableName], - queryFn: async () => { - if (!accessToken || !token || !userRole || !userID) { - return { audit_logs: [], total: 0, page: 1, page_size: PAGE_SIZE, total_pages: 0 }; - } - return uiAuditLogsCall({ - accessToken, - page, - page_size: PAGE_SIZE, - params: { - object_id: objectId || undefined, - changed_by: changedBy || undefined, - object_key_hash: keyHash || undefined, - object_team_id: teamId || undefined, - action: action || undefined, - table_name: tableName || undefined, - sort_by: "updated_at", - sort_order: "desc", - }, - }); - }, - enabled: !!accessToken && !!token && !!userRole && !!userID && isActive, - placeholderData: keepPreviousData, - }); - - const resetPage = () => setPage(1); - - const handleRowClick = (log: AuditLogEntry) => { - setSelectedLog(log); - setDrawerOpen(true); - }; - - const columns: ColumnsType = [ - { - title: "Timestamp", - dataIndex: "updated_at", - key: "updated_at", - width: 200, - render: (val: string) => , - }, - { - title: "Action", - dataIndex: "action", - key: "action", - width: 100, - render: (val: string) => ( - - {val} - - ), - }, - { - title: "Table", - dataIndex: "table_name", - key: "table_name", - width: 130, - render: (val: string) => TABLE_NAME_DISPLAY[val] ?? val, - }, - { - title: "Object ID", - dataIndex: "object_id", - key: "object_id", - render: (val: string) => , - }, - { - title: "Changed By", - dataIndex: "changed_by", - key: "changed_by", - width: 200, - render: (val: string) => , - }, - { - title: "API Key (Hash)", - dataIndex: "changed_by_api_key", - key: "changed_by_api_key", - width: 140, - render: (val: string) => , - }, - ]; - - if (!premiumUser) { - return ( -
-

✨ Enterprise Feature.

-

- This is a LiteLLM Enterprise feature, and requires a valid key to use. -

-

- Here's a preview of what Audit Logs offer: -

- Audit Logs Preview { - (e.target as HTMLImageElement).style.display = "none"; - }} - /> -
- ); - } - - const auditLogs: AuditLogEntry[] = query.data?.audit_logs ?? []; - const total: number = query.data?.total ?? 0; - - return ( - <> -
- {/* Header */} -
-
-

Audit Logs

-
- - {/* Filters + pagination on same row */} -
- { - setObjectId(val); - resetPage(); - }} - onChange={(e) => { - if (!e.target.value) { - setObjectId(""); - resetPage(); - } - }} - /> - { - setChangedBy(val); - resetPage(); - }} - onChange={(e) => { - if (!e.target.value) { - setChangedBy(""); - resetPage(); - } - }} - /> - { - setTeamId(val); - resetPage(); - }} - onChange={(e) => { - if (!e.target.value) { - setTeamId(""); - resetPage(); - } - }} - /> - { - setKeyHash(val); - resetPage(); - }} - onChange={(e) => { - if (!e.target.value) { - setKeyHash(""); - resetPage(); - } - }} - /> - { - setTableName(val); - resetPage(); - }} - /> - - {/* Pagination + refresh pushed to the right */} -
-
-
-
- - {/* Table — pagination handled in header */} - - columns={columns} - dataSource={auditLogs} - rowKey="id" - loading={{ - spinning: query.isLoading, - indicator: } size="small" />, - }} - size="small" - pagination={false} - onRow={(record) => ({ - onClick: () => handleRowClick(record), - style: { cursor: "pointer" }, - })} - /> -
- - setDrawerOpen(false)} log={selectedLog} /> - - ); -} diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.tsx index 0ce5e8e4717..d3ba90d4e2d 100644 --- a/ui/litellm-dashboard/src/components/view_logs/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/columns.tsx @@ -539,15 +539,3 @@ const CollapsibleJsonCell = ({ jsonData }: { jsonData: any }) => { ); }; - -export type AuditLogEntry = { - id: string; - updated_at: string; - changed_by: string; - changed_by_api_key: string; - action: string; - table_name: string; - object_id: string; - before_value: Record; - updated_values: Record; -}; diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index ee08712e56b..aa8077a02e9 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -8,7 +8,7 @@ import { KeyResponse } from "../key_team_helpers/key_list"; import FilterComponent from "../molecules/filter"; import { keyInfoV1Call } from "../networking"; import KeyInfoView from "../templates/key_info_view"; -import AuditLogs from "./audit_logs"; +import AuditLogsPanel from "./AuditLogsPanel"; import { createColumns, LogEntry, type LogsSortField } from "./columns"; import { AGENT_CALL_TYPES, MCP_CALL_TYPES } from "./constants"; import { getLogFilterOptions } from "./filter_options"; @@ -296,7 +296,7 @@ export default function SpendLogsTable({ accessToken, token, userRole, userID, p )} -