refactor: more ui improvements

This commit is contained in:
Krrish Dholakia 2026-02-25 23:05:12 -08:00
parent fffe8253ea
commit 41d24b0c91
8 changed files with 467 additions and 324 deletions

View file

@ -390,6 +390,7 @@ model LiteLLM_DeletedVerificationToken {
config Json @default("{}")
user_id String?
team_id String?
agent_id String?
project_id String?
permissions Json @default("{}")
max_parallel_requests Int?

View file

@ -390,6 +390,7 @@ model LiteLLM_DeletedVerificationToken {
config Json @default("{}")
user_id String?
team_id String?
agent_id String?
project_id String?
permissions Json @default("{}")
max_parallel_requests Int?

View file

@ -390,6 +390,7 @@ model LiteLLM_DeletedVerificationToken {
config Json @default("{}")
user_id String?
team_id String?
agent_id String?
project_id String?
permissions Json @default("{}")
max_parallel_requests Int?

View file

@ -38,7 +38,7 @@ import Usage from "@/components/usage";
import UserDashboard from "@/components/user_dashboard";
import { AccessGroupsPage } from "@/components/AccessGroups/AccessGroupsPage";
import VectorStoreManagement from "@/components/vector_store_management";
import ToolPolicies from "@/components/ToolPolicies";
import ToolPoliciesView from "@/components/ToolPoliciesView";
import SpendLogsTable from "@/components/view_logs";
import ViewUserDashboard from "@/components/view_users";
import { ThemeProvider } from "@/contexts/ThemeContext";
@ -550,7 +550,7 @@ function CreateKeyPageContent() {
) : page == "vector-stores" ? (
<VectorStoreManagement accessToken={accessToken} userRole={userRole} userID={userID} />
) : page == "tool-policies" ? (
<ToolPolicies accessToken={accessToken} userRole={userRole} />
<ToolPoliciesView accessToken={accessToken} userRole={userRole} />
) : page == "guardrails-monitor" ? (
<GuardrailsMonitorView accessToken={accessToken} />
) : page == "new_usage" ? (

View file

@ -0,0 +1,317 @@
"use client";
import { ArrowLeftOutlined, ToolOutlined } from "@ant-design/icons";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Button, Select, Spin } from "antd";
import React, { useCallback, useMemo, useState } from "react";
import TeamDropdown from "@/components/common_components/team_dropdown";
import { PolicySelect } from "@/components/ToolPolicies/PolicySelect";
import {
deleteToolPolicyOverride,
fetchToolDetail,
keyListCall,
teamListCall,
updateToolPolicy,
type ToolPolicyOverrideRow,
} from "@/components/networking";
import type { Team } from "@/components/key_team_helpers/key_list";
interface ToolDetailProps {
toolName: string;
onBack: () => void;
accessToken: string | null;
}
interface TeamOption {
team_id: string;
team_alias?: string;
}
interface KeyOption {
token: string;
key_alias?: string;
}
const TOOL_DETAIL_QUERY_KEY = "tool-detail";
export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
const queryClient = useQueryClient();
const [overrideSaving, setOverrideSaving] = useState(false);
const [policySaving, setPolicySaving] = useState(false);
const [blockScope, setBlockScope] = useState<"team" | "key">("team");
const [blockTeamId, setBlockTeamId] = useState<string | null>(null);
const [blockKey, setBlockKey] = useState<KeyOption | null>(null);
const { data: detail, isLoading: detailLoading, error: detailError } = useQuery({
queryKey: [TOOL_DETAIL_QUERY_KEY, toolName],
queryFn: () => fetchToolDetail(accessToken!, toolName),
enabled: !!accessToken && !!toolName,
});
const { data: teamsData } = useQuery({
queryKey: ["teams-list-tool-detail"],
queryFn: () => teamListCall(accessToken!, null, null),
enabled: !!accessToken,
});
const { data: keysData } = useQuery({
queryKey: ["keys-list-tool-detail"],
queryFn: () => keyListCall(accessToken!, null, null, null, null, null, 1, 100),
enabled: !!accessToken,
});
const teams: Team[] = useMemo(() => {
const arr = Array.isArray(teamsData) ? teamsData : teamsData?.data ?? [];
return arr.map((t: { team_id?: string; id?: string; team_alias?: string }) => ({
team_id: t.team_id ?? t.id ?? "",
team_alias: t.team_alias ?? t.team_id ?? "",
models: [],
max_budget: null,
budget_duration: null,
tpm_limit: null,
rpm_limit: null,
organization_id: "",
created_at: "",
keys: [],
members_with_roles: [],
spend: 0,
}));
}, [teamsData]);
const keys: KeyOption[] = useMemo(() => {
const keysRes = keysData?.keys ?? keysData?.data ?? [];
return keysRes.map((k: { token?: string; api_key?: string; key_hash?: string; key_alias?: string }) => ({
token: k.token ?? k.api_key ?? k.key_hash ?? "",
key_alias: k.key_alias ?? (k.token ?? k.api_key ?? k.key_hash)?.toString?.()?.substring?.(0, 8),
}));
}, [keysData]);
const invalidateDetail = useCallback(() => {
queryClient.invalidateQueries({ queryKey: [TOOL_DETAIL_QUERY_KEY, toolName] });
}, [queryClient, toolName]);
const handlePolicyChange = useCallback(
async (name: string, newPolicy: string) => {
if (!accessToken) return;
setPolicySaving(true);
try {
await updateToolPolicy(accessToken, name, newPolicy);
invalidateDetail();
} catch (e: unknown) {
alert(`Failed to update policy: ${e instanceof Error ? e.message : String(e)}`);
} finally {
setPolicySaving(false);
}
},
[accessToken, invalidateDetail]
);
const handleAddOverride = useCallback(async () => {
if (!accessToken || !toolName) return;
const isTeam = blockScope === "team";
if (isTeam && !blockTeamId) return;
if (!isTeam && !blockKey?.token) return;
setOverrideSaving(true);
try {
await updateToolPolicy(accessToken, toolName, "blocked", {
team_id: isTeam ? blockTeamId : undefined,
key_hash: !isTeam ? blockKey!.token : undefined,
key_alias: !isTeam ? blockKey!.key_alias : undefined,
});
invalidateDetail();
setBlockTeamId(null);
setBlockKey(null);
} catch (e: unknown) {
alert(`Failed to add override: ${e instanceof Error ? e.message : String(e)}`);
} finally {
setOverrideSaving(false);
}
}, [accessToken, toolName, blockScope, blockTeamId, blockKey, invalidateDetail]);
const handleRemoveOverride = useCallback(
async (override: ToolPolicyOverrideRow) => {
if (!accessToken || !toolName) return;
setOverrideSaving(true);
try {
await deleteToolPolicyOverride(accessToken, toolName, {
team_id: override.team_id ?? undefined,
key_hash: override.key_hash ?? undefined,
});
invalidateDetail();
} catch (e: unknown) {
alert(`Failed to remove override: ${e instanceof Error ? e.message : String(e)}`);
} finally {
setOverrideSaving(false);
}
},
[accessToken, toolName, invalidateDetail]
);
if (detailLoading && !detail) {
return (
<div className="flex items-center justify-center py-12">
<Spin size="large" />
</div>
);
}
if (detailError && !detail) {
return (
<div>
<Button type="link" icon={<ArrowLeftOutlined />} onClick={onBack} className="pl-0 mb-4">
Back to Tool Policies
</Button>
<p className="text-red-600">Failed to load tool details.</p>
</div>
);
}
if (!detail) {
return null;
}
const { tool, overrides } = detail;
return (
<div>
<div className="mb-6">
<Button
type="link"
icon={<ArrowLeftOutlined />}
onClick={onBack}
className="pl-0 mb-4"
>
Back to Tool Policies
</Button>
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-3 mb-1 flex-wrap">
<ToolOutlined className="text-xl text-gray-400" />
<h1 className="text-xl font-semibold text-gray-900 font-mono">{tool.tool_name}</h1>
<span className="inline-flex items-center px-2.5 py-1 text-xs font-medium rounded-md bg-gray-100 text-gray-700 border border-gray-200">
{tool.origin ?? "—"}
</span>
<span className="inline-flex items-center px-2.5 py-1 text-xs font-medium rounded-md bg-indigo-50 text-indigo-700 border border-indigo-200">
{(tool.call_count ?? 0).toLocaleString()} calls
</span>
</div>
</div>
</div>
</div>
<div className="space-y-6">
<section className="bg-white rounded-lg border border-gray-200 p-5 shadow-sm">
<h2 className="text-sm font-semibold text-gray-700 mb-3">Global policy</h2>
<PolicySelect
value={tool.call_policy}
toolName={tool.tool_name}
saving={policySaving}
onChange={handlePolicyChange}
size="middle"
minWidth={140}
stopPropagation={false}
/>
</section>
{overrides.length > 0 && (
<section className="bg-white rounded-lg border border-gray-200 p-5 shadow-sm">
<h2 className="text-sm font-semibold text-gray-700 mb-3">Blocked for team or key</h2>
<ul className="border rounded-md divide-y divide-gray-100 bg-red-50/30">
{overrides.map((ov) => (
<li
key={ov.override_id}
className="flex items-center justify-between px-3 py-2.5 text-sm"
>
<span className="text-gray-700">
{ov.team_id ? `Team: ${ov.team_id}` : ""}
{ov.team_id && ov.key_hash ? " · " : ""}
{ov.key_hash ? `Key: ${ov.key_alias || ov.key_hash.substring(0, 8)}` : ""}
{!ov.team_id && !ov.key_hash ? "—" : ""}
</span>
<Button
type="link"
danger
size="small"
disabled={overrideSaving}
onClick={() => handleRemoveOverride(ov)}
>
Remove
</Button>
</li>
))}
</ul>
</section>
)}
<section className="bg-white rounded-lg border border-gray-200 p-5 shadow-sm">
<h2 className="text-sm font-semibold text-gray-700 mb-3">Block for team or key</h2>
<div className="flex flex-col gap-4 max-w-md">
<div>
<span className="text-sm font-medium text-gray-700 block mb-2">Scope</span>
<div className="flex items-center gap-6">
<label className="flex items-center gap-2 cursor-pointer text-sm text-gray-700">
<input
type="radio"
checked={blockScope === "team"}
onChange={() => setBlockScope("team")}
className="align-middle"
/>
Team
</label>
<label className="flex items-center gap-2 cursor-pointer text-sm text-gray-700">
<input
type="radio"
checked={blockScope === "key"}
onChange={() => setBlockScope("key")}
className="align-middle"
/>
Key
</label>
</div>
</div>
<div>
<span className="text-sm font-medium text-gray-700 block mb-2">
{blockScope === "team" ? "Team" : "Key"}
</span>
{blockScope === "team" ? (
<TeamDropdown
teams={teams}
value={blockTeamId ?? undefined}
onChange={(id) => setBlockTeamId(id || null)}
/>
) : (
<Select
placeholder="Select key"
allowClear
showSearch
optionFilterProp="label"
value={blockKey ? blockKey.token : undefined}
onChange={(token) => {
const k = keys.find((x) => x.token === token);
setBlockKey(k ?? null);
}}
options={keys.map((k) => ({
value: k.token,
label: k.key_alias || k.token?.substring?.(0, 12) || k.token,
}))}
className="w-full"
style={{ minWidth: 200 }}
/>
)}
</div>
<Button
type="primary"
danger
disabled={overrideSaving || (blockScope === "team" ? !blockTeamId : !blockKey?.token)}
loading={overrideSaving}
onClick={handleAddOverride}
>
Block for {blockScope}
</Button>
</div>
</section>
</div>
</div>
);
}

View file

@ -1,24 +1,19 @@
"use client";
import React, { useCallback, useDeferredValue, useEffect, useMemo, useState } from "react";
import { Button, Modal, Select, Switch, Tooltip } from "antd";
import { Button, Switch, Tooltip } from "antd";
import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react";
import { TimeCell } from "./view_logs/time_cell";
import { TableHeaderSortDropdown } from "./common_components/TableHeaderSortDropdown/TableHeaderSortDropdown";
import type { SortState } from "./common_components/TableHeaderSortDropdown/TableHeaderSortDropdown";
import FilterComponent, { FilterOption } from "./molecules/filter";
import { MetricCard } from "./GuardrailsMonitor/MetricCard";
import TeamDropdown from "./common_components/team_dropdown";
import { PolicySelect, POLICY_OPTIONS } from "./ToolPolicies/PolicySelect";
import {
fetchToolDetail,
fetchToolsList,
updateToolPolicy,
deleteToolPolicyOverride,
ToolRow,
ToolDetailResponse,
ToolPolicyOverrideRow,
} from "./networking";
import { teamListCall, keyListCall } from "./networking";
// --- Date helpers (UTC) for "new tools" counts ---
function getUTCDateKey(date: Date): string {
@ -46,15 +41,6 @@ function getTrendSubtitle(newToday: number, newYesterday: number): string | unde
return `${diff} since yesterday`;
}
const POLICY_OPTIONS = [
{ value: "trusted", label: "trusted", color: "#065f46", bg: "#d1fae5", border: "#6ee7b7" },
{ value: "blocked", label: "blocked", color: "#991b1b", bg: "#fee2e2", border: "#fca5a5" },
] as const;
type PolicyValue = "trusted" | "blocked";
const policyStyle = (p: string) => POLICY_OPTIONS.find((o) => o.value === p) ?? POLICY_OPTIONS[1];
type SortField = "tool_name" | "call_policy" | "team_id" | "key_alias" | "created_at" | "call_count";
interface FilterValues {
@ -64,99 +50,16 @@ interface FilterValues {
interface ToolPoliciesProps {
accessToken: string | null;
userRole?: string;
onSelectTool?: (toolName: string) => void;
}
const PolicySelect: React.FC<{
value: string;
toolName: string;
saving: boolean;
onChange: (toolName: string, policy: string) => void;
}> = ({ value, toolName, saving, onChange }) => {
const style = policyStyle(value);
return (
<Select
size="small"
value={value}
disabled={saving}
loading={saving}
onChange={(v) => onChange(toolName, v)}
onClick={(e) => e.stopPropagation()}
style={{
minWidth: 110,
fontWeight: 500,
}}
styles={{
selector: {
backgroundColor: style.bg,
borderColor: style.border,
color: style.color,
borderRadius: 999,
fontSize: 11,
fontWeight: 600,
paddingLeft: 8,
paddingRight: 4,
},
}}
popupMatchSelectWidth={false}
options={POLICY_OPTIONS.map((o) => ({
value: o.value,
label: (
<span
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
fontSize: 12,
fontWeight: 500,
color: o.color,
}}
>
<span
style={{
width: 8,
height: 8,
borderRadius: "50%",
backgroundColor: o.color,
display: "inline-block",
flexShrink: 0,
}}
/>
{o.label}
</span>
),
}))}
/>
);
};
interface TeamOption {
team_id: string;
team_alias?: string;
}
interface KeyOption {
token: string;
key_alias?: string;
}
export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken }) => {
export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelectTool }) => {
const [tools, setTools] = useState<ToolRow[]>([]);
const [loading, setLoading] = useState(true);
const [isFetching, setIsFetching] = useState(false);
const [error, setError] = useState<string | null>(null);
const [saving, setSaving] = useState<string | null>(null);
const [detailModalOpen, setDetailModalOpen] = useState(false);
const [detailToolName, setDetailToolName] = useState<string | null>(null);
const [detail, setDetail] = useState<ToolDetailResponse | null>(null);
const [detailLoading, setDetailLoading] = useState(false);
const [teams, setTeams] = useState<TeamOption[]>([]);
const [keys, setKeys] = useState<KeyOption[]>([]);
const [overrideSaving, setOverrideSaving] = useState(false);
const [blockScope, setBlockScope] = useState<"team" | "key">("team");
const [blockTeamId, setBlockTeamId] = useState<string | null>(null);
const [blockKey, setBlockKey] = useState<{ token: string; key_alias?: string } | null>(null);
const [searchTerm, setSearchTerm] = useState("");
const [sortField, setSortField] = useState<SortField>("created_at");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
@ -199,9 +102,6 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken }) => {
try {
await updateToolPolicy(accessToken, toolName, newPolicy);
setTools((prev) => prev.map((t) => (t.tool_name === toolName ? { ...t, call_policy: newPolicy } : t)));
if (detailToolName === toolName && detail) {
setDetail((d) => (d ? { ...d, tool: { ...d.tool, call_policy: newPolicy } } : null));
}
} catch (e: any) {
alert(`Failed to update policy: ${e.message}`);
} finally {
@ -209,91 +109,6 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken }) => {
}
};
const openDetailModal = useCallback(
async (toolName: string) => {
if (!accessToken) return;
setDetailToolName(toolName);
setDetailModalOpen(true);
setDetail(null);
setDetailLoading(true);
setBlockTeamId(null);
setBlockKey(null);
try {
const [detailRes, teamsRes, keysRes] = await Promise.all([
fetchToolDetail(accessToken, toolName),
teamListCall(accessToken, null, null),
keyListCall(accessToken, null, null, null, null, null, 1, 100),
]);
setDetail(detailRes);
const teamsArray = Array.isArray(teamsRes) ? teamsRes : teamsRes?.data ?? [];
setTeams(
teamsArray.map((t: any) => ({ team_id: t.team_id ?? t.id, team_alias: t.team_alias ?? t.team_id }))
);
const keysArray = keysRes?.keys ?? keysRes?.data ?? [];
setKeys(
keysArray.map((k: any) => ({
token: k.token ?? k.api_key ?? k.key_hash ?? "",
key_alias: k.key_alias ?? k.token?.substring?.(0, 8),
}))
);
} catch (e: any) {
setError(e.message ?? "Failed to load tool detail");
} finally {
setDetailLoading(false);
}
},
[accessToken]
);
const closeDetailModal = useCallback(() => {
setDetailModalOpen(false);
setDetailToolName(null);
setDetail(null);
}, []);
const handleAddOverride = useCallback(async () => {
if (!accessToken || !detailToolName) return;
const isTeam = blockScope === "team";
if (isTeam && !blockTeamId) return;
if (!isTeam && !blockKey?.token) return;
setOverrideSaving(true);
try {
await updateToolPolicy(accessToken, detailToolName, "blocked", {
team_id: isTeam ? blockTeamId! : undefined,
key_hash: !isTeam ? blockKey!.token : undefined,
key_alias: !isTeam ? blockKey!.key_alias : undefined,
});
const refreshed = await fetchToolDetail(accessToken, detailToolName);
setDetail(refreshed);
setBlockTeamId(null);
setBlockKey(null);
} catch (e: any) {
alert(`Failed to add override: ${e.message}`);
} finally {
setOverrideSaving(false);
}
}, [accessToken, detailToolName, blockScope, blockTeamId, blockKey]);
const handleRemoveOverride = useCallback(
async (override: ToolPolicyOverrideRow) => {
if (!accessToken || !detailToolName) return;
setOverrideSaving(true);
try {
await deleteToolPolicyOverride(accessToken, detailToolName, {
team_id: override.team_id ?? undefined,
key_hash: override.key_hash ?? undefined,
});
const refreshed = await fetchToolDetail(accessToken, detailToolName);
setDetail(refreshed);
} catch (e: any) {
alert(`Failed to remove override: ${e.message}`);
} finally {
setOverrideSaving(false);
}
},
[accessToken, detailToolName]
);
const handleSortChange = (field: SortField, newState: SortState) => {
if (newState === false) {
setSortField("created_at");
@ -431,7 +246,7 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken }) => {
};
return (
<div className="p-6 w-full">
<div className="w-full">
<h1 className="text-2xl font-semibold text-gray-900 mb-6">Tool Policies</h1>
{/* Summary cards */}
@ -644,10 +459,10 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken }) => {
<TableCell className="py-0.5 max-h-8 overflow-hidden">
<button
type="button"
onClick={() => openDetailModal(tool.tool_name)}
onClick={() => onSelectTool?.(tool.tool_name)}
className="text-left w-full font-mono text-xs max-w-[20ch] truncate block font-medium text-blue-600 hover:text-blue-800 hover:underline focus:outline-none focus:ring-0"
>
<Tooltip title="Click to view details and block for team/key">
<Tooltip title={onSelectTool ? "Click to view details and block for team/key" : tool.tool_name}>
<span>{tool.tool_name}</span>
</Tooltip>
</button>
@ -660,8 +475,10 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken }) => {
onChange={handlePolicyChange}
/>
</TableCell>
<TableCell className="py-0.5 max-h-8 text-right tabular-nums text-sm font-mono text-gray-700">
{(tool.call_count ?? 0).toLocaleString()}
<TableCell className="py-0.5 max-h-8">
<div className="flex items-center justify-end h-8 tabular-nums text-sm font-mono text-gray-700">
{(tool.call_count ?? 0).toLocaleString()}
</div>
</TableCell>
<TableCell className="py-0.5 max-h-8 overflow-hidden whitespace-nowrap">
<Tooltip title={tool.team_id ?? "-"}>
@ -718,134 +535,6 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken }) => {
)}
</div>
{/* Tool detail modal: view tool, global policy, overrides, block for team/key */}
<Modal
title={detailToolName ? `Tool: ${detailToolName}` : "Tool details"}
open={detailModalOpen}
onCancel={closeDetailModal}
footer={null}
width={640}
destroyOnClose
>
{detailLoading ? (
<p className="text-gray-500 py-4">Loading…</p>
) : detail ? (
<div className="space-y-4">
<div className="flex flex-wrap gap-4 text-sm">
<span>
<strong>Origin:</strong> {detail.tool.origin ?? "—"}
</span>
<span>
<strong># Calls:</strong> {(detail.tool.call_count ?? 0).toLocaleString()}
</span>
</div>
<div>
<strong className="block text-sm text-gray-700 mb-1">Global policy</strong>
<PolicySelect
value={detail.tool.call_policy}
toolName={detail.tool.tool_name}
saving={saving === detail.tool.tool_name}
onChange={handlePolicyChange}
/>
</div>
{detail.overrides.length > 0 && (
<div>
<strong className="block text-sm text-gray-700 mb-2">Blocked for team/key</strong>
<ul className="border rounded-md divide-y divide-gray-100">
{detail.overrides.map((ov) => (
<li
key={ov.override_id}
className="flex items-center justify-between px-3 py-2 text-sm bg-red-50/50"
>
<span>
{ov.team_id ? `Team: ${ov.team_id}` : ""}
{ov.team_id && ov.key_hash ? " · " : ""}
{ov.key_hash ? `Key: ${ov.key_alias || ov.key_hash.substring(0, 8)}` : ""}
{!ov.team_id && !ov.key_hash ? "—" : ""}
</span>
<Button
type="link"
danger
size="small"
disabled={overrideSaving}
onClick={() => handleRemoveOverride(ov)}
>
Remove
</Button>
</li>
))}
</ul>
</div>
)}
<div className="border-t pt-4">
<strong className="block text-sm text-gray-700 mb-2">Block for team or key</strong>
<div className="flex flex-wrap items-end gap-3">
<div className="flex items-center gap-2">
<label className="text-sm">
<input
type="radio"
checked={blockScope === "team"}
onChange={() => setBlockScope("team")}
className="mr-1"
/>
Team
</label>
<label className="text-sm">
<input
type="radio"
checked={blockScope === "key"}
onChange={() => setBlockScope("key")}
className="mr-1"
/>
Key
</label>
</div>
{blockScope === "team" ? (
<div className="min-w-[200px]">
<TeamDropdown
teams={teams}
value={blockTeamId}
onChange={(id) => setBlockTeamId(id ?? null)}
/>
</div>
) : (
<Select
placeholder="Select key"
allowClear
showSearch
optionFilterProp="label"
value={blockKey ? blockKey.token : undefined}
onChange={(token) => {
const k = keys.find((x) => x.token === token);
setBlockKey(k ?? null);
}}
options={keys.map((k) => ({
value: k.token,
label: k.key_alias || k.token?.substring?.(0, 12) || k.token,
}))}
style={{ minWidth: 200 }}
/>
)}
<Button
type="primary"
danger
disabled={
overrideSaving || (blockScope === "team" ? !blockTeamId : !blockKey?.token)
}
loading={overrideSaving}
onClick={handleAddOverride}
>
Block for {blockScope}
</Button>
</div>
</div>
</div>
) : (
<p className="text-gray-500 py-4">No data</p>
)}
</Modal>
</div>
);
};

View file

@ -0,0 +1,90 @@
"use client";
import React from "react";
import { Select } from "antd";
// DB policy values: "trusted" | "untrusted" | "dual_llm" | "blocked" — we expose all except dual_llm in the Policy dropdown
export const POLICY_OPTIONS = [
{ value: "trusted", label: "trusted", color: "#065f46", bg: "#d1fae5", border: "#6ee7b7" },
{ value: "untrusted", label: "untrusted", color: "#92400e", bg: "#fef3c7", border: "#fcd34d" },
{ value: "blocked", label: "blocked", color: "#991b1b", bg: "#fee2e2", border: "#fca5a5" },
] as const;
export const policyStyle = (p: string) =>
POLICY_OPTIONS.find((o) => o.value === p) ?? POLICY_OPTIONS[1];
export interface PolicySelectProps {
value: string;
toolName: string;
saving: boolean;
onChange: (toolName: string, policy: string) => void;
size?: "small" | "middle";
minWidth?: number;
stopPropagation?: boolean;
}
export const PolicySelect: React.FC<PolicySelectProps> = ({
value,
toolName,
saving,
onChange,
size = "small",
minWidth = 110,
stopPropagation = true,
}) => {
const style = policyStyle(value);
return (
<Select
size={size}
value={value}
disabled={saving}
loading={saving}
onChange={(v) => onChange(toolName, v)}
onClick={(e) => stopPropagation && e.stopPropagation()}
style={{
minWidth,
fontWeight: 500,
}}
styles={{
selector: {
backgroundColor: style.bg,
borderColor: style.border,
color: style.color,
borderRadius: 999,
fontSize: size === "small" ? 11 : 12,
fontWeight: 600,
paddingLeft: 8,
paddingRight: 4,
},
}}
popupMatchSelectWidth={false}
options={POLICY_OPTIONS.map((o) => ({
value: o.value,
label: (
<span
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
fontSize: 12,
fontWeight: 500,
color: o.color,
}}
>
<span
style={{
width: 8,
height: 8,
borderRadius: "50%",
backgroundColor: o.color,
display: "inline-block",
flexShrink: 0,
}}
/>
{o.label}
</span>
),
}))}
/>
);
};

View file

@ -0,0 +1,44 @@
"use client";
import React, { useState } from "react";
import { ToolDetail } from "@/components/ToolDetail";
import { ToolPolicies } from "@/components/ToolPolicies";
type View =
| { type: "overview" }
| { type: "detail"; toolName: string };
interface ToolPoliciesViewProps {
accessToken: string | null;
userRole?: string;
}
export default function ToolPoliciesView({ accessToken, userRole }: ToolPoliciesViewProps) {
const [view, setView] = useState<View>({ type: "overview" });
const handleSelectTool = (toolName: string) => {
setView({ type: "detail", toolName });
};
const handleBack = () => {
setView({ type: "overview" });
};
return (
<div className="p-6 w-full min-w-0 flex-1">
{view.type === "detail" ? (
<ToolDetail
toolName={view.toolName}
onBack={handleBack}
accessToken={accessToken}
/>
) : (
<ToolPolicies
accessToken={accessToken}
userRole={userRole}
onSelectTool={handleSelectTool}
/>
)}
</div>
);
}