mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
Merge pull request #38306 from BerriAI/litellm_/page-header-spec-rollout-46f0dd
refactor(ui): move every page header onto the shared PageHeader
This commit is contained in:
commit
1a431687f0
16 changed files with 196 additions and 226 deletions
|
|
@ -99,6 +99,7 @@ describe("AccessGroupsPage", () => {
|
|||
renderWithProviders(<AccessGroupsPage />);
|
||||
expect(screen.getByRole("heading", { name: "Access Groups" })).toBeInTheDocument();
|
||||
expect(screen.getByText("Manage resource permissions for your organization")).toBeInTheDocument();
|
||||
expect(document.querySelector(".lucide-boxes")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("shows the Create Access Group button for an admin", () => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { AccessGroupResponse, useAccessGroups } from "@/app/(dashboard)/hooks/accessGroups/useAccessGroups";
|
||||
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 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 { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group";
|
||||
import { AccessGroupDetail } from "./AccessGroupsDetailsPage";
|
||||
|
|
@ -59,23 +59,22 @@ export function AccessGroupsPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 px-12">
|
||||
<div className="mb-4">
|
||||
<LegacyPageHeader
|
||||
title="Access Groups"
|
||||
subtitle="Manage resource permissions for your organization"
|
||||
actions={
|
||||
canModify ? (
|
||||
<Button onClick={() => setIsCreateModalVisible(true)}>
|
||||
<Plus className="size-4" />
|
||||
Create Access Group
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-8">
|
||||
<PageHeader
|
||||
icon={<Boxes />}
|
||||
title="Access Groups"
|
||||
subtitle="Manage resource permissions for your organization"
|
||||
primaryAction={
|
||||
canModify ? (
|
||||
<Button onClick={() => setIsCreateModalVisible(true)}>
|
||||
<Plus className="size-4" />
|
||||
Create Access Group
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="mb-3 flex items-center">
|
||||
<div className="mt-6 mb-3 flex items-center">
|
||||
<InputGroup className="max-w-[400px]">
|
||||
<InputGroupAddon>
|
||||
<SearchIcon className="size-4 text-muted-foreground" />
|
||||
|
|
|
|||
|
|
@ -85,6 +85,14 @@ describe("Budget Panel", () => {
|
|||
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 () => {
|
||||
renderPanel();
|
||||
await waitFor(() => expect(getMock).toHaveBeenCalled());
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
|||
import { prism } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
|
||||
import { useSyntaxTheme } from "@/hooks/useSyntaxTheme";
|
||||
import { LegacyPageHeader } from "@/components/shared/LegacyPageHeader";
|
||||
import { ToolbarSeparator } from "@/components/shared/ToolbarSeparator";
|
||||
import { PageHeader } from "@/components/shared/PageHeader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import DeleteResourceModal from "@/components/common_components/DeleteResourceModal";
|
||||
|
|
@ -79,34 +78,37 @@ const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-4 p-6 px-12">
|
||||
<LegacyPageHeader
|
||||
icon={<Wallet className="size-5" />}
|
||||
title="Budgets"
|
||||
subtitle="Spend, TPM and RPM limits you can assign to customers."
|
||||
/>
|
||||
<Tabs defaultValue="budgets" className="min-h-0 flex-1 gap-0">
|
||||
<div className="flex items-center gap-4 border-b border-border">
|
||||
{canModify && (
|
||||
<>
|
||||
<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>
|
||||
<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>
|
||||
<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} />
|
||||
{selectedBudget && (
|
||||
<EditBudgetModal
|
||||
|
|
@ -172,7 +174,7 @@ const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
|
|||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,14 @@ describe("CostOptimizationView", () => {
|
|||
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", () => {
|
||||
const { getByText } = renderView();
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Info, PiggyBank } from "lucide-react";
|
|||
import useCan from "@/app/(dashboard)/hooks/useCan";
|
||||
import PaginationStatusAlerts from "@/components/shared/PaginationStatusAlerts";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { PageHeader } from "@/components/shared/PageHeader";
|
||||
import UsageTab from "./UsageTab";
|
||||
import PromptCompressionTab from "./PromptCompressionTab";
|
||||
import PromptCachingTab from "./PromptCachingTab";
|
||||
|
|
@ -32,63 +33,63 @@ const CostOptimizationView: React.FC<CostOptimizationViewProps> = ({ accessToken
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-6 p-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<PiggyBank className="size-6 text-primary" strokeWidth={1.75} />
|
||||
<h1 className="text-xl font-semibold text-foreground">Cost Optimization</h1>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
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>
|
||||
</div>
|
||||
|
||||
<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}
|
||||
/>
|
||||
<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
|
||||
<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>
|
||||
<TabsTrigger value="caching" className="flex-none rounded-none px-4 py-2">
|
||||
Prompt Caching
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="autorouter-usage" className="flex-none rounded-none px-4 py-2">
|
||||
Auto-Router
|
||||
</TabsTrigger>
|
||||
</>
|
||||
{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>
|
||||
</>
|
||||
)}
|
||||
</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")}>
|
||||
<UsageTab accessToken={accessToken} activity={activity} />
|
||||
</TabsContent>
|
||||
|
|
@ -106,7 +107,7 @@ const CostOptimizationView: React.FC<CostOptimizationViewProps> = ({ accessToken
|
|||
</>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -41,27 +41,32 @@ export default function GuardrailsMonitorView({ accessToken = null }: Guardrails
|
|||
setView({ type: "overview" });
|
||||
};
|
||||
|
||||
const dateRangeControl = (
|
||||
<AdvancedDatePicker value={dateValue} onValueChange={handleDateChange} label="" showTimeRange={false} />
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6 w-full min-w-0 flex-1">
|
||||
<div className="flex items-center justify-end mb-4">
|
||||
<AdvancedDatePicker value={dateValue} onValueChange={handleDateChange} label="" showTimeRange={false} />
|
||||
</div>
|
||||
<main className="w-full min-w-0 flex-1 p-8">
|
||||
{view.type === "overview" ? (
|
||||
<GuardrailsOverview
|
||||
accessToken={accessToken}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onSelectGuardrail={handleSelectGuardrail}
|
||||
dateRangeControl={dateRangeControl}
|
||||
/>
|
||||
) : (
|
||||
<GuardrailDetail
|
||||
guardrailId={view.guardrailId}
|
||||
onBack={handleBack}
|
||||
accessToken={accessToken}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
/>
|
||||
<>
|
||||
<div className="mb-4 flex items-center justify-end">{dateRangeControl}</div>
|
||||
<GuardrailDetail
|
||||
guardrailId={view.guardrailId}
|
||||
onBack={handleBack}
|
||||
accessToken={accessToken}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ describe("GuardrailsOverview", () => {
|
|||
|
||||
expect(await screen.findByRole("heading", { name: "Guardrails Monitor", level: 1 })).toBeInTheDocument();
|
||||
expect(screen.getByText("Monitor guardrail performance across all requests")).toBeInTheDocument();
|
||||
expect(document.querySelector(".lucide-heart-pulse")).not.toBeNull();
|
||||
expect(screen.getByRole("button", { name: /Export Data/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
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 { DataTable, DataTableSortHeader } from "@/components/shared/DataTable";
|
||||
import { getGuardrailsUsageOverview } from "@/components/networking";
|
||||
import { type PerformanceRow } from "@/components/GuardrailsMonitor/mockData";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PageHeader } from "@/components/shared/PageHeader";
|
||||
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
|
||||
import { EvaluationSettingsModal } from "./EvaluationSettingsModal";
|
||||
import { MetricCard } from "@/components/GuardrailsMonitor/MetricCard";
|
||||
|
|
@ -16,6 +17,7 @@ interface GuardrailsOverviewProps {
|
|||
startDate: string;
|
||||
endDate: string;
|
||||
onSelectGuardrail: (id: string) => void;
|
||||
dateRangeControl?: React.ReactNode;
|
||||
}
|
||||
|
||||
type SortKey = "failRate" | "requestsEvaluated" | "avgLatency" | "falsePositiveRate" | "falseNegativeRate";
|
||||
|
|
@ -43,6 +45,7 @@ export function GuardrailsOverview({
|
|||
startDate,
|
||||
endDate,
|
||||
onSelectGuardrail,
|
||||
dateRangeControl,
|
||||
}: GuardrailsOverviewProps) {
|
||||
const [sortBy, setSortBy] = useState<SortKey>("failRate");
|
||||
const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
|
||||
|
|
@ -197,23 +200,22 @@ export function GuardrailsOverview({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-start justify-between mb-5">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Shield className="size-5 text-indigo-500" />
|
||||
<h1 className="text-xl font-semibold text-foreground">Guardrails Monitor</h1>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Monitor guardrail performance across all requests</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline" title="Coming soon">
|
||||
<Download className="size-4" />
|
||||
Export Data
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<PageHeader
|
||||
icon={<HeartPulse />}
|
||||
title="Guardrails Monitor"
|
||||
subtitle="Monitor guardrail performance across all requests"
|
||||
utilities={
|
||||
<>
|
||||
{dateRangeControl}
|
||||
<Button variant="outline" title="Coming soon">
|
||||
<Download className="size-4" />
|
||||
Export Data
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<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="Blocked Requests"
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ describe("ProjectsPage", () => {
|
|||
mockUseProjects.mockReturnValue({ data: [], isLoading: false });
|
||||
renderWithProviders(<ProjectsPage />);
|
||||
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", () => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { useProjects } from "@/app/(dashboard)/hooks/projects/useProjects";
|
||||
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 { useMemo, useState } from "react";
|
||||
import { LegacyPageHeader } from "@/components/shared/LegacyPageHeader";
|
||||
import { PageHeader } 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";
|
||||
|
|
@ -54,21 +54,20 @@ export function ProjectsPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 px-12">
|
||||
<div className="mb-4">
|
||||
<LegacyPageHeader
|
||||
title="Projects"
|
||||
subtitle="Manage projects within your teams"
|
||||
actions={
|
||||
<Button onClick={() => setIsCreateModalVisible(true)}>
|
||||
<Plus className="size-4" />
|
||||
Create Project
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-8">
|
||||
<PageHeader
|
||||
icon={<Folder />}
|
||||
title="Projects"
|
||||
subtitle="Manage projects within your teams"
|
||||
primaryAction={
|
||||
<Button onClick={() => setIsCreateModalVisible(true)}>
|
||||
<Plus className="size-4" />
|
||||
Create Project
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="mb-3 flex items-center">
|
||||
<div className="mt-6 mb-3 flex items-center">
|
||||
<InputGroup className="max-w-[400px]">
|
||||
<InputGroupAddon>
|
||||
<SearchIcon className="size-4 text-muted-foreground" />
|
||||
|
|
|
|||
|
|
@ -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>} />);
|
||||
|
||||
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" });
|
||||
expect(ctas).toHaveLength(1);
|
||||
const cta = ctas[0];
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
DataTableToolbar,
|
||||
} from "@/components/shared/DataTable";
|
||||
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 { useDebouncedValue } from "@tanstack/react-pacer/debouncer";
|
||||
import { ColumnFiltersState, OnChangeFn, PaginationState, SortingState } from "@tanstack/react-table";
|
||||
|
|
@ -182,13 +182,13 @@ export function VirtualKeysTable({ headerActions }: VirtualKeysTableProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-4 overflow-hidden py-2">
|
||||
<LegacyPageHeader
|
||||
icon={<KeyRound className="size-5" />}
|
||||
<div className="flex h-full flex-col gap-6 overflow-hidden">
|
||||
<PageHeader
|
||||
icon={<KeyRound />}
|
||||
title="Virtual Keys"
|
||||
subtitle="Every key that authenticates requests to the gateway."
|
||||
primaryAction={headerActions}
|
||||
/>
|
||||
{headerActions}
|
||||
<DataTable
|
||||
data={keyList}
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -216,27 +216,25 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
|||
const canCreateKey = userRole !== "Admin Viewer" && userRole !== "proxy_admin_viewer";
|
||||
|
||||
return (
|
||||
<div className="mx-4 h-[75vh]">
|
||||
<div className="grid grid-cols-1 gap-2 p-8 w-full mt-2">
|
||||
<div className="col-span-1 flex flex-col gap-2">
|
||||
<VirtualKeysTable
|
||||
headerActions={
|
||||
canCreateKey ? (
|
||||
<CreateKey
|
||||
key={selectedTeam ? selectedTeam.team_id : null}
|
||||
team={selectedTeam as Team | null}
|
||||
teams={teams as Team[]}
|
||||
data={keys}
|
||||
addKey={addKey}
|
||||
autoOpenCreate={autoOpenCreate}
|
||||
prefillData={prefillData}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<main className="h-[75vh] p-8">
|
||||
<div className="flex h-full flex-col">
|
||||
<VirtualKeysTable
|
||||
headerActions={
|
||||
canCreateKey ? (
|
||||
<CreateKey
|
||||
key={selectedTeam ? selectedTeam.team_id : null}
|
||||
team={selectedTeam as Team | null}
|
||||
teams={teams as Team[]}
|
||||
data={keys}
|
||||
addKey={addKey}
|
||||
autoOpenCreate={autoOpenCreate}
|
||||
prefillData={prefillData}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue