From 8d254dc9770f52ed728e6b8c0528d8946063069b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 11:05:18 +0000 Subject: [PATCH] feat(ui): UsageExportHeader to shadcn - @tremor Button/Text + antd Select (single and multiple modes) \u2192 shadcn Button + Select (single) / Popover + Badge chip-list (multiple). Reuses the established multi-select pattern. - Inline SVG export icon replaced with lucide Download. - DateRangePickerValue import kept as type-only with an inline eslint-disable on the import line (this is the shared tremor date-picker type exposed phase-1-wide). Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang --- .../EntityUsageExport/UsageExportHeader.tsx | 165 +++++++++++++----- 1 file changed, 120 insertions(+), 45 deletions(-) diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.tsx index 3c9b695a7dd..12a08bde19e 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.tsx +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.tsx @@ -1,16 +1,30 @@ +// eslint-disable-next-line litellm-ui/no-banned-ui-imports import type { DateRangePickerValue } from "@tremor/react"; -import { Button, Text } from "@tremor/react"; -import { Select } from "antd"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Badge } from "@/components/ui/badge"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Download, X } from "lucide-react"; import React, { useState } from "react"; import EntityUsageExportModal from "./EntityUsageExportModal"; import type { EntitySpendData, EntityType } from "./types"; import type { Team } from "@/components/key_team_helpers/key_list"; +import { cn } from "@/lib/utils"; interface UsageExportHeaderProps { dateValue: DateRangePickerValue; entityType: EntityType; spendData: EntitySpendData; - // Optional filter props showFilters?: boolean; filterLabel?: string; filterPlaceholder?: string; @@ -19,10 +33,13 @@ interface UsageExportHeaderProps { filterOptions?: Array<{ label: string; value: string }>; filterMode?: "multiple" | "single"; customTitle?: string; + // eslint-disable-next-line @typescript-eslint/no-unused-vars compactLayout?: boolean; teams?: Team[]; } +const ALL = "__all__"; + const UsageExportHeader: React.FC = ({ dateValue, entityType, @@ -35,63 +52,121 @@ const UsageExportHeader: React.FC = ({ filterOptions = [], filterMode = "multiple", customTitle, - compactLayout = false, teams = [], }) => { const [isExportModalOpen, setIsExportModalOpen] = useState(false); + const [multiOpen, setMultiOpen] = useState(false); - // Determine grid layout based on what's visible - const getGridCols = () => { - const hasFilters = showFilters && filterOptions.length > 0; + const hasFilters = showFilters && filterOptions.length > 0; + const gridCols = hasFilters ? "grid-cols-[1fr_auto]" : "grid-cols-[auto]"; - if (hasFilters) return "grid-cols-[1fr_auto]"; - return "grid-cols-[auto]"; - }; + const labelFor = (v: string) => + filterOptions.find((o) => o.value === v)?.label ?? v; return ( <>
- {/** - * Use CSS grid with items-end so all cells (filter, button) - * align to the same baseline regardless of label heights. This removes - * vertical drift when the right column has a label above the input. - */} -
- {showFilters && filterOptions.length > 0 && ( +
+ {hasFilters && (
- {filterLabel && {filterLabel}} - + onFiltersChange?.(v === ALL ? [] : [v]) } - }} - options={filterOptions} - allowClear - /> + > + + + + + All + {filterOptions.map((o) => ( + + {o.label} + + ))} + + + ) : ( + + + + + + {filterOptions + .filter((o) => !selectedFilters.includes(o.value)) + .map((opt) => ( + + ))} + {filterOptions.every((o) => + selectedFilters.includes(o.value), + ) && ( +
+ All selected +
+ )} +
+
+ )}
)}
-