diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx index aaeb8ebb4b..9fa73dd172 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx @@ -25,6 +25,7 @@ import { import { ExportOutlined, LoadingOutlined } from "@ant-design/icons"; import { Alert, Button } from "antd"; import React, { useMemo, useState } from "react"; +import TeamMultiSelect from "../../../common_components/team_multi_select"; import { ActivityMetrics, processActivityData } from "../../../activity_metrics"; import { UsageExportHeader } from "../../../EntityUsageExport"; import type { EntityType } from "../../../EntityUsageExport/types"; @@ -468,11 +469,20 @@ const EntityUsage: React.FC = ({ accessToken, entityType, enti } /> )} + {entityType === "team" && ( +
+ Filter by team + +
+ )} 0} + showFilters={entityType !== "team" && entityList !== null && entityList.length > 0} filterLabel={getFilterLabel(entityType)} filterPlaceholder={getFilterPlaceholder(entityType)} selectedFilters={selectedTags} diff --git a/ui/litellm-dashboard/src/components/common_components/team_multi_select.tsx b/ui/litellm-dashboard/src/components/common_components/team_multi_select.tsx new file mode 100644 index 0000000000..eec08c58db --- /dev/null +++ b/ui/litellm-dashboard/src/components/common_components/team_multi_select.tsx @@ -0,0 +1,112 @@ +import React, { useMemo, useState, type UIEvent } from "react"; +import { Select, Typography } from "antd"; +import { LoadingOutlined } from "@ant-design/icons"; +import { useDebouncedState } from "@tanstack/react-pacer/debouncer"; +import { useInfiniteTeams } from "@/app/(dashboard)/hooks/teams/useTeams"; +import { Team } from "../key_team_helpers/key_list"; + +const { Text } = Typography; + +interface TeamMultiSelectProps { + value?: string[]; + onChange?: (value: string[]) => void; + disabled?: boolean; + organizationId?: string | null; + pageSize?: number; + placeholder?: string; +} + +const SCROLL_THRESHOLD = 0.8; +const DEBOUNCE_MS = 300; + +const TeamMultiSelect: React.FC = ({ + value = [], + onChange, + disabled, + organizationId, + pageSize = 20, + placeholder = "Search teams by name or ID...", +}) => { + const [searchInput, setSearchInput] = useState(""); + const [debouncedSearch, setDebouncedSearch] = useDebouncedState("", { + wait: DEBOUNCE_MS, + }); + + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + } = useInfiniteTeams( + pageSize, + debouncedSearch || undefined, + organizationId, + ); + + const teams = useMemo(() => { + if (!data?.pages) return []; + const seen = new Set(); + const result: Team[] = []; + for (const page of data.pages) { + for (const team of page.teams) { + if (seen.has(team.team_id)) continue; + seen.add(team.team_id); + result.push(team); + } + } + return result; + }, [data]); + + const handlePopupScroll = (e: UIEvent) => { + const target = e.currentTarget; + const scrollRatio = + (target.scrollTop + target.clientHeight) / target.scrollHeight; + if (scrollRatio >= SCROLL_THRESHOLD && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }; + + const handleSearch = (val: string) => { + setSearchInput(val); + setDebouncedSearch(val); + }; + + return ( + + ); +}; + +export default TeamMultiSelect;