mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
Merge pull request #36913 from BerriAI/litellm_shadcn_permissions_0814
refactor(ui): migrate key info and permissions views off antd and tremor
This commit is contained in:
commit
3d35cff9f5
8 changed files with 416 additions and 384 deletions
|
|
@ -2749,11 +2749,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/permissions/AgentPermissions.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/components/permissions/MCPServerPermissions.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 3
|
||||
|
|
@ -2762,11 +2757,6 @@
|
|||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/components/permissions/VectorStorePermissions.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/policies/PolicySelector.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
|
|
@ -3045,9 +3035,6 @@
|
|||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
|
|
@ -3062,11 +3049,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/templates/KeyInfoHeader.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/components/templates/key_edit_view.tsx": {
|
||||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
|
|
@ -3096,9 +3078,6 @@
|
|||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Text, Badge } from "@tremor/react";
|
||||
import { UserGroupIcon } from "@heroicons/react/outline";
|
||||
import { Tooltip } from "antd";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { getAgentsList } from "../networking";
|
||||
|
||||
interface Agent {
|
||||
|
|
@ -58,10 +58,8 @@ export function AgentPermissions({ agents, agentAccessGroups = [], accessToken }
|
|||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<UserGroupIcon className="h-4 w-4 text-purple-600" />
|
||||
<Text className="font-semibold text-gray-900">Agents</Text>
|
||||
<Badge color="purple" size="xs">
|
||||
{totalCount}
|
||||
</Badge>
|
||||
<p className="text-sm font-semibold text-gray-900">Agents</p>
|
||||
<Badge variant="secondary">{totalCount}</Badge>
|
||||
</div>
|
||||
|
||||
{totalCount > 0 ? (
|
||||
|
|
@ -71,14 +69,17 @@ export function AgentPermissions({ agents, agentAccessGroups = [], accessToken }
|
|||
<div className="flex items-center gap-3 py-2 px-3 rounded-lg border border-gray-200 bg-white">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
{item.type === "agent" ? (
|
||||
<Tooltip title={`Full ID: ${item.value}`} placement="top">
|
||||
<div className="inline-flex items-center gap-2 min-w-0">
|
||||
<span className="inline-block w-1.5 h-1.5 bg-purple-500 rounded-full shrink-0"></span>
|
||||
<span className="text-sm font-medium text-gray-900 truncate">
|
||||
{getAgentDisplayName(item.value)}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<TooltipProvider delay={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<div className="inline-flex items-center gap-2 min-w-0" />}>
|
||||
<span className="inline-block w-1.5 h-1.5 bg-purple-500 rounded-full shrink-0"></span>
|
||||
<span className="text-sm font-medium text-gray-900 truncate">
|
||||
{getAgentDisplayName(item.value)}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{`Full ID: ${item.value}`}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<div className="inline-flex items-center gap-2 min-w-0">
|
||||
<span className="inline-block w-1.5 h-1.5 bg-green-500 rounded-full shrink-0"></span>
|
||||
|
|
@ -96,7 +97,7 @@ export function AgentPermissions({ agents, agentAccessGroups = [], accessToken }
|
|||
) : (
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-50 border border-gray-200">
|
||||
<UserGroupIcon className="h-4 w-4 text-gray-400" />
|
||||
<Text className="text-gray-500 text-sm">No agents or access groups configured</Text>
|
||||
<p className="text-gray-500 text-sm">No agents or access groups configured</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Text, Badge } from "@tremor/react";
|
||||
import { DatabaseIcon } from "@heroicons/react/outline";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { vectorStoreListCall } from "../networking";
|
||||
|
||||
interface VectorStoreDetails {
|
||||
|
|
@ -52,10 +52,8 @@ export function VectorStorePermissions({ vectorStores, accessToken }: VectorStor
|
|||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<DatabaseIcon className="h-4 w-4 text-blue-600" />
|
||||
<Text className="font-semibold text-gray-900">Vector Stores</Text>
|
||||
<Badge color="blue" size="xs">
|
||||
{vectorStores.length}
|
||||
</Badge>
|
||||
<p className="text-sm font-semibold text-gray-900">Vector Stores</p>
|
||||
<Badge variant="secondary">{vectorStores.length}</Badge>
|
||||
</div>
|
||||
|
||||
{vectorStores.length > 0 ? (
|
||||
|
|
@ -63,7 +61,7 @@ export function VectorStorePermissions({ vectorStores, accessToken }: VectorStor
|
|||
{vectorStores.map((store, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="inline-flex items-center px-3 py-1.5 rounded-lg bg-blue-50 border border-blue-200 text-blue-800 text-sm font-medium"
|
||||
className="inline-flex min-w-0 items-center px-3 py-1.5 rounded-lg bg-blue-50 border border-blue-200 text-blue-800 text-sm font-medium break-words"
|
||||
>
|
||||
{getVectorStoreDisplayName(store)}
|
||||
</div>
|
||||
|
|
@ -72,7 +70,7 @@ export function VectorStorePermissions({ vectorStores, accessToken }: VectorStor
|
|||
) : (
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-50 border border-gray-200">
|
||||
<DatabaseIcon className="h-4 w-4 text-gray-400" />
|
||||
<Text className="text-gray-500 text-sm">No vector stores configured</Text>
|
||||
<p className="text-gray-500 text-sm">No vector stores configured</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import * as networking from "@/components/networking";
|
||||
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { act, fireEvent, screen, waitFor, within } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import MemberPermissions from "./member_permissions";
|
||||
|
|
@ -9,6 +9,9 @@ vi.mock("@/components/networking", () => ({
|
|||
teamPermissionsUpdateCall: vi.fn(),
|
||||
}));
|
||||
|
||||
const checkboxFor = (endpoint: string) =>
|
||||
within(screen.getByText(endpoint).closest("tr") as HTMLElement).getByRole("checkbox");
|
||||
|
||||
describe("MemberPermissions", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
|
@ -69,32 +72,27 @@ describe("MemberPermissions", () => {
|
|||
expect(screen.getByText("Member Permissions")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const checkboxes = screen.getAllByRole("checkbox");
|
||||
const unselectedCheckbox = checkboxes.find((cb) => !(cb as HTMLInputElement).checked);
|
||||
expect(checkboxFor("/key/generate")).toBeChecked();
|
||||
expect(checkboxFor("/key/list")).not.toBeChecked();
|
||||
|
||||
if (unselectedCheckbox) {
|
||||
await act(async () => {
|
||||
fireEvent.click(unselectedCheckbox);
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(checkboxFor("/key/list"));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const saveButton = screen.getByRole("button", { name: /save changes/i });
|
||||
expect(saveButton).toBeInTheDocument();
|
||||
});
|
||||
expect(checkboxFor("/key/list")).toBeChecked();
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /save changes/i });
|
||||
await act(async () => {
|
||||
fireEvent.click(saveButton);
|
||||
});
|
||||
const saveButton = await screen.findByRole("button", { name: /save changes/i });
|
||||
await act(async () => {
|
||||
fireEvent.click(saveButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(networking.teamPermissionsUpdateCall).toHaveBeenCalledWith(
|
||||
"token-123",
|
||||
"team-123",
|
||||
expect.arrayContaining(["/key/generate", "/key/list"]),
|
||||
);
|
||||
});
|
||||
}
|
||||
await waitFor(() => {
|
||||
expect(networking.teamPermissionsUpdateCall).toHaveBeenCalledWith(
|
||||
"token-123",
|
||||
"team-123",
|
||||
expect.arrayContaining(["/key/generate", "/key/list"]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should render team daily activity permission with correct method and description", async () => {
|
||||
|
|
@ -123,11 +121,13 @@ describe("MemberPermissions", () => {
|
|||
expect(screen.getByText("Member Permissions")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const checkboxes = screen.getAllByRole("checkbox");
|
||||
checkboxes.forEach((checkbox) => {
|
||||
expect(checkbox).toBeDisabled();
|
||||
expect(checkboxFor("/key/list")).not.toBeChecked();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(checkboxFor("/key/list"));
|
||||
});
|
||||
|
||||
expect(checkboxFor("/key/list")).not.toBeChecked();
|
||||
expect(screen.queryByRole("button", { name: /save changes/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -143,32 +143,27 @@ describe("MemberPermissions", () => {
|
|||
expect(screen.getByText("Member Permissions")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const checkboxes = screen.getAllByRole("checkbox");
|
||||
const unselectedCheckbox = checkboxes.find((cb) => !(cb as HTMLInputElement).checked);
|
||||
await act(async () => {
|
||||
fireEvent.click(checkboxFor("/key/list"));
|
||||
});
|
||||
|
||||
if (unselectedCheckbox) {
|
||||
await act(async () => {
|
||||
fireEvent.click(unselectedCheckbox);
|
||||
});
|
||||
expect(checkboxFor("/key/list")).toBeChecked();
|
||||
|
||||
await waitFor(() => {
|
||||
const resetButton = screen.getByRole("button", { name: /reset/i });
|
||||
expect(resetButton).toBeInTheDocument();
|
||||
});
|
||||
vi.mocked(networking.getTeamPermissionsCall).mockResolvedValueOnce({
|
||||
all_available_permissions: ["/key/generate", "/key/list"],
|
||||
team_member_permissions: ["/key/generate"],
|
||||
});
|
||||
|
||||
vi.mocked(networking.getTeamPermissionsCall).mockResolvedValueOnce({
|
||||
all_available_permissions: ["/key/generate", "/key/list"],
|
||||
team_member_permissions: ["/key/generate"],
|
||||
});
|
||||
const resetButton = await screen.findByRole("button", { name: /reset/i });
|
||||
await act(async () => {
|
||||
fireEvent.click(resetButton);
|
||||
});
|
||||
|
||||
const resetButton = screen.getByRole("button", { name: /reset/i });
|
||||
await act(async () => {
|
||||
fireEvent.click(resetButton);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(networking.getTeamPermissionsCall).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(networking.getTeamPermissionsCall).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
}
|
||||
expect(checkboxFor("/key/list")).not.toBeChecked();
|
||||
expect(screen.queryByRole("button", { name: /save changes/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { getTeamPermissionsCall, teamPermissionsUpdateCall } from "@/components/networking";
|
||||
import { ReloadOutlined, SaveOutlined } from "@ant-design/icons";
|
||||
import { Card, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Text, Title } from "@tremor/react";
|
||||
import { Button, Checkbox, Empty } from "antd";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { RotateCw, Save } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import { getPermissionInfo } from "./permission_definitions";
|
||||
|
|
@ -75,36 +77,38 @@ const MemberPermissions: React.FC<MemberPermissionsProps> = ({ teamId, accessTok
|
|||
const hasPermissions = permissions.length > 0;
|
||||
|
||||
return (
|
||||
<Card className="bg-white shadow-md rounded-md p-6">
|
||||
<Card className="block bg-white shadow-md rounded-md p-6">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center border-b pb-4 mb-6">
|
||||
<Title className="mb-2 sm:mb-0">Member Permissions</Title>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2 sm:mb-0">Member Permissions</h3>
|
||||
{canEditTeam && hasChanges && (
|
||||
<div className="flex gap-3">
|
||||
<Button icon={<ReloadOutlined />} onClick={handleReset}>
|
||||
<Button variant="outline" onClick={handleReset}>
|
||||
<RotateCw className="size-3.5" />
|
||||
Reset
|
||||
</Button>
|
||||
<Button onClick={handleSave} loading={saving} type="primary" icon={<SaveOutlined />}>
|
||||
<Button onClick={handleSave} disabled={saving}>
|
||||
<Save className="size-3.5" />
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Text className="mb-6 text-gray-600">Control what team members can do when they are not team admins.</Text>
|
||||
<p className="mb-6 text-sm text-gray-600">Control what team members can do when they are not team admins.</p>
|
||||
|
||||
{hasPermissions ? (
|
||||
<div className="overflow-x-auto">
|
||||
<Table className=" min-w-full">
|
||||
<TableHead>
|
||||
<Table className="min-w-full">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Method</TableHeaderCell>
|
||||
<TableHeaderCell>Endpoint</TableHeaderCell>
|
||||
<TableHeaderCell>Description</TableHeaderCell>
|
||||
<TableHeaderCell className="sticky right-0 bg-white shadow-[-4px_0_4px_-4px_rgba(0,0,0,0.1)] text-center">
|
||||
<TableHead>Method</TableHead>
|
||||
<TableHead>Endpoint</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
<TableHead className="sticky right-0 bg-white shadow-[-4px_0_4px_-4px_rgba(0,0,0,0.1)] text-center">
|
||||
Allow Access
|
||||
</TableHeaderCell>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{permissions.map((permission) => {
|
||||
const permInfo = getPermissionInfo(permission);
|
||||
|
|
@ -125,8 +129,9 @@ const MemberPermissions: React.FC<MemberPermissionsProps> = ({ teamId, accessTok
|
|||
<TableCell className="text-gray-700">{permInfo.description}</TableCell>
|
||||
<TableCell className="sticky right-0 bg-white shadow-[-4px_0_4px_-4px_rgba(0,0,0,0.1)] text-center">
|
||||
<Checkbox
|
||||
className="mx-auto"
|
||||
checked={selectedPermissions.includes(permission)}
|
||||
onChange={(e) => handlePermissionChange(permission, e.target.checked)}
|
||||
onCheckedChange={(checked) => handlePermissionChange(permission, checked)}
|
||||
disabled={!canEditTeam}
|
||||
/>
|
||||
</TableCell>
|
||||
|
|
@ -138,7 +143,7 @@ const MemberPermissions: React.FC<MemberPermissionsProps> = ({ teamId, accessTok
|
|||
</div>
|
||||
) : (
|
||||
<div className="py-12">
|
||||
<Empty description="No permissions available" />
|
||||
<p className="text-center text-sm text-gray-500">No permissions available</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -1,27 +1,35 @@
|
|||
import React from "react";
|
||||
import { Button, Typography, Tooltip, Space, Divider, Flex, Popover, Dropdown, Tag } from "antd";
|
||||
import type { MenuProps } from "antd";
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
SyncOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
UserOutlined,
|
||||
CalendarOutlined,
|
||||
ClockCircleOutlined,
|
||||
ThunderboltOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
TransactionOutlined,
|
||||
FieldTimeOutlined,
|
||||
MoreOutlined,
|
||||
StopOutlined,
|
||||
CheckCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
ArrowLeft,
|
||||
ArrowLeftRight,
|
||||
Ban,
|
||||
Calendar,
|
||||
CircleCheck,
|
||||
Clock,
|
||||
MoreVertical,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
ShieldCheck,
|
||||
Timer,
|
||||
Trash2,
|
||||
User,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import CopyButton from "@/components/shared/CopyButton";
|
||||
import LabeledField from "../common_components/LabeledField";
|
||||
import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
export interface KeyInfoData {
|
||||
keyName: string;
|
||||
keyId: string;
|
||||
|
|
@ -52,14 +60,12 @@ interface KeyInfoHeaderProps {
|
|||
|
||||
function UserField({ userAlias, userEmail, userId }: { userAlias?: string | null; userEmail: string; userId: string }) {
|
||||
const labelEl = (
|
||||
<Space size={4}>
|
||||
<Text type="secondary">
|
||||
<UserOutlined />
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12, textTransform: "uppercase", letterSpacing: "0.05em" }}>
|
||||
User
|
||||
</Text>
|
||||
</Space>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-muted-foreground">
|
||||
<User className="size-3.5" />
|
||||
</span>
|
||||
<span className="text-xs uppercase tracking-[0.05em] text-muted-foreground">User</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const isEmpty = !userAlias && !userEmail && !userId;
|
||||
|
|
@ -68,7 +74,7 @@ function UserField({ userAlias, userEmail, userId }: { userAlias?: string | null
|
|||
<div>
|
||||
{labelEl}
|
||||
<div>
|
||||
<Text strong>-</Text>
|
||||
<span className="font-semibold">-</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -87,14 +93,12 @@ function UserField({ userAlias, userEmail, userId }: { userAlias?: string | null
|
|||
<div key={label} className="flex flex-col min-w-0">
|
||||
<span className="text-gray-400">{label}</span>
|
||||
{value ? (
|
||||
<Typography.Text
|
||||
className="font-mono text-xs"
|
||||
style={{ maxWidth: 220 }}
|
||||
ellipsis={{ tooltip: value }}
|
||||
copyable
|
||||
>
|
||||
{value}
|
||||
</Typography.Text>
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
<span className="min-w-0 flex-1 truncate font-mono text-xs" title={value}>
|
||||
{value}
|
||||
</span>
|
||||
<CopyButton value={value} label={`Copy ${label}`} iconClassName="size-3.5" />
|
||||
</div>
|
||||
) : (
|
||||
<span className="font-mono">-</span>
|
||||
)}
|
||||
|
|
@ -108,11 +112,18 @@ function UserField({ userAlias, userEmail, userId }: { userAlias?: string | null
|
|||
<div>
|
||||
{labelEl}
|
||||
<div>
|
||||
<Popover content={popoverContent} trigger="hover" placement="bottomLeft">
|
||||
<span className="cursor-default">
|
||||
<DefaultProxyAdminTag userId={userId} />
|
||||
</span>
|
||||
</Popover>
|
||||
<HoverCard>
|
||||
<HoverCardTrigger
|
||||
render={
|
||||
<span className="cursor-default">
|
||||
<DefaultProxyAdminTag userId={userId} />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<HoverCardContent side="bottom" align="start" className="w-auto">
|
||||
{popoverContent}
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -122,11 +133,14 @@ function UserField({ userAlias, userEmail, userId }: { userAlias?: string | null
|
|||
<div>
|
||||
{labelEl}
|
||||
<div>
|
||||
<Popover content={popoverContent} trigger="hover" placement="bottomLeft">
|
||||
<Text strong ellipsis style={{ cursor: "default", maxWidth: 200, display: "block" }}>
|
||||
{displayValue}
|
||||
</Text>
|
||||
</Popover>
|
||||
<HoverCard>
|
||||
<HoverCardTrigger
|
||||
render={<span className="block max-w-[200px] cursor-default truncate font-semibold">{displayValue}</span>}
|
||||
/>
|
||||
<HoverCardContent side="bottom" align="start" className="w-auto">
|
||||
{popoverContent}
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -146,104 +160,124 @@ export function KeyInfoHeader({
|
|||
regenerateDisabled = false,
|
||||
regenerateTooltip,
|
||||
}: KeyInfoHeaderProps) {
|
||||
const destructiveActionItems: MenuProps["items"] = [
|
||||
...(onToggleBlocked
|
||||
? [
|
||||
isBlocked
|
||||
? { key: "unblock", label: "Unblock Key", icon: <CheckCircleOutlined /> }
|
||||
: { key: "block", label: "Block Key", icon: <StopOutlined />, danger: true },
|
||||
]
|
||||
: []),
|
||||
...(onResetSpend
|
||||
? [{ key: "reset-spend", label: "Reset Spend", icon: <TransactionOutlined />, danger: true }]
|
||||
: []),
|
||||
{ key: "delete", label: "Delete Key", icon: <DeleteOutlined />, danger: true },
|
||||
];
|
||||
|
||||
const handleDestructiveActionClick: MenuProps["onClick"] = ({ key }) => {
|
||||
if (key === "block" || key === "unblock") onToggleBlocked?.();
|
||||
if (key === "reset-spend") onResetSpend?.();
|
||||
if (key === "delete") onDelete?.();
|
||||
};
|
||||
const regenerateButton = (
|
||||
<span>
|
||||
<Button variant="outline" onClick={onRegenerate} disabled={regenerateDisabled}>
|
||||
<RefreshCw className="size-3.5" />
|
||||
Regenerate Key
|
||||
</Button>
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{onCreateNew && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={onCreateNew}>
|
||||
<Button onClick={onCreateNew}>
|
||||
<Plus className="size-3.5" />
|
||||
Create New Key
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Button type="text" icon={<ArrowLeftOutlined />} onClick={onBack}>
|
||||
<Button variant="ghost" onClick={onBack}>
|
||||
<ArrowLeft className="size-3.5" />
|
||||
{backButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Flex justify="space-between" align="start" style={{ marginBottom: 20 }}>
|
||||
<div>
|
||||
<Space align="center">
|
||||
<Title level={3} copyable={{ tooltips: ["Copy Key Alias", "Copied!"] }} style={{ margin: 0 }}>
|
||||
<div className="flex items-start justify-between" style={{ marginBottom: 20 }}>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="m-0 flex items-center gap-1 text-2xl font-semibold">
|
||||
{data.keyName}
|
||||
</Title>
|
||||
<CopyButton value={data.keyName} label="Copy Key Alias" iconClassName="size-4" />
|
||||
</h3>
|
||||
{isBlocked && (
|
||||
<Tag color="red" icon={<StopOutlined />}>
|
||||
<Badge variant="destructive">
|
||||
<Ban className="size-3" />
|
||||
Blocked
|
||||
</Tag>
|
||||
</Badge>
|
||||
)}
|
||||
</Space>
|
||||
<Text type="secondary" copyable={{ text: data.keyId, tooltips: ["Copy Key ID", "Copied!"] }}>
|
||||
Key ID: {data.keyId}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
<span className="min-w-0 break-words text-muted-foreground">Key ID: {data.keyId}</span>
|
||||
<CopyButton value={data.keyId} label="Copy Key ID" iconClassName="size-3.5" />
|
||||
</div>
|
||||
</div>
|
||||
{canModifyKey && (
|
||||
<Space>
|
||||
<Tooltip title={regenerateTooltip || ""}>
|
||||
<span>
|
||||
<Button icon={<SyncOutlined />} onClick={onRegenerate} disabled={regenerateDisabled}>
|
||||
Regenerate Key
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Dropdown
|
||||
menu={{ items: destructiveActionItems, onClick: handleDestructiveActionClick }}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button icon={<MoreOutlined />} aria-label="More key actions" />
|
||||
</Dropdown>
|
||||
</Space>
|
||||
<div className="flex items-center gap-2">
|
||||
{regenerateTooltip ? (
|
||||
<TooltipProvider delay={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={regenerateButton} />
|
||||
<TooltipContent>{regenerateTooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
regenerateButton
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger render={<Button variant="outline" size="icon" aria-label="More key actions" />}>
|
||||
<MoreVertical className="size-3.5" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-auto">
|
||||
{onToggleBlocked &&
|
||||
(isBlocked ? (
|
||||
<DropdownMenuItem onClick={onToggleBlocked}>
|
||||
<CircleCheck className="size-3.5" />
|
||||
Unblock Key
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem variant="destructive" onClick={onToggleBlocked}>
|
||||
<Ban className="size-3.5" />
|
||||
Block Key
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{onResetSpend && (
|
||||
<DropdownMenuItem variant="destructive" onClick={onResetSpend}>
|
||||
<ArrowLeftRight className="size-3.5" />
|
||||
Reset Spend
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem variant="destructive" onClick={onDelete}>
|
||||
<Trash2 className="size-3.5" />
|
||||
Delete Key
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
<Flex align="stretch" gap={40} style={{ marginBottom: 40 }}>
|
||||
<Space direction="vertical" size={16}>
|
||||
<div className="flex items-stretch gap-10" style={{ marginBottom: 40 }}>
|
||||
<div className="flex min-w-0 flex-col gap-4">
|
||||
<UserField userAlias={data.userAlias} userEmail={data.userEmail} userId={data.userId} />
|
||||
<LabeledField label="Expires" value={data.expires} icon={<FieldTimeOutlined />} />
|
||||
</Space>
|
||||
<LabeledField label="Expires" value={data.expires} icon={<Timer className="size-3.5" />} />
|
||||
</div>
|
||||
|
||||
<Divider type="vertical" style={{ height: "auto" }} />
|
||||
<Separator orientation="vertical" />
|
||||
|
||||
<Space direction="vertical" size={16}>
|
||||
<LabeledField label="Created At" value={data.createdAt} icon={<CalendarOutlined />} />
|
||||
<div className="flex min-w-0 flex-col gap-4">
|
||||
<LabeledField label="Created At" value={data.createdAt} icon={<Calendar className="size-3.5" />} />
|
||||
<LabeledField
|
||||
label="Created By"
|
||||
value={data.createdBy}
|
||||
icon={<SafetyCertificateOutlined />}
|
||||
icon={<ShieldCheck className="size-3.5" />}
|
||||
truncate
|
||||
copyable
|
||||
defaultUserIdCheck
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Divider type="vertical" style={{ height: "auto" }} />
|
||||
<Separator orientation="vertical" />
|
||||
|
||||
<Space direction="vertical" size={16}>
|
||||
<LabeledField label="Last Updated" value={data.lastUpdated} icon={<ClockCircleOutlined />} />
|
||||
<LabeledField label="Last Active" value={data.lastActive} icon={<ThunderboltOutlined />} />
|
||||
</Space>
|
||||
</Flex>
|
||||
<div className="flex min-w-0 flex-col gap-4">
|
||||
<LabeledField label="Last Updated" value={data.lastUpdated} icon={<Clock className="size-3.5" />} />
|
||||
<LabeledField label="Last Active" value={data.lastActive} icon={<Zap className="size-3.5" />} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,20 +182,6 @@ vi.mock("@heroicons/react/outline", async () => {
|
|||
return { ArrowLeftIcon, TrashIcon, RefreshIcon };
|
||||
});
|
||||
|
||||
vi.mock("lucide-react", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("lucide-react")>();
|
||||
const React = await import("react");
|
||||
function CopyIcon() {
|
||||
return React.createElement("span");
|
||||
}
|
||||
(CopyIcon as any).displayName = "CopyIcon";
|
||||
function CheckIcon() {
|
||||
return React.createElement("span");
|
||||
}
|
||||
(CheckIcon as any).displayName = "CheckIcon";
|
||||
return { ...actual, CopyIcon, CheckIcon };
|
||||
});
|
||||
|
||||
// Heavy children -> async factories & local React
|
||||
vi.mock("../organisms/RegenerateKeyModal", () => {
|
||||
function RegenerateKeyModal() {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings"
|
|||
import useTeams from "@/app/(dashboard)/hooks/useTeams";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { mapEmptyStringToNull } from "@/utils/keyUpdateUtils";
|
||||
import { ArrowLeftIcon } from "@heroicons/react/outline";
|
||||
import { Badge, Button, Card, Grid, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title } from "@tremor/react";
|
||||
import { Modal, Tag } from "antd";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { KeyInfoHeader } from "./KeyInfoHeader";
|
||||
import { useEffect, useState } from "react";
|
||||
import { isProxyAdminRole, isUserTeamAdminForSingleTeam, rolesWithWriteAccess } from "../../utils/roles";
|
||||
|
|
@ -150,10 +153,11 @@ export default function KeyInfoView({
|
|||
if (!currentKeyData) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Button icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
|
||||
<Button variant="ghost" onClick={onClose} className="mb-4">
|
||||
<ArrowLeft className="size-4" />
|
||||
{backButtonText}
|
||||
</Button>
|
||||
<Text>Key not found</Text>
|
||||
<p className="text-sm">Key not found</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -534,93 +538,111 @@ export default function KeyInfoView({
|
|||
/>
|
||||
|
||||
{/* Reset Spend Confirmation Modal */}
|
||||
<Modal
|
||||
title="Reset Key Spend"
|
||||
open={isResetSpendModalOpen}
|
||||
onOk={handleResetSpend}
|
||||
onCancel={() => setIsResetSpendModalOpen(false)}
|
||||
okText="Reset"
|
||||
okButtonProps={{ danger: true }}
|
||||
confirmLoading={resetSpendLoading}
|
||||
>
|
||||
<p>
|
||||
Reset spend for <strong>{currentKeyData?.key_alias || currentKeyData?.token_id || "this key"}</strong> to{" "}
|
||||
<strong>$0</strong>?
|
||||
</p>
|
||||
<p style={{ color: "#666", fontSize: "0.875rem", marginTop: 8 }}>
|
||||
Current spend: <strong>${formatNumberWithCommas(currentKeyData.spend, 4)}</strong>. Spend history is preserved
|
||||
in logs. This resets the current period spend counter, the same as an automatic budget reset.
|
||||
</p>
|
||||
</Modal>
|
||||
<Dialog open={isResetSpendModalOpen} onOpenChange={(open) => setIsResetSpendModalOpen(open)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Reset Key Spend</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p>
|
||||
Reset spend for <strong>{currentKeyData?.key_alias || currentKeyData?.token_id || "this key"}</strong> to{" "}
|
||||
<strong>$0</strong>?
|
||||
</p>
|
||||
<p style={{ color: "#666", fontSize: "0.875rem", marginTop: 8 }}>
|
||||
Current spend: <strong>${formatNumberWithCommas(currentKeyData.spend, 4)}</strong>. Spend history is
|
||||
preserved in logs. This resets the current period spend counter, the same as an automatic budget reset.
|
||||
</p>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsResetSpendModalOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleResetSpend} disabled={resetSpendLoading}>
|
||||
Reset
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Modal
|
||||
title={isBlocked ? "Unblock Key" : "Block Key"}
|
||||
open={isBlockModalOpen}
|
||||
onOk={handleToggleBlocked}
|
||||
onCancel={() => setIsBlockModalOpen(false)}
|
||||
okText={isBlocked ? "Unblock" : "Block"}
|
||||
okButtonProps={isBlocked ? undefined : { danger: true }}
|
||||
confirmLoading={blockLoading}
|
||||
>
|
||||
<p>
|
||||
{isBlocked ? "Unblock" : "Block"}{" "}
|
||||
<strong>{currentKeyData?.key_alias || currentKeyData?.token_id || "this key"}</strong>?
|
||||
</p>
|
||||
<p style={{ color: "#666", fontSize: "0.875rem", marginTop: 8 }}>
|
||||
{isBlocked
|
||||
? "Requests using this key will be accepted again."
|
||||
: "Requests using this key will be rejected with a 401 error until it is unblocked. The key is not deleted and can be unblocked at any time."}
|
||||
</p>
|
||||
</Modal>
|
||||
<Dialog open={isBlockModalOpen} onOpenChange={(open) => setIsBlockModalOpen(open)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isBlocked ? "Unblock Key" : "Block Key"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p>
|
||||
{isBlocked ? "Unblock" : "Block"}{" "}
|
||||
<strong>{currentKeyData?.key_alias || currentKeyData?.token_id || "this key"}</strong>?
|
||||
</p>
|
||||
<p style={{ color: "#666", fontSize: "0.875rem", marginTop: 8 }}>
|
||||
{isBlocked
|
||||
? "Requests using this key will be accepted again."
|
||||
: "Requests using this key will be rejected with a 401 error until it is unblocked. The key is not deleted and can be unblocked at any time."}
|
||||
</p>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsBlockModalOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant={isBlocked ? "default" : "destructive"}
|
||||
onClick={handleToggleBlocked}
|
||||
disabled={blockLoading}
|
||||
>
|
||||
{isBlocked ? "Unblock" : "Block"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<TabGroup>
|
||||
<TabList className="mb-4">
|
||||
<Tab>Overview</Tab>
|
||||
<Tab>Settings</Tab>
|
||||
</TabList>
|
||||
<Tabs defaultValue="overview">
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabPanels>
|
||||
<div>
|
||||
{/* Overview Panel */}
|
||||
<TabPanel>
|
||||
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6">
|
||||
<Card>
|
||||
<Text>Spend</Text>
|
||||
<TabsContent value="overview" keepMounted>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<Card className="block p-6">
|
||||
<p className="text-sm">Spend</p>
|
||||
<div className="mt-2">
|
||||
<Title>${formatNumberWithCommas(currentKeyData.spend, 4)}</Title>
|
||||
<Text>of {budgetDisplay}</Text>
|
||||
<h3 className="text-lg font-medium">${formatNumberWithCommas(currentKeyData.spend, 4)}</h3>
|
||||
<p className="text-sm">of {budgetDisplay}</p>
|
||||
{currentKeyData.budget_reset_at && (
|
||||
<Text>Resets {formatTimestamp(currentKeyData.budget_reset_at)}</Text>
|
||||
<p className="text-sm">Resets {formatTimestamp(currentKeyData.budget_reset_at)}</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text>Rate Limits</Text>
|
||||
<Card className="block p-6">
|
||||
<p className="text-sm">Rate Limits</p>
|
||||
<div className="mt-2">
|
||||
<Text>TPM: {currentKeyData.tpm_limit !== null ? currentKeyData.tpm_limit : "Unlimited"}</Text>
|
||||
<Text>RPM: {currentKeyData.rpm_limit !== null ? currentKeyData.rpm_limit : "Unlimited"}</Text>
|
||||
<p className="text-sm">
|
||||
TPM: {currentKeyData.tpm_limit !== null ? currentKeyData.tpm_limit : "Unlimited"}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
RPM: {currentKeyData.rpm_limit !== null ? currentKeyData.rpm_limit : "Unlimited"}
|
||||
</p>
|
||||
{Boolean(currentKeyData.metadata?.throttle_on_budget_exceeded) && (
|
||||
<Text>Throttle on budget exceeded: Yes</Text>
|
||||
<p className="text-sm">Throttle on budget exceeded: Yes</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text>Models</Text>
|
||||
<Card className="block p-6">
|
||||
<p className="text-sm">Models</p>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{currentKeyData.models && currentKeyData.models.length > 0 ? (
|
||||
currentKeyData.models.map((model, index) => (
|
||||
<Badge key={index} color="red">
|
||||
<Badge key={index} variant="secondary" className="min-w-0 break-words">
|
||||
{model}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<Text>No models specified</Text>
|
||||
<p className="text-sm">No models specified</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card className="block p-6">
|
||||
<ObjectPermissionsView
|
||||
objectPermission={currentKeyData.object_permission}
|
||||
variant="inline"
|
||||
|
|
@ -628,43 +650,45 @@ export default function KeyInfoView({
|
|||
/>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text className="font-medium mb-3">Guardrails</Text>
|
||||
<Card className="block p-6">
|
||||
<p className="text-sm font-medium mb-3">Guardrails</p>
|
||||
{Array.isArray(currentKeyData.metadata?.guardrails) && currentKeyData.metadata.guardrails.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{currentKeyData.metadata.guardrails.map((guardrail: string, index: number) => (
|
||||
<Badge key={index} color="blue">
|
||||
<Badge key={index} variant="secondary" className="min-w-0 break-words">
|
||||
{guardrail}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Text className="text-gray-500">No guardrails configured</Text>
|
||||
<p className="text-sm text-gray-500">No guardrails configured</p>
|
||||
)}
|
||||
{typeof currentKeyData.metadata?.disable_global_guardrails === "boolean" &&
|
||||
currentKeyData.metadata.disable_global_guardrails === true && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-200">
|
||||
<Badge color="yellow">Global Guardrails Disabled</Badge>
|
||||
<Badge variant="destructive">Global Guardrails Disabled</Badge>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text className="font-medium mb-3">Policies</Text>
|
||||
<Card className="block p-6">
|
||||
<p className="text-sm font-medium mb-3">Policies</p>
|
||||
{Array.isArray(currentKeyData.metadata?.policies) && currentKeyData.metadata.policies.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{currentKeyData.metadata.policies.map((policy: string, index: number) => (
|
||||
<div key={index} className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge color="purple">{policy}</Badge>
|
||||
{loadingPolicies && <Text className="text-xs text-gray-400">Loading guardrails...</Text>}
|
||||
<Badge variant="secondary" className="min-w-0 break-words">
|
||||
{policy}
|
||||
</Badge>
|
||||
{loadingPolicies && <p className="text-xs text-gray-400">Loading guardrails...</p>}
|
||||
</div>
|
||||
{!loadingPolicies && policyGuardrails[policy] && policyGuardrails[policy].length > 0 && (
|
||||
<div className="ml-4 pl-3 border-l-2 border-gray-200">
|
||||
<Text className="text-xs text-gray-500 mb-1">Resolved Guardrails:</Text>
|
||||
<p className="text-xs text-gray-500 mb-1">Resolved Guardrails:</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{policyGuardrails[policy].map((guardrail: string, gIndex: number) => (
|
||||
<Badge key={gIndex} color="blue" size="xs">
|
||||
<Badge key={gIndex} variant="secondary" className="min-w-0 break-words">
|
||||
{guardrail}
|
||||
</Badge>
|
||||
))}
|
||||
|
|
@ -675,7 +699,7 @@ export default function KeyInfoView({
|
|||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Text className="text-gray-500">No policies configured</Text>
|
||||
<p className="text-sm text-gray-500">No policies configured</p>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
|
|
@ -697,15 +721,19 @@ export default function KeyInfoView({
|
|||
nextRotationAt={currentKeyData.next_rotation_at}
|
||||
variant="card"
|
||||
/>
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Settings Panel */}
|
||||
<TabPanel>
|
||||
<Card>
|
||||
<TabsContent value="settings" keepMounted>
|
||||
<Card className="block p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>Key Settings</Title>
|
||||
{!isEditing && canModifyKey && <Button onClick={() => setIsEditing(true)}>Edit Settings</Button>}
|
||||
<h3 className="text-lg font-medium">Key Settings</h3>
|
||||
{!isEditing && canModifyKey && (
|
||||
<Button variant="outline" onClick={() => setIsEditing(true)}>
|
||||
Edit Settings
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
|
|
@ -722,29 +750,29 @@ export default function KeyInfoView({
|
|||
) : (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text className="font-medium">Key ID</Text>
|
||||
<Text className="font-mono">{currentKeyData.token_id || currentKeyData.token}</Text>
|
||||
<p className="text-sm font-medium">Key ID</p>
|
||||
<p className="text-sm font-mono">{currentKeyData.token_id || currentKeyData.token}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Key Alias</Text>
|
||||
<Text>{currentKeyData.key_alias || "Not Set"}</Text>
|
||||
<p className="text-sm font-medium">Key Alias</p>
|
||||
<p className="text-sm">{currentKeyData.key_alias || "Not Set"}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Secret Key</Text>
|
||||
<Text className="font-mono">{currentKeyData.key_name}</Text>
|
||||
<p className="text-sm font-medium">Secret Key</p>
|
||||
<p className="text-sm font-mono">{currentKeyData.key_name}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Team ID</Text>
|
||||
<Text>{currentKeyData.team_id || "Not Set"}</Text>
|
||||
<p className="text-sm font-medium">Team ID</p>
|
||||
<p className="text-sm">{currentKeyData.team_id || "Not Set"}</p>
|
||||
</div>
|
||||
|
||||
{enableProjectsUI && (
|
||||
<div>
|
||||
<Text className="font-medium">Project</Text>
|
||||
<Text>
|
||||
<p className="text-sm font-medium">Project</p>
|
||||
<p className="text-sm">
|
||||
{currentKeyData.project_id
|
||||
? (() => {
|
||||
const project = projects?.find((p) => p.project_id === currentKeyData.project_id);
|
||||
|
|
@ -753,41 +781,43 @@ export default function KeyInfoView({
|
|||
: currentKeyData.project_id;
|
||||
})()
|
||||
: "Not Set"}
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Organization</Text>
|
||||
<Text>{(currentKeyData.organization_id ?? currentKeyData.org_id) || "Not Set"}</Text>
|
||||
<p className="text-sm font-medium">Organization</p>
|
||||
<p className="text-sm">{(currentKeyData.organization_id ?? currentKeyData.org_id) || "Not Set"}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Created</Text>
|
||||
<Text>{formatTimestamp(currentKeyData.created_at)}</Text>
|
||||
<p className="text-sm font-medium">Created</p>
|
||||
<p className="text-sm">{formatTimestamp(currentKeyData.created_at)}</p>
|
||||
</div>
|
||||
|
||||
{lastRegeneratedAt && (
|
||||
<div>
|
||||
<Text className="font-medium">Last Regenerated</Text>
|
||||
<p className="text-sm font-medium">Last Regenerated</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Text>{formatTimestamp(lastRegeneratedAt)}</Text>
|
||||
<Badge color="green" size="xs">
|
||||
Recent
|
||||
</Badge>
|
||||
<p className="text-sm">{formatTimestamp(lastRegeneratedAt)}</p>
|
||||
<Badge variant="secondary">Recent</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Expires</Text>
|
||||
<Text>{currentKeyData.expires ? formatTimestamp(currentKeyData.expires) : "Never"}</Text>
|
||||
<p className="text-sm font-medium">Expires</p>
|
||||
<p className="text-sm">
|
||||
{currentKeyData.expires ? formatTimestamp(currentKeyData.expires) : "Never"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{Boolean(currentKeyData.metadata?.enable_prompt_caching) && (
|
||||
<div>
|
||||
<Text className="font-medium">Prompt Caching</Text>
|
||||
<Text>Enabled (auto-injects cache_control markers on Anthropic and Bedrock Claude requests)</Text>
|
||||
<p className="text-sm font-medium">Prompt Caching</p>
|
||||
<p className="text-sm">
|
||||
Enabled (auto-injects cache_control markers on Anthropic and Bedrock Claude requests)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -802,31 +832,31 @@ export default function KeyInfoView({
|
|||
/>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Spend</Text>
|
||||
<Text>${formatNumberWithCommas(currentKeyData.spend, 4)} USD</Text>
|
||||
<p className="text-sm font-medium">Spend</p>
|
||||
<p className="text-sm">${formatNumberWithCommas(currentKeyData.spend, 4)} USD</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Budget</Text>
|
||||
<Text>
|
||||
<p className="text-sm font-medium">Budget</p>
|
||||
<p className="text-sm">
|
||||
{currentKeyData.max_budget !== null
|
||||
? `$${formatNumberWithCommas(currentKeyData.max_budget, 2)}`
|
||||
: "Unlimited"}
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Budget Reset</Text>
|
||||
<Text>
|
||||
<p className="text-sm font-medium">Budget Reset</p>
|
||||
<p className="text-sm">
|
||||
{currentKeyData.budget_reset_at
|
||||
? `${currentKeyData.budget_duration ? `Every ${currentKeyData.budget_duration}, next ` : ""}${formatTimestamp(currentKeyData.budget_reset_at)}`
|
||||
: "Never"}
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{currentKeyData.budget_fallbacks && Object.keys(currentKeyData.budget_fallbacks).length > 0 && (
|
||||
<div>
|
||||
<Text className="font-medium">Budget Fallbacks</Text>
|
||||
<p className="text-sm font-medium">Budget Fallbacks</p>
|
||||
<div className="mt-1 space-y-1">
|
||||
{Object.entries(currentKeyData.budget_fallbacks).map(([model, fallbacks]) => (
|
||||
<div key={model} className="text-xs text-gray-600">
|
||||
|
|
@ -841,7 +871,7 @@ export default function KeyInfoView({
|
|||
|
||||
{hasRouterSettings(currentKeyData.router_settings) && (
|
||||
<div>
|
||||
<Text className="font-medium">Router Settings</Text>
|
||||
<p className="text-sm font-medium">Router Settings</p>
|
||||
<div className="mt-1">
|
||||
<RouterSettingsSummary routerSettings={currentKeyData.router_settings} />
|
||||
</div>
|
||||
|
|
@ -849,7 +879,7 @@ export default function KeyInfoView({
|
|||
)}
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Tags</Text>
|
||||
<p className="text-sm font-medium">Tags</p>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{Array.isArray(currentKeyData.metadata?.tags) && currentKeyData.metadata.tags.length > 0
|
||||
? currentKeyData.metadata.tags.map((tag, index) => (
|
||||
|
|
@ -862,8 +892,8 @@ export default function KeyInfoView({
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Prompts</Text>
|
||||
<Text>
|
||||
<p className="text-sm font-medium">Prompts</p>
|
||||
<p className="text-sm">
|
||||
{Array.isArray(currentKeyData.metadata?.prompts) && currentKeyData.metadata.prompts.length > 0
|
||||
? currentKeyData.metadata.prompts.map((prompt, index) => (
|
||||
<span key={index} className="px-2 mr-2 py-1 bg-blue-100 rounded-sm text-xs">
|
||||
|
|
@ -871,11 +901,11 @@ export default function KeyInfoView({
|
|||
</span>
|
||||
))
|
||||
: "No prompts specified"}
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Allowed Routes</Text>
|
||||
<p className="text-sm font-medium">Allowed Routes</p>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{Array.isArray(currentKeyData.allowed_routes) && currentKeyData.allowed_routes.length > 0 ? (
|
||||
currentKeyData.allowed_routes.map((route, index) => (
|
||||
|
|
@ -884,14 +914,14 @@ export default function KeyInfoView({
|
|||
</span>
|
||||
))
|
||||
) : (
|
||||
<Tag color="green">All routes allowed</Tag>
|
||||
<Badge variant="secondary">All routes allowed</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Allowed Pass Through Routes</Text>
|
||||
<Text>
|
||||
<p className="text-sm font-medium">Allowed Pass Through Routes</p>
|
||||
<p className="text-sm">
|
||||
{Array.isArray(currentKeyData.metadata?.allowed_passthrough_routes) &&
|
||||
currentKeyData.metadata.allowed_passthrough_routes.length > 0
|
||||
? currentKeyData.metadata.allowed_passthrough_routes.map((route, index) => (
|
||||
|
|
@ -900,22 +930,22 @@ export default function KeyInfoView({
|
|||
</span>
|
||||
))
|
||||
: "No pass through routes specified"}
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Disable Global Guardrails</Text>
|
||||
<Text>
|
||||
<p className="text-sm font-medium">Disable Global Guardrails</p>
|
||||
<p className="text-sm">
|
||||
{currentKeyData.metadata?.disable_global_guardrails === true ? (
|
||||
<Badge color="yellow">Enabled - Global guardrails bypassed</Badge>
|
||||
<Badge variant="destructive">Enabled - Global guardrails bypassed</Badge>
|
||||
) : (
|
||||
<Badge color="green">Disabled - Global guardrails active</Badge>
|
||||
<Badge variant="secondary">Disabled - Global guardrails active</Badge>
|
||||
)}
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Models</Text>
|
||||
<p className="text-sm font-medium">Models</p>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{currentKeyData.models && currentKeyData.models.length > 0 ? (
|
||||
currentKeyData.models.map((model, index) => (
|
||||
|
|
@ -924,56 +954,60 @@ export default function KeyInfoView({
|
|||
</span>
|
||||
))
|
||||
) : (
|
||||
<Text>No models specified</Text>
|
||||
<p className="text-sm">No models specified</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Rate Limits</Text>
|
||||
<Text>TPM: {currentKeyData.tpm_limit !== null ? currentKeyData.tpm_limit : "Unlimited"}</Text>
|
||||
<Text>RPM: {currentKeyData.rpm_limit !== null ? currentKeyData.rpm_limit : "Unlimited"}</Text>
|
||||
<Text>
|
||||
<p className="text-sm font-medium">Rate Limits</p>
|
||||
<p className="text-sm">
|
||||
TPM: {currentKeyData.tpm_limit !== null ? currentKeyData.tpm_limit : "Unlimited"}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
RPM: {currentKeyData.rpm_limit !== null ? currentKeyData.rpm_limit : "Unlimited"}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
Max Parallel Requests:{" "}
|
||||
{currentKeyData.max_parallel_requests !== null
|
||||
? currentKeyData.max_parallel_requests
|
||||
: "Unlimited"}
|
||||
</Text>
|
||||
<Text>
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
Model TPM Limits:{" "}
|
||||
{currentKeyData.metadata?.model_tpm_limit
|
||||
? JSON.stringify(currentKeyData.metadata.model_tpm_limit)
|
||||
: "Unlimited"}
|
||||
</Text>
|
||||
<Text>
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
Model RPM Limits:{" "}
|
||||
{currentKeyData.metadata?.model_rpm_limit
|
||||
? JSON.stringify(currentKeyData.metadata.model_rpm_limit)
|
||||
: "Unlimited"}
|
||||
</Text>
|
||||
<Text>
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
Tag RPM Limits:{" "}
|
||||
{currentKeyData.metadata?.tag_rpm_limit &&
|
||||
Object.keys(currentKeyData.metadata.tag_rpm_limit).length > 0
|
||||
? JSON.stringify(currentKeyData.metadata.tag_rpm_limit)
|
||||
: "Unlimited"}
|
||||
</Text>
|
||||
<Text>
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
Estimated Output Tokens:{" "}
|
||||
{currentKeyData.metadata?.default_estimated_output_tokens != null
|
||||
? String(currentKeyData.metadata.default_estimated_output_tokens)
|
||||
: "Default"}
|
||||
</Text>
|
||||
<Text>
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
Estimated Output Tokens Per Model:{" "}
|
||||
{currentKeyData.metadata?.default_estimated_output_tokens_per_model
|
||||
? JSON.stringify(currentKeyData.metadata.default_estimated_output_tokens_per_model)
|
||||
: "Default"}
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Metadata</Text>
|
||||
<p className="text-sm font-medium">Metadata</p>
|
||||
<pre className="bg-gray-100 p-2 rounded-sm text-xs overflow-auto mt-1">
|
||||
{formatMetadataForDisplay(stripTagsFromMetadata(currentKeyData.metadata))}
|
||||
</pre>
|
||||
|
|
@ -999,9 +1033,9 @@ export default function KeyInfoView({
|
|||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue