From dbd55d32023157fa1e13db994645878f8f870fd8 Mon Sep 17 00:00:00 2001 From: moe-berri Date: Mon, 26 Oct 2026 14:55:08 -0700 Subject: [PATCH] fix(ui): keep Aria accessible in the dashboard header (#48454) * fix(ui): keep Aria accessible in the dashboard header * test(ui): wait for every pagination fixture log * test(ui): update assistant setup for Aria branding --- tests/e2e/ui/globalSetup.ts | 4 +- tests/e2e/ui/helpers/navigation.ts | 23 ++++-- .../e2e/ui/tests/logs/logsPagination.spec.ts | 61 ++++++++++++++- ui/litellm-dashboard/AGENTS.md | 2 +- .../src/components/DashboardHeader.test.tsx | 3 +- .../src/components/DashboardHeader.tsx | 75 ++++++++++--------- .../liteadmin/LiteAdmin.integration.test.tsx | 67 ++++++++--------- .../src/components/liteadmin/LiteAdmin.tsx | 37 +++------ 8 files changed, 161 insertions(+), 111 deletions(-) diff --git a/tests/e2e/ui/globalSetup.ts b/tests/e2e/ui/globalSetup.ts index d97789c227e..e18d1be390c 100644 --- a/tests/e2e/ui/globalSetup.ts +++ b/tests/e2e/ui/globalSetup.ts @@ -2,7 +2,7 @@ import { chromium, expect, request } from "@playwright/test"; import { users, Role, STORAGE_PATHS } from "./fixtures/users"; import { ARTIFACT_DIR, UI_BASE_URL } from "./constants"; import { expectUnrestrictedDashboard, setInvitedUserPassword } from "./helpers/userOnboarding"; -import { hideLiteAdmin } from "./helpers/navigation"; +import { setAriaHidden } from "./helpers/navigation"; import { masterKey as getMasterKey } from "./helpers/traffic"; import * as fs from "fs"; import * as path from "path"; @@ -78,7 +78,7 @@ async function globalSetup() { await dismiss.click(); } if (role === Role.ProxyAdmin) { - await hideLiteAdmin(page); + await setAriaHidden(page, true); } // The login flow stores a post-login return URL in the litellm_return_url // cookie. If the snapshot captures it before the app consumes it, every diff --git a/tests/e2e/ui/helpers/navigation.ts b/tests/e2e/ui/helpers/navigation.ts index 78288fab743..be46fcf0395 100644 --- a/tests/e2e/ui/helpers/navigation.ts +++ b/tests/e2e/ui/helpers/navigation.ts @@ -62,18 +62,29 @@ export async function dismissFeedbackPopup(page: PlaywrightPage): Promise } } -export async function hideLiteAdmin(page: PlaywrightPage): Promise { +export async function setAriaHidden(page: PlaywrightPage, hidden: boolean): Promise { await page.getByRole("button", { name: /Account menu/i }).click(); const panel = page.getByTestId("sidebar-account-menu-panel"); await expect(panel).toBeVisible({ timeout: 5_000 }); - const toggle = panel.getByRole("switch", { name: "Toggle hide LiteAdmin" }); - if ((await toggle.getAttribute("aria-checked")) !== "true") { - await toggle.click(); + const premium = await panel.getByText("Premium", { exact: true }).isVisible(); + const toggle = panel.getByRole("switch", { name: "Toggle hide Aria Agent", exact: true }); + if (premium) { + await expect(toggle).toBeVisible(); + if ((await toggle.getAttribute("aria-checked")) !== String(hidden)) { + await toggle.click(); + } + await expect(toggle).toHaveAttribute("aria-checked", String(hidden)); + } else { + await expect(toggle).toHaveCount(0); } - await expect(toggle).toHaveAttribute("aria-checked", "true"); await page.keyboard.press("Escape"); await expect(panel).toBeHidden(); - await expect(page.getByRole("button", { name: "LiteAdmin", exact: true })).toBeHidden(); + const aria = page.getByRole("banner").getByRole("button", { name: "Aria Agent", exact: true }); + if (premium && hidden) { + await expect(aria).toBeHidden(); + } else { + await expect(aria).toBeVisible(); + } } /** diff --git a/tests/e2e/ui/tests/logs/logsPagination.spec.ts b/tests/e2e/ui/tests/logs/logsPagination.spec.ts index 416e1c3171f..838a829a3e2 100644 --- a/tests/e2e/ui/tests/logs/logsPagination.spec.ts +++ b/tests/e2e/ui/tests/logs/logsPagination.spec.ts @@ -1,8 +1,14 @@ import { test, expect, type APIRequestContext, type Locator, type Page as PlaywrightPage } from "@playwright/test"; import { ADMIN_STORAGE_PATH } from "../../constants"; -import { navigateToPage, dismissFeedbackPopup } from "../../helpers/navigation"; +import { navigateToPage, dismissFeedbackPopup, setAriaHidden } from "../../helpers/navigation"; import { Page } from "../../fixtures/pages"; -import { CHAT_MODEL_A, createVirtualKey, sendChatCompletion, waitForSpendLog } from "../../helpers/traffic"; +import { + CHAT_MODEL_A, + createVirtualKey, + deleteVirtualKey, + sendChatCompletion, + waitForSpendLog, +} from "../../helpers/traffic"; /** * Session-grouped pagination (#38060): a page of N rows must render exactly N session rows, a @@ -45,6 +51,57 @@ async function setRowsPerPage(page: PlaywrightPage, size: "25" | "50" | "100"): await page.getByRole("option", { name: size, exact: true }).click(); } +test.describe("Logs pagination with Aria in the header", () => { + test.use({ storageState: ADMIN_STORAGE_PATH }); + + for (const viewport of [ + { width: 1440, height: 900 }, + { width: 390, height: 844 }, + ]) { + test(`keeps pagination reachable and Aria available at ${viewport.width}px`, async ({ page, request }) => { + await page.setViewportSize(viewport); + const alias = `e2e-aria-header-${uniqueSuffix()}`; + const key = await createVirtualKey(request, { key_alias: alias }); + try { + const ids = await Promise.all( + Array.from({ length: 26 }, (_, index) => + sendChatCompletion(request, { + model: CHAT_MODEL_A, + prompt: `${alias}-${index}`, + apiKey: key.key, + }), + ), + ); + await Promise.all(ids.map((id) => waitForSpendLog(request, id))); + await openLogs(page); + await setAriaHidden(page, false); + await applyKeyAliasFilter(page, await openFilterDrawer(page), alias); + await setRowsPerPage(page, "25"); + await expect(visibleTestId(page, "pagination-range")).toHaveText("Showing 1-25 of 26"); + await visibleTestId(page, "pagination-next").click(); + await expect(visibleTestId(page, "pagination-range")).toHaveText("Showing 26-26 of 26"); + + const aria = page.getByRole("banner").getByRole("button", { name: "Aria Agent", exact: true }); + await aria.click(); + await expect(page.getByRole("dialog", { name: "Aria Agent", exact: true })).toBeVisible(); + await page.keyboard.press("Escape"); + await expect(aria).toBeFocused(); + await visibleTestId(page, "pagination-first").click(); + await expect(visibleTestId(page, "pagination-range")).toHaveText("Showing 1-25 of 26"); + for (const destination of [Page.Usage, Page.LlmPlayground, Page.ApiKeys]) { + await navigateToPage(page, destination); + await aria.click(); + await expect(page.getByRole("dialog", { name: "Aria Agent", exact: true })).toBeVisible(); + await page.keyboard.press("Escape"); + await expect(aria).toBeFocused(); + } + } finally { + await deleteVirtualKey(request, key.token); + } + }); + } +}); + test.describe("Logs page session-grouped pagination", () => { test.use({ storageState: ADMIN_STORAGE_PATH }); diff --git a/ui/litellm-dashboard/AGENTS.md b/ui/litellm-dashboard/AGENTS.md index bbf2e49aad7..b6fe5219022 100644 --- a/ui/litellm-dashboard/AGENTS.md +++ b/ui/litellm-dashboard/AGENTS.md @@ -30,7 +30,7 @@ Type tests are `*.test-d.ts` files run by the `types` vitest project (`npm run t -## This is NOT the Next.js you know +# This is NOT the Next.js you know This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. diff --git a/ui/litellm-dashboard/src/components/DashboardHeader.test.tsx b/ui/litellm-dashboard/src/components/DashboardHeader.test.tsx index 770315af49f..202a111ea97 100644 --- a/ui/litellm-dashboard/src/components/DashboardHeader.test.tsx +++ b/ui/litellm-dashboard/src/components/DashboardHeader.test.tsx @@ -124,7 +124,8 @@ describe("DashboardHeader breadcrumb", () => { const tools = await screen.findByRole("dialog", { name: "Gateway tools" }); expect(within(tools).getByRole("button", { name: "Switch workspace, current: AI Gateway" })).toBeInTheDocument(); expect(within(tools).getByRole("link", { name: "Docs" })).toBeInTheDocument(); - expect(within(tools).getByRole("button", { name: "Aria Agent" })).toBeInTheDocument(); + expect(within(screen.getByRole("banner")).getByRole("button", { name: "Aria Agent" })).toBeInTheDocument(); + expect(screen.getAllByRole("button", { name: "Aria Agent" })).toHaveLength(1); }); it("closes mobile tools when switching to desktop and keeps them closed when returning", async () => { diff --git a/ui/litellm-dashboard/src/components/DashboardHeader.tsx b/ui/litellm-dashboard/src/components/DashboardHeader.tsx index b31fc794f79..55f163c2495 100644 --- a/ui/litellm-dashboard/src/components/DashboardHeader.tsx +++ b/ui/litellm-dashboard/src/components/DashboardHeader.tsx @@ -73,43 +73,46 @@ export function DashboardHeader({ -
- {showWorkerSwitch && ( - <> - - - - )} +
- - - {!hideCommunityLinks && } - - - {deploymentHealth} - -
-
- - {deploymentHealth} - - - }> - - - - Gateway tools - - {showWorkerSwitch && } -
- - - - - {!hideCommunityLinks && } -
-
-
+
+ {showWorkerSwitch && ( + <> + + + + )} + + + {!hideCommunityLinks && } + + + {deploymentHealth} + +
+
+ + {deploymentHealth} + + + } + > + + + + Gateway tools + + {showWorkerSwitch && } +
+ + + + {!hideCommunityLinks && } +
+
+
+
); diff --git a/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.integration.test.tsx b/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.integration.test.tsx index f32c99bedf3..ed66f7ce2ef 100644 --- a/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.integration.test.tsx +++ b/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.integration.test.tsx @@ -240,7 +240,7 @@ describe("Aria Agent in the gateway", () => { session(role); const requests = gateway([answer("Here is your weekly spend.")]); renderWidget(); - await userEvent.setup().click(await screen.findByRole("button", { name: "Launch Aria Agent" })); + await userEvent.setup().click(await screen.findByRole("button", { name: "Aria Agent" })); const question = "How much have I spent this week?"; const suggestion = await screen.findByRole("button", { name: question }); await waitFor(() => expect(suggestion).toBeEnabled()); @@ -268,7 +268,7 @@ describe("Aria Agent in the gateway", () => { it("should disable suggested questions when no chat model is available", async () => { const requests = gateway([], { models: [] }); renderWidget(); - await userEvent.setup().click(await screen.findByRole("button", { name: "Launch Aria Agent" })); + await userEvent.setup().click(await screen.findByRole("button", { name: "Aria Agent" })); const picker = await screen.findByRole("combobox", { name: "Aria Agent model" }); expect(picker).toBeEnabled(); expect(screen.queryByText("No chat models are available for your account.")).not.toBeInTheDocument(); @@ -285,7 +285,7 @@ describe("Aria Agent in the gateway", () => { expect(requests.filter((request) => request.url.endsWith("/chat/completions"))).toHaveLength(0); }); - it("should restore launcher focus and preserve the conversation when the popup closes with Escape", async () => { + it("should restore header trigger focus and preserve the conversation when the popup closes with Escape", async () => { const requests = gateway([answer("Your current usage is $12.34."), answer("Last month was $10.")]); const user = userEvent.setup(); const backgroundAction = vi.fn(); @@ -293,7 +293,7 @@ describe("Aria Agent in the gateway", () => { return ; } renderWidget(PageAction); - await user.click(await screen.findByRole("button", { name: "Launch Aria Agent" })); + await user.click(await screen.findByRole("button", { name: "Aria Agent" })); const popup = await screen.findByRole("dialog", { name: "Aria Agent" }); expect(popup).toBeVisible(); await waitFor(() => expect(screen.getByPlaceholderText("Ask Aria…")).toBeEnabled()); @@ -306,8 +306,8 @@ describe("Aria Agent in the gateway", () => { await user.click(screen.getByPlaceholderText("Ask Aria…")); await user.keyboard("{Escape}"); await waitFor(() => expect(screen.queryByRole("dialog", { name: "Aria Agent" })).not.toBeInTheDocument()); - const launcher = screen.getByRole("button", { name: "Launch Aria Agent" }); - await waitFor(() => expect(launcher).toHaveFocus()); + const trigger = screen.getByRole("button", { name: "Aria Agent" }); + await waitFor(() => expect(trigger).toHaveFocus()); await user.keyboard("{Enter}"); expect(await screen.findByRole("dialog", { name: "Aria Agent" })).toBeVisible(); expect(screen.getByText("Your current usage is $12.34.")).toBeInTheDocument(); @@ -324,26 +324,25 @@ describe("Aria Agent in the gateway", () => { ]); await user.click(screen.getByRole("button", { name: "Close Aria Agent" })); await waitFor(() => expect(screen.queryByRole("dialog", { name: "Aria Agent" })).not.toBeInTheDocument()); - await waitFor(() => expect(screen.getByRole("button", { name: "Launch Aria Agent" })).toHaveFocus()); + await waitFor(() => expect(screen.getByRole("button", { name: "Aria Agent" })).toHaveFocus()); }); - it("should show the same product name on Home, Usage, the launcher and the popup", async () => { + it("should show the same product name on Home, Usage, the header trigger and the popup", async () => { gateway([]); const view = renderWidget(undefined, true); const home = await screen.findByRole("region", { name: "Ask Aria Agent" }); expect(within(home).getByRole("heading", { name: "Aria Agent" })).toHaveTextContent("Aria Agent"); expect(screen.getByRole("button", { name: "Aria Agent" })).toHaveTextContent("Aria Agent"); - expect(screen.getByRole("button", { name: "Launch Aria Agent" })).toHaveTextContent("Aria Agent"); view.refresh(false, true); const usage = await screen.findByRole("region", { name: "Ask Aria about usage" }); expect(within(usage).getByRole("heading", { name: "Aria Agent" })).toHaveTextContent("Aria Agent"); expect(screen.getByRole("button", { name: "Open Aria Agent" })).toHaveTextContent("Aria Agent"); - act(() => fireEvent.click(screen.getByRole("button", { name: "Launch Aria Agent" }))); + act(() => fireEvent.click(screen.getByRole("button", { name: "Aria Agent" }))); const popup = await screen.findByRole("dialog", { name: "Aria Agent" }); expect(within(popup).getByRole("heading", { name: "Aria Agent" })).toHaveTextContent("Aria Agent"); }); - it("should keep the same scoped conversation when leaving Usage and reopening the launcher", async () => { + it("should keep the same scoped conversation when leaving Usage and reopening the header trigger", async () => { session("internal_user"); const requests = gateway( [answer("Your usage is within budget."), answer("The same usage conversation continues.")], @@ -355,7 +354,7 @@ describe("Aria Agent in the gateway", () => { }, ); const view = renderWidget(undefined, false, true); - expect(screen.queryByRole("button", { name: "Launch Aria Agent" })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Aria Agent" })).not.toBeInTheDocument(); const usage = await screen.findByRole("region", { name: "Ask Aria about usage" }); await selectModel("other-chat-model"); act(() => { @@ -366,14 +365,13 @@ describe("Aria Agent in the gateway", () => { view.refresh(false, false); expect(screen.queryByRole("region", { name: "Ask Aria about usage" })).not.toBeInTheDocument(); - expect(screen.queryByRole("button", { name: "Aria Agent" })).not.toBeInTheDocument(); - const launcher = screen.getByRole("button", { name: "Launch Aria Agent" }); - expect(launcher).toHaveTextContent("Aria Agent"); - act(() => launcher.focus()); + const trigger = screen.getByRole("button", { name: "Aria Agent" }); + expect(trigger).toHaveTextContent("Aria Agent"); + act(() => trigger.focus()); await userEvent.setup().keyboard("{Enter}"); expect(await screen.findByPlaceholderText("Ask Aria…")).toHaveValue("Check my usage"); expect(screen.getByRole("combobox", { name: "Aria Agent model" })).toHaveValue("other-chat-model"); - expect(screen.queryByRole("button", { name: "Launch Aria Agent" })).not.toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Aria Agent" })).toHaveAttribute("aria-expanded", "true"); act(() => fireEvent.click(screen.getByRole("button", { name: "Send message" }))); await screen.findByText("Your usage is within budget."); act(() => { @@ -385,7 +383,7 @@ describe("Aria Agent in the gateway", () => { const restoredUsage = await screen.findByRole("region", { name: "Ask Aria about usage" }); expect(within(restoredUsage).getByRole("textbox", { name: "Message Aria Agent" })).toHaveValue("And last month?"); view.refresh(false, false); - act(() => fireEvent.click(screen.getByRole("button", { name: "Launch Aria Agent" }))); + act(() => fireEvent.click(screen.getByRole("button", { name: "Aria Agent" }))); expect(screen.getAllByRole("dialog", { name: "Aria Agent" })).toHaveLength(1); expect(screen.getByText("Your usage is within budget.")).toBeInTheDocument(); expect(screen.getByPlaceholderText("Ask Aria…")).toHaveValue("And last month?"); @@ -405,13 +403,13 @@ describe("Aria Agent in the gateway", () => { ]); }); - it("should keep the launcher unavailable without a signed-in session", async () => { + it("should keep the header trigger unavailable without a signed-in session", async () => { document.cookie = "token=; Max-Age=0; Path=/"; const requests = gateway([]); const { client } = renderWidget(); await screen.findByText("Session ready"); await waitFor(() => expect(client.isFetching()).toBe(0)); - expect(screen.queryByRole("button", { name: "Launch Aria Agent" })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Aria Agent" })).not.toBeInTheDocument(); act(() => fireEvent.keyDown(document, { key: "j", ctrlKey: true })); expect(screen.queryByRole("dialog", { name: "Aria Agent" })).not.toBeInTheDocument(); expect(requests.every((request) => request.url.endsWith("/litellm-ui-config"))).toBe(true); @@ -494,7 +492,7 @@ describe("Aria Agent in the gateway", () => { expect(await screen.findByText("Your usage totals $12.34.")).toBeInTheDocument(); expect(screen.getAllByRole("dialog", { name: "Aria Agent" })).toHaveLength(1); expect(usage).not.toBeVisible(); - if (!writable) expect(screen.queryByRole("button", { name: "Aria Agent", hidden: true })).not.toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Aria Agent" })).toHaveAttribute("aria-expanded", "true"); const read = requests.find((request) => request.url.includes("/user/daily/activity/aggregated")); expect(read).toBeDefined(); expect(new URL(read!.url).searchParams.get("user_id")).toBe(scope === "user" ? "first-admin" : null); @@ -553,7 +551,7 @@ describe("Aria Agent in the gateway", () => { await waitFor(() => expect(usageInput).toBeEnabled()); expect(usageInput).toHaveValue(""); expect(screen.queryByRole("button", { name: "Confirm change" })).not.toBeInTheDocument(); - expect(screen.queryByRole("button", { name: "Aria Agent" })).not.toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Aria Agent" })).toHaveAttribute("aria-expanded", "false"); expect(screen.queryByRole("dialog", { name: "Aria Agent" })).not.toBeInTheDocument(); act(() => send("Show my usage")); expect(await screen.findByText("Only your own usage is available.")).toBeInTheDocument(); @@ -812,7 +810,6 @@ describe("Aria Agent in the gateway", () => { await user.click(toggle); expect(toggle).toBeChecked(); expect(screen.queryByRole("button", { name: "Aria Agent" })).not.toBeInTheDocument(); - expect(screen.queryByRole("button", { name: "Launch Aria Agent" })).not.toBeInTheDocument(); expect(screen.queryByRole("region", { name: "Ask Aria Agent", hidden: true })).not.toBeInTheDocument(); view.unmount(); @@ -820,13 +817,12 @@ describe("Aria Agent in the gateway", () => { await screen.findByText("Session ready"); await waitFor(() => expect(restored.client.isFetching()).toBe(0)); expect(screen.queryByRole("button", { name: "Aria Agent" })).not.toBeInTheDocument(); - expect(screen.queryByRole("button", { name: "Launch Aria Agent" })).not.toBeInTheDocument(); await user.click(screen.getByRole("button", { name: /account menu/i })); const savedToggle = await screen.findByRole("switch", { name: "Toggle hide Aria Agent" }); expect(savedToggle).toBeChecked(); await user.click(savedToggle); expect(await screen.findByRole("button", { name: "Aria Agent" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Launch Aria Agent" })).toBeVisible(); + expect(screen.getByRole("button", { name: "Aria Agent" })).toBeVisible(); expect(await screen.findByRole("region", { name: "Ask Aria Agent" })).toBeVisible(); for (const premiumUser of [false, null, undefined, "true"]) { @@ -838,27 +834,26 @@ describe("Aria Agent in the gateway", () => { }); it.each([false, null, undefined, "true"])( - "should offer the scoped launcher without admin Home or header access when premium_user is %s", + "should offer the scoped header trigger without admin Home access when premium_user is %s", async (premiumUser) => { session("proxy_admin", "first-admin", { premium_user: premiumUser }); const requests = gateway([]); const { client } = renderWidget(undefined, true); await screen.findByText("Session ready"); await waitFor(() => expect(client.isFetching()).toBe(0)); - expect(screen.queryByRole("button", { name: "Aria Agent" })).not.toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Aria Agent" })).toBeVisible(); expect(screen.queryByRole("region", { name: "Ask Aria Agent", hidden: true })).not.toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Launch Aria Agent" })).toBeVisible(); expect(requests.every((request) => request.url.endsWith("/litellm-ui-config"))).toBe(true); act(() => fireEvent.keyDown(document, { key: "j", metaKey: true })); expect(await screen.findByRole("dialog", { name: "Aria Agent" })).toBeInTheDocument(); - expect(screen.queryByRole("button", { name: "Launch Aria Agent" })).not.toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Aria Agent" })).toHaveAttribute("aria-expanded", "true"); act(() => fireEvent.keyDown(document, { key: "j", ctrlKey: true })); expect(screen.queryByRole("dialog", { name: "Aria Agent" })).not.toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Launch Aria Agent" })).toBeVisible(); + expect(screen.getByRole("button", { name: "Aria Agent" })).toBeVisible(); }, ); - it("should discard admin state and preserve scoped launcher access when enterprise access is lost", async () => { + it("should discard admin state and preserve scoped header trigger access when enterprise access is lost", async () => { const requests = gateway([answer("Your usage remains available.")]); const view = renderWidget(); await openWidget(); @@ -866,9 +861,9 @@ describe("Aria Agent in the gateway", () => { await waitFor(() => expect(view.client.isFetching()).toBe(0)); session("proxy_admin", "first-admin", { premium_user: false }); view.refresh(); - expect(screen.queryByRole("button", { name: "Aria Agent" })).not.toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Aria Agent" })).toHaveAttribute("aria-expanded", "false"); expect(screen.queryByRole("dialog", { name: "Aria Agent" })).not.toBeInTheDocument(); - act(() => fireEvent.click(screen.getByRole("button", { name: "Launch Aria Agent" }))); + act(() => fireEvent.click(screen.getByRole("button", { name: "Aria Agent" }))); await waitFor(() => expect(screen.getByPlaceholderText("Ask Aria…")).toBeEnabled()); expect(screen.getByPlaceholderText("Ask Aria…")).toHaveValue(""); act(() => send("Show my usage")); @@ -997,17 +992,17 @@ describe("Aria Agent in the gateway", () => { }); it.each(["proxy_admin_viewer", "internal_user", "internal_user_viewer", "org_admin"])( - "should expose only scoped usage tools from the dashboard launcher to %s", + "should expose only scoped usage tools from the dashboard header trigger to %s", async (role) => { session(role); const requests = gateway([answer("Usage is available.")]); const { client } = renderWidget(undefined, true); await screen.findByText("Session ready"); await waitFor(() => expect(client.isFetching()).toBe(0)); - expect(screen.queryByRole("button", { name: "Aria Agent" })).not.toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Aria Agent" })).toBeVisible(); expect(screen.queryByRole("region", { name: "Ask Aria Agent", hidden: true })).not.toBeInTheDocument(); expect(requests.every((request) => request.url.endsWith("/litellm-ui-config"))).toBe(true); - act(() => fireEvent.click(screen.getByRole("button", { name: "Launch Aria Agent" }))); + act(() => fireEvent.click(screen.getByRole("button", { name: "Aria Agent" }))); await waitFor(() => expect(screen.getByPlaceholderText("Ask Aria…")).toBeEnabled()); act(() => send("Show my usage")); await screen.findByText("Usage is available."); diff --git a/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.tsx b/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.tsx index 66c1ffbc9b7..02194aabe7b 100644 --- a/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.tsx +++ b/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.tsx @@ -45,7 +45,7 @@ type LiteAdminState = { toggle: () => void; show: () => void; access: LiteAdminSession["access"]; - launcherRef: RefObject; + triggerRef: RefObject; homeContainer: HTMLDivElement | null; setHomeContainer: (container: HTMLDivElement | null) => void; usageContainer: HTMLDivElement | null; @@ -78,12 +78,11 @@ function useLiteAdminSession() { }; } -/** Keeps one Aria conversation available from the dashboard and its floating launcher. */ export function LiteAdminFrame({ children }: { children: ReactNode }) { const configured = useLiteAdminSession(); const sessionKey = configured?.key ?? null; const [open, setOpen] = useState(false); - const launcherRef = useRef(null); + const triggerRef = useRef(null); const [started, setStarted] = useState(false); const [homeContainer, setHomeContainer] = useState(null); const [usageContainer, updateUsageContainer] = useState(null); @@ -128,7 +127,7 @@ export function LiteAdminFrame({ children }: { children: ReactNode }) { toggle, show, access: configured.session.access, - launcherRef, + triggerRef, homeContainer, setHomeContainer, usageContainer, @@ -138,7 +137,6 @@ export function LiteAdminFrame({ children }: { children: ReactNode }) { } > {children} - {configured && !open && } {configured && showPanel && ( setOpen(false)} /> )} @@ -146,23 +144,6 @@ export function LiteAdminFrame({ children }: { children: ReactNode }) { ); } -function AriaLauncher({ show, buttonRef }: { show: () => void; buttonRef: RefObject }) { - return ( - - ); -} - export function LiteAdminHome() { const state = useContext(LiteAdminContext); if (!state || state.access.mode !== "admin") return null; @@ -189,18 +170,20 @@ export function LiteAdminHome() { export default function LiteAdminTrigger() { const state = useContext(LiteAdminContext); - if (!state || state.access.mode !== "admin") return null; + if (!state) return null; + const { triggerRef, open, toggle } = state; return ( ); @@ -358,7 +341,7 @@ function Panel({ initialFocus={() => initialFocus?.current ?? ref.current?.querySelector("textarea:enabled") ?? ref.current } - finalFocus={state?.launcherRef} + finalFocus={state?.triggerRef} className="flex h-[min(48rem,calc(100dvh-2rem))] flex-col gap-0 overflow-hidden p-0 shadow-xl sm:max-w-4xl" > {children}