mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-28 06:23:07 +00:00
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:
parent
ccd783d907
commit
dbd55d3202
8 changed files with 161 additions and 111 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue