refactor(ui): move every page header onto PageHeader and drop the legacy one

Virtual Keys, Budgets, Projects, Access Groups, Guardrails Monitor and Cost
Optimization all move onto the shared PageHeader, matching the Teams page.
That empties LegacyPageHeader, so it and its test are deleted.

Each page now uses its own sidebar icon, so the nav and the page agree:
Virtual Keys keeps KeyRound and Budgets keeps Wallet, Projects picks up
Folder and Access Groups picks up Boxes, and Guardrails Monitor swaps the
indigo Shield for the sidebar's HeartPulse. Cost Optimization keeps
PiggyBank but drops its hardcoded size and stroke, which PageHeader owns.

Control rows follow the spec instead of each page inventing one. Virtual
Keys had its create button rendered as a sibling below the header, Budgets
hand-rolled a row with a bottom border that closed the header off, and
Projects and Access Groups sat their button next to the title. All four now
pass primaryAction. Guardrails Monitor's date picker moves out of the parent
and joins Export Data in utilities. Cost Optimization's tabs move into the
tabs slot with the standard 22px spacing.

Page insets go to p-8 with a 24px gap to content, replacing p-6 px-12,
p-6, mx-4 and py-2.

Every page test now asserts its heading, subtext and sidebar icon. Swapping
any of the six icons fails its suite.
This commit is contained in:
Yuneng Jiang 2026-08-25 23:19:22 -07:00
parent 3e2927de9a
commit ca77b32454
No known key found for this signature in database
16 changed files with 198 additions and 226 deletions

View file

