diff --git a/tests/e2e/ui/tests/modelsPage/autoRouterTemplateSelect.spec.ts b/tests/e2e/ui/tests/modelsPage/autoRouterTemplateSelect.spec.ts new file mode 100644 index 00000000000..98bd1b84f11 --- /dev/null +++ b/tests/e2e/ui/tests/modelsPage/autoRouterTemplateSelect.spec.ts @@ -0,0 +1,59 @@ +import { expect, test, type Page as PlaywrightPage } from "@playwright/test"; +import { ADMIN_STORAGE_PATH } from "../../constants"; +import { navigateToPage } from "../../helpers/navigation"; +import { Page } from "../../fixtures/pages"; + +/** + * Opens Add Auto Router and returns the Template select's trigger, which is the + * shallowest real page that renders SelectContent with tall multi-line options. + */ +async function openTemplateSelect(page: PlaywrightPage) { + await navigateToPage(page, Page.Models); + await page.getByRole("tab", { name: "Auto-Routers" }).click(); + await page.getByRole("button", { name: "Add Auto Router" }).click(); + + const trigger = page.getByTestId("template-selector"); + await expect(trigger).toBeVisible(); + return trigger; +} + +test.describe("Auto Router template select anchoring", () => { + test.use({ storageState: ADMIN_STORAGE_PATH }); + + test("opens the options below the trigger rather than over it", async ({ page }) => { + await page.setViewportSize({ width: 1280, height: 900 }); + const trigger = await openTemplateSelect(page); + const triggerBox = await trigger.boundingBox(); + + await trigger.click(); + const popup = page.locator('[data-slot="select-content"]'); + await expect(popup).toBeVisible(); + const popupBox = await popup.boundingBox(); + + expect(triggerBox).not.toBeNull(); + expect(popupBox).not.toBeNull(); + + // Item-aligned mode reports "none" and puts the active item over the trigger. + await expect(popup).toHaveAttribute("data-side", "bottom"); + expect(popupBox!.y).toBeGreaterThanOrEqual(triggerBox!.y + triggerBox!.height); + }); + + test("flips above the trigger instead of covering it when there is no room below", async ({ page }) => { + await page.setViewportSize({ width: 1280, height: 560 }); + const trigger = await openTemplateSelect(page); + await trigger.scrollIntoViewIfNeeded(); + const triggerBox = await trigger.boundingBox(); + + await trigger.click(); + const popup = page.locator('[data-slot="select-content"]'); + await expect(popup).toBeVisible(); + const popupBox = await popup.boundingBox(); + + expect(triggerBox).not.toBeNull(); + expect(popupBox).not.toBeNull(); + + const overlaps = + popupBox!.y < triggerBox!.y + triggerBox!.height && popupBox!.y + popupBox!.height > triggerBox!.y; + expect(overlaps).toBe(false); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/TeamGuardrailsTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/TeamGuardrailsTab.tsx index c9de758f50d..e309e8130f1 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/TeamGuardrailsTab.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/TeamGuardrailsTab.tsx @@ -1106,7 +1106,7 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) { > - + {GUARDRAIL_MODES.map((mode) => ( {mode.label} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/CategoryTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/CategoryTable.tsx index fa40ce6ab54..f012923d32f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/CategoryTable.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/CategoryTable.tsx @@ -64,7 +64,7 @@ const CategoryTable: React.FC = ({ - + {SEVERITY_ITEMS.map((item) => ( {item.label} @@ -93,7 +93,7 @@ const CategoryTable: React.FC = ({ - + {ACTION_ITEMS.map((item) => ( {item.label} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/CompetitorIntentConfiguration.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/CompetitorIntentConfiguration.tsx index d77dd216c2a..aa9b6798fff 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/CompetitorIntentConfiguration.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/CompetitorIntentConfiguration.tsx @@ -194,7 +194,7 @@ const CompetitorIntentConfiguration: React.FC - + {INTENT_TYPES.map((type) => ( {type.label} @@ -268,7 +268,7 @@ const CompetitorIntentConfiguration: React.FC - + {COMPETITOR_COMPARISON_POLICIES.map((policy) => ( {policy.label} @@ -292,7 +292,7 @@ const CompetitorIntentConfiguration: React.FC - + {POSSIBLE_COMPETITOR_COMPARISON_POLICIES.map((policy) => ( {policy.label} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/ContentCategoryConfiguration.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/ContentCategoryConfiguration.tsx index 1924133a6cc..f2226a3bc6e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/ContentCategoryConfiguration.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/ContentCategoryConfiguration.tsx @@ -199,7 +199,7 @@ const ContentCategoryConfiguration: React.FC - + {ACTION_ITEMS.map((item) => ( {item.value} @@ -224,7 +224,7 @@ const ContentCategoryConfiguration: React.FC - + {SEVERITY_ITEMS.map((item) => ( {item.label} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/CustomPatternModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/CustomPatternModal.tsx index 64441c90e5a..9813169e85e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/CustomPatternModal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/CustomPatternModal.tsx @@ -71,7 +71,7 @@ const CustomPatternModal: React.FC = ({ - + {ACTION_ITEMS.map((item) => ( {item.label} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/KeywordModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/KeywordModal.tsx index 177c0d2fac6..393a508cd18 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/KeywordModal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/KeywordModal.tsx @@ -61,7 +61,7 @@ const KeywordModal: React.FC = ({ - + {ACTION_ITEMS.map((item) => ( {item.label} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/KeywordTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/KeywordTable.tsx index 5c7e3ef3ab8..5b69b04955f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/KeywordTable.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/KeywordTable.tsx @@ -38,7 +38,7 @@ const KeywordTable: React.FC = ({ keywords, onActionChange, o - + {ACTION_ITEMS.map((item) => ( {item.label} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/PatternModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/PatternModal.tsx index e703711a03a..fd353f7d743 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/PatternModal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/PatternModal.tsx @@ -115,7 +115,7 @@ const PatternModal: React.FC = ({ - + {ACTION_ITEMS.map((item) => ( {item.label} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/PatternTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/PatternTable.tsx index 6dd266f07a0..f4e87119d7b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/PatternTable.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/PatternTable.tsx @@ -58,7 +58,7 @@ const PatternTable: React.FC = ({ patterns, onActionChange, o - + {ACTION_ITEMS.map((item) => ( {item.label} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/custom_code/CustomCodeModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/custom_code/CustomCodeModal.tsx index 77bac8bb0aa..a69824f32d3 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/custom_code/CustomCodeModal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/custom_code/CustomCodeModal.tsx @@ -556,7 +556,7 @@ const CustomCodeModal: React.FC = ({ visible, onClose, onS - + STANDARD {TEMPLATE_ITEMS.map((template) => ( diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/pii_components.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/pii_components.tsx index 5f8e833af8d..0de7eb1c9ce 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/pii_components.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/pii_components.tsx @@ -179,7 +179,7 @@ export const PiiEntityList: React.FC = ({ - + {actions.map((action) => ( diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/tool_permission/ToolPermissionRulesEditor.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/tool_permission/ToolPermissionRulesEditor.tsx index 8fe2bf5bf21..c154d102314 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/tool_permission/ToolPermissionRulesEditor.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/tool_permission/ToolPermissionRulesEditor.tsx @@ -280,7 +280,7 @@ const ToolPermissionRulesEditor: React.FC = ({ v - + {DECISION_ITEMS.map((item) => ( {item.label} @@ -313,7 +313,7 @@ const ToolPermissionRulesEditor: React.FC = ({ v - + {DECISION_ITEMS.map((item) => ( {item.label} @@ -350,7 +350,7 @@ const ToolPermissionRulesEditor: React.FC = ({ v - + {ON_DISALLOWED_ITEMS.map((item) => ( {item.label} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/CreateVectorStore.tsx b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/CreateVectorStore.tsx index eae4a3fd799..61d31c2d3d4 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/CreateVectorStore.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/CreateVectorStore.tsx @@ -342,7 +342,7 @@ const CreateVectorStore: React.FC = ({ accessToken, onSu - + {providerItems.map((item) => ( diff --git a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreForm.tsx b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreForm.tsx index 0c7d8cebb4b..6e2701be51b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreForm.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreForm.tsx @@ -298,7 +298,7 @@ const VectorStoreForm: React.FC = ({ }} - + {Object.entries(VectorStoreProviders).map(([providerEnum, providerDisplayName]) => ( = ({ }} - + {Object.entries(Providers) .filter(([providerEnum]) => providerEnum === "Bedrock") .map(([providerEnum, providerDisplayName]) => ( diff --git a/ui/litellm-dashboard/src/components/ui/select.test.tsx b/ui/litellm-dashboard/src/components/ui/select.test.tsx index 15dd266d38c..308b5ecec3b 100644 --- a/ui/litellm-dashboard/src/components/ui/select.test.tsx +++ b/ui/litellm-dashboard/src/components/ui/select.test.tsx @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; const ENVIRONMENTS = [ @@ -63,3 +64,40 @@ describe("SelectValue label resolution", () => { expect(screen.getByTestId("trigger")).toHaveTextContent("Any environment"); }); }); + +function renderOpenableSelect(contentProps?: React.ComponentProps) { + return render( + , + ); +} + +describe("SelectContent anchoring", () => { + it("anchors to the edge of the trigger rather than over it by default", async () => { + const user = userEvent.setup(); + renderOpenableSelect(); + + await user.click(screen.getByTestId("trigger")); + + expect(await screen.findByTestId("content")).toHaveAttribute("data-align-trigger", "false"); + }); + + it("still lets a caller opt into item-aligned anchoring", async () => { + const user = userEvent.setup(); + renderOpenableSelect({ alignItemWithTrigger: true }); + + await user.click(screen.getByTestId("trigger")); + + expect(await screen.findByTestId("content")).toHaveAttribute("data-align-trigger", "true"); + }); +}); diff --git a/ui/litellm-dashboard/src/components/ui/select.tsx b/ui/litellm-dashboard/src/components/ui/select.tsx index 7d009b53084..bca68fee9df 100644 --- a/ui/litellm-dashboard/src/components/ui/select.tsx +++ b/ui/litellm-dashboard/src/components/ui/select.tsx @@ -49,7 +49,7 @@ function SelectContent({ sideOffset = 4, align = "center", alignOffset = 0, - alignItemWithTrigger = true, + alignItemWithTrigger = false, ...props }: SelectPrimitive.Popup.Props & Pick) {