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
This commit is contained in:
moe-berri 2026-10-26 14:55:08 -07:00 • committed by GitHub
parent ccd783d907
commit dbd55d3202
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 161 additions and 111 deletions

View file

@ -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

View file

@ -62,18 +62,29 @@ export async function dismissFeedbackPopup(page: PlaywrightPage): Promise<void>
}
}
export async function hideLiteAdmin(page: PlaywrightPage): Promise<void> {
export async function setAriaHidden(page: PlaywrightPage, hidden: boolean): Promise<void> {
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();
}
}
/**

View file

@ -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 });

View file

@ -30,7 +30,7 @@ Type tests are `*.test-d.ts` files run by the `types` vitest project (`npm run t
<!-- BEGIN:nextjs-agent-rules -->
## 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.

View file

@ -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 () => {

View file

@ -73,43 +73,46 @@ export function DashboardHeader({
<CommandPaletteTrigger />
<div className="flex flex-none items-center gap-1 max-md:hidden">
{showWorkerSwitch && (
<>
<WorkerDropdown onWorkerSwitch={handleWorkerSwitch} />
<ToolbarSeparator />
</>
)}
<div className="flex shrink-0 items-center gap-1">
<LiteAdmin />
<DocsLink />
<BlogDropdown />
{!hideCommunityLinks && <CommunityEngagementButtons />}
<ToolbarSeparator />
<ThemeToggle />
{deploymentHealth}
<NotificationsBell accessToken={accessToken} />
</div>
<div className="flex shrink-0 items-center gap-1 md:hidden">
<CommandPaletteTrigger mobile />
{deploymentHealth}
<NotificationsBell accessToken={accessToken} />
<Popover open={!isDesktop && mobileToolsOpen} onOpenChange={setMobileToolsOpen}>
<PopoverTrigger render={<Button variant="ghost" size="icon" className="size-11" aria-label="More options" />}>
<Ellipsis />
</PopoverTrigger>
<PopoverContent align="end" className="max-w-[calc(100vw-2rem)]">
<PopoverTitle>Gateway tools</PopoverTitle>
<ViewSwitcher />
{showWorkerSwitch && <WorkerDropdown onWorkerSwitch={handleWorkerSwitch} />}
<div className="flex flex-wrap items-center gap-2">
<LiteAdmin />
<ThemeToggle />
<DocsLink />
<BlogDropdown />
{!hideCommunityLinks && <CommunityEngagementButtons />}
</div>
</PopoverContent>
</Popover>
<div className="flex flex-none items-center gap-1 max-md:hidden">
{showWorkerSwitch && (
<>
<WorkerDropdown onWorkerSwitch={handleWorkerSwitch} />
<ToolbarSeparator />
</>
)}
<DocsLink />
<BlogDropdown />
{!hideCommunityLinks && <CommunityEngagementButtons />}
<ToolbarSeparator />
<ThemeToggle />
{deploymentHealth}
<NotificationsBell accessToken={accessToken} />
</div>
<div className="flex shrink-0 items-center gap-1 md:hidden">
<CommandPaletteTrigger mobile />
{deploymentHealth}
<NotificationsBell accessToken={accessToken} />
<Popover open={!isDesktop && mobileToolsOpen} onOpenChange={setMobileToolsOpen}>
<PopoverTrigger
render={<Button variant="ghost" size="icon" className="size-11" aria-label="More options" />}
>
<Ellipsis />
</PopoverTrigger>
<PopoverContent align="end" className="max-w-[calc(100vw-2rem)]">
<PopoverTitle>Gateway tools</PopoverTitle>
<ViewSwitcher />
{showWorkerSwitch && <WorkerDropdown onWorkerSwitch={handleWorkerSwitch} />}
<div className="flex flex-wrap items-center gap-2">
<ThemeToggle />
<DocsLink />
<BlogDropdown />
{!hideCommunityLinks && <CommunityEngagementButtons />}
</div>
</PopoverContent>
</Popover>
</div>
</div>
</header>
);

View file

@ -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 <button onClick={backgroundAction}>Background page action</button>;
}
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.");

View file

@ -45,7 +45,7 @@ type LiteAdminState = {
toggle: () => void;
show: () => void;
access: LiteAdminSession["access"];
launcherRef: RefObject<HTMLButtonElement | null>;
triggerRef: RefObject<HTMLButtonElement | null>;
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<HTMLButtonElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
const [started, setStarted] = useState(false);
const [homeContainer, setHomeContainer] = useState<HTMLDivElement | null>(null);
const [usageContainer, updateUsageContainer] = useState<HTMLDivElement | null>(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 && <AriaLauncher show={show} buttonRef={launcherRef} />}
{configured && showPanel && (
<AriaPopup key={configured.key} session={configured.session} open={open} close={() => setOpen(false)} />
)}
@ -146,23 +144,6 @@ export function LiteAdminFrame({ children }: { children: ReactNode }) {
);
}
function AriaLauncher({ show, buttonRef }: { show: () => void; buttonRef: RefObject<HTMLButtonElement | null> }) {
return (
<Button
ref={buttonRef}
variant="outline"
aria-label={`Launch ${ARIA_AGENT_LABEL}`}
aria-keyshortcuts="Meta+J Control+J"
aria-expanded={false}
onClick={show}
className="fixed bottom-4 right-4 z-floating h-11 gap-2 rounded-full border-blue-200 bg-background pl-2 pr-4 text-blue-700 shadow-md hover:bg-blue-50 hover:text-blue-800 dark:border-blue-400/30 dark:text-blue-300 dark:hover:bg-blue-400/10"
>
<AriaAvatar />
<span>{ARIA_AGENT_LABEL}</span>
</Button>
);
}
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 (
<Button
variant="ghost"
ref={triggerRef}
aria-label={ARIA_AGENT_LABEL}
aria-keyshortcuts="Meta+J Control+J"
aria-expanded={state.open}
onClick={state.toggle}
aria-expanded={open}
onClick={toggle}
className="gap-2 rounded-full border border-blue-200/70 bg-blue-50/80 pl-1.5 pr-3 text-blue-700 hover:bg-blue-100 hover:text-blue-800 aria-expanded:bg-blue-100 dark:border-blue-400/20 dark:bg-blue-400/10 dark:text-blue-300 dark:hover:bg-blue-400/20"
>
<AriaAvatar />
<span className="md:hidden lg:inline">{ARIA_AGENT_LABEL}</span>
<span className="hidden lg:inline">{ARIA_AGENT_LABEL}</span>
<kbd className="hidden rounded-full border border-info/30 px-1.5 font-sans text-xs lg:inline">⌘J</kbd>
</Button>
);
@ -358,7 +341,7 @@ function Panel({
initialFocus={() =>
initialFocus?.current ?? ref.current?.querySelector<HTMLElement>("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}