mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
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:
parent
3e2927de9a
commit
ca77b32454
16 changed files with 198 additions and 226 deletions
|
|
@ -99,6 +99,8 @@ 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();
|
||||
expect(screen.getByText("Manage resource permissions for your organization")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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,8 @@ 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.getByText("Monitor guardrail performance across all requests")).toBeInTheDocument();
|
||||
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