diff --git a/tests/e2e/ui/tests/internal-user/internalUser.spec.ts b/tests/e2e/ui/tests/internal-user/internalUser.spec.ts index 07a75dc007d..b8424b06115 100644 --- a/tests/e2e/ui/tests/internal-user/internalUser.spec.ts +++ b/tests/e2e/ui/tests/internal-user/internalUser.spec.ts @@ -19,12 +19,11 @@ test.describe("Internal User", () => { // Open the team dropdown — seeded internal user is a member of // e2e-team-crud and e2e-team-org, so we expect at least the CRUD alias. - const teamSelect = page.locator(".ant-select", { hasText: "Search or select a team" }); + const teamSelect = page.getByTestId("team-dropdown").getByRole("combobox"); await teamSelect.click(); await page.keyboard.type(E2E_TEAM_CRUD_ALIAS); - await expect(page.locator(".ant-select-dropdown:visible").getByText(E2E_TEAM_CRUD_ALIAS).first()).toBeVisible({ - timeout: 5_000, - }); + const dropdown = page.locator('[data-slot="combobox-content"]:visible'); + await expect(dropdown.getByText(E2E_TEAM_CRUD_ALIAS).first()).toBeVisible({ timeout: 5_000 }); }); test("Team info page omits the Settings tab for non-admin members", async ({ page }) => { diff --git a/tests/e2e/ui/tests/internal-user/internalUserNoTeam.spec.ts b/tests/e2e/ui/tests/internal-user/internalUserNoTeam.spec.ts index 1b048198456..c44305187f1 100644 --- a/tests/e2e/ui/tests/internal-user/internalUserNoTeam.spec.ts +++ b/tests/e2e/ui/tests/internal-user/internalUserNoTeam.spec.ts @@ -27,18 +27,18 @@ test.describe("Internal User with no team memberships", () => { await page.getByRole("button", { name: /Create New Key/i }).click(); await expect(page.getByText("Key Ownership")).toBeVisible({ timeout: 10_000 }); - const teamSelect = page.locator(".ant-select", { hasText: "Search or select a team" }); + const teamSelect = page.getByTestId("team-dropdown").getByRole("combobox"); await teamSelect.click(); - const dropdown = page.locator(".ant-select-dropdown:visible").first(); + const dropdown = page.locator('[data-slot="combobox-content"]:visible').first(); await expect(dropdown).toBeVisible({ timeout: 5_000 }); // Wait for the settled-empty state, not a transient one. The dropdown shows - // a spinner while teams load and only swaps in "No teams found" once the - // request resolves with nothing (team_dropdown.tsx renders the spinner when - // isLoading and this copy otherwise). Asserting on it means a regression - // where teams DO load for this user fails here instead of racing a one-shot - // count() against an in-flight request. + // "Loading teams…" while teams load and only swaps in "No teams found" once + // the request resolves with nothing (team_dropdown.tsx passes both copies to + // PaginatedSearchSelect). Asserting on it means a regression where teams DO + // load for this user fails here instead of racing a one-shot count() against + // an in-flight request. await expect(dropdown.getByText("No teams found")).toBeVisible({ timeout: 10_000 }); await expect(dropdown.getByRole("option")).toHaveCount(0); }); diff --git a/tests/e2e/ui/tests/internal-user/internalUserWithTeams.spec.ts b/tests/e2e/ui/tests/internal-user/internalUserWithTeams.spec.ts index 7d5058a8140..68319154554 100644 --- a/tests/e2e/ui/tests/internal-user/internalUserWithTeams.spec.ts +++ b/tests/e2e/ui/tests/internal-user/internalUserWithTeams.spec.ts @@ -18,10 +18,10 @@ test.describe("Internal User with team memberships", () => { await page.getByRole("button", { name: /Create New Key/i }).click(); await expect(page.getByText("Key Ownership")).toBeVisible({ timeout: 10_000 }); - const teamSelect = page.locator(".ant-select", { hasText: "Search or select a team" }); + const teamSelect = page.getByTestId("team-dropdown").getByRole("combobox"); await teamSelect.click(); - const dropdown = page.locator(".ant-select-dropdown:visible").first(); + const dropdown = page.locator('[data-slot="combobox-content"]:visible').first(); await expect(dropdown).toBeVisible({ timeout: 5_000 }); // Both seeded memberships render, and nothing else does — proving the diff --git a/tests/e2e/ui/tests/modelsPage/addModel.spec.ts b/tests/e2e/ui/tests/modelsPage/addModel.spec.ts index 461d9dfd9f8..1b11ea69f97 100644 --- a/tests/e2e/ui/tests/modelsPage/addModel.spec.ts +++ b/tests/e2e/ui/tests/modelsPage/addModel.spec.ts @@ -328,11 +328,11 @@ test.describe("Add Model", () => { const teamByokRow = page.locator(".ant-form-item", { hasText: "Team-BYOK Model" }); await teamByokRow.getByRole("switch").click(); - // TeamDropdown's options carry custom markup and no role="option", so match by text. - const teamDropdown = page.getByTestId("team-dropdown"); + // TeamDropdown options show the alias above the team id, so match on the id line by text. + const teamDropdown = page.getByTestId("team-dropdown").getByRole("combobox"); await expect(teamDropdown).toBeVisible({ timeout: 5_000 }); await teamDropdown.click(); - const teamOption = page.locator(".ant-select-dropdown:visible").getByText(E2E_TEAM_CRUD_ID).first(); + const teamOption = page.locator('[data-slot="combobox-content"]:visible').getByText(E2E_TEAM_CRUD_ID).first(); await expect(teamOption).toBeVisible({ timeout: 5_000 }); await teamOption.click(); diff --git a/tests/e2e/ui/tests/proxy-admin/keys.spec.ts b/tests/e2e/ui/tests/proxy-admin/keys.spec.ts index 1ff3ef274b6..d9b0f959c9f 100644 --- a/tests/e2e/ui/tests/proxy-admin/keys.spec.ts +++ b/tests/e2e/ui/tests/proxy-admin/keys.spec.ts @@ -40,11 +40,11 @@ test.describe("Proxy Admin - Keys", () => { const keyName = `e2e-admin-key-${Date.now()}`; await page.getByTestId("base-input").fill(keyName); - // Select team — the team dropdown has placeholder "Search or select a team" - const teamSelect = page.locator(".ant-select", { hasText: "Search or select a team" }); + // Select team + const teamSelect = page.getByTestId("team-dropdown").getByRole("combobox"); await teamSelect.click(); await page.keyboard.type(E2E_TEAM_CRUD_ALIAS); - await page.locator(".ant-select-dropdown:visible").getByText(E2E_TEAM_CRUD_ALIAS).first().click(); + await page.locator('[data-slot="combobox-content"]:visible').getByText(E2E_TEAM_CRUD_ALIAS).first().click(); // Select models await page.locator(".ant-select-selection-overflow").click(); @@ -157,7 +157,7 @@ test.describe("Proxy Admin - Keys", () => { await page.getByRole("button", { name: "More key actions" }).click(); await page.getByRole("menuitem", { name: "Delete Key" }).click(); - const modal = page.locator(".ant-modal:visible"); + const modal = page.getByRole("dialog", { name: "Delete Key" }); await expect(modal).toBeVisible({ timeout: 5_000 }); await modal.locator("input").fill(E2E_DELETE_KEY_ALIAS); diff --git a/tests/e2e/ui/tests/proxy-admin/teams.spec.ts b/tests/e2e/ui/tests/proxy-admin/teams.spec.ts index 92d22f11f4d..37693c5d49d 100644 --- a/tests/e2e/ui/tests/proxy-admin/teams.spec.ts +++ b/tests/e2e/ui/tests/proxy-admin/teams.spec.ts @@ -129,7 +129,7 @@ test.describe("Proxy Admin - Teams", () => { await teamRow.locator('[data-testid^="team-actions-"]').click(); await page.getByTestId("team-action-delete").click(); - const modal = page.locator(".ant-modal:visible"); + const modal = page.getByRole("dialog", { name: "Delete Team?" }); await expect(modal).toBeVisible({ timeout: 5_000 }); await modal.locator("input").fill(E2E_TEAM_DELETE_ALIAS); await modal.getByRole("button", { name: /Force Delete|Delete/i }).click(); diff --git a/tests/e2e/ui/tests/team-admin/teamAdmin.spec.ts b/tests/e2e/ui/tests/team-admin/teamAdmin.spec.ts index be4526f7089..d71d5e6c0fe 100644 --- a/tests/e2e/ui/tests/team-admin/teamAdmin.spec.ts +++ b/tests/e2e/ui/tests/team-admin/teamAdmin.spec.ts @@ -105,7 +105,7 @@ test.describe("Team Admin", () => { await expect(row).toBeVisible({ timeout: 10_000 }); await row.getByTestId("delete-member").click(); - const modal = page.locator(".ant-modal:visible"); + const modal = page.getByRole("dialog", { name: "Delete Team Member" }); await expect(modal).toBeVisible({ timeout: 5_000 }); const remove = await captureRequestBody(page, { method: "POST", urlIncludes: "/team/member_delete" }, async () => { @@ -139,10 +139,10 @@ test.describe("Team Admin", () => { await page.getByTestId("base-input").fill(keyName); // Team selector — same locator pattern as the proxy-admin keys test. - const teamSelect = page.locator(".ant-select", { hasText: "Search or select a team" }); + const teamSelect = page.getByTestId("team-dropdown").getByRole("combobox"); await teamSelect.click(); await page.keyboard.type(E2E_TEAM_CRUD_ALIAS); - await page.locator(".ant-select-dropdown:visible").getByText(E2E_TEAM_CRUD_ALIAS).first().click(); + await page.locator('[data-slot="combobox-content"]:visible').getByText(E2E_TEAM_CRUD_ALIAS).first().click(); // Models — pick "All Team Models" await page.locator(".ant-select-selection-overflow").click(); diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 5e322598a10..346fa4eaee5 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -2377,15 +2377,7 @@ "count": 1 } }, - "src/components/common_components/DefaultProxyAdminTag.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/common_components/DeleteResourceModal.tsx": { - "no-restricted-imports": { - "count": 1 - }, "react-hooks/set-state-in-effect": { "count": 1 } @@ -2434,17 +2426,11 @@ } }, "src/components/common_components/ModelAliasManager.tsx": { - "no-restricted-imports": { - "count": 1 - }, "react-hooks/set-state-in-effect": { "count": 1 } }, "src/components/common_components/ModelSelector.tsx": { - "no-restricted-imports": { - "count": 2 - }, "react-hooks/set-state-in-effect": { "count": 1 } @@ -2454,14 +2440,6 @@ "count": 1 } }, - "src/components/common_components/OrganizationDropdown.tsx": { - "local/no-complex-jsx-arrow": { - "count": 1 - }, - "no-restricted-imports": { - "count": 1 - } - }, "src/components/common_components/PassThroughGuardrailsSection.tsx": { "no-restricted-imports": { "count": 2 @@ -2470,29 +2448,11 @@ "count": 1 } }, - "src/components/common_components/PassThroughRoutesSelector.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/common_components/PassThroughSecuritySection.tsx": { "no-restricted-imports": { "count": 2 } }, - "src/components/common_components/PremiumLoggingSettings.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/components/common_components/ProjectDropdown.tsx": { - "local/no-complex-jsx-arrow": { - "count": 1 - }, - "no-restricted-imports": { - "count": 1 - } - }, "src/components/common_components/RateLimitTypeFormItem.test.tsx": { "no-restricted-imports": { "count": 1 @@ -2503,22 +2463,9 @@ "count": 1 } }, - "src/components/common_components/RouterSettingsAccordion.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/common_components/budget_duration_dropdown.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 - } - }, - "src/components/common_components/chartUtils.test.tsx": { - "no-restricted-imports": { - "count": 1 } }, "src/components/common_components/chartUtils.tsx": { @@ -2527,9 +2474,6 @@ }, "no-nested-ternary": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "src/components/common_components/check_openapi_schema.tsx": { @@ -2554,17 +2498,11 @@ }, "no-nested-ternary": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "src/components/common_components/team_dropdown.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "src/components/common_components/team_multi_select.tsx": { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsPage.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsPage.test.tsx index a1484ffb5c5..5e212901bd1 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsPage.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsPage.test.tsx @@ -1,4 +1,5 @@ import { renderWithProviders, screen, within } from "@/../tests/test-utils"; +import { waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { AccessGroupsPage } from "./AccessGroupsPage"; @@ -215,7 +216,9 @@ describe("AccessGroupsPage", () => { await user.click(await openRowMenu(user, "ag-1")); const dialog = screen.getByRole("dialog", { name: "Delete Access Group" }); await user.click(within(dialog).getByRole("button", { name: "Cancel" })); - expect(screen.queryByRole("dialog", { name: "Delete Access Group" })).not.toBeInTheDocument(); + await waitFor(() => { + expect(screen.queryByRole("dialog", { name: "Delete Access Group" })).not.toBeInTheDocument(); + }); expect(mockMutate).not.toHaveBeenCalled(); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users/user_info_view.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users/user_info_view.test.tsx index 237e3b2c842..29c9b17cb21 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users/user_info_view.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users/user_info_view.test.tsx @@ -244,7 +244,7 @@ describe("UserInfoView", () => { }); // The DeleteResourceModal's OK button has text "Delete" - find it within the modal - const modal = screen.getByText("Remove from Team").closest(".ant-modal") as HTMLElement; + const modal = screen.getByRole("dialog", { name: "Remove from Team" }); const deleteConfirmButton = within(modal).getByRole("button", { name: /delete/i }); await user.click(deleteConfirmButton); diff --git a/ui/litellm-dashboard/src/components/common_components/DefaultProxyAdminTag.tsx b/ui/litellm-dashboard/src/components/common_components/DefaultProxyAdminTag.tsx index e42e3cee6da..9ec24bb929b 100644 --- a/ui/litellm-dashboard/src/components/common_components/DefaultProxyAdminTag.tsx +++ b/ui/litellm-dashboard/src/components/common_components/DefaultProxyAdminTag.tsx @@ -1,6 +1,4 @@ -import { Tag, Typography } from "antd"; - -const { Text } = Typography; +import { Badge } from "@/components/ui/badge"; const DEFAULT_USER_ID = "default_user_id"; @@ -8,15 +6,10 @@ interface DefaultProxyAdminTagProps { userId: string | null | undefined; } -/** - * Renders "Default Proxy Admin" as a blue Tag when the given userId is - * the well-known `default_user_id`, otherwise renders the raw value as - * plain text. - */ export default function DefaultProxyAdminTag({ userId }: DefaultProxyAdminTagProps) { if (userId === DEFAULT_USER_ID) { - return Default Proxy Admin; + return Default Proxy Admin; } - return {userId}; + return {userId}; } diff --git a/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.tsx b/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.tsx index 5a6160483a0..b45f164b2a5 100644 --- a/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.tsx +++ b/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.tsx @@ -1,6 +1,10 @@ -import { Alert, Card, Descriptions, Input, Modal, Typography, theme } from "antd"; -import { ExclamationCircleOutlined } from "@ant-design/icons"; +import { CircleAlert } from "lucide-react"; import React, { useState, useEffect } from "react"; +import { Alert, AlertTitle } from "@/components/shared/Alert"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; interface DeleteResourceModalProps { isOpen: boolean; @@ -8,12 +12,11 @@ interface DeleteResourceModalProps { alertMessage?: string; message: string; resourceInformationTitle?: string; - resourceInformation?: Array< - { - label: string; - value: string | number | undefined | null; - } & Omit, "children"> - >; + resourceInformation?: Array<{ + label: string; + value: string | number | undefined | null; + code?: boolean; + }>; onCancel: () => void; onOk: () => void; confirmLoading: boolean; @@ -32,8 +35,6 @@ export default function DeleteResourceModal({ confirmLoading, requiredConfirmation, }: DeleteResourceModalProps) { - const { Text } = Typography; - const { token } = theme.useToken(); const [requiredConfirmationInput, setRequiredConfirmationInput] = useState(""); useEffect(() => { @@ -43,69 +44,69 @@ export default function DeleteResourceModal({ }, [isOpen]); return ( - -
- {alertMessage && } - - - {resourceInformation && - resourceInformation.map(({ label, value, ...textProps }) => ( - {label}}> - {value ?? "-"} - - ))} - - -
- {message} -
- {requiredConfirmation && ( -
- - Type - - {requiredConfirmation} - - to confirm deletion: - - setRequiredConfirmationInput(e.target.value)} - placeholder={requiredConfirmation} - className="rounded-md" - prefix={} - autoFocus - /> + !open && onCancel()}> + + + {title} + +
+ {alertMessage && ( + + {alertMessage} + + )} + + {resourceInformationTitle && ( + + {resourceInformationTitle} + + )} + +
+ {resourceInformation?.map(({ label, value, code }) => ( + +
{label}
+
{code ? {value ?? "-"} : value ?? "-"}
+
+ ))} +
+
+
+
+ {message}
- )} -
- + {requiredConfirmation && ( +
+

+ Type {requiredConfirmation} to confirm deletion: +

+ + + + + setRequiredConfirmationInput(e.target.value)} + placeholder={requiredConfirmation} + autoFocus + /> + +
+ )} +
+ + + + + + ); } diff --git a/ui/litellm-dashboard/src/components/common_components/ModelAliasManager.tsx b/ui/litellm-dashboard/src/components/common_components/ModelAliasManager.tsx index c3540ce757a..9b89d85507b 100644 --- a/ui/litellm-dashboard/src/components/common_components/ModelAliasManager.tsx +++ b/ui/litellm-dashboard/src/components/common_components/ModelAliasManager.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from "react"; import { PlusCircleIcon, PencilIcon, TrashIcon } from "@heroicons/react/outline"; -import { Card, Title, Text, Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react"; +import { Card, CardTitle } from "@/components/ui/card"; +import { Table, TableHeader, TableHead, TableBody, TableRow, TableCell } from "@/components/ui/table"; import ModelSelector from "./ModelSelector"; import NotificationsManager from "../molecules/notifications_manager"; @@ -141,7 +142,7 @@ const ModelAliasManager: React.FC = ({ return (
- Add New Alias +

Add New Alias

@@ -186,17 +187,17 @@ const ModelAliasManager: React.FC = ({
- Manage Existing Aliases +

Manage Existing Aliases

- + - Alias Name - Target Model - Actions + Alias Name + Target Model + Actions - + {aliases.map((alias) => ( @@ -284,9 +285,9 @@ const ModelAliasManager: React.FC = ({ {/* Configuration Example */} {showExampleConfig && ( - - Configuration Example - Here's how your current aliases would look in the config: + + Configuration Example +

Here's how your current aliases would look in the config:

model_aliases: diff --git a/ui/litellm-dashboard/src/components/common_components/ModelSelector.test.tsx b/ui/litellm-dashboard/src/components/common_components/ModelSelector.test.tsx index 857e4d3296b..bd7b56f1817 100644 --- a/ui/litellm-dashboard/src/components/common_components/ModelSelector.test.tsx +++ b/ui/litellm-dashboard/src/components/common_components/ModelSelector.test.tsx @@ -1,28 +1,20 @@ import { act, fireEvent, render, screen } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, it, vi } from "vitest"; import ModelSelector from "./ModelSelector"; vi.mock("@/components/llm_calls/fetch_models", () => ({ fetchAvailableModels: vi.fn().mockResolvedValue([]), })); -const openCustomModelInput = () => { - const selector = document.querySelector(".ant-select-selector"); - expect(selector).toBeTruthy(); - act(() => { - fireEvent.mouseDown(selector!); - }); - act(() => { - fireEvent.click(screen.getByText("Enter custom model")); - }); +const openCustomModelInput = async () => { + const user = userEvent.setup(); + await user.click(screen.getByRole("combobox")); + await user.click(await screen.findByText("Enter custom model")); return screen.getByPlaceholderText("Enter custom model name"); }; describe("ModelSelector custom model debounce", () => { - beforeEach(() => { - vi.useFakeTimers(); - }); - afterEach(() => { act(() => { vi.runOnlyPendingTimers(); @@ -30,11 +22,12 @@ describe("ModelSelector custom model debounce", () => { vi.useRealTimers(); }); - it("does not call onChange before the debounce wait elapses", () => { + it("does not call onChange before the debounce wait elapses", async () => { const onChange = vi.fn(); render(); - const input = openCustomModelInput(); + const input = await openCustomModelInput(); + vi.useFakeTimers(); act(() => { fireEvent.change(input, { target: { value: "gpt-4o" } }); @@ -49,11 +42,12 @@ describe("ModelSelector custom model debounce", () => { expect(onChange).not.toHaveBeenCalled(); }); - it("calls onChange exactly once with the last typed value after the wait", () => { + it("calls onChange exactly once with the last typed value after the wait", async () => { const onChange = vi.fn(); render(); - const input = openCustomModelInput(); + const input = await openCustomModelInput(); + vi.useFakeTimers(); act(() => { fireEvent.change(input, { target: { value: "g" } }); @@ -71,11 +65,12 @@ describe("ModelSelector custom model debounce", () => { expect(onChange).toHaveBeenCalledWith("gpt-5.2"); }); - it("does not call onChange when unmounted mid-wait", () => { + it("does not call onChange when unmounted mid-wait", async () => { const onChange = vi.fn(); const { unmount } = render(); - const input = openCustomModelInput(); + const input = await openCustomModelInput(); + vi.useFakeTimers(); act(() => { fireEvent.change(input, { target: { value: "gpt-4o" } }); diff --git a/ui/litellm-dashboard/src/components/common_components/ModelSelector.tsx b/ui/litellm-dashboard/src/components/common_components/ModelSelector.tsx index f2621cd1acb..a50131256fa 100644 --- a/ui/litellm-dashboard/src/components/common_components/ModelSelector.tsx +++ b/ui/litellm-dashboard/src/components/common_components/ModelSelector.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect } from "react"; -import { TextInput, Text } from "@tremor/react"; -import { Select } from "antd"; -import { RobotOutlined } from "@ant-design/icons"; +import { Bot } from "lucide-react"; import { useDebouncedCallback } from "@tanstack/react-pacer/debouncer"; +import { Input } from "@/components/ui/input"; +import { SearchSelect } from "@/components/shared/SearchSelect"; import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models"; const MODEL_SELECT_DEBOUNCE_MS = 500; @@ -80,32 +80,30 @@ const ModelSelector: React.FC = ({ return (
{showLabel && ( - - {labelText} - +

+ {labelText} +

)} - { - if (!option) return false; - const org = organizations?.find((o) => o.organization_id === option.key); - if (!org) return false; - - const searchTerm = input.toLowerCase().trim(); - const orgAlias = (org.organization_alias || "").toLowerCase(); - const orgId = (org.organization_id || "").toLowerCase(); - - return orgAlias.includes(searchTerm) || orgId.includes(searchTerm); - }} - > - {organizations?.map((org) => ( - - {org.organization_alias}{" "} - ({org.organization_id}) - - ))} - +
+ ({ + label: org.organization_alias || org.organization_id, + value: org.organization_id, + sublabel: org.organization_id, + }))} + value={value} + onValueChange={(organizationId) => onChange?.(organizationId)} + placeholder={placeholder} + emptyText={loading ? "Loading organizations…" : "No organizations found"} + disabled={disabled} + inputId={id} + /> +
); }; diff --git a/ui/litellm-dashboard/src/components/common_components/PassThroughRoutesSelector.tsx b/ui/litellm-dashboard/src/components/common_components/PassThroughRoutesSelector.tsx index e02125dea56..330f852383d 100644 --- a/ui/litellm-dashboard/src/components/common_components/PassThroughRoutesSelector.tsx +++ b/ui/litellm-dashboard/src/components/common_components/PassThroughRoutesSelector.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import { Select } from "antd"; +import { MultiSelect, type MultiSelectOption } from "@/components/shared/MultiSelect"; import { getPassThroughEndpointsCall } from "../networking"; interface PassThroughRoutesSelectorProps { @@ -17,6 +17,11 @@ interface PassThroughEndpoint { methods?: string[]; } +const routeOption = (endpoint: PassThroughEndpoint): MultiSelectOption => ({ + label: endpoint.methods?.length ? `${endpoint.methods.join(", ")} ${endpoint.path}` : endpoint.path, + value: endpoint.path, +}); + const PassThroughRoutesSelector: React.FC = ({ onChange, value, @@ -26,7 +31,7 @@ const PassThroughRoutesSelector: React.FC = ({ disabled = false, teamId, }) => { - const [passThroughRoutes, setPassThroughRoutes] = useState>([]); + const [passThroughRoutes, setPassThroughRoutes] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { @@ -37,27 +42,7 @@ const PassThroughRoutesSelector: React.FC = ({ try { const response = await getPassThroughEndpointsCall(accessToken, teamId); if (response.endpoints) { - const routes = response.endpoints.flatMap((endpoint: PassThroughEndpoint) => { - const path = endpoint.path; - const methods = endpoint.methods; - - // If methods are specified, create one entry per method - if (methods && methods.length > 0) { - return methods.map((method) => ({ - label: `${method} ${path}`, - value: path, // Keep value as path for backward compatibility - })); - } - - // If no methods specified, show just the path (all methods supported) - return [ - { - label: path, - value: path, - }, - ]; - }); - setPassThroughRoutes(routes); + setPassThroughRoutes(response.endpoints.map(routeOption)); } } catch (error) { console.error("Error fetching pass through routes:", error); @@ -70,19 +55,16 @@ const PassThroughRoutesSelector: React.FC = ({ }, [accessToken, teamId]); return ( - ({ + label: project.project_alias || project.project_id, + value: project.project_id, + sublabel: project.project_id, + })) + } value={value} - onChange={onChange} + onValueChange={(projectId) => onChange?.(projectId)} + placeholder="Search or select a project" + emptyText={loading ? "Loading projects…" : "No projects found"} disabled={disabled} - loading={loading} - allowClear - notFoundContent={loading ? } size="small" /> : undefined} - filterOption={(input, option) => { - if (!option) return false; - const project = filtered?.find((p) => p.project_id === option.key); - if (!project) return false; - - const searchTerm = input.toLowerCase().trim(); - const alias = (project.project_alias || "").toLowerCase(); - const id = (project.project_id || "").toLowerCase(); - - return alias.includes(searchTerm) || id.includes(searchTerm); - }} - optionFilterProp="children" - > - {!loading && - filtered?.map((project) => ( - - {project.project_alias || project.project_id}{" "} - ({project.project_id}) - - ))} - + inputId={id} + /> ); }; diff --git a/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.test.tsx b/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.test.tsx index 5ac3b8b2b64..6f819607e3f 100644 --- a/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.test.tsx +++ b/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.test.tsx @@ -21,14 +21,6 @@ vi.mock("../Settings/RouterSettings/Fallbacks/FallbackSelectionForm", () => ({ ), })); -vi.mock("@tremor/react", () => ({ - TabGroup: ({ children }: { children: ReactNode }) =>
{children}
, - TabList: ({ children }: { children: ReactNode }) =>
{children}
, - Tab: ({ children }: { children: ReactNode }) =>
{children}
, - TabPanels: ({ children }: { children: ReactNode }) =>
{children}
, - TabPanel: ({ children }: { children: ReactNode }) =>
{children}
, -})); - vi.mock("../router_settings/RouterSettingsForm", () => ({ default: ({ value, diff --git a/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx b/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx index 56227abe9ea..7570806182d 100644 --- a/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx +++ b/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState, useImperativeHandle, forwardRef, useRef } from "react"; -import { TabPanel, TabPanels, TabGroup, TabList, Tab } from "@tremor/react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useQuery } from "@tanstack/react-query"; import { useDebouncedCallback } from "@tanstack/react-pacer/debouncer"; import { getRouterSettingsCall } from "../networking"; @@ -344,13 +344,13 @@ const RouterSettingsAccordion = forwardRef - - - Loadbalancing - Fallbacks - - - + + + Loadbalancing + Fallbacks + +
+ - - + + - - - + +
+
); }, diff --git a/ui/litellm-dashboard/src/components/common_components/budget_duration_dropdown.tsx b/ui/litellm-dashboard/src/components/common_components/budget_duration_dropdown.tsx index 4db36f27553..e283f0550ec 100644 --- a/ui/litellm-dashboard/src/components/common_components/budget_duration_dropdown.tsx +++ b/ui/litellm-dashboard/src/components/common_components/budget_duration_dropdown.tsx @@ -1,10 +1,16 @@ import React from "react"; -import { Select } from "antd"; - -const { Option } = Select; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; export const NEVER_RESETS_BUDGET_DURATION = "none"; +const DURATION_LABELS: Record = { + [NEVER_RESETS_BUDGET_DURATION]: "Never resets", + "1h": "hourly", + "24h": "daily", + "7d": "weekly", + "30d": "monthly", +}; + interface BudgetDurationDropdownProps { value?: string | null; onChange?: (value: string | undefined) => void; @@ -24,18 +30,21 @@ const BudgetDurationDropdown: React.FC = ({ }) => { return ( ); }; diff --git a/ui/litellm-dashboard/src/components/common_components/chartUtils.test.tsx b/ui/litellm-dashboard/src/components/common_components/chartUtils.test.tsx index b924021863a..01a4d8373c4 100644 --- a/ui/litellm-dashboard/src/components/common_components/chartUtils.test.tsx +++ b/ui/litellm-dashboard/src/components/common_components/chartUtils.test.tsx @@ -1,9 +1,11 @@ import { render, screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import { CustomLegend, CustomTooltip } from "./chartUtils"; -import type { CustomTooltipProps } from "@tremor/react"; +import type { ChartTooltipProps } from "@/components/shared/charts/chart_tooltip"; import { SpendMetrics } from "../UsagePage/types"; +type TooltipPayload = NonNullable; + describe("CustomTooltip", () => { const mockPayload = [ { @@ -28,9 +30,9 @@ describe("CustomTooltip", () => { ]; it("should render", () => { - const props: CustomTooltipProps = { + const props: ChartTooltipProps = { active: true, - payload: mockPayload, + payload: mockPayload as unknown as TooltipPayload, label: "2024-01-15", }; render(); @@ -38,9 +40,9 @@ describe("CustomTooltip", () => { }); it("should return null when not active", () => { - const props: CustomTooltipProps = { + const props: ChartTooltipProps = { active: false, - payload: mockPayload, + payload: mockPayload as unknown as TooltipPayload, label: "2024-01-15", }; const { container } = render(); @@ -48,9 +50,9 @@ describe("CustomTooltip", () => { }); it("should return null when payload is empty", () => { - const props: CustomTooltipProps = { + const props: ChartTooltipProps = { active: true, - payload: [], + payload: [] as unknown as TooltipPayload, label: "2024-01-15", }; const { container } = render(); @@ -58,9 +60,9 @@ describe("CustomTooltip", () => { }); it("should display formatted category names", () => { - const props: CustomTooltipProps = { + const props: ChartTooltipProps = { active: true, - payload: mockPayload, + payload: mockPayload as unknown as TooltipPayload, label: "2024-01-15", }; render(); @@ -89,9 +91,9 @@ describe("CustomTooltip", () => { }, }, ]; - const props: CustomTooltipProps = { + const props: ChartTooltipProps = { active: true, - payload: payloadWithUnderscores, + payload: payloadWithUnderscores as unknown as TooltipPayload, label: "2024-01-15", }; render(); @@ -120,9 +122,9 @@ describe("CustomTooltip", () => { }, }, ]; - const props: CustomTooltipProps = { + const props: ChartTooltipProps = { active: true, - payload: spendPayload, + payload: spendPayload as unknown as TooltipPayload, label: "2024-01-15", }; render(); @@ -130,9 +132,9 @@ describe("CustomTooltip", () => { }); it("should format non-spend numeric values with locale string", () => { - const props: CustomTooltipProps = { + const props: ChartTooltipProps = { active: true, - payload: mockPayload, + payload: mockPayload as unknown as TooltipPayload, label: "2024-01-15", }; render(); @@ -161,9 +163,9 @@ describe("CustomTooltip", () => { }, }, ]; - const props: CustomTooltipProps = { + const props: ChartTooltipProps = { active: true, - payload: payloadWithUndefined, + payload: payloadWithUndefined as unknown as TooltipPayload, label: "2024-01-15", }; render(); @@ -211,9 +213,9 @@ describe("CustomTooltip", () => { }, }, ]; - const props: CustomTooltipProps = { + const props: ChartTooltipProps = { active: true, - payload: multiplePayload, + payload: multiplePayload as unknown as TooltipPayload, label: "2024-01-15", }; render(); @@ -222,9 +224,9 @@ describe("CustomTooltip", () => { }); it("should convert color names to hex values", () => { - const props: CustomTooltipProps = { + const props: ChartTooltipProps = { active: true, - payload: mockPayload, + payload: mockPayload as unknown as TooltipPayload, label: "2024-01-15", }; const { container } = render(); @@ -254,9 +256,9 @@ describe("CustomTooltip", () => { }, }, ]; - const props: CustomTooltipProps = { + const props: ChartTooltipProps = { active: true, - payload: payloadWithHexColor, + payload: payloadWithHexColor as unknown as TooltipPayload, label: "2024-01-15", }; const { container } = render(); @@ -286,9 +288,9 @@ describe("CustomTooltip", () => { }, }, ]; - const props: CustomTooltipProps = { + const props: ChartTooltipProps = { active: true, - payload: payloadWithoutDataKey as any, + payload: payloadWithoutDataKey as unknown as TooltipPayload, label: "2024-01-15", }; render(); @@ -304,9 +306,9 @@ describe("CustomTooltip", () => { payload: undefined, }, ]; - const props: CustomTooltipProps = { + const props: ChartTooltipProps = { active: true, - payload: payloadWithoutPayload as any, + payload: payloadWithoutPayload as unknown as TooltipPayload, label: "2024-01-15", }; render(); diff --git a/ui/litellm-dashboard/src/components/common_components/chartUtils.tsx b/ui/litellm-dashboard/src/components/common_components/chartUtils.tsx index c0930f290f6..0abf004803b 100644 --- a/ui/litellm-dashboard/src/components/common_components/chartUtils.tsx +++ b/ui/litellm-dashboard/src/components/common_components/chartUtils.tsx @@ -1,4 +1,4 @@ -import type { CustomTooltipProps } from "@tremor/react"; +import type { ChartTooltipProps } from "@/components/shared/charts/chart_tooltip"; import { SpendMetrics } from "../UsagePage/types"; interface ChartDataPoint { @@ -16,7 +16,7 @@ const colorNameToHex: { [key: string]: string } = { emerald: "#37bc7d", }; -export const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => { +export const CustomTooltip = ({ active, payload, label }: ChartTooltipProps) => { if (active && payload && payload.length) { const formatCategoryName = (name: string): string => { return name diff --git a/ui/litellm-dashboard/src/components/common_components/routerSettingsWiring.test.tsx b/ui/litellm-dashboard/src/components/common_components/routerSettingsWiring.test.tsx index d6ed0ae07db..15c52e71e8f 100644 --- a/ui/litellm-dashboard/src/components/common_components/routerSettingsWiring.test.tsx +++ b/ui/litellm-dashboard/src/components/common_components/routerSettingsWiring.test.tsx @@ -1,6 +1,6 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { render, screen, waitFor } from "@testing-library/react"; -import type { ReactElement, ReactNode } from "react"; +import type { ReactElement } from "react"; import { describe, expect, it, vi } from "vitest"; import type { FallbackGroup } from "../Settings/RouterSettings/Fallbacks/FallbackGroupConfig"; import type { RouterSettingsFormValue } from "../router_settings/RouterSettingsForm"; @@ -16,14 +16,6 @@ vi.mock("@/components/llm_calls/fetch_models", () => ({ fetchAvailableModelsForTeam: vi.fn().mockResolvedValue([]), })); -vi.mock("@tremor/react", () => ({ - TabGroup: ({ children }: { children: ReactNode }) =>
{children}
, - TabList: ({ children }: { children: ReactNode }) =>
{children}
, - Tab: ({ children }: { children: ReactNode }) =>
{children}
, - TabPanels: ({ children }: { children: ReactNode }) =>
{children}
, - TabPanel: ({ children }: { children: ReactNode }) =>
{children}
, -})); - vi.mock("../router_settings/RouterSettingsForm", () => ({ default: ({ value }: { value: RouterSettingsFormValue }) => (
{JSON.stringify(value.routerSettings)}
diff --git a/ui/litellm-dashboard/src/components/common_components/simple_table.tsx b/ui/litellm-dashboard/src/components/common_components/simple_table.tsx index 4a858a346d4..17e8d46d21d 100644 --- a/ui/litellm-dashboard/src/components/common_components/simple_table.tsx +++ b/ui/litellm-dashboard/src/components/common_components/simple_table.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Table, TableHead, TableRow, TableHeaderCell, TableBody, TableCell, Text } from "@tremor/react"; +import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table"; export interface SimpleTableColumn { header: string; @@ -31,20 +31,20 @@ export function SimpleTable({ }: SimpleTableProps) { return (
- + {columns.map((column, index) => ( - + {column.header} - + ))} - + {isLoading ? ( - {loadingMessage} + {loadingMessage} ) : data.length > 0 ? ( @@ -60,7 +60,7 @@ export function SimpleTable({ ) : ( - {emptyMessage} + {emptyMessage} )} diff --git a/ui/litellm-dashboard/src/components/common_components/team_dropdown.tsx b/ui/litellm-dashboard/src/components/common_components/team_dropdown.tsx index 7d27886c7f5..35121f41598 100644 --- a/ui/litellm-dashboard/src/components/common_components/team_dropdown.tsx +++ b/ui/litellm-dashboard/src/components/common_components/team_dropdown.tsx @@ -1,13 +1,8 @@ -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 React, { useMemo, useState } from "react"; +import { PaginatedSearchSelect } from "@/components/shared/PaginatedSearchSelect"; import { useInfiniteTeams } from "@/app/(dashboard)/hooks/teams/useTeams"; -import { DEBOUNCE_WAIT_MS } from "@/utils/debounceConstants"; import { Team } from "../key_team_helpers/key_list"; -const { Text } = Typography; - interface TeamDropdownProps { value?: string; onChange?: (value: string) => void; @@ -17,10 +12,9 @@ interface TeamDropdownProps { /** Filter teams by organization. */ organizationId?: string | null; pageSize?: number; + id?: string; } -const SCROLL_THRESHOLD = 0.8; - const TeamDropdown: React.FC = ({ value, onChange, @@ -28,15 +22,13 @@ const TeamDropdown: React.FC = ({ disabled, organizationId, pageSize = 20, + id, }) => { - const [searchInput, setSearchInput] = useState(""); - const [debouncedSearch, setDebouncedSearch] = useDebouncedState("", { - wait: DEBOUNCE_WAIT_MS, - }); + const [search, setSearch] = useState(""); const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useInfiniteTeams( pageSize, - debouncedSearch || undefined, + search || undefined, organizationId, ); @@ -54,59 +46,35 @@ const TeamDropdown: React.FC = ({ 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); - }; - - const handleChange = (teamId: string | undefined) => { - onChange?.(teamId ?? ""); + const handleChange = (teamId: string) => { + onChange?.(teamId); if (onTeamSelect) { - const team = teamId ? teams.find((t) => t.team_id === teamId) ?? null : null; - onTeamSelect(team); + onTeamSelect(teamId ? teams.find((t) => t.team_id === teamId) ?? null : null); } }; return ( - +
+ ({ + label: team.team_alias || team.team_id, + value: team.team_id, + sublabel: team.team_id, + }))} + value={value || undefined} + onValueChange={handleChange} + onSearchChange={setSearch} + onLoadMore={fetchNextPage} + hasNextPage={hasNextPage} + isLoading={isLoading} + isFetchingNextPage={isFetchingNextPage} + placeholder="Search or select a team" + emptyText="No teams found" + loadingText="Loading teams…" + disabled={disabled} + inputId={id} + /> +
); }; diff --git a/ui/litellm-dashboard/src/components/shared/SearchSelect.tsx b/ui/litellm-dashboard/src/components/shared/SearchSelect.tsx index f67a1cffa1d..1bfc19cbbe2 100644 --- a/ui/litellm-dashboard/src/components/shared/SearchSelect.tsx +++ b/ui/litellm-dashboard/src/components/shared/SearchSelect.tsx @@ -24,6 +24,7 @@ interface SearchSelectProps { emptyText?: string; disabled?: boolean; className?: string; + inputId?: string; } const matchesQuery = (option: SearchSelectOption, query: string): boolean => { @@ -40,6 +41,7 @@ export function SearchSelect({ emptyText = "No results", disabled = false, className, + inputId, }: SearchSelectProps) { const selected = options.find((option) => option.value === value) ?? null; @@ -54,6 +56,7 @@ export function SearchSelect({ disabled={disabled} > { const user = userEvent.setup({ delay: null }); const resetBudgetItem = await openSettingsEditorForTeam(user, { budget_duration: "30d" }); - const clearIcon = resetBudgetItem.querySelector(".ant-select-clear"); - expect(clearIcon).not.toBeNull(); - fireEvent.mouseDown(clearIcon as Element); + await user.click(within(resetBudgetItem).getByRole("combobox")); + await user.click(await screen.findByText("Never resets")); await waitFor(() => { expect(within(resetBudgetItem).getByText("Never resets")).toBeInTheDocument(); @@ -1554,13 +1553,14 @@ describe("TeamInfoView", () => { await user.click(within(routesFormItem).getByRole("combobox")); - const option = await screen.findByTitle("POST /bedrock-passthrough"); + const option = await screen.findByText("POST /bedrock-passthrough"); await user.click(option); await waitFor(() => { expect(within(routesFormItem).getByText(/\/bedrock-passthrough/)).toBeInTheDocument(); }); + await user.keyboard("{Escape}"); await user.click(screen.getByRole("button", { name: /save changes/i })); await waitFor(() => { diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx index caad6fc30fc..cdbd3197f7d 100644 --- a/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx @@ -961,9 +961,8 @@ describe("KeyEditView", () => { ); const resetBudgetItem = (await screen.findByText("Reset Budget")).closest(".ant-form-item") as HTMLElement; - const clearIcon = resetBudgetItem.querySelector(".ant-select-clear"); - expect(clearIcon).not.toBeNull(); - fireEvent.mouseDown(clearIcon as Element); + await userEvent.click(within(resetBudgetItem).getByRole("combobox")); + await userEvent.click(await screen.findByText("Never resets")); await waitFor(() => { expect(within(resetBudgetItem).getByText("Never resets")).toBeInTheDocument(); @@ -995,7 +994,8 @@ describe("KeyEditView", () => { ); const resetBudgetItem = (await screen.findByText("Reset Budget")).closest(".ant-form-item") as HTMLElement; - fireEvent.mouseDown(resetBudgetItem.querySelector(".ant-select-clear") as Element); + await userEvent.click(within(resetBudgetItem).getByRole("combobox")); + await userEvent.click(await screen.findByText("Never resets")); await userEvent.click(screen.getByRole("button", { name: /save changes/i })); @@ -1251,9 +1251,10 @@ describe("KeyEditView", () => { expect(screen.getByText("Organization")).toBeInTheDocument(); }); - const orgFormItem = screen.getByText("Organization").closest(".ant-form-item"); - const disabledSelect = orgFormItem?.querySelector(".ant-select-disabled"); - expect(disabledSelect).toBeTruthy(); + const orgFormItem = screen.getByText("Organization").closest(".ant-form-item") as HTMLElement; + await userEvent.click(within(orgFormItem).getByRole("combobox")); + + expect(screen.queryByText("Engineering")).not.toBeInTheDocument(); }); it("should not disable the organization dropdown for admin users", async () => { @@ -1273,9 +1274,10 @@ describe("KeyEditView", () => { expect(screen.getByText("Organization")).toBeInTheDocument(); }); - const orgFormItem = screen.getByText("Organization").closest(".ant-form-item"); - const disabledSelect = orgFormItem?.querySelector(".ant-select-disabled"); - expect(disabledSelect).toBeFalsy(); + const orgFormItem = screen.getByText("Organization").closest(".ant-form-item") as HTMLElement; + await userEvent.click(within(orgFormItem).getByRole("combobox")); + + expect(await screen.findByText("Engineering")).toBeInTheDocument(); }); it("should initialize organization from keyData", async () => { @@ -1296,8 +1298,9 @@ describe("KeyEditView", () => { />, ); + const orgFormItem = (await screen.findByText("Organization")).closest(".ant-form-item") as HTMLElement; await waitFor(() => { - expect(screen.getByText("Engineering")).toBeInTheDocument(); + expect(within(orgFormItem).getByRole("combobox")).toHaveValue("Engineering"); }); }); });