diff --git a/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.test.tsx
index 7de188bd75d..b4a2d4b459c 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.test.tsx
@@ -6,7 +6,7 @@ import OrganizationFilters, { FilterState } from "./OrganizationFilters";
describe("OrganizationFilters", () => {
const defaultFilters: FilterState = {
org_id: "",
- org_alias: "",
+ search: "",
};
it("should render", () => {
@@ -24,7 +24,7 @@ describe("OrganizationFilters", () => {
/>,
);
- expect(screen.getByPlaceholderText("Search by Organization Name")).toBeInTheDocument();
+ expect(screen.getByPlaceholderText("Search by organization name or ID")).toBeInTheDocument();
expect(screen.getByRole("button", { name: /^filters$/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /reset filters/i })).toBeInTheDocument();
});
@@ -63,12 +63,12 @@ describe("OrganizationFilters", () => {
/>,
);
- const input = screen.getByPlaceholderText("Search by Organization Name");
+ const input = screen.getByPlaceholderText("Search by organization name or ID");
fireEvent.change(input, { target: { value: "test" } });
await waitFor(
() => {
- expect(onChange).toHaveBeenCalledWith("org_alias", expect.any(String));
+ expect(onChange).toHaveBeenCalledWith("search", expect.any(String));
},
{ timeout: 500 },
);
@@ -103,7 +103,7 @@ describe("OrganizationFilters", () => {
const filtersWithActive: FilterState = {
...defaultFilters,
- org_alias: "test org",
+ search: "test org",
};
const { container } = render(
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.tsx b/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.tsx
index 6ad2f00fdb0..88c9edc37c0 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.tsx
@@ -13,7 +13,7 @@ interface OrganizationFiltersProps {
type FilterState = {
org_id: string;
- org_alias: string;
+ search: string;
};
const OrganizationFilters = ({
@@ -23,16 +23,16 @@ const OrganizationFilters = ({
onChange,
onReset,
}: OrganizationFiltersProps) => {
- const hasActiveFilters = !!(filters.org_id || filters.org_alias);
+ const hasActiveFilters = !!(filters.org_id || filters.search);
return (
{/* Search and Filter Controls */}
onChange("org_alias", value)}
+ placeholder="Search by organization name or ID"
+ value={filters.search}
+ onChange={(value) => onChange("search", value)}
icon={Search}
className="w-64"
/>
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsPanel.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsPanel.test.tsx
index c15b9fcaddb..1ee0087ba09 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsPanel.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsPanel.test.tsx
@@ -1,9 +1,10 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { act, render, screen, waitFor } from "@testing-library/react";
+import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { NuqsTestingAdapter, type UrlUpdateEvent } from "nuqs/adapters/testing";
import React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type OrganizationsTableComponent from "./OrganizationsTable";
+import type { Organization } from "@/components/networking";
import type OrganizationInfoViewComponent from "@/components/organization/organization_view";
vi.mock("@/components/vector_store_management/VectorStoreSelector", () => ({
@@ -14,13 +15,54 @@ vi.mock("@/components/mcp_server_management/MCPServerSelector", () => ({
__esModule: true,
default: () => null,
}));
+type AuthState = { accessToken: string | null; userId: string | null; userRole: string | null };
+const SIGNED_OUT: AuthState = { accessToken: null, userId: null, userRole: null };
+const SIGNED_IN: AuthState = { accessToken: "sk-test", userId: "user-1", userRole: "Admin" };
+let authState: AuthState = SIGNED_OUT;
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
- default: () => ({
- accessToken: null,
- userId: null,
- userRole: null,
- }),
+ default: () => authState,
}));
+
+const makeOrganization = (organization_id: string, organization_alias: string): Organization => ({
+ organization_id,
+ organization_alias,
+ budget_id: "",
+ metadata: {},
+ models: [],
+ spend: 0,
+ model_spend: {},
+ created_at: "2024-01-01T00:00:00Z",
+ created_by: "user-1",
+ updated_at: "2024-01-01T00:00:00Z",
+ updated_by: "user-1",
+ litellm_budget_table: null,
+ teams: null,
+ users: null,
+ members: null,
+});
+const ALPHA_ORG_ID = "ff9eb074-3f07-4e13-91fb-5337fb10d760";
+const BETA_ORG_ID = "0a1b2c3d-1111-4222-8333-444455556666";
+const SERVER_ORGANIZATIONS: Organization[] = [
+ makeOrganization(ALPHA_ORG_ID, "Alpha Org"),
+ makeOrganization(BETA_ORG_ID, "Beta Org"),
+ makeOrganization("9f8e7d6c-9999-4888-8777-666655554444", "Gamma Org"),
+];
+const matchesServerFilters = (organization: Organization, orgId: string | null, orgAlias: string | null) => {
+ const idMatches = !orgId || organization.organization_id === orgId;
+ const aliasMatches = !orgAlias || organization.organization_alias.toLowerCase().includes(orgAlias.toLowerCase());
+ return idMatches && aliasMatches;
+};
+vi.mock("@/components/networking", async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ organizationListCall: (_accessToken: string, orgId: string | null = null, orgAlias: string | null = null) =>
+ Promise.resolve(
+ SERVER_ORGANIZATIONS.filter((organization) => matchesServerFilters(organization, orgId, orgAlias)),
+ ),
+ modelAvailableCall: () => Promise.resolve({ data: [] }),
+ };
+});
type OrganizationsTableProps = React.ComponentProps;
type OrganizationInfoViewProps = React.ComponentProps;
@@ -80,6 +122,7 @@ const expectQueryString = (queryString: string) =>
waitFor(() => expect(onUrlUpdate).toHaveBeenLastCalledWith(expect.objectContaining({ queryString })));
beforeEach(() => {
+ authState = SIGNED_OUT;
capturedTableProps = null;
mockOrgInfoView.mockClear();
onUrlUpdate.mockClear();
@@ -168,3 +211,38 @@ describe("OrganizationsPanel - org detail deep link (?org=)", () => {
);
});
});
+
+describe("OrganizationsPanel - search by organization name or ID", () => {
+ const renderSignedInPanel = async () => {
+ authState = SIGNED_IN;
+ renderPanel();
+ await waitFor(() => expect(capturedTableProps?.organizations).toHaveLength(SERVER_ORGANIZATIONS.length));
+ };
+
+ const search = (value: string) =>
+ fireEvent.change(screen.getByPlaceholderText("Search by organization name or ID"), { target: { value } });
+
+ const visibleOrganizationIds = () =>
+ capturedTableProps?.organizations.map((organization) => organization.organization_id);
+
+ it.each([
+ ["a full organization_id", ALPHA_ORG_ID, ALPHA_ORG_ID],
+ ["a substring of an organization_id", "5337fb10", ALPHA_ORG_ID],
+ ["an alias substring, ignoring case", "BETA", BETA_ORG_ID],
+ ])("keeps only the organization matching %s and drops the others", async (_label, query, expectedId) => {
+ await renderSignedInPanel();
+
+ search(query);
+
+ await waitFor(() => expect(visibleOrganizationIds()).toEqual([expectedId]));
+ });
+
+ it("shows the search empty state when neither an alias nor an id matches", async () => {
+ await renderSignedInPanel();
+
+ search("no-such-org");
+
+ await waitFor(() => expect(capturedTableProps?.organizations).toEqual([]));
+ expect(capturedTableProps?.searchActive).toBe(true);
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsPanel.tsx
index 4fe4cf47b9c..f1377bf6364 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsPanel.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsPanel.tsx
@@ -3,10 +3,10 @@ import { useUserModels } from "@/app/(dashboard)/hooks/models/useModels";
import OrganizationFilters, { FilterState } from "@/app/(dashboard)/organizations/OrganizationFilters";
import { useQueryClient } from "@tanstack/react-query";
import { parseAsString, useQueryState } from "nuqs";
-import React, { useState } from "react";
+import React, { useMemo, useState } from "react";
import DeleteResourceModal from "@/components/common_components/DeleteResourceModal";
import { toast } from "@/lib/toast";
-import { organizationDeleteCall } from "@/components/networking";
+import { Organization, organizationDeleteCall } from "@/components/networking";
import { OrgCreateDialog } from "@/components/organization/org-create/OrgCreateDialog";
import OrganizationInfoView from "@/components/organization/organization_view";
import { Button } from "@/components/ui/button";
@@ -19,6 +19,15 @@ interface OrganizationsPanelProps {
premiumUser: boolean;
}
+const matchesOrganizationSearch = (organization: Organization, search: string): boolean => {
+ const needle = search.trim().toLowerCase();
+ return (
+ needle === "" ||
+ organization.organization_alias.toLowerCase().includes(needle) ||
+ organization.organization_id.toLowerCase().includes(needle)
+ );
+};
+
const OrganizationsPanel: React.FC = ({ userRole, accessToken, premiumUser }) => {
const [selectedOrgId, setSelectedOrgId] = useQueryState("org", parseAsString.withOptions({ history: "push" }));
const [editOrg, setEditOrg] = useState(false);
@@ -27,16 +36,18 @@ const OrganizationsPanel: React.FC = ({ userRole, acces
const [isDeleting, setIsDeleting] = useState(false);
const [isOrgModalVisible, setIsOrgModalVisible] = useState(false);
const [showFilters, setShowFilters] = useState(false);
- const [filters, setFilters] = useState({ org_id: "", org_alias: "" });
+ const [filters, setFilters] = useState({ org_id: "", search: "" });
const queryClient = useQueryClient();
- const { data: organizations = [], isLoading } = useOrganizations({
- org_id: filters.org_id,
- org_alias: filters.org_alias,
- });
+ const { data: organizations = [], isLoading } = useOrganizations({ org_id: filters.org_id });
const { data: userModels = [] } = useUserModels();
- const searchActive = Boolean(filters.org_id || filters.org_alias);
+ const visibleOrganizations = useMemo(
+ () => organizations.filter((organization) => matchesOrganizationSearch(organization, filters.search)),
+ [organizations, filters.search],
+ );
+
+ const searchActive = Boolean(filters.org_id || filters.search);
const refetchOrganizations = () => queryClient.invalidateQueries({ queryKey: organizationKeys.lists() });
@@ -45,7 +56,7 @@ const OrganizationsPanel: React.FC = ({ userRole, acces
};
const handleFilterReset = () => {
- setFilters({ org_id: "", org_alias: "" });
+ setFilters({ org_id: "", search: "" });
};
const handleDelete = (orgId: string | null) => {
@@ -129,7 +140,7 @@ const OrganizationsPanel: React.FC = ({ userRole, acces
onReset={handleFilterReset}
/>