@ -99,6 +99,8 @@ describe("AccessGroupsPage", () => {
renderWithProviders(<AccessGroupsPage />); renderWithProviders(<AccessGroupsPage />);
expect(screen.getByRole("heading", { name: "Access Groups" })).toBeInTheDocument(); expect(screen.getByRole("heading", { name: "Access Groups" })).toBeInTheDocument();
expect(screen.getByText("Manage resource permissions for your organization")).toBeInTheDocument(); expect(screen.getByText("Manage resource permissions for your organization")).toBeInTheDocument();
expect(document.querySelector(".lucide-boxes")).not.toBeNull();
expect(screen.getByText("Manage resource permissions for your organization")).toBeInTheDocument();
}); });
it("shows the Create Access Group button for an admin", () => { it("shows the Create Access Group button for an admin", () => {

View file

@ -1,9 +1,9 @@
import { AccessGroupResponse, useAccessGroups } from "@/app/(dashboard)/hooks/accessGroups/useAccessGroups"; import { AccessGroupResponse, useAccessGroups } from "@/app/(dashboard)/hooks/accessGroups/useAccessGroups";
import { useDeleteAccessGroup } from "@/app/(dashboard)/hooks/accessGroups/useDeleteAccessGroup"; import { useDeleteAccessGroup } from "@/app/(dashboard)/hooks/accessGroups/useDeleteAccessGroup";
import { Plus, SearchIcon, X } from "lucide-react"; import { Boxes, Plus, SearchIcon, X } from "lucide-react";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import DeleteResourceModal from "@/components/common_components/DeleteResourceModal"; import DeleteResourceModal from "@/components/common_components/DeleteResourceModal";
import { LegacyPageHeader } from "@/components/shared/LegacyPageHeader"; import { PageHeader } from "@/components/shared/PageHeader";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group"; import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group";
import { AccessGroupDetail } from "./AccessGroupsDetailsPage"; import { AccessGroupDetail } from "./AccessGroupsDetailsPage";
@ -59,23 +59,22 @@ export function AccessGroupsPage() {
} }
return ( return (
<div className="p-6 px-12"> <div className="p-8">
<div className="mb-4"> <PageHeader
<LegacyPageHeader icon={<Boxes />}
title="Access Groups" title="Access Groups"
subtitle="Manage resource permissions for your organization" subtitle="Manage resource permissions for your organization"
actions={ primaryAction={
canModify ? ( canModify ? (
<Button onClick={() => setIsCreateModalVisible(true)}> <Button onClick={() => setIsCreateModalVisible(true)}>
<Plus className="size-4" /> <Plus className="size-4" />
Create Access Group Create Access Group
</Button> </Button>
) : undefined ) : undefined
} }
/> />
</div>
<div className="mb-3 flex items-center"> <div className="mt-6 mb-3 flex items-center">
<InputGroup className="max-w-[400px]"> <InputGroup className="max-w-[400px]">
<InputGroupAddon> <InputGroupAddon>
<SearchIcon className="size-4 text-muted-foreground" /> <SearchIcon className="size-4 text-muted-foreground" />

View file

@ -85,6 +85,14 @@ describe("Budget Panel", () => {
respondWith(DEFAULT_ROWS, 1); respondWith(DEFAULT_ROWS, 1);
}); });
it("renders the standard page header with the sidebar's Budgets icon", async () => {
const { container } = renderPanel();
expect(await screen.findByRole("heading", { level: 1, name: "Budgets" })).toBeInTheDocument();
expect(screen.getByText("Spend, TPM and RPM limits you can assign to customers.")).toBeInTheDocument();
expect(container.querySelector(".lucide-wallet")).not.toBeNull();
});
it("loads the first page of budgets, newest first", async () => { it("loads the first page of budgets, newest first", async () => {
renderPanel(); renderPanel();
await waitFor(() => expect(getMock).toHaveBeenCalled()); await waitFor(() => expect(getMock).toHaveBeenCalled());

View file

@ -9,8 +9,7 @@ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { prism } from "react-syntax-highlighter/dist/esm/styles/prism"; import { prism } from "react-syntax-highlighter/dist/esm/styles/prism";
import { useSyntaxTheme } from "@/hooks/useSyntaxTheme"; import { useSyntaxTheme } from "@/hooks/useSyntaxTheme";
import { LegacyPageHeader } from "@/components/shared/LegacyPageHeader"; import { PageHeader } from "@/components/shared/PageHeader";
import { ToolbarSeparator } from "@/components/shared/ToolbarSeparator";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import DeleteResourceModal from "@/components/common_components/DeleteResourceModal"; import DeleteResourceModal from "@/components/common_components/DeleteResourceModal";
@ -79,34 +78,37 @@ const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
}; };
return ( return (
<div className="flex h-full flex-col gap-4 p-6 px-12"> <main className="flex h-full flex-col p-8">
<LegacyPageHeader <Tabs defaultValue="budgets" className="min-h-0 flex-1 gap-6">
icon={<Wallet className="size-5" />} <PageHeader
title="Budgets" icon={<Wallet />}
subtitle="Spend, TPM and RPM limits you can assign to customers." title="Budgets"
/> subtitle="Spend, TPM and RPM limits you can assign to customers."
<Tabs defaultValue="budgets" className="min-h-0 flex-1 gap-0"> primaryAction={
<div className="flex items-center gap-4 border-b border-border"> canModify ? (
{canModify && (
<>
<Button onClick={() => setIsCreateModelVisible(true)}> <Button onClick={() => setIsCreateModelVisible(true)}>
<Plus className="size-4" /> <Plus className="size-4" />
Create Budget Create Budget
</Button> </Button>
<ToolbarSeparator className="h-6" /> ) : 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>
)} )}
<TabsList variant="line"> />
<TabsTrigger value="budgets" className="flex-none px-4">
Budgets
</TabsTrigger>
<TabsTrigger value="examples" className="flex-none px-4">
Examples
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="budgets" className="flex min-h-0 flex-1 flex-col" keepMounted> <TabsContent value="budgets" className="flex min-h-0 flex-1 flex-col" keepMounted>
<div className="flex min-h-0 flex-1 flex-col pt-6"> <div className="flex min-h-0 flex-1 flex-col">
<BudgetModal isModalVisible={isCreateModelVisible} setIsModalVisible={setIsCreateModelVisible} /> <BudgetModal isModalVisible={isCreateModelVisible} setIsModalVisible={setIsCreateModelVisible} />
{selectedBudget && ( {selectedBudget && (
<EditBudgetModal <EditBudgetModal
@ -172,7 +174,7 @@ const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
</div> </div>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </main>
); );
}; };

View file

@ -44,6 +44,14 @@ describe("CostOptimizationView", () => {
useAuthorizedMock.mockReturnValue({ accessToken: "test-token", userId: "u1", userRole: "Admin" }); useAuthorizedMock.mockReturnValue({ accessToken: "test-token", userId: "u1", userRole: "Admin" });
}); });
it("renders the standard page header with the sidebar's Cost Optimization icon", () => {
const { container, getByRole, getByText } = renderView();
expect(getByRole("heading", { level: 1, name: "Cost Optimization" })).toBeInTheDocument();
expect(getByText(/Track and configure the mechanisms that save you money/)).toBeInTheDocument();
expect(container.querySelector(".lucide-piggy-bank")).not.toBeNull();
});
it("renders the four cost-optimization tabs", () => { it("renders the four cost-optimization tabs", () => {
const { getByText } = renderView(); const { getByText } = renderView();

View file

@ -6,6 +6,7 @@ import { Info, PiggyBank } from "lucide-react";
import useCan from "@/app/(dashboard)/hooks/useCan"; import useCan from "@/app/(dashboard)/hooks/useCan";
import PaginationStatusAlerts from "@/components/shared/PaginationStatusAlerts"; import PaginationStatusAlerts from "@/components/shared/PaginationStatusAlerts";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { PageHeader } from "@/components/shared/PageHeader";
import UsageTab from "./UsageTab"; import UsageTab from "./UsageTab";
import PromptCompressionTab from "./PromptCompressionTab"; import PromptCompressionTab from "./PromptCompressionTab";
import PromptCachingTab from "./PromptCachingTab"; import PromptCachingTab from "./PromptCachingTab";
@ -32,63 +33,63 @@ const CostOptimizationView: React.FC<CostOptimizationViewProps> = ({ accessToken
}; };
return ( return (
<div className="w-full space-y-6 p-6"> <main className="w-full p-8">
<div> <Tabs defaultValue="usage" onValueChange={handleTabChange} className="gap-6">
<div className="flex items-center gap-2"> <PageHeader
<PiggyBank className="size-6 text-primary" strokeWidth={1.75} /> icon={<PiggyBank />}
<h1 className="text-xl font-semibold text-foreground">Cost Optimization</h1> title="Cost Optimization"
</div> 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"
<p className="mt-1 text-sm text-muted-foreground"> tabs={({ leadingControls }) => (
Track and configure the mechanisms that save you money: prompt compression and prompt caching. Auto routers <TabsList
live under Models + Endpoints, on the Auto-Routers tab variant="line"
</p> className="gap-0 p-0 [&>[data-slot=tabs-trigger]+[data-slot=tabs-trigger]]:ml-[22px]"
</div> >
{leadingControls}
<div <TabsTrigger value="usage" className="flex-none px-0 py-[7px] data-active:font-semibold">
role="alert" Overall
className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 rounded-lg border border-border bg-muted/50 px-4 py-4"
>
<Info className="mt-0.5 size-5 text-primary" aria-hidden="true" />
<p className="font-medium text-foreground">This is an experimental dashboard</p>
<p className="col-start-2 text-sm text-muted-foreground">
Have feedback? Join the discussion{" "}
<a
href="https://github.com/BerriAI/litellm/discussions/32168"
target="_blank"
rel="noopener noreferrer"
className="text-primary underline underline-offset-2"
>
here
</a>
</p>
</div>
<PaginationStatusAlerts
isFetchingMore={activity.isFetchingMore}
cancelled={activity.cancelled}
progress={activity.progress}
cancel={activity.cancel}
/>
<Tabs defaultValue="usage" onValueChange={handleTabChange}>
<TabsList variant="line" className="h-auto w-full justify-start rounded-none p-0">
<TabsTrigger value="usage" className="flex-none rounded-none px-4 py-2">
Overall
</TabsTrigger>
{canViewProxyWideCostData && (
<>
<TabsTrigger value="compression" className="flex-none rounded-none px-4 py-2">
Prompt Compression
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="caching" className="flex-none rounded-none px-4 py-2"> {canViewProxyWideCostData && (
Prompt Caching <>
</TabsTrigger> <TabsTrigger value="compression" className="flex-none px-0 py-[7px] data-active:font-semibold">
<TabsTrigger value="autorouter-usage" className="flex-none rounded-none px-4 py-2"> Prompt Compression
Auto-Router </TabsTrigger>
</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>
</>
)}
</TabsList>
)} )}
</TabsList> />
<div
role="alert"
className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 rounded-lg border border-border bg-muted/50 px-4 py-4"
>
<Info className="mt-0.5 size-5 text-primary" aria-hidden="true" />
<p className="font-medium text-foreground">This is an experimental dashboard</p>
<p className="col-start-2 text-sm text-muted-foreground">
Have feedback? Join the discussion{" "}
<a
href="https://github.com/BerriAI/litellm/discussions/32168"
target="_blank"
rel="noopener noreferrer"
className="text-primary underline underline-offset-2"
>
here
</a>
</p>
</div>
<PaginationStatusAlerts
isFetchingMore={activity.isFetchingMore}
cancelled={activity.cancelled}
progress={activity.progress}
cancel={activity.cancel}
/>
<TabsContent value="usage" keepMounted={visitedTabs.includes("usage")}> <TabsContent value="usage" keepMounted={visitedTabs.includes("usage")}>
<UsageTab accessToken={accessToken} activity={activity} /> <UsageTab accessToken={accessToken} activity={activity} />
</TabsContent> </TabsContent>
@ -106,7 +107,7 @@ const CostOptimizationView: React.FC<CostOptimizationViewProps> = ({ accessToken
</> </>
)} )}
</Tabs> </Tabs>
</div> </main>
); );
}; };

View file

@ -41,27 +41,32 @@ export default function GuardrailsMonitorView({ accessToken = null }: Guardrails
setView({ type: "overview" }); setView({ type: "overview" });
}; };
const dateRangeControl = (
<AdvancedDatePicker value={dateValue} onValueChange={handleDateChange} label="" showTimeRange={false} />
);
return ( return (
<div className="p-6 w-full min-w-0 flex-1"> <main className="w-full min-w-0 flex-1 p-8">
<div className="flex items-center justify-end mb-4">
<AdvancedDatePicker value={dateValue} onValueChange={handleDateChange} label="" showTimeRange={false} />
</div>
{view.type === "overview" ? ( {view.type === "overview" ? (
<GuardrailsOverview <GuardrailsOverview
accessToken={accessToken} accessToken={accessToken}
startDate={startDate} startDate={startDate}
endDate={endDate} endDate={endDate}
onSelectGuardrail={handleSelectGuardrail} onSelectGuardrail={handleSelectGuardrail}
dateRangeControl={dateRangeControl}
/> />
) : ( ) : (
<GuardrailDetail <>
guardrailId={view.guardrailId} <div className="mb-4 flex items-center justify-end">{dateRangeControl}</div>
onBack={handleBack} <GuardrailDetail
accessToken={accessToken} guardrailId={view.guardrailId}
startDate={startDate} onBack={handleBack}
endDate={endDate} accessToken={accessToken}
/> startDate={startDate}
endDate={endDate}
/>
</>
)} )}
</div> </main>
); );
} }

View file

@ -110,6 +110,8 @@ describe("GuardrailsOverview", () => {
expect(await screen.findByRole("heading", { name: "Guardrails Monitor", level: 1 })).toBeInTheDocument(); expect(await screen.findByRole("heading", { name: "Guardrails Monitor", level: 1 })).toBeInTheDocument();
expect(screen.getByText("Monitor guardrail performance across all requests")).toBeInTheDocument(); expect(screen.getByText("Monitor guardrail performance across all requests")).toBeInTheDocument();
expect(document.querySelector(".lucide-heart-pulse")).not.toBeNull();
expect(screen.getByText("Monitor guardrail performance across all requests")).toBeInTheDocument();
expect(screen.getByRole("button", { name: /Export Data/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /Export Data/i })).toBeInTheDocument();
}); });

View file

@ -1,11 +1,12 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import type { ColumnDef, OnChangeFn, SortingState } from "@tanstack/react-table"; import type { ColumnDef, OnChangeFn, SortingState } from "@tanstack/react-table";
import { Download, Settings, Shield, TrendingUp, TriangleAlert } from "lucide-react"; import { Download, HeartPulse, Settings, TrendingUp, TriangleAlert } from "lucide-react";
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { DataTable, DataTableSortHeader } from "@/components/shared/DataTable"; import { DataTable, DataTableSortHeader } from "@/components/shared/DataTable";
import { getGuardrailsUsageOverview } from "@/components/networking"; import { getGuardrailsUsageOverview } from "@/components/networking";
import { type PerformanceRow } from "@/components/GuardrailsMonitor/mockData"; import { type PerformanceRow } from "@/components/GuardrailsMonitor/mockData";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { PageHeader } from "@/components/shared/PageHeader";
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { EvaluationSettingsModal } from "./EvaluationSettingsModal"; import { EvaluationSettingsModal } from "./EvaluationSettingsModal";
import { MetricCard } from "@/components/GuardrailsMonitor/MetricCard"; import { MetricCard } from "@/components/GuardrailsMonitor/MetricCard";
@ -16,6 +17,7 @@ interface GuardrailsOverviewProps {
startDate: string; startDate: string;
endDate: string; endDate: string;
onSelectGuardrail: (id: string) => void; onSelectGuardrail: (id: string) => void;
dateRangeControl?: React.ReactNode;
} }
type SortKey = "failRate" | "requestsEvaluated" | "avgLatency" | "falsePositiveRate" | "falseNegativeRate"; type SortKey = "failRate" | "requestsEvaluated" | "avgLatency" | "falsePositiveRate" | "falseNegativeRate";
@ -43,6 +45,7 @@ export function GuardrailsOverview({
startDate, startDate,
endDate, endDate,
onSelectGuardrail, onSelectGuardrail,
dateRangeControl,
}: GuardrailsOverviewProps) { }: GuardrailsOverviewProps) {
const [sortBy, setSortBy] = useState<SortKey>("failRate"); const [sortBy, setSortBy] = useState<SortKey>("failRate");
const [sortDir, setSortDir] = useState<"asc" | "desc">("desc"); const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
@ -197,23 +200,22 @@ export function GuardrailsOverview({
return ( return (
<div> <div>
<div className="flex items-start justify-between mb-5"> <PageHeader
<div> icon={<HeartPulse />}
<div className="flex items-center gap-2 mb-1"> title="Guardrails Monitor"
<Shield className="size-5 text-indigo-500" /> subtitle="Monitor guardrail performance across all requests"
<h1 className="text-xl font-semibold text-foreground">Guardrails Monitor</h1> utilities={
</div> <>
<p className="text-sm text-muted-foreground">Monitor guardrail performance across all requests</p> {dateRangeControl}
</div> <Button variant="outline" title="Coming soon">
<div className="flex items-center gap-3"> <Download className="size-4" />
<Button variant="outline" title="Coming soon"> Export Data
<Download className="size-4" /> </Button>
Export Data </>
</Button> }
</div> />
</div>
<div className="mb-6 grid grid-cols-[repeat(auto-fit,minmax(7rem,1fr))] gap-4"> <div className="mt-6 mb-6 grid grid-cols-[repeat(auto-fit,minmax(7rem,1fr))] gap-4">
<MetricCard label="Total Evaluations" value={metrics.totalRequests.toLocaleString()} /> <MetricCard label="Total Evaluations" value={metrics.totalRequests.toLocaleString()} />
<MetricCard <MetricCard
label="Blocked Requests" label="Blocked Requests"

View file

@ -82,6 +82,8 @@ describe("ProjectsPage", () => {
mockUseProjects.mockReturnValue({ data: [], isLoading: false }); mockUseProjects.mockReturnValue({ data: [], isLoading: false });
renderWithProviders(<ProjectsPage />); renderWithProviders(<ProjectsPage />);
expect(screen.getByRole("heading", { name: /projects/i })).toBeInTheDocument(); expect(screen.getByRole("heading", { name: /projects/i })).toBeInTheDocument();
expect(screen.getByText("Manage projects within your teams")).toBeInTheDocument();
expect(document.querySelector(".lucide-folder")).not.toBeNull();
}); });
it("should show a 'Create Project' button", () => { it("should show a 'Create Project' button", () => {

View file

@ -1,9 +1,9 @@
import { useProjects } from "@/app/(dashboard)/hooks/projects/useProjects"; import { useProjects } from "@/app/(dashboard)/hooks/projects/useProjects";
import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams"; import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
import { Plus, SearchIcon, X } from "lucide-react"; import { Folder, Plus, SearchIcon, X } from "lucide-react";
import { parseAsString, useQueryState } from "nuqs"; import { parseAsString, useQueryState } from "nuqs";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { LegacyPageHeader } from "@/components/shared/LegacyPageHeader"; import { PageHeader } from "@/components/shared/PageHeader";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group"; import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group";
import { CreateProjectModal } from "./ProjectModals/CreateProjectModal"; import { CreateProjectModal } from "./ProjectModals/CreateProjectModal";
@ -54,21 +54,20 @@ export function ProjectsPage() {
} }
return ( return (
<div className="p-6 px-12"> <div className="p-8">
<div className="mb-4"> <PageHeader
<LegacyPageHeader icon={<Folder />}
title="Projects" title="Projects"
subtitle="Manage projects within your teams" subtitle="Manage projects within your teams"
actions={ primaryAction={
<Button onClick={() => setIsCreateModalVisible(true)}> <Button onClick={() => setIsCreateModalVisible(true)}>
<Plus className="size-4" /> <Plus className="size-4" />
Create Project Create Project
</Button> </Button>
} }
/> />
</div>
<div className="mb-3 flex items-center"> <div className="mt-6 mb-3 flex items-center">
<InputGroup className="max-w-[400px]"> <InputGroup className="max-w-[400px]">
<InputGroupAddon> <InputGroupAddon>
<SearchIcon className="size-4 text-muted-foreground" /> <SearchIcon className="size-4 text-muted-foreground" />

View file

@ -207,6 +207,8 @@ it("left-anchors the create-key CTA below the title, between the header and the
renderWithProviders(<VirtualKeysTable headerActions={<button>Create New Key</button>} />); renderWithProviders(<VirtualKeysTable headerActions={<button>Create New Key</button>} />);
const heading = screen.getByRole("heading", { name: "Virtual Keys" }); const heading = screen.getByRole("heading", { name: "Virtual Keys" });
expect(screen.getByText("Every key that authenticates requests to the gateway.")).toBeInTheDocument();
expect(document.querySelector(".lucide-key-round")).not.toBeNull();
const ctas = screen.getAllByRole("button", { name: "Create New Key" }); const ctas = screen.getAllByRole("button", { name: "Create New Key" });
expect(ctas).toHaveLength(1); expect(ctas).toHaveLength(1);
const cta = ctas[0]; const cta = ctas[0];

View file

@ -12,7 +12,7 @@ import {
DataTableToolbar, DataTableToolbar,
} from "@/components/shared/DataTable"; } from "@/components/shared/DataTable";
import { SearchSelect } from "@/components/shared/SearchSelect"; import { SearchSelect } from "@/components/shared/SearchSelect";
import { LegacyPageHeader } from "@/components/shared/LegacyPageHeader"; import { PageHeader } from "@/components/shared/PageHeader";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { useDebouncedValue } from "@tanstack/react-pacer/debouncer"; import { useDebouncedValue } from "@tanstack/react-pacer/debouncer";
import { ColumnFiltersState, OnChangeFn, PaginationState, SortingState } from "@tanstack/react-table"; import { ColumnFiltersState, OnChangeFn, PaginationState, SortingState } from "@tanstack/react-table";
@ -182,13 +182,13 @@ export function VirtualKeysTable({ headerActions }: VirtualKeysTableProps) {
} }
return ( return (
<div className="flex h-full flex-col gap-4 overflow-hidden py-2"> <div className="flex h-full flex-col gap-6 overflow-hidden">
<LegacyPageHeader <PageHeader
icon={<KeyRound className="size-5" />} icon={<KeyRound />}
title="Virtual Keys" title="Virtual Keys"
subtitle="Every key that authenticates requests to the gateway." subtitle="Every key that authenticates requests to the gateway."
primaryAction={headerActions}
/> />
{headerActions}
<DataTable <DataTable
data={keyList} data={keyList}
columns={columns} columns={columns}

View file

@ -1,33 +0,0 @@
import { renderWithProviders, screen } from "@/../tests/test-utils";
import { describe, expect, it } from "vitest";
import { LegacyPageHeader } from "./LegacyPageHeader";
describe("LegacyPageHeader", () => {
it("should render the title as a heading", () => {
renderWithProviders(<LegacyPageHeader title="Virtual Keys" />);
expect(screen.getByRole("heading", { name: "Virtual Keys" })).toBeInTheDocument();
});
it("should render the optional identity and actions", () => {
renderWithProviders(
<LegacyPageHeader
title="Virtual Keys"
subtitle="Every key that authenticates requests"
icon={<span>Key icon</span>}
actions={<button>Create New Key</button>}
/>,
);
expect(screen.getByText("Every key that authenticates requests")).toBeInTheDocument();
expect(screen.getByText("Key icon")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Create New Key" })).toBeInTheDocument();
});
it("should omit optional actions when none are provided", () => {
renderWithProviders(<LegacyPageHeader title="Virtual Keys" />);
expect(screen.queryByRole("button")).not.toBeInTheDocument();
});
});

View file

@ -1,25 +0,0 @@
"use client";
import * as React from "react";
interface LegacyPageHeaderProps {
title: React.ReactNode;
subtitle?: React.ReactNode;
icon?: React.ReactNode;
actions?: React.ReactNode;
}
export function LegacyPageHeader({ title, subtitle, icon, actions }: LegacyPageHeaderProps) {
return (
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="flex items-center gap-2.5">
{icon != null && <span className="flex flex-none items-center text-foreground">{icon}</span>}
<div className="min-w-0">
<h1 className="text-xl font-semibold tracking-tight text-foreground">{title}</h1>
{subtitle != null && <p className="mt-0.5 text-sm text-muted-foreground">{subtitle}</p>}
</div>
</div>
{actions != null && <div className="flex items-center gap-2">{actions}</div>}
</div>
);
}

View file

@ -216,27 +216,25 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
const canCreateKey = userRole !== "Admin Viewer" && userRole !== "proxy_admin_viewer"; const canCreateKey = userRole !== "Admin Viewer" && userRole !== "proxy_admin_viewer";
return ( return (
<div className="mx-4 h-[75vh]"> <main className="h-[75vh] p-8">
<div className="grid grid-cols-1 gap-2 p-8 w-full mt-2"> <div className="flex h-full flex-col">
<div className="col-span-1 flex flex-col gap-2"> <VirtualKeysTable
<VirtualKeysTable headerActions={
headerActions={ canCreateKey ? (
canCreateKey ? ( <CreateKey
<CreateKey key={selectedTeam ? selectedTeam.team_id : null}
key={selectedTeam ? selectedTeam.team_id : null} team={selectedTeam as Team | null}
team={selectedTeam as Team | null} teams={teams as Team[]}
teams={teams as Team[]} data={keys}
data={keys} addKey={addKey}
addKey={addKey} autoOpenCreate={autoOpenCreate}
autoOpenCreate={autoOpenCreate} prefillData={prefillData}
prefillData={prefillData} />
/> ) : undefined
) : undefined }
} />
/>
</div>
</div> </div>
</div> </main>
); );
}; };