= ({ accessToken
>
)}
-
-
+
+
);
};
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsMonitorView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsMonitorView.tsx
index f90a46e19e4..1dd6686d7fe 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsMonitorView.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsMonitorView.tsx
@@ -1,3 +1,4 @@
+import { Page } from "@/components/shared/Page";
import type { DateRangePickerValue } from "@/components/shared/date_picker_types";
import { parseAsString, useQueryState } from "nuqs";
import React, { useCallback, useMemo, useState } from "react";
@@ -48,7 +49,7 @@ export default function GuardrailsMonitorView({ accessToken = null }: Guardrails
);
return (
-
+
{!selectedGuardrailId ? (
>
)}
-
+
);
}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.tsx
index 468e6967d81..5627e7fc3cb 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.tsx
@@ -18,7 +18,7 @@ import {
type UsageUnits,
} from "@/components/GuardrailsMonitor/usageUnits";
import { Button } from "@/components/ui/button";
-import { PageHeader } from "@/components/shared/PageHeader";
+import { PageHeader, PageHeaderControls, PageHeaderDescription, PageHeaderTitle } from "@/components/shared/PageHeader";
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { EvaluationSettingsModal } from "./EvaluationSettingsModal";
import { MetricCard } from "@/components/GuardrailsMonitor/MetricCard";
@@ -282,20 +282,20 @@ export function GuardrailsOverview({
return (
-
}
- title="Guardrails Monitor"
- subtitle="Monitor guardrail performance across all requests"
- utilities={
- <>
- {dateRangeControl}
-
- >
- }
- />
+
+
+
+ Guardrails Monitor
+
+ Monitor guardrail performance across all requests
+
+ {dateRangeControl}
+
+
+
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx
index 72f26919060..9d8718cb84a 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx
@@ -149,7 +149,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
- {children}
+ {children}
{!isPlayground && }
diff --git a/ui/litellm-dashboard/src/components/view_logs/index.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.integration.test.tsx
similarity index 90%
rename from ui/litellm-dashboard/src/components/view_logs/index.integration.test.tsx
rename to ui/litellm-dashboard/src/app/(dashboard)/logs/page.integration.test.tsx
index f2d70b74c96..dc8a57e50ba 100644
--- a/ui/litellm-dashboard/src/components/view_logs/index.integration.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.integration.test.tsx
@@ -1,8 +1,8 @@
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
-import SpendLogsTable from "./index";
-import { renderWithProviders, testQueryClient } from "../../../tests/test-utils";
+import LogsPage from "./page";
+import { renderWithProviders, testQueryClient } from "../../../../tests/test-utils";
const { useAuthorizedMock, useOrganizationsMock } = vi.hoisted(() => ({
useAuthorizedMock: vi.fn(),
@@ -17,7 +17,7 @@ vi.mock("@/app/(dashboard)/hooks/organizations/useOrganizations", () => ({
useOrganizations: useOrganizationsMock,
}));
-vi.mock("./RequestLogsPanel", () => ({
+vi.mock("@/components/view_logs/RequestLogsPanel", () => ({
default: function RequestLogsPanelMock() {
return
;
},
@@ -47,17 +47,13 @@ const defaultProps = {
const ORG_ADMIN_MEMBERSHIPS = [{ organization_id: "org-1", members: [{ user_id: "user-1", user_role: "org_admin" }] }];
const renderAs = (sessionRole: string, organizations: unknown[] = []) => {
- useAuthorizedMock.mockReturnValue({
- accessToken: "sk-test",
- userId: "user-1",
- userRole: sessionRole,
- premiumUser: true,
- });
+ const session = { ...defaultProps, userId: defaultProps.userID, userRole: sessionRole };
+ useAuthorizedMock.mockReturnValue(session);
useOrganizationsMock.mockReturnValue({ data: organizations });
- return renderWithProviders(
);
+ return renderWithProviders(
);
};
-describe("SpendLogsTable network access by role", () => {
+describe("LogsPage network access by role", () => {
beforeEach(() => {
testQueryClient.clear();
vi.clearAllMocks();
diff --git a/ui/litellm-dashboard/src/components/view_logs/index.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.test.tsx
similarity index 88%
rename from ui/litellm-dashboard/src/components/view_logs/index.test.tsx
rename to ui/litellm-dashboard/src/app/(dashboard)/logs/page.test.tsx
index 7c8c14771bd..c7e0a82a363 100644
--- a/ui/litellm-dashboard/src/components/view_logs/index.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.test.tsx
@@ -1,8 +1,8 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
-import SpendLogsTable from "./index";
-import { renderWithProviders } from "../../../tests/test-utils";
+import LogsPage from "./page";
+import { renderWithProviders } from "../../../../tests/test-utils";
const { useAuthorizedMock, useOrganizationsMock } = vi.hoisted(() => ({
useAuthorizedMock: vi.fn(),
@@ -17,25 +17,25 @@ vi.mock("@/app/(dashboard)/hooks/organizations/useOrganizations", () => ({
useOrganizations: useOrganizationsMock,
}));
-vi.mock("./RequestLogsPanel", () => ({
+vi.mock("@/components/view_logs/RequestLogsPanel", () => ({
default: function RequestLogsPanelMock({ isActive }: { isActive: boolean }) {
return
{isActive ? "active" : "inactive"}
;
},
}));
-vi.mock("./AuditLogsPanel", () => ({
+vi.mock("@/components/view_logs/AuditLogsPanel", () => ({
default: function AuditLogsPanelMock({ isActive }: { isActive: boolean }) {
return
{isActive ? "active" : "inactive"}
;
},
}));
-vi.mock("../DeletedKeysPage/DeletedKeysPage", () => ({
+vi.mock("@/components/DeletedKeysPage/DeletedKeysPage", () => ({
default: function DeletedKeysPageMock() {
return
;
},
}));
-vi.mock("../DeletedTeamsPage/DeletedTeamsPage", () => ({
+vi.mock("@/components/DeletedTeamsPage/DeletedTeamsPage", () => ({
default: function DeletedTeamsPageMock() {
return
;
},
@@ -52,16 +52,16 @@ const defaultProps = {
const ORG_ADMIN_MEMBERSHIPS = [{ organization_id: "org-1", members: [{ user_id: "user-1", user_role: "org_admin" }] }];
const renderAs = (sessionRole: string, organizations: unknown[] = []) => {
- useAuthorizedMock.mockReturnValue({ userId: "user-1", userRole: sessionRole });
+ useAuthorizedMock.mockReturnValue({ ...defaultProps, userId: defaultProps.userID, userRole: sessionRole });
useOrganizationsMock.mockReturnValue({ data: organizations });
- return renderWithProviders(
);
+ return renderWithProviders(
);
};
const tabNames = () => screen.getAllByRole("tab").map((tab) => tab.textContent);
-describe("SpendLogsTable", () => {
+describe("LogsPage", () => {
beforeEach(() => {
- useAuthorizedMock.mockReturnValue({ userId: "user-1", userRole: "Admin" });
+ useAuthorizedMock.mockReturnValue({ ...defaultProps, userId: defaultProps.userID });
useOrganizationsMock.mockReturnValue({ data: [] });
});
@@ -182,17 +182,17 @@ describe("SpendLogsTable", () => {
describe("auth-not-ready guard", () => {
it("shows a loading spinner when credentials are not yet resolved", () => {
- useAuthorizedMock.mockReturnValue({ userRole: "Admin" });
- renderWithProviders(
);
+ useAuthorizedMock.mockReturnValue({ ...defaultProps, userId: defaultProps.userID, accessToken: null });
+ renderWithProviders(
);
- expect(document.querySelector('[aria-busy="true"]')).toBeInTheDocument();
+ expect(screen.getByRole("status", { name: "Loading" })).toHaveAttribute("aria-busy", "true");
expect(screen.queryByRole("tab", { name: "Request Logs" })).not.toBeInTheDocument();
});
it("renders the tabs (no spinner) once all credentials are present", () => {
renderAs("Admin");
- expect(document.querySelector('[aria-busy="true"]')).not.toBeInTheDocument();
+ expect(screen.queryByRole("status", { name: "Loading" })).not.toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Request Logs" })).toBeInTheDocument();
});
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx
index 88909e3b87f..51603dec29e 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx
@@ -1,17 +1,74 @@
"use client";
-import SpendLogsTable from "@/components/view_logs";
+import { useState } from "react";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
+import useCan from "@/app/(dashboard)/hooks/useCan";
+import DeletedKeysPage from "@/components/DeletedKeysPage/DeletedKeysPage";
+import DeletedTeamsPage from "@/components/DeletedTeamsPage/DeletedTeamsPage";
+import AuditLogsPanel from "@/components/view_logs/AuditLogsPanel";
+import RequestLogsPanel from "@/components/view_logs/RequestLogsPanel";
+import { Page, PageTabs, PageTabsList, PageTabsTrigger, PageTabsContent } from "@/components/shared/Page";
+import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
-export default function Logs() {
+type LogsTab = "request logs" | "audit logs" | "deleted keys" | "deleted teams";
+
+export default function LogsPage() {
const { accessToken, userRole, userId, token, premiumUser } = useAuthorized();
+ const [activeTab, setActiveTab] = useState
("request logs");
+ const canViewAuditLogs = useCan("viewAuditLogs");
+ const canViewDeletedTeams = useCan("viewDeletedTeams");
+
+ const credentialsPending = !accessToken || !token;
+ const identityPending = !userRole || !userId;
+
+ if (credentialsPending || identityPending) {
+ return (
+
+
+
+ );
+ }
+
return (
-
+
+ setActiveTab(value)}>
+
+ Request Logs
+ {canViewAuditLogs && Audit Logs}
+ Deleted Keys
+ {canViewDeletedTeams && Deleted Teams}
+
+
+
+
+
+ {canViewAuditLogs && (
+
+
+
+ )}
+
+
+
+ {canViewDeletedTeams && (
+
+
+
+ )}
+
+
);
}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx
index c946e8aabe0..ab406597589 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx
@@ -1,5 +1,6 @@
"use client";
+import { Page } from "@/components/shared/Page";
import React from "react";
import { Bar, BarChart, CartesianGrid, Treemap, XAxis, YAxis } from "recharts";
import { ArrowDownRight, ArrowUpRight, BarChart3, Layers, Minus } from "lucide-react";
@@ -7,7 +8,7 @@ import { ArrowDownRight, ArrowUpRight, BarChart3, Layers, Minus } from "lucide-r
import { apiClient } from "@/components/networking";
import { extractErrorMessage } from "@/utils/errorUtils";
import { ProviderLogo } from "@/components/molecules/models/ProviderLogo";
-import { PageHeader } from "@/components/shared/PageHeader";
+import { PageHeader, PageHeaderDescription, PageHeaderTitle } from "@/components/shared/PageHeader";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
@@ -210,12 +211,16 @@ export default function ModelInsightsView({ accessToken }: { accessToken: string
) satisfies ChartConfig;
return (
-
- }
- title="Model Leaderboard"
- subtitle={`See which models your gateway used from ${data.start_date} through ${data.end_date}`}
- />
+
+
+
+
+ Model Leaderboard
+
+
+ See which models your gateway used from {data.start_date} through {data.end_date}
+
+
@@ -375,6 +380,6 @@ export default function ModelInsightsView({ accessToken }: { accessToken: string
-
+
);
}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.tsx
index 2d3c1acf75e..cb595916041 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.tsx
@@ -1,9 +1,10 @@
+import { Page, PageContent } from "@/components/shared/Page";
import { useProjects } from "@/app/(dashboard)/hooks/projects/useProjects";
import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
import { Folder, Plus, SearchIcon, X } from "lucide-react";
import { parseAsString, useQueryState } from "nuqs";
import { useMemo, useState } from "react";
-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 { CreateProjectModal } from "./ProjectModals/CreateProjectModal";
@@ -56,49 +57,53 @@ export function ProjectsPage() {
}
return (
-
-
}
- title="Projects"
- subtitle="Manage projects within your teams"
- primaryAction={
+
+
+
+
+ Projects
+
+ Manage projects within your teams
+
- }
- />
+
+
-
-
-
-
-
- setSearchText(e.target.value)}
- />
- {searchText && (
-
- setSearchText("")}>
-
-
+
+
+
+
+
- )}
-
-
+ setSearchText(e.target.value)}
+ />
+ {searchText && (
+
+ setSearchText("")}>
+
+
+
+ )}
+
+
- 0}
- onProjectClick={(id) => void setSelectedProjectId(id)}
- teamAliasMap={teamAliasMap}
- isTeamsLoading={isTeamsLoading}
- />
+ 0}
+ onProjectClick={(id) => void setSelectedProjectId(id)}
+ teamAliasMap={teamAliasMap}
+ isTeamsLoading={isTeamsLoading}
+ />
+
setIsCreateModalVisible(false)} />
-
+
);
}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROICalculatorView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROICalculatorView.tsx
index 40c1104354d..2b17d2019ee 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROICalculatorView.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/roi-calculator/_components/ROICalculatorView.tsx
@@ -1,11 +1,12 @@
"use client";
+import { Page } from "@/components/shared/Page";
import React from "react";
import { Calculator, RefreshCw } from "lucide-react";
import { apiClient } from "@/components/networking";
-import { PageHeader } from "@/components/shared/PageHeader";
import { DemoNotice } from "@/components/shared/DemoNotice";
+import { PageHeader, PageHeaderDescription, PageHeaderTitle } from "@/components/shared/PageHeader";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
@@ -219,33 +220,33 @@ export default function ROICalculatorView({
const syncedAt = syncIsUpToDate ? summary?.synced_at : null;
return (
-
- }
- title="ROI Calculator"
- titleAction={
- !liveSummary && showLiveStatus ? (
+
+
+
+
+
+ ROI Calculator
+
+ {!liveSummary && showLiveStatus && (
- ) : undefined
- }
- subtitle={
- <>
- {summary
- ? `${summary.start} through ${summary.end} · UTC`
- : "Compare gateway spend with estimated engineering effort for merged pull requests"}
- {syncedAt && (
-
- Last synced {formatSyncedAt(syncedAt)}
- {!status.running && status.phase === "complete" && status.reused > 0
- ? ` · ${status.reused} of ${status.total} estimates reused`
- : ""}
-
- )}
- >
- }
- />
+ )}
+
+
+ {summary
+ ? `${summary.start} through ${summary.end} · UTC`
+ : "Compare gateway spend with estimated engineering effort for merged pull requests"}
+ {syncedAt && (
+
+ Last synced {formatSyncedAt(syncedAt)}
+ {!status.running && status.phase === "complete" && status.reused > 0
+ ? ` · ${status.reused} of ${status.total} estimates reused`
+ : ""}
+
+ )}
+
+
{sampleSummary && setSampleSummary(null)} />}
{liveSummary && showLiveStatus && (
@@ -365,6 +366,6 @@ export default function ROICalculatorView({
onSave={updateIdentity}
/>
)}
-
+
);
}
diff --git a/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysPage.tsx b/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysPage.tsx
index 0af668b5e9c..2686806c95b 100644
--- a/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysPage.tsx
+++ b/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysPage.tsx
@@ -1,4 +1,5 @@
"use client";
+import { PageContent } from "@/components/shared/Page";
import { useState } from "react";
import { PaginationState } from "@tanstack/react-table";
import { Info } from "lucide-react";
@@ -14,9 +15,9 @@ export default function DeletedKeysPage() {
const { data: keysData, isLoading } = useDeletedKeys(pagination.pageIndex + 1, pagination.pageSize);
return (
-
+
{!premiumUser && (
-
+
Coming soon to Enterprise
@@ -31,6 +32,6 @@ export default function DeletedKeysPage() {
pagination={pagination}
onPaginationChange={setPagination}
/>
-
+
);
}
diff --git a/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysTable/DeletedKeysTable.tsx b/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysTable/DeletedKeysTable.tsx
index bc6941a7860..afe507366eb 100644
--- a/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysTable/DeletedKeysTable.tsx
+++ b/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysTable/DeletedKeysTable.tsx
@@ -44,6 +44,7 @@ export function DeletedKeysTable({
return (
key.token || String(index)}
diff --git a/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsPage.tsx b/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsPage.tsx
index 8c3aac2cac7..a491fe138f6 100644
--- a/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsPage.tsx
+++ b/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsPage.tsx
@@ -1,6 +1,7 @@
"use client";
import { PaginationState } from "@tanstack/react-table";
import { Info } from "lucide-react";
+import { PageContent } from "@/components/shared/Page";
import { useState } from "react";
import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert";
import { DEFAULT_PAGE_SIZE_OPTIONS } from "@/components/shared/DataTable";
@@ -17,9 +18,9 @@ export default function DeletedTeamsPage() {
const { data: teamsData, isLoading } = useDeletedTeams(pagination.pageIndex + 1, pagination.pageSize);
return (
-
+
{!premiumUser && (
-
+
Coming soon to Enterprise
@@ -34,6 +35,6 @@ export default function DeletedTeamsPage() {
onPaginationChange={setPagination}
rowCount={teamsData?.total ?? 0}
/>
-
+
);
}
diff --git a/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsTable/DeletedTeamsTable.tsx b/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsTable/DeletedTeamsTable.tsx
index c7e759754b8..236b8b7cf18 100644
--- a/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsTable/DeletedTeamsTable.tsx
+++ b/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsTable/DeletedTeamsTable.tsx
@@ -44,6 +44,7 @@ export function DeletedTeamsTable({
return (
team.team_id || String(index)}
diff --git a/ui/litellm-dashboard/src/components/Teams.test.tsx b/ui/litellm-dashboard/src/components/Teams.test.tsx
index 5ff95d2af0c..3b89ebdd318 100644
--- a/ui/litellm-dashboard/src/components/Teams.test.tsx
+++ b/ui/litellm-dashboard/src/components/Teams.test.tsx
@@ -529,15 +529,6 @@ describe("Teams - team detail deep link (?team=)", () => {
expect(onUrlUpdate.mock.calls.at(-1)![0].searchParams.has("team")).toBe(false);
await waitFor(() => expect(screen.queryByTestId("team-info-view")).not.toBeInTheDocument());
});
-
- it("should preserve the legacy inset for the team detail view", async () => {
- renderWithQueryClient(, {
- searchParams: "?team=team-from-url",
- });
-
- await waitFor(() => expect(mockTeamInfoView).toHaveBeenCalled());
- expect(screen.getByRole("main")).toHaveClass("px-12", "py-6");
- });
});
describe("Teams - Create Team CTA is grouped with the tabs on the left", () => {
@@ -553,7 +544,6 @@ describe("Teams - Create Team CTA is grouped with the tabs on the left", () => {
const createButton = within(tabNav).getByTestId("create-team-button");
const firstTab = within(tabNav).getByRole("tab", { name: "Your Teams" });
- expect(screen.getByRole("main")).toHaveClass("p-8");
expect(within(tabNav).getByRole("separator")).toBeInTheDocument();
expect(createButton.compareDocumentPosition(firstTab) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});
diff --git a/ui/litellm-dashboard/src/components/Teams.tsx b/ui/litellm-dashboard/src/components/Teams.tsx
index c2a23cef83a..dfc0a59f0a0 100644
--- a/ui/litellm-dashboard/src/components/Teams.tsx
+++ b/ui/litellm-dashboard/src/components/Teams.tsx
@@ -1,3 +1,4 @@
+import { Page, PageTabs, PageTabsList, PageTabsTrigger } from "@/components/shared/Page";
import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
import useCan from "@/app/(dashboard)/hooks/useCan";
import AvailableTeamsPanel from "@/components/team/AvailableTeamsPanel";
@@ -15,12 +16,13 @@ import { SearchSelect } from "@/components/shared/SearchSelect";
import { labelWithDocsHint, labelWithHint } from "@/components/shared/form/LabelWithHint";
import { useZodForm } from "@/lib/forms/useZodForm";
import { TagsInput } from "@/app/(dashboard)/guardrails/_components/content_filter/TagsInput";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { TabsContent } from "@/components/ui/tabs";
import { ChevronDown, Plus, Users } from "lucide-react";
import React, { useEffect, useMemo, useState } from "react";
import { z } from "zod/v4";
import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { PageHeader } from "@/components/shared/PageHeader";
+import { PageHeader, PageHeaderControls, PageHeaderDescription, PageHeaderTitle } from "@/components/shared/PageHeader";
+import { ToolbarSeparator } from "@/components/shared/ToolbarSeparator";
import { Button as UIButton } from "@/components/ui/button";
import { teamsTableKeys } from "@/app/(dashboard)/hooks/teams/useTeams";
import { parseAsString, useQueryState } from "nuqs";
@@ -652,7 +654,7 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser
];
return (
-
+
{selectedTeamId ? (
= ({ accessToken, userID, userRole, premiumUser
premiumUser={premiumUser}
/>
) : (
-
- }
- title="Teams"
- subtitle="Manage teams, members, and their access to models and budgets"
- primaryAction={
- canCreateOrManageTeams(userRole, userID, organizations) ? (
-
-
- Create Team
-
- ) : undefined
- }
- tabs={({ leadingControls }) => (
-
- {leadingControls}
+
+
+
+
+ Teams
+
+ Manage teams, members, and their access to models and budgets
+
+
+ {canCreateOrManageTeams(userRole, userID, organizations) && (
+ <>
+
+
+ Create Team
+
+
+ >
+ )}
{tabItems.map((item) => (
-
+
{item.label}
-
+
))}
-
- )}
- />
+
+
+
{tabItems.map((item) => (
{item.children}
))}
-
+
)}
{canCreateOrManageTeams(userRole, userID, organizations) && (
@@ -1312,7 +1309,7 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser
)}
-
+
);
};
diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx
index ec405ee9cad..cc25d22e546 100644
--- a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx
+++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx
@@ -16,7 +16,8 @@ import {
type UrlTableStateOptions,
} from "@/components/shared/DataTable";
import { SearchSelect } from "@/components/shared/SearchSelect";
-import { PageHeader } from "@/components/shared/PageHeader";
+import { PageContent } from "@/components/shared/Page";
+import { PageHeader, PageHeaderControls, PageHeaderDescription, PageHeaderTitle } from "@/components/shared/PageHeader";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useDebouncedValue } from "@tanstack/react-pacer/debouncer";
@@ -229,13 +230,15 @@ export function VirtualKeysTable({ headerActions }: VirtualKeysTableProps) {
}
return (
-
-
}
- title="Virtual Keys"
- subtitle="Every key that authenticates requests to the gateway."
- primaryAction={headerActions}
- />
+
+
+
+
+ Virtual Keys
+
+ Every key that authenticates requests to the gateway.
+ {headerActions != null && {headerActions}}
+
)}
/>
-
+
);
}
diff --git a/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.test.tsx b/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.test.tsx
index df88287b369..05ba7e04c39 100644
--- a/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.test.tsx
+++ b/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.test.tsx
@@ -899,28 +899,6 @@ describe("DataTable layout", () => {
expect(screen.getByTestId("data-table-head")).toHaveClass("sticky", "bg-background");
});
- it("caps fillHeight at the parent's height instead of stretching to it, so a short table stays short", () => {
- render();
- const outer = screen.getByTestId("data-table-root");
- const frame = screen.getByTestId("data-table-frame");
- const scroller = screen.getByTestId("data-table-scroller");
-
- // A ceiling, not a stretch: flex-1 here would hold the footer at the bottom on a two-row table.
- expect(outer).toHaveClass("max-h-full", "flex-col");
- expect(outer).not.toHaveClass("flex-1");
- expect(frame).toHaveClass("flex-col");
- expect(frame).not.toHaveClass("flex-1");
- expect(scroller).not.toHaveClass("flex-1");
-
- expect(scroller).toHaveClass("min-h-0", "overflow-auto");
- expect(scroller).toHaveStyle({ maxHeight: "" });
- // Without this the Table primitive's own overflow container captures the sticky header.
- expect(scroller).toHaveClass("[&_[data-slot=table-container]]:overflow-visible");
-
- // Rows pass under the header, so the semi-transparent row tint alone would let them show through.
- expect(screen.getByTestId("data-table-head")).toHaveClass("sticky", "bg-background");
- });
-
it("leaves the default layout untouched when neither height mode is set", () => {
render();
const scroller = screen.getByTestId("data-table-scroller");
diff --git a/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.tsx b/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.tsx
index feb1615b4e1..a94c4e34611 100644
--- a/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.tsx
+++ b/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.tsx
@@ -40,7 +40,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
-import { cn } from "@/lib/cva.config";
+import { cn, cva } from "@/lib/cva.config";
import "./columnMeta";
import { DataTablePagination, DEFAULT_PAGE_SIZE_OPTIONS } from "./DataTablePagination";
@@ -58,25 +58,40 @@ const INTERACTIVE_SELECTOR = "button, a, input, select, textarea, [role=checkbox
const noop = () => {};
-/**
- * Height-filling mode. The table still sizes to its rows; the parent's height is only a ceiling, so
- * a short table keeps its footer under the last row and a long one scrolls its rows instead of the
- * page.
- */
-const FILL_CLASSES = {
- outer: "flex max-h-full min-h-0 flex-col",
- frame: "flex min-h-0 flex-col",
- body: "min-h-0",
-} as const;
+const dataTableRoot = cva("w-full", {
+ variants: { fill: { true: "flex h-full min-h-0 flex-1 flex-col", false: null } },
+});
-const NO_FILL_CLASSES = { outer: "", frame: "", body: "" } as const;
+const dataTableFrame = cva("overflow-hidden rounded-lg border border-border", {
+ variants: { fill: { true: "flex min-h-0 flex-1 flex-col", false: null } },
+});
-const STICKY_CLASSES = {
- body: "[&_[data-slot=table-container]]:overflow-visible",
- header: "bg-background",
-} as const;
+const dataTableScroller = cva("", {
+ variants: {
+ sticky: { true: "overflow-auto [&_[data-slot=table-container]]:overflow-visible", false: "overflow-x-auto" },
+ fill: { true: "min-h-0 flex-1", false: null },
+ stretchEmpty: { true: "[container-type:inline-size] [&_[data-slot=table-container]]:h-full", false: null },
+ },
+});
-const NO_STICKY_CLASSES = { body: "", header: "" } as const;
+const dataTableTable = cva("", {
+ variants: {
+ resizable: { true: "table-fixed", false: null },
+ stretchEmpty: { true: "h-full", false: null },
+ },
+});
+
+const dataTableHeader = cva("", {
+ variants: { sticky: { true: "sticky top-0 z-sticky bg-background", false: null } },
+});
+
+const dataTableBody = cva("", {
+ variants: { stretchEmpty: { true: "h-full", false: null } },
+});
+
+const messageCell = cva("h-24 text-center align-middle text-sm whitespace-normal text-muted-foreground", {
+ variants: { stretch: { true: "p-0", false: null } },
+});
function columnDefId(column: ColumnDef): string | undefined {
if ("id" in column && typeof column.id === "string") {
@@ -314,14 +329,23 @@ function DataTableBodyRow({
);
}
-function MessageRow({ colSpan, children }: { colSpan: number; children: React.ReactNode }) {
+function MessageRow({
+ colSpan,
+ children,
+ stretch = false,
+}: {
+ colSpan: number;
+ children: React.ReactNode;
+ stretch?: boolean;
+}) {
return (
-
- {children}
+
+ {stretch ? (
+ {children}
+ ) : (
+ children
+ )}
);
@@ -598,8 +622,7 @@ export function DataTable(props: DataTableProps {
@@ -636,7 +659,11 @@ export function DataTable(props: DataTableProps{noDataMessage ?? };
+ return (
+
+ {noDataMessage ?? }
+
+ );
}
return rows.map((row) => (
(props: DataTableProps
-
+
+
{toolbar !== undefined &&
{toolbar(table)}
}
-
-
+
+
{table.getHeaderGroups().map((headerGroup) => (
{headerGroup.headers.map((header) => (
@@ -682,7 +706,7 @@ export function DataTable(props: DataTableProps
))}
- {renderBody()}
+ {renderBody()}
{footer !== undefined && {footer(table)}}
diff --git a/ui/litellm-dashboard/src/components/shared/Page.tsx b/ui/litellm-dashboard/src/components/shared/Page.tsx
new file mode 100644
index 00000000000..2799ab43911
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/shared/Page.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import type { ComponentProps } from "react";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { cn } from "@/lib/cva.config";
+
+export function Page({ className, ...props }: ComponentProps<"div">) {
+ return
;
+}
+
+export function PageContent({ className, ...props }: ComponentProps<"div">) {
+ return
;
+}
+
+export function PageTabs({ className, ...props }: ComponentProps
) {
+ return ;
+}
+
+export function PageTabsList({ className, ...props }: ComponentProps) {
+ return (
+
+ );
+}
+
+export function PageTabsTrigger({ className, ...props }: ComponentProps) {
+ return (
+
+ );
+}
+
+export function PageTabsContent({ className, ...props }: ComponentProps) {
+ return ;
+}
diff --git a/ui/litellm-dashboard/src/components/shared/PageHeader.test.tsx b/ui/litellm-dashboard/src/components/shared/PageHeader.test.tsx
index 3741542abad..b3283611997 100644
--- a/ui/litellm-dashboard/src/components/shared/PageHeader.test.tsx
+++ b/ui/litellm-dashboard/src/components/shared/PageHeader.test.tsx
@@ -1,77 +1,54 @@
import { renderWithProviders, screen, within } from "@/../tests/test-utils";
+import { Users } from "lucide-react";
import { describe, expect, it } from "vitest";
-import { PageHeader } from "./PageHeader";
-
-const identity = {
- icon: Teams icon,
- title: "Teams",
- subtitle: "Manage teams, members, and their access to models and budgets",
-};
+import { PageHeader, PageHeaderControls, PageHeaderDescription, PageHeaderTitle } from "./PageHeader";
describe("PageHeader", () => {
- it("should render the page identity", () => {
- renderWithProviders();
-
- expect(screen.getByRole("heading", { name: "Teams" })).toBeInTheDocument();
- expect(screen.getByText("Teams icon").parentElement).toHaveAttribute("aria-hidden", "true");
- expect(screen.getByText(identity.subtitle)).toBeInTheDocument();
- });
-
- it("should apply the standard title and subtext typography", () => {
- renderWithProviders();
-
- const icon = screen.getByText("Teams icon").parentElement;
- expect(screen.getByRole("heading", { name: "Teams" })).toHaveClass("text-2xl", "font-semibold", "tracking-tight");
- expect(screen.getByText(identity.subtitle)).toHaveClass("mt-1.5", "text-sm", "text-muted-foreground");
- expect(icon).toHaveClass("size-5", "[&_svg]:size-5", "[&_svg]:stroke-[1.75]");
- expect(icon?.parentElement).toHaveClass("gap-2.5");
- });
-
- it("should render the primary action, divider, tabs, and utilities in the standard control row", () => {
+ it("should name the page heading by its text alone when it carries an icon", () => {
renderWithProviders(
- Create Team}
- tabs={
-
-
-
- }
- utilities={}
- />,
+
+
+
+ Teams
+
+ Manage teams
+ ,
+ );
+
+ expect(screen.getByRole("heading", { level: 1, name: "Teams" })).toBeInTheDocument();
+ expect(screen.getByText("Manage teams")).toBeInTheDocument();
+ });
+
+ it("should group the composed controls under one accessible label", () => {
+ renderWithProviders(
+
+ Teams
+
+
+
+
+ ,
);
const controls = screen.getByRole("group", { name: "Page controls" });
- expect(controls).toHaveClass("mt-5", "h-9");
- expect(within(controls).getByRole("separator")).toHaveClass("mx-4", "h-6");
- expect(controls).toHaveTextContent("Create TeamYour TeamsRefresh");
+ expect(
+ within(controls)
+ .getAllByRole("button")
+ .map((button) => button.textContent),
+ ).toEqual(["Create Team", "Refresh"]);
});
- it("should omit the divider when tabs are absent", () => {
- renderWithProviders(Create Team} />);
-
- expect(screen.queryByRole("separator")).not.toBeInTheDocument();
- });
-
- it("should provide standard controls to an embedded tab shell", () => {
+ it("should forward native attributes to each part", () => {
renderWithProviders(
- Create Team}
- tabs={({ leadingControls, utilities }) => (
-
- {leadingControls}
-
- {utilities}
-
- )}
- utilities={}
- />,
+
+ Teams
+
+ ,
);
- const tabs = screen.getByRole("tablist");
- expect(within(tabs).getByRole("separator")).toBeInTheDocument();
- expect(tabs).toHaveTextContent("Create TeamYour TeamsRefresh");
+ expect(screen.getByTestId("header")).toContainElement(screen.getByRole("heading", { name: "Teams" }));
+ expect(screen.getByRole("heading", { name: "Teams" })).toHaveAttribute("id", "page-title");
+ expect(screen.getByRole("group", { name: "Team controls" })).toBeInTheDocument();
});
});
diff --git a/ui/litellm-dashboard/src/components/shared/PageHeader.tsx b/ui/litellm-dashboard/src/components/shared/PageHeader.tsx
index 6fc5b0f3238..28a22f0a69f 100644
--- a/ui/litellm-dashboard/src/components/shared/PageHeader.tsx
+++ b/ui/litellm-dashboard/src/components/shared/PageHeader.tsx
@@ -1,62 +1,36 @@
"use client";
-import * as React from "react";
+import type { ComponentProps } from "react";
-import { ToolbarSeparator } from "./ToolbarSeparator";
+import { cn } from "@/lib/cva.config";
-interface EmbeddedTabsSlots {
- leadingControls: React.ReactNode;
- utilities: React.ReactNode;
+export function PageHeader({ className, ...props }: ComponentProps<"div">) {
+ return ;
}
-interface PageHeaderProps {
- title: React.ReactNode;
- subtitle: React.ReactNode;
- icon: React.ReactNode;
- primaryAction?: React.ReactNode;
- titleAction?: React.ReactNode;
- tabs?: React.ReactNode | ((slots: EmbeddedTabsSlots) => React.ReactNode);
- utilities?: React.ReactNode;
-}
-
-export function PageHeader({ title, subtitle, icon, primaryAction, titleAction, tabs, utilities }: PageHeaderProps) {
- const leadingControls =
- primaryAction == null ? null : (
-
- {primaryAction}
- {tabs != null && }
-
- );
- const utilityControls = utilities == null ? null : {utilities}
;
- const hasControlRow = primaryAction != null || tabs != null || utilities != null;
-
+export function PageHeaderTitle({ className, ...props }: ComponentProps<"h1">) {
return (
-
-
-
-
- {icon}
-
-
{title}
-
- {titleAction}
-
-
{subtitle}
-
- {typeof tabs === "function" ? (
-
{tabs({ leadingControls, utilities: utilityControls })}
- ) : (
- hasControlRow && (
-
- {leadingControls}
- {tabs}
- {utilityControls != null &&
{utilityControls}
}
-
- )
+
+ {...props}
+ />
+ );
+}
+
+export function PageHeaderDescription({ className, ...props }: ComponentProps<"p">) {
+ return
;
+}
+
+export function PageHeaderControls({ className, ...props }: ComponentProps<"div">) {
+ return (
+
);
}
diff --git a/ui/litellm-dashboard/src/components/view_logs/AuditLogsPanel.tsx b/ui/litellm-dashboard/src/components/view_logs/AuditLogsPanel.tsx
index abc00b75884..6480cb59682 100644
--- a/ui/litellm-dashboard/src/components/view_logs/AuditLogsPanel.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/AuditLogsPanel.tsx
@@ -100,7 +100,7 @@ export default function AuditLogsPanel({
if (!premiumUser) {
return (
-
+
✨ Enterprise Feature.
This is a LiteLLM Enterprise feature, and requires a valid key to use.
@@ -128,10 +128,6 @@ export default function AuditLogsPanel({
return (
<>
-
-
Audit Logs
-
-
row.id}
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogsTableToolbar.tsx b/ui/litellm-dashboard/src/components/view_logs/LogsTableToolbar.tsx
index 60657872ccc..3496b709d9b 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogsTableToolbar.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/LogsTableToolbar.tsx
@@ -2,72 +2,59 @@
import moment from "moment";
import { CalendarDays } from "lucide-react";
-import { useState } from "react";
+import { type ComponentProps, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Switch } from "@/components/ui/switch";
+import { cn } from "@/lib/cva.config";
import { QUICK_SELECT_OPTIONS } from "./constants";
import { getTimeRangeDisplay } from "./logs_utils";
-interface LogsTableToolbarProps {
+const DATETIME_LOCAL_FORMAT = "YYYY-MM-DDTHH:mm";
+
+export interface LogsTimeRange {
startTime: string;
- onStartTimeChange: (value: string) => void;
endTime: string;
- onEndTimeChange: (value: string) => void;
isCustomDate: boolean;
- onIsCustomDateChange: (value: boolean) => void;
- selectedTimeInterval: { value: number; unit: string };
- onSelectedTimeIntervalChange: (value: { value: number; unit: string }) => void;
- isLiveTail: boolean;
- onIsLiveTailChange: (value: boolean) => void;
- excludeInternalHealthChecks: boolean;
- onExcludeInternalHealthChecksChange: (value: boolean) => void;
- onResetToFirstPage: () => void;
- onResetFilters: () => void;
+ interval: { value: number; unit: string };
}
-export function LogsTableToolbar({
- startTime,
- onStartTimeChange,
- endTime,
- onEndTimeChange,
- isCustomDate,
- onIsCustomDateChange,
- selectedTimeInterval,
- onSelectedTimeIntervalChange,
- isLiveTail,
- onIsLiveTailChange,
- excludeInternalHealthChecks,
- onExcludeInternalHealthChecksChange,
- onResetToFirstPage,
- onResetFilters,
-}: LogsTableToolbarProps) {
- const [quickSelectOpen, setQuickSelectOpen] = useState(false);
+const relativeTimeRange = (interval: LogsTimeRange["interval"]): LogsTimeRange => ({
+ startTime: moment()
+ .subtract(interval.value, interval.unit as moment.unitOfTime.DurationConstructor)
+ .format(DATETIME_LOCAL_FORMAT),
+ endTime: moment().format(DATETIME_LOCAL_FORMAT),
+ isCustomDate: false,
+ interval,
+});
- const applyQuickSelect = (option: { label: string; value: number; unit: string }) => {
- onResetToFirstPage();
- onEndTimeChange(moment().format("YYYY-MM-DDTHH:mm"));
- onStartTimeChange(
- moment()
- .subtract(option.value, option.unit as moment.unitOfTime.DurationConstructor)
- .format("YYYY-MM-DDTHH:mm"),
- );
- onSelectedTimeIntervalChange({ value: option.value, unit: option.unit });
- onIsCustomDateChange(false);
- setQuickSelectOpen(false);
- };
+export const defaultLogsTimeRange = (): LogsTimeRange => relativeTimeRange({ value: 24, unit: "hours" });
+
+export function LogsToolbar({ className, ...props }: ComponentProps<"div">) {
+ return ;
+}
+
+interface LogsTimeRangePickerProps {
+ value: LogsTimeRange;
+ onValueChange: (value: LogsTimeRange) => void;
+}
+
+export function LogsTimeRangePicker({ value, onValueChange }: LogsTimeRangePickerProps) {
+ const [open, setOpen] = useState(false);
const selectedOption = QUICK_SELECT_OPTIONS.find(
- (option) => option.value === selectedTimeInterval.value && option.unit === selectedTimeInterval.unit,
+ (option) => option.value === value.interval.value && option.unit === value.interval.unit,
);
- const displayLabel = isCustomDate ? getTimeRangeDisplay(isCustomDate, startTime, endTime) : selectedOption?.label;
+ const displayLabel = value.isCustomDate
+ ? getTimeRangeDisplay(true, value.startTime, value.endTime)
+ : selectedOption?.label;
return (
-
-
+ <>
+
@@ -83,7 +70,10 @@ export function LogsTableToolbar({
key={option.label}
variant="ghost"
className="w-full justify-start font-normal"
- onClick={() => applyQuickSelect(option)}
+ onClick={() => {
+ onValueChange(relativeTimeRange({ value: option.value, unit: option.unit }));
+ setOpen(false);
+ }}
>
{option.label}
@@ -92,10 +82,7 @@ export function LogsTableToolbar({
@@ -103,58 +90,32 @@ export function LogsTableToolbar({
- {isCustomDate && (
+ {value.isCustomDate && (
{
- onStartTimeChange(event.target.value);
- onResetToFirstPage();
- }}
+ value={value.startTime}
+ onChange={(event) => onValueChange({ ...value, startTime: event.target.value })}
/>
to
{
- onEndTimeChange(event.target.value);
- onResetToFirstPage();
- }}
+ value={value.endTime}
+ onChange={(event) => onValueChange({ ...value, endTime: event.target.value })}
/>
)}
-
-
- Live Tail
-
-
-
-
- Hide Health Checks
-
-
-
-
-
+ >
);
}
-export function LiveTailBanner({ onStop }: { onStop: () => void }) {
+export function LogsToolbarSwitch({ label, ...props }: ComponentProps & { label: string }) {
return (
-
-
Auto-refreshing every 15 seconds
-
+
+ {label}
+
);
}
diff --git a/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.test.tsx b/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.test.tsx
index 8a0e529e2e8..aefbf68d662 100644
--- a/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.test.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.test.tsx
@@ -792,15 +792,15 @@ describe("RequestLogsPanel", () => {
});
describe("live tail", () => {
- it("shows the auto-refresh banner on the first page and hides it once stopped", async () => {
+ it("shows the refresh status on the first page and hides it when live tail is disabled", async () => {
const user = userEvent.setup();
renderPanel();
- expect(await screen.findByText("Auto-refreshing every 15 seconds")).toBeInTheDocument();
+ expect(await screen.findByText("Refreshing every 15s")).toBeInTheDocument();
- await user.click(screen.getByRole("button", { name: "Stop" }));
+ await user.click(screen.getByRole("switch", { name: "Live Tail" }));
- expect(screen.queryByText("Auto-refreshing every 15 seconds")).not.toBeInTheDocument();
+ expect(screen.queryByText("Refreshing every 15s")).not.toBeInTheDocument();
});
});
});
diff --git a/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx b/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx
index 0b5e5ff9616..ff26e30baca 100644
--- a/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx
@@ -7,6 +7,7 @@ import moment from "moment";
import { useCallback, useEffect, useMemo, useState } from "react";
import { DEFAULT_PAGE_SIZE_OPTIONS } from "@/components/shared/DataTable";
+import { Button } from "@/components/ui/button";
import { AutoRouterModelGroupsProvider } from "@/components/shared/table_cells";
import { DEBOUNCE_WAIT_MS } from "@/utils/debounceConstants";
import type { KeyResponse } from "../key_team_helpers/key_list";
@@ -23,11 +24,16 @@ import {
} from "./log_filter_logic";
import { useLogDetailRouting } from "./logDetailRouting";
import { LogDetailsDrawer } from "./LogDetailsDrawer";
-import { LiveTailBanner, LogsTableToolbar } from "./LogsTableToolbar";
+import {
+ defaultLogsTimeRange,
+ type LogsTimeRange,
+ LogsTimeRangePicker,
+ LogsToolbar,
+ LogsToolbarSwitch,
+} from "./LogsTableToolbar";
import { RequestLogsTable } from "./RequestLogsTable";
const PAGE_SIZE = DEFAULT_PAGE_SIZE_OPTIONS[0];
-const DEFAULT_INTERVAL = { value: 24, unit: "hours" };
const matchesLogId = (log: LogEntry, logId: string) => log.request_id === logId || log.litellm_call_id === logId;
const findLogById = (logs: readonly LogEntry[], logId: string): LogEntry | null =>
logs.find((log) => log.request_id === logId) ?? logs.find((log) => log.litellm_call_id === logId) ?? null;
@@ -46,10 +52,8 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
const [columnFilters, setColumnFilters] = useState
([]);
const [sessionCursors, setSessionCursors] = useState>({});
- const [startTime, setStartTime] = useState(moment().subtract(24, "hours").format("YYYY-MM-DDTHH:mm"));
- const [endTime, setEndTime] = useState(moment().format("YYYY-MM-DDTHH:mm"));
- const [isCustomDate, setIsCustomDate] = useState(false);
- const [selectedTimeInterval, setSelectedTimeInterval] = useState<{ value: number; unit: string }>(DEFAULT_INTERVAL);
+ const [timeRange, setTimeRange] = useState(defaultLogsTimeRange);
+ const { startTime, endTime, isCustomDate } = timeRange;
const [selectedKeyIdInfoView, setSelectedKeyIdInfoView] = useState(null);
const [selectedLog, setSelectedLog] = useState(null);
@@ -232,12 +236,17 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
[resetToFirstPage],
);
+ const handleTimeRangeChange = useCallback(
+ (value: LogsTimeRange) => {
+ setTimeRange(value);
+ resetToFirstPage();
+ },
+ [resetToFirstPage],
+ );
+
const handleResetFilters = useCallback(() => {
setColumnFilters([]);
- setStartTime(moment().subtract(24, "hours").format("YYYY-MM-DDTHH:mm"));
- setEndTime(moment().format("YYYY-MM-DDTHH:mm"));
- setIsCustomDate(false);
- setSelectedTimeInterval(DEFAULT_INTERVAL);
+ setTimeRange(defaultLogsTimeRange());
resetToFirstPage();
}, [resetToFirstPage]);
@@ -288,12 +297,6 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
return (
-
-
Request Logs
-
-
- {isLiveTail && pagination.pageIndex === 0 && setIsLiveTail(false)} />}
-
+
+
+
+ {isLiveTail && pagination.pageIndex === 0 && (
+
+ Refreshing every 15s
+
+ )}
+
+
+
}
/>
diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx
deleted file mode 100644
index fe3b99a1882..00000000000
--- a/ui/litellm-dashboard/src/components/view_logs/index.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { useState } from "react";
-import useCan from "@/app/(dashboard)/hooks/useCan";
-import DeletedKeysPage from "../DeletedKeysPage/DeletedKeysPage";
-import DeletedTeamsPage from "../DeletedTeamsPage/DeletedTeamsPage";
-import AuditLogsPanel from "./AuditLogsPanel";
-import RequestLogsPanel from "./RequestLogsPanel";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
-
-interface SpendLogsTableProps {
- accessToken: string | null;
- token: string | null;
- userRole: string | null;
- userID: string | null;
- premiumUser: boolean;
-}
-
-type LogsTabId = "request logs" | "audit logs" | "deleted keys" | "deleted teams";
-
-interface LogsTab {
- id: LogsTabId;
- label: string;
-}
-
-const REQUEST_LOGS_TAB: LogsTab = { id: "request logs", label: "Request Logs" };
-const AUDIT_LOGS_TAB: LogsTab = { id: "audit logs", label: "Audit Logs" };
-const DELETED_KEYS_TAB: LogsTab = { id: "deleted keys", label: "Deleted Keys" };
-const DELETED_TEAMS_TAB: LogsTab = { id: "deleted teams", label: "Deleted Teams" };
-
-const tabContentClassName = (tabId: LogsTabId): string =>
- tabId === REQUEST_LOGS_TAB.id ? "flex min-h-0 flex-1 flex-col" : "min-h-0 flex-1 overflow-y-auto";
-
-export default function SpendLogsTable({ accessToken, token, userRole, userID, premiumUser }: SpendLogsTableProps) {
- const [activeTab, setActiveTab] = useState(REQUEST_LOGS_TAB.id);
- const canViewAuditLogs = useCan("viewAuditLogs");
- const canViewDeletedTeams = useCan("viewDeletedTeams");
-
- if (!accessToken || !token || !userRole || !userID) {
- return (
-
-
-
- );
- }
-
- const tabs: LogsTab[] = [
- REQUEST_LOGS_TAB,
- ...(canViewAuditLogs ? [AUDIT_LOGS_TAB] : []),
- DELETED_KEYS_TAB,
- ...(canViewDeletedTeams ? [DELETED_TEAMS_TAB] : []),
- ];
-
- const renderPanel = (tabId: LogsTabId) => {
- switch (tabId) {
- case "request logs":
- return (
-
- );
- case "audit logs":
- return (
-
- );
- case "deleted keys":
- return ;
- case "deleted teams":
- return ;
- }
- };
-
- return (
-
- setActiveTab(value as LogsTabId)}
- className="min-h-0 flex-1 gap-0"
- >
-
- {tabs.map((tab) => (
-
- {tab.label}
-
- ))}
-
- {tabs.map((tab) => (
-
- {renderPanel(tab.id)}
-
- ))}
-
-
- );
-}