Repository navigation
[Refactor] UI - Spend Logs: consolidate filter state and extract components #25847
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ryan-crabbe-berri
merged 16 commits into
litellm_internal_staging
from
litellm_refactor-spend-logs-view
May 19, 2026
Merged
Changes from 1 commit
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
62100a6
[Refactor] UI - Spend Logs: consolidate filter state, extract compone…
ryan-crabbe-berri b5455d1
Fix PR feedback: use antd Switch instead of Tremor in new file, fix typo
ryan-crabbe-berri f307ad9
Collapse dual-path filtering into single React Query
ryan-crabbe-berri 5816246
Clean up remaining smells: remove isFetchingDeferred, internalize sel…
ryan-crabbe-berri 4fe54aa
Fix quick-select dropdown overlapping sidebar
ryan-crabbe-berri 34aea7c
Fix stale quick-select label after Reset Filters
ryan-crabbe-berri f36c901
Merge remote-tracking branch 'origin/litellm_internal_staging' into l…
ryan-crabbe-berri 5ae669a
Merge branch 'litellm_internal_staging' into litellm_refactor-spend-l…
ryan-crabbe-berri 3db143b
refactor useLogFilterLogic tests for controlled-hook + backend-query …
ryan-crabbe-berri 8b633a2
cover new useLogFilterLogic invariants: activeTab gate, filterByCurre…
ryan-crabbe-berri 85e8e4d
fix typo dropping the live-tail banner border
ryan-crabbe-berri 4c8eaba
memoize columns and derived table data in SpendLogsTable
ryan-crabbe-berri d9878ca
apply dropdown filters instantly, debounce only text inputs
ryan-crabbe-berri 0d8b5c6
Merge remote-tracking branch 'origin/litellm_internal_staging' into l…
ryan-crabbe-berri 112b7c0
fix(ui/spend-logs): restore lost loading/debounce behavior + cover dr…
ryan-crabbe-berri 40e7ef3
test(ui/spend-logs): cover the cold-load auth-not-ready spinner guard
ryan-crabbe-berri File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
246 changes: 246 additions & 0 deletions
246
ui/litellm-dashboard/src/components/view_logs/LogsTableToolbar.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"> | ||
|
ryan-crabbe-berri marked this conversation as resolved.
Resolved
|
||
| <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> | ||
| )} | ||
| </> | ||
| ); | ||
| } | ||
71 changes: 71 additions & 0 deletions
71
ui/litellm-dashboard/src/components/view_logs/filter_options.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| }, | ||
| ]; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.