Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions tests/e2e/ui/tests/logs/logs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,75 @@ test.describe("Logs page", () => {
permissions: ["clipboard-read", "clipboard-write"],
});

test("log tables fill the available height and empty requests stay centered after resizing", async ({
page,
}) => {
await navigateToPage(page, Page.Logs);
await dismissFeedbackPopup(page);
await visibleTestId(page, "datatable-search").fill(
`missing-request-${uniqueSuffix()}`,
);
const emptyTitle = page.getByText("No matching requests", { exact: true });
await expect(emptyTitle).toBeVisible();

for (const viewport of [
{ width: 1440, height: 900 },
{ width: 1024, height: 720 },
]) {
await page.setViewportSize(viewport);
await expect
.poll(async () => {
const frame = await visibleTestId(
page,
"data-table-frame",
).boundingBox();
return frame
? Math.abs(viewport.height - frame.y - frame.height - 24)
: Infinity;
})
.toBeLessThanOrEqual(2);
await expect
.poll(async () => {
const body = await page
.locator("table")
.filter({ visible: true })
.first()
.locator("tbody")
.boundingBox();
const scroller = await visibleTestId(
page,
"data-table-scroller",
).boundingBox();
const message = await emptyTitle.locator("..").boundingBox();
if (!body || !message || !scroller) return Infinity;
return Math.max(
Math.abs(
message.x + message.width / 2 - scroller.x - scroller.width / 2,
),
Math.abs(message.y + message.height / 2 - body.y - body.height / 2),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Centering check misses visible bounds

The vertical check compares the message with the whole tbody, not the area users can see. An overflowing body can pass even when the message is off-center on screen. Compare against the intersection of the body and scroller bounds instead

);
})
.toBeLessThanOrEqual(4);
for (const tab of ["Deleted Keys", "Deleted Teams"]) {
await page.getByRole("tab", { name: tab, exact: true }).click();
await expect
.poll(async () => {
const frame = await visibleTestId(
page,
"data-table-frame",
).boundingBox();
return frame
? Math.abs(viewport.height - frame.y - frame.height - 24)
: Infinity;
})
.toBeLessThanOrEqual(2);
}
await page
.getByRole("tab", { name: "Request Logs", exact: true })
.click();
}
});

test("a chat sent from the Playground lands in Logs with its content", async ({ page, request }) => {
const prompt = `logs-playground-prompt-${uniqueSuffix()}`;
await openPlayground(page);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Page, PageContent } from "@/components/shared/Page";
import { AccessGroupResponse, useAccessGroups } from "@/app/(dashboard)/hooks/accessGroups/useAccessGroups";
import { useDeleteAccessGroup } from "@/app/(dashboard)/hooks/accessGroups/useDeleteAccessGroup";
import { Boxes, Plus, SearchIcon, X } from "lucide-react";
import { useMemo, useState } from "react";
import DeleteResourceModal from "@/components/common_components/DeleteResourceModal";
import { PageHeader } from "@/components/shared/PageHeader";
import { PageHeader, PageHeaderControls, PageHeaderDescription, PageHeaderTitle } from "@/components/shared/PageHeader";
import { Button } from "@/components/ui/button";
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group";
import { AccessGroupDetail } from "./AccessGroupsDetailsPage";
Expand Down Expand Up @@ -59,49 +60,53 @@ export function AccessGroupsPage() {
}

return (
<div className="p-8">
<PageHeader
icon={<Boxes />}
title="Access Groups"
subtitle="Manage resource permissions for your organization"
primaryAction={
canModify ? (
<Page>
<PageHeader>
<PageHeaderTitle>
<Boxes />
Access Groups
</PageHeaderTitle>
<PageHeaderDescription>Manage resource permissions for your organization</PageHeaderDescription>
{canModify && (
<PageHeaderControls>
<Button onClick={() => setIsCreateModalVisible(true)}>
<Plus className="size-4" />
Create Access Group
</Button>
) : undefined
}
/>
</PageHeaderControls>
)}
</PageHeader>

<div className="mt-6 mb-3 flex items-center">
<InputGroup className="max-w-[400px]">
<InputGroupAddon>
<SearchIcon className="size-4 text-muted-foreground" />
</InputGroupAddon>
<InputGroupInput
placeholder="Search groups by name, ID, or description..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
{searchText && (
<InputGroupAddon align="inline-end">
<InputGroupButton size="icon-xs" aria-label="Clear search" onClick={() => setSearchText("")}>
<X />
</InputGroupButton>
<PageContent className="gap-3">
<div className="flex items-center">
<InputGroup className="max-w-[400px]">
<InputGroupAddon>
<SearchIcon className="size-4 text-muted-foreground" />
</InputGroupAddon>
)}
</InputGroup>
</div>
<InputGroupInput
placeholder="Search groups by name, ID, or description..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
{searchText && (
<InputGroupAddon align="inline-end">
<InputGroupButton size="icon-xs" aria-label="Clear search" onClick={() => setSearchText("")}>
<X />
</InputGroupButton>
</InputGroupAddon>
)}
</InputGroup>
</div>

<AccessGroupsTable
groups={filteredGroups}
isLoading={isLoading}
isFiltered={searchText.trim().length > 0}
canModify={canModify}
onGroupClick={setSelectedGroupId}
onDeleteClick={setGroupToDelete}
/>
<AccessGroupsTable
groups={filteredGroups}
isLoading={isLoading}
isFiltered={searchText.trim().length > 0}
canModify={canModify}
onGroupClick={setSelectedGroupId}
onDeleteClick={setGroupToDelete}
/>
</PageContent>

<AccessGroupCreateDialog open={isCreateModalVisible} onOpenChange={setIsCreateModalVisible} />

Expand All @@ -126,6 +131,6 @@ export function AccessGroupsPage() {
}}
confirmLoading={deleteMutation.isPending}
/>
</div>
</Page>
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { Page } from "@/components/shared/Page";
import { teamListCall as v2TeamListCall } from "@/app/(dashboard)/hooks/teams/useTeams";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { KeyResponse, Team } from "@/components/key_team_helpers/key_list";
Expand All @@ -25,7 +26,7 @@
const modelsParam = searchParams.get("models");
const keyType = searchParams.get("key_type");

if (!ownedBy && !teamId && !keyAlias && !modelsParam && !keyType) {

Check warning on line 29 in ui/litellm-dashboard/src/app/(dashboard)/api-keys/ApiKeysDashboard.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Boolean expression combines 5 conditions; extract it into a named variable
return undefined;
}

Expand Down Expand Up @@ -71,7 +72,7 @@
}, [accessToken, userID, userRole]);

return (
<main className="flex h-full flex-col p-8">
<Page className="h-full">
<VirtualKeysTable
headerActions={
isViewOnly ? undefined : (
Expand All @@ -86,6 +87,6 @@
)
}
/>
</main>
</Page>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
*
*/

import { Page, PageTabs, PageTabsList, PageTabsTrigger } from "@/components/shared/Page";
import { Plus, Wallet } from "lucide-react";
import React, { useCallback, useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { prism } from "react-syntax-highlighter/dist/esm/styles/prism";

import { useSyntaxTheme } from "@/hooks/useSyntaxTheme";
import { PageHeader } from "@/components/shared/PageHeader";
import { PageHeader, PageHeaderControls, PageHeaderDescription, PageHeaderTitle } from "@/components/shared/PageHeader";
import { ToolbarSeparator } from "@/components/shared/ToolbarSeparator";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import DeleteResourceModal from "@/components/common_components/DeleteResourceModal";
Expand Down Expand Up @@ -78,35 +80,30 @@ const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
};

return (
<main className="flex h-full flex-col p-8">
<Tabs defaultValue="budgets" className="min-h-0 flex-1 gap-6">
<PageHeader
icon={<Wallet />}
title="Budgets"
subtitle="Spend, TPM and RPM limits you can assign to customers."
primaryAction={
canModify ? (
<Button onClick={() => setIsCreateModelVisible(true)}>
<Plus className="size-4" />
Create Budget
</Button>
) : undefined
}
tabs={({ leadingControls }) => (
<TabsList
variant="line"
className="gap-0 p-0 [&>[data-slot=tabs-trigger]+[data-slot=tabs-trigger]]:ml-[22px]"
>
{leadingControls}
<TabsTrigger value="budgets" className="flex-none px-0 py-[7px] data-active:font-semibold">
Budgets
</TabsTrigger>
<TabsTrigger value="examples" className="flex-none px-0 py-[7px] data-active:font-semibold">
Examples
</TabsTrigger>
</TabsList>
)}
/>
<Page className="h-full">
<PageTabs defaultValue="budgets">
<PageHeader>
<PageHeaderTitle>
<Wallet />
Budgets
</PageHeaderTitle>
<PageHeaderDescription>Spend, TPM and RPM limits you can assign to customers.</PageHeaderDescription>
<PageHeaderControls>
<PageTabsList>
{canModify && (
<>
<Button onClick={() => setIsCreateModelVisible(true)}>
<Plus className="size-4" />
Create Budget
</Button>
<ToolbarSeparator className="mx-0 h-6" />
</>
)}
<PageTabsTrigger value="budgets">Budgets</PageTabsTrigger>
<PageTabsTrigger value="examples">Examples</PageTabsTrigger>
</PageTabsList>
</PageHeaderControls>
</PageHeader>
<TabsContent value="budgets" className="flex min-h-0 flex-1 flex-col" keepMounted>
<div className="flex min-h-0 flex-1 flex-col">
<BudgetModal isModalVisible={isCreateModelVisible} setIsModalVisible={setIsCreateModelVisible} />
Expand Down Expand Up @@ -174,8 +171,8 @@ const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
</Tabs>
</div>
</TabsContent>
</Tabs>
</main>
</PageTabs>
</Page>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"use client";

import { Page, PageTabs, PageTabsList, PageTabsTrigger } from "@/components/shared/Page";
import React from "react";
import { Info, PiggyBank } from "lucide-react";

import useCan from "@/app/(dashboard)/hooks/useCan";
import { Alert, AlertDescription } from "@/components/shared/Alert";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { PageHeader } from "@/components/shared/PageHeader";
import { TabsContent } from "@/components/ui/tabs";
import { PageHeader, PageHeaderControls, PageHeaderDescription, PageHeaderTitle } from "@/components/shared/PageHeader";
import UsageTab from "./UsageTab";
import PromptCompressionTab from "./PromptCompressionTab";
import PromptCachingTab from "./PromptCachingTab";
Expand All @@ -33,37 +34,30 @@ const CostOptimizationView: React.FC<CostOptimizationViewProps> = ({ accessToken
};

return (
<main className="w-full p-8">
<Tabs defaultValue="usage" onValueChange={handleTabChange} className="gap-6">
<PageHeader
icon={<PiggyBank />}
title="Cost Optimization"
subtitle="Track and configure the mechanisms that save you money: prompt compression and prompt caching. Auto routers live under Models + Endpoints, on the Auto-Routers tab"
tabs={({ leadingControls }) => (
<TabsList
variant="line"
className="gap-0 p-0 [&>[data-slot=tabs-trigger]+[data-slot=tabs-trigger]]:ml-[22px]"
>
{leadingControls}
<TabsTrigger value="usage" className="flex-none px-0 py-[7px] data-active:font-semibold">
Overall
</TabsTrigger>
<Page>
<PageTabs defaultValue="usage" onValueChange={handleTabChange}>
<PageHeader>
<PageHeaderTitle>
<PiggyBank />
Cost Optimization
</PageHeaderTitle>
<PageHeaderDescription>
Track and configure the mechanisms that save you money: prompt compression and prompt caching. Auto routers
live under Models + Endpoints, on the Auto-Routers tab
</PageHeaderDescription>
<PageHeaderControls>
<PageTabsList>
<PageTabsTrigger value="usage">Overall</PageTabsTrigger>
{canViewProxyWideCostData && (
<>
<TabsTrigger value="compression" className="flex-none px-0 py-[7px] data-active:font-semibold">
Prompt Compression
</TabsTrigger>
<TabsTrigger value="caching" className="flex-none px-0 py-[7px] data-active:font-semibold">
Prompt Caching
</TabsTrigger>
<TabsTrigger value="autorouter-usage" className="flex-none px-0 py-[7px] data-active:font-semibold">
Auto-Router
</TabsTrigger>
<PageTabsTrigger value="compression">Prompt Compression</PageTabsTrigger>
<PageTabsTrigger value="caching">Prompt Caching</PageTabsTrigger>
<PageTabsTrigger value="autorouter-usage">Auto-Router</PageTabsTrigger>
</>
)}
</TabsList>
)}
/>
</PageTabsList>
</PageHeaderControls>
</PageHeader>

<div
role="alert"
Expand Down Expand Up @@ -108,8 +102,8 @@ const CostOptimizationView: React.FC<CostOptimizationViewProps> = ({ accessToken
</TabsContent>
</>
)}
</Tabs>
</main>
</PageTabs>
</Page>
);
};

Expand Down
Loading
Loading