feat(ui): i18n for team guardrails tab

This commit is contained in:
吹雪 2026-06-10 17:38:38 +09:00
parent fbb8477d23
commit 6484d548ec
3 changed files with 340 additions and 129 deletions

View file

@ -1,6 +1,9 @@
"use client";
import React, { useState, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { Trans } from "react-i18next";
import type { TFunction } from "i18next";
import {
SearchIcon,
PlusIcon,
@ -109,26 +112,30 @@ function submissionToTeamGuardrail(item: GuardrailSubmissionItem): TeamGuardrail
};
}
const STATUS_CONFIG: Record<GuardrailStatus, { label: string; bg: string; text: string; dot: string }> = {
active: {
label: "Active",
bg: "bg-green-50",
text: "text-green-700",
dot: "bg-green-500",
},
pending: {
label: "Pending Review",
bg: "bg-yellow-50",
text: "text-yellow-700",
dot: "bg-yellow-500",
},
rejected: {
label: "Rejected",
bg: "bg-red-50",
text: "text-red-700",
dot: "bg-red-500",
},
};
type StatusConfig = { label: string; bg: string; text: string; dot: string };
function getStatusConfig(t: TFunction): Record<GuardrailStatus, StatusConfig> {
return {
active: {
label: t("guardrails.teamGuardrailsTab.statusActive"),
bg: "bg-green-50",
text: "text-green-700",
dot: "bg-green-500",
},
pending: {
label: t("guardrails.teamGuardrailsTab.statusPendingReview"),
bg: "bg-yellow-50",
text: "text-yellow-700",
dot: "bg-yellow-500",
},
rejected: {
label: t("guardrails.teamGuardrailsTab.statusRejected"),
bg: "bg-red-50",
text: "text-red-700",
dot: "bg-red-500",
},
};
}
const TEAM_COLORS: Record<string, string> = {
"ML Platform": "bg-purple-100 text-purple-700",
@ -227,7 +234,9 @@ function GuardrailCard({
onApprove,
onReject,
}: GuardrailCardProps) {
const status = STATUS_CONFIG[g.status];
const { t } = useTranslation();
const statusConfig = getStatusConfig(t);
const status = statusConfig[g.status];
const teamColor = TEAM_COLORS[g.team] ?? "bg-gray-100 text-gray-700";
return (
<div
@ -238,7 +247,9 @@ function GuardrailCard({
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1.5 flex-wrap">
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${teamColor}`}>Team: {g.team}</span>
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${teamColor}`}>
{t("guardrails.teamGuardrailsTab.teamLabel", { team: g.team })}
</span>
<span
className={`inline-flex items-center gap-1.5 text-xs font-medium px-2 py-0.5 rounded-full ${status.bg} ${status.text}`}
>
@ -254,16 +265,20 @@ function GuardrailCard({
</div>
<div className="flex items-center gap-4 text-xs text-gray-500">
<span>
Model: <span className="font-medium text-gray-700">{g.model}</span>
{t("guardrails.teamGuardrailsTab.modelLabel")}{" "}
<span className="font-medium text-gray-700">{g.model}</span>
</span>
<span>
Submitted: <span className="font-medium text-gray-700">{g.submittedAt}</span>
{t("guardrails.teamGuardrailsTab.submittedLabel")}{" "}
<span className="font-medium text-gray-700">{g.submittedAt}</span>
</span>
</div>
</div>
<div className="flex flex-col items-end gap-2 flex-shrink-0">
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500 whitespace-nowrap">Forward API Key</span>
<span className="text-xs text-gray-500 whitespace-nowrap">
{t("guardrails.teamGuardrailsTab.forwardApiKey")}
</span>
<Toggle enabled={g.forwardKey} onToggle={onToggleForwardKey} />
</div>
<div className="flex items-center gap-2 mt-1">
@ -272,7 +287,7 @@ function GuardrailCard({
onClick={onSelect}
className="text-xs border border-gray-300 text-gray-600 hover:bg-gray-50 px-3 py-1.5 rounded-md transition-colors font-medium"
>
{isSelected ? "Close" : "Review"}
{isSelected ? t("common.close") : t("guardrails.teamGuardrailsTab.review")}
</button>
{g.status === "pending" && (
<>
@ -281,14 +296,14 @@ function GuardrailCard({
onClick={onApprove}
className="text-xs bg-green-500 hover:bg-green-600 text-white px-3 py-1.5 rounded-md transition-colors font-medium"
>
Approve
{t("guardrails.teamGuardrailsTab.approve")}
</button>
<button
type="button"
onClick={onReject}
className="text-xs border border-red-300 text-red-600 hover:bg-red-50 px-3 py-1.5 rounded-md transition-colors font-medium"
>
Reject
{t("guardrails.teamGuardrailsTab.reject")}
</button>
</>
)}
@ -302,7 +317,7 @@ function GuardrailCard({
className="flex items-center gap-1.5 text-xs text-gray-500 hover:text-gray-700 transition-colors"
>
{isHeadersExpanded ? <ChevronUpIcon className="h-3.5 w-3.5" /> : <ChevronDownIcon className="h-3.5 w-3.5" />}
Static headers
{t("guardrails.teamGuardrailsTab.staticHeaders")}
{g.customHeaders.length > 0 && (
<span className="ml-1 bg-gray-100 text-gray-600 rounded-full px-1.5 py-0.5 text-xs">
{g.customHeaders.length}
@ -312,7 +327,7 @@ function GuardrailCard({
{isHeadersExpanded && (
<div className="mt-2">
{g.customHeaders.length === 0 ? (
<p className="text-xs text-gray-400 italic">No static headers configured.</p>
<p className="text-xs text-gray-400 italic">{t("guardrails.teamGuardrailsTab.noStaticHeaders")}</p>
) : (
<div className="space-y-1">
{g.customHeaders.map((h, i) => (
@ -361,11 +376,13 @@ function DetailPanel({
onUpdateCustomHeaders,
onUpdateExtraHeaders,
}: DetailPanelProps) {
const { t } = useTranslation();
const [configExpanded, setConfigExpanded] = useState(false);
const [newExtraHeader, setNewExtraHeader] = useState("");
const [newStaticHeaderKey, setNewStaticHeaderKey] = useState("");
const [newStaticHeaderValue, setNewStaticHeaderValue] = useState("");
const status = STATUS_CONFIG[g.status];
const statusConfig = getStatusConfig(t);
const status = statusConfig[g.status];
const teamColor = TEAM_COLORS[g.team] ?? "bg-gray-100 text-gray-700";
return (
<div className="w-96 flex-shrink-0 bg-white overflow-auto">
@ -373,7 +390,9 @@ function DetailPanel({
<div className="flex items-start justify-between mb-4">
<div>
<div className="flex items-center gap-2 mb-1">
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${teamColor}`}>Team: {g.team}</span>
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${teamColor}`}>
{t("guardrails.teamGuardrailsTab.teamLabel", { team: g.team })}
</span>
<span
className={`inline-flex items-center gap-1.5 text-xs font-medium px-2 py-0.5 rounded-full ${status.bg} ${status.text}`}
>
@ -383,21 +402,21 @@ function DetailPanel({
</div>
<h2 className="text-base font-semibold text-gray-900">{g.name}</h2>
<p className="text-xs text-gray-500 mt-0.5">
Submitted by {g.submittedBy} on {g.submittedAt}
{t("guardrails.teamGuardrailsTab.submittedBy", { by: g.submittedBy, at: g.submittedAt })}
</p>
</div>
<button
type="button"
onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
aria-label="Close detail panel"
aria-label={t("guardrails.teamGuardrailsTab.closeDetailPanel")}
>
<XIcon className="h-4 w-4" />
</button>
</div>
<p className="text-sm text-gray-600 mb-5">{g.description}</p>
<div className="space-y-4">
<ConfigRow label="Endpoint">
<ConfigRow label={t("guardrails.teamGuardrailsTab.endpoint")}>
<div className="flex items-center gap-1.5">
<code className="text-xs font-mono text-gray-700 break-all">{g.endpoint}</code>
<a
@ -410,7 +429,7 @@ function DetailPanel({
</a>
</div>
</ConfigRow>
<ConfigRow label="Method">
<ConfigRow label={t("guardrails.teamGuardrailsTab.method")}>
<span className="text-xs font-mono font-medium text-gray-700 bg-gray-100 px-2 py-0.5 rounded">
{g.method}
</span>
@ -419,29 +438,33 @@ function DetailPanel({
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-1.5">
<KeyIcon className="h-3.5 w-3.5 text-blue-500" />
<span className="text-xs font-semibold text-blue-800">Forward LiteLLM API Key</span>
<span className="text-xs font-semibold text-blue-800">
{t("guardrails.teamGuardrailsTab.forwardLiteLLMApiKey")}
</span>
</div>
<Toggle enabled={g.forwardKey} onToggle={onToggleForwardKey} />
</div>
<p className="text-xs text-blue-700 leading-relaxed">
When enabled, the caller&apos;s LiteLLM API key is forwarded as an{" "}
<code className="font-mono bg-blue-100 px-1 rounded">Authorization</code> header to your guardrail
endpoint. This allows your guardrail to authenticate model calls using the original caller&apos;s
credentials.
<Trans
i18nKey="guardrails.teamGuardrailsTab.forwardKeyDesc"
components={{ code: <code className="font-mono bg-blue-100 px-1 rounded" /> }}
/>
</p>
</div>
<div>
<div className="flex items-center gap-1.5 mb-2">
<span className="text-xs font-semibold text-gray-700">Static headers</span>
<span className="text-xs font-semibold text-gray-700">
{t("guardrails.teamGuardrailsTab.staticHeaders")}
</span>
{g.customHeaders.length > 0 && (
<span className="bg-gray-100 text-gray-600 rounded-full px-1.5 py-0.5 text-xs">
{g.customHeaders.length}
</span>
)}
</div>
<p className="text-xs text-gray-400 mb-2">Sent with every request to the guardrail.</p>
<p className="text-xs text-gray-400 mb-2">{t("guardrails.teamGuardrailsTab.staticHeadersDesc")}</p>
{g.customHeaders.length === 0 ? (
<p className="text-xs text-gray-400 italic mb-2">No static headers configured.</p>
<p className="text-xs text-gray-400 italic mb-2">{t("guardrails.teamGuardrailsTab.noStaticHeaders")}</p>
) : (
<ul className="list-none space-y-1 mb-2">
{g.customHeaders.map((h, i) => (
@ -456,7 +479,7 @@ function DetailPanel({
type="button"
onClick={() => onUpdateCustomHeaders(g.customHeaders.filter((_, idx) => idx !== i))}
className="text-gray-400 hover:text-red-600 flex-shrink-0"
aria-label={`Remove ${h.key}`}
aria-label={t("guardrails.teamGuardrailsTab.removeHeader", { name: h.key })}
>
<XIcon className="h-3.5 w-3.5" />
</button>
@ -469,7 +492,7 @@ function DetailPanel({
type="text"
value={newStaticHeaderKey}
onChange={(e) => setNewStaticHeaderKey(e.target.value)}
placeholder="Header name (e.g. X-API-Key)"
placeholder={t("guardrails.teamGuardrailsTab.headerNamePlaceholder")}
className="flex-1 min-w-0 text-xs font-mono border border-gray-200 rounded px-2 py-1.5 text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500"
onKeyDown={(e) => {
if (e.key === "Enter") {
@ -488,7 +511,7 @@ function DetailPanel({
type="text"
value={newStaticHeaderValue}
onChange={(e) => setNewStaticHeaderValue(e.target.value)}
placeholder="Value"
placeholder={t("guardrails.teamGuardrailsTab.headerValuePlaceholder")}
className="flex-1 min-w-0 text-xs font-mono border border-gray-200 rounded px-2 py-1.5 text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500"
onKeyDown={(e) => {
if (e.key === "Enter") {
@ -516,24 +539,26 @@ function DetailPanel({
}}
className="text-xs font-medium text-blue-600 hover:text-blue-700 border border-blue-200 bg-blue-50 hover:bg-blue-100 px-2 py-1.5 rounded transition-colors flex-shrink-0"
>
Add
{t("common.add")}
</button>
</div>
</div>
<div>
<div className="flex items-center gap-1.5 mb-2">
<span className="text-xs font-semibold text-gray-700">Forward client headers</span>
<span className="text-xs font-semibold text-gray-700">
{t("guardrails.teamGuardrailsTab.forwardClientHeaders")}
</span>
{g.extraHeaders.length > 0 && (
<span className="bg-gray-100 text-gray-600 rounded-full px-1.5 py-0.5 text-xs">
{g.extraHeaders.length}
</span>
)}
</div>
<p className="text-xs text-gray-400 mb-2">
Allowed header names to forward from the client request to the guardrail (e.g. x-request-id).
</p>
<p className="text-xs text-gray-400 mb-2">{t("guardrails.teamGuardrailsTab.forwardClientHeadersDesc")}</p>
{g.extraHeaders.length === 0 ? (
<p className="text-xs text-gray-400 italic mb-2">No forward client headers configured.</p>
<p className="text-xs text-gray-400 italic mb-2">
{t("guardrails.teamGuardrailsTab.noForwardClientHeaders")}
</p>
) : (
<ul className="list-none space-y-1 mb-2">
{g.extraHeaders.map((name, i) => (
@ -546,7 +571,7 @@ function DetailPanel({
type="button"
onClick={() => onUpdateExtraHeaders(g.extraHeaders.filter((_, idx) => idx !== i))}
className="text-gray-400 hover:text-red-600 flex-shrink-0"
aria-label={`Remove ${name}`}
aria-label={t("guardrails.teamGuardrailsTab.removeHeader", { name })}
>
<XIcon className="h-3.5 w-3.5" />
</button>
@ -559,7 +584,7 @@ function DetailPanel({
type="text"
value={newExtraHeader}
onChange={(e) => setNewExtraHeader(e.target.value)}
placeholder="e.g. x-request-id"
placeholder={t("guardrails.teamGuardrailsTab.extraHeaderPlaceholder")}
className="flex-1 min-w-0 text-xs font-mono border border-gray-200 rounded px-2 py-1.5 text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500"
onKeyDown={(e) => {
if (e.key === "Enter") {
@ -583,7 +608,7 @@ function DetailPanel({
}}
className="text-xs font-medium text-blue-600 hover:text-blue-700 border border-blue-200 bg-blue-50 hover:bg-blue-100 px-2 py-1.5 rounded transition-colors"
>
Add
{t("common.add")}
</button>
</div>
</div>
@ -593,7 +618,7 @@ function DetailPanel({
onClick={() => setConfigExpanded(!configExpanded)}
className="w-full flex items-center justify-between px-3 py-2 text-left text-xs font-semibold text-gray-700 bg-gray-50 hover:bg-gray-100 transition-colors"
>
<span>Equivalent config</span>
<span>{t("guardrails.teamGuardrailsTab.equivalentConfig")}</span>
{configExpanded ? (
<ChevronUpIcon className="h-3.5 w-3.5 text-gray-500" />
) : (
@ -609,17 +634,19 @@ function DetailPanel({
<div className="flex items-start gap-2 bg-gray-50 border border-gray-200 rounded-lg p-3">
<InfoIcon className="h-3.5 w-3.5 text-gray-400 flex-shrink-0 mt-0.5" />
<p className="text-xs text-gray-500 leading-relaxed">
This guardrail runs on a separate instance. It receives the user request and forwards the result to the
next step in the pipeline. See{" "}
<a
href="https://docs.litellm.ai/docs/adding_provider/generic_guardrail_api"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
LiteLLM Generic Guardrail API docs
</a>{" "}
for configuration details.
<Trans
i18nKey="guardrails.teamGuardrailsTab.guardrailInfoNote"
components={{
docsLink: (
<a
href="https://docs.litellm.ai/docs/adding_provider/generic_guardrail_api"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
/>
),
}}
/>
</p>
</div>
</div>
@ -629,7 +656,7 @@ function DetailPanel({
className="w-full flex items-center justify-center gap-2 border border-gray-300 text-gray-700 hover:bg-gray-50 text-sm font-medium py-2 rounded-md transition-colors"
>
<ExternalLinkIcon className="h-4 w-4" />
Test Endpoint
{t("guardrails.teamGuardrailsTab.testEndpoint")}
</button>
{g.status === "pending" && (
<div className="flex gap-2">
@ -639,7 +666,7 @@ function DetailPanel({
className="flex-1 flex items-center justify-center gap-1.5 bg-green-500 hover:bg-green-600 text-white text-sm font-medium py-2 rounded-md transition-colors"
>
<CheckIcon className="h-4 w-4" />
Approve
{t("guardrails.teamGuardrailsTab.approve")}
</button>
<button
type="button"
@ -647,7 +674,7 @@ function DetailPanel({
className="flex-1 flex items-center justify-center gap-1.5 border border-red-300 text-red-600 hover:bg-red-50 text-sm font-medium py-2 rounded-md transition-colors"
>
<XIcon className="h-4 w-4" />
Reject
{t("guardrails.teamGuardrailsTab.reject")}
</button>
</div>
)}
@ -665,6 +692,7 @@ type ConfirmDialogProps = {
};
function ConfirmDialog({ action, guardrailName, onConfirm, onCancel }: ConfirmDialogProps) {
const { t } = useTranslation();
const isApprove = action === "approve";
return (
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50">
@ -681,14 +709,14 @@ function ConfirmDialog({ action, guardrailName, onConfirm, onCancel }: ConfirmDi
)}
</div>
<h3 className="text-base font-semibold text-gray-900 mb-1">
{isApprove ? "Approve Guardrail" : "Reject Guardrail"}
{isApprove
? t("guardrails.teamGuardrailsTab.approveGuardrailTitle")
: t("guardrails.teamGuardrailsTab.rejectGuardrailTitle")}
</h3>
<p className="text-sm text-gray-500 mb-5">
Are you sure you want to {action}{" "}
<span className="font-medium text-gray-700">&quot;{guardrailName}&quot;</span>?{" "}
{isApprove
? "This will make it active and available for use."
: "This will mark it as rejected and notify the team."}
? t("guardrails.teamGuardrailsTab.approveGuardrailConfirm", { name: guardrailName })
: t("guardrails.teamGuardrailsTab.rejectGuardrailConfirm", { name: guardrailName })}
</p>
<div className="flex gap-3">
<button
@ -696,7 +724,7 @@ function ConfirmDialog({ action, guardrailName, onConfirm, onCancel }: ConfirmDi
onClick={onCancel}
className="flex-1 border border-gray-300 text-gray-700 hover:bg-gray-50 text-sm font-medium py-2 rounded-md transition-colors"
>
Cancel
{t("common.cancel")}
</button>
<button
type="button"
@ -705,7 +733,7 @@ function ConfirmDialog({ action, guardrailName, onConfirm, onCancel }: ConfirmDi
isApprove ? "bg-green-500 hover:bg-green-600" : "bg-red-500 hover:bg-red-600"
}`}
>
{isApprove ? "Approve" : "Reject"}
{isApprove ? t("guardrails.teamGuardrailsTab.approve") : t("guardrails.teamGuardrailsTab.reject")}
</button>
</div>
</div>
@ -718,6 +746,7 @@ interface TeamGuardrailsTabProps {
}
export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
const { t } = useTranslation();
const [guardrails, setGuardrails] = useState<TeamGuardrail[]>([]);
const [summary, setSummary] = useState({
total: 0,
@ -741,8 +770,8 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
const registerGuardrail = useRegisterGuardrail();
useEffect(() => {
const t = setTimeout(() => setSearchDebounced(search), 300);
return () => clearTimeout(t);
const timer = setTimeout(() => setSearchDebounced(search), 300);
return () => clearTimeout(timer);
}, [search]);
const fetchSubmissions = useCallback(async () => {
@ -762,12 +791,12 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
setGuardrails(res.submissions.map(submissionToTeamGuardrail));
setSummary(res.summary);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load submissions");
setError(err instanceof Error ? err.message : t("guardrails.teamGuardrailsTab.failedToLoadSubmissions"));
setGuardrails([]);
} finally {
setIsLoading(false);
}
}, [accessToken, statusFilter, searchDebounced]);
}, [accessToken, statusFilter, searchDebounced, t]);
useEffect(() => {
fetchSubmissions();
@ -790,9 +819,13 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
litellm_params: { forward_api_key: newValue },
});
setGuardrails((prev) => prev.map((x) => (x.id === id ? { ...x, forwardKey: newValue } : x)));
NotificationsManager.success(newValue ? "Forward API key enabled" : "Forward API key disabled");
NotificationsManager.success(
newValue
? t("guardrails.teamGuardrailsTab.forwardApiKeyEnabled")
: t("guardrails.teamGuardrailsTab.forwardApiKeyDisabled"),
);
} catch {
NotificationsManager.fromBackend("Failed to update forward API key");
NotificationsManager.fromBackend(t("guardrails.teamGuardrailsTab.failedToUpdateForwardApiKey"));
}
}
@ -816,9 +849,9 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
: x,
),
);
NotificationsManager.success("Static headers updated");
NotificationsManager.success(t("guardrails.teamGuardrailsTab.staticHeadersUpdated"));
} catch {
NotificationsManager.fromBackend("Failed to update static headers");
NotificationsManager.fromBackend(t("guardrails.teamGuardrailsTab.failedToUpdateStaticHeaders"));
}
}
@ -829,9 +862,9 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
litellm_params: { extra_headers: extraHeaders },
});
setGuardrails((prev) => prev.map((x) => (x.id === id ? { ...x, extraHeaders } : x)));
NotificationsManager.success("Forward client headers updated");
NotificationsManager.success(t("guardrails.teamGuardrailsTab.forwardClientHeadersUpdated"));
} catch {
NotificationsManager.fromBackend("Failed to update forward client headers");
NotificationsManager.fromBackend(t("guardrails.teamGuardrailsTab.failedToUpdateForwardClientHeaders"));
}
}
@ -842,9 +875,9 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
setConfirmAction(null);
if (selectedId === id) setSelectedId(null);
await fetchSubmissions();
NotificationsManager.success("Guardrail approved");
NotificationsManager.success(t("guardrails.teamGuardrailsTab.guardrailApproved"));
} catch {
NotificationsManager.fromBackend("Failed to approve guardrail");
NotificationsManager.fromBackend(t("guardrails.teamGuardrailsTab.failedToApproveGuardrail"));
}
}
@ -855,9 +888,9 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
setConfirmAction(null);
if (selectedId === id) setSelectedId(null);
await fetchSubmissions();
NotificationsManager.success("Guardrail rejected");
NotificationsManager.success(t("guardrails.teamGuardrailsTab.guardrailRejected"));
} catch {
NotificationsManager.fromBackend("Failed to reject guardrail");
NotificationsManager.fromBackend(t("guardrails.teamGuardrailsTab.failedToRejectGuardrail"));
}
}
@ -874,17 +907,25 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
<div className="flex h-full">
<div className={`flex-1 min-w-0 p-6 overflow-auto ${selected ? "border-r border-gray-200" : ""}`}>
<div className="grid grid-cols-4 gap-4 mb-6">
<StatCard label="Total Submitted" value={totalCount} color="text-gray-900" />
<StatCard label="Pending Review" value={pendingCount} color="text-yellow-600" />
<StatCard label="Active" value={activeCount} color="text-green-600" />
<StatCard label="Rejected" value={rejectedCount} color="text-red-600" />
<StatCard label={t("guardrails.teamGuardrailsTab.totalSubmitted")} value={totalCount} color="text-gray-900" />
<StatCard
label={t("guardrails.teamGuardrailsTab.statusPendingReview")}
value={pendingCount}
color="text-yellow-600"
/>
<StatCard label={t("guardrails.teamGuardrailsTab.statusActive")} value={activeCount} color="text-green-600" />
<StatCard
label={t("guardrails.teamGuardrailsTab.statusRejected")}
value={rejectedCount}
color="text-red-600"
/>
</div>
<div className="flex items-center gap-3 mb-5">
<div className="relative flex-1 max-w-xs">
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Search guardrails..."
placeholder={t("guardrails.teamGuardrailsTab.searchPlaceholder")}
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full pl-9 pr-4 py-2 border border-gray-200 rounded-md text-sm text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
@ -895,10 +936,10 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
onChange={(e) => setStatusFilter(e.target.value as typeof statusFilter)}
className="border border-gray-200 rounded-md px-3 py-2 text-sm text-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 bg-white"
>
<option value="all">All Status</option>
<option value="pending">Pending Review</option>
<option value="active">Active</option>
<option value="rejected">Rejected</option>
<option value="all">{t("guardrails.teamGuardrailsTab.filterAllStatus")}</option>
<option value="pending">{t("guardrails.teamGuardrailsTab.statusPendingReview")}</option>
<option value="active">{t("guardrails.teamGuardrailsTab.statusActive")}</option>
<option value="rejected">{t("guardrails.teamGuardrailsTab.statusRejected")}</option>
</select>
<button
type="button"
@ -906,14 +947,20 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
className="ml-auto flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium px-4 py-2 rounded-md transition-colors"
>
<PlusIcon className="h-4 w-4" />
Add Guardrail
{t("guardrails.teamGuardrailsTab.addGuardrail")}
</button>
</div>
<div className="space-y-3">
{isLoading && <div className="text-center py-12 text-gray-500 text-sm">Loading submissions…</div>}
{isLoading && (
<div className="text-center py-12 text-gray-500 text-sm">
{t("guardrails.teamGuardrailsTab.loadingSubmissions")}
</div>
)}
{error && <div className="text-center py-12 text-red-600 text-sm">{error}</div>}
{!isLoading && !error && filtered.length === 0 && (
<div className="text-center py-12 text-gray-400 text-sm">No guardrails match your filters.</div>
<div className="text-center py-12 text-gray-400 text-sm">
{t("guardrails.teamGuardrailsTab.noGuardrailsMatch")}
</div>
)}
{!isLoading &&
!error &&
@ -955,17 +1002,17 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
)}
<Modal
title="Submit Guardrail for Review"
title={t("guardrails.teamGuardrailsTab.submitModalTitle")}
open={isSubmitModalOpen}
onCancel={() => {
setIsSubmitModalOpen(false);
submitForm.resetFields();
}}
onOk={() => submitForm.submit()}
okText="Submit for Review"
okText={t("guardrails.teamGuardrailsTab.submitForReview")}
>
<div className="rounded-md bg-blue-50 border border-blue-200 px-4 py-3 text-sm text-blue-800 mb-4">
Your guardrail will be sent for admin review before it becomes active.
{t("guardrails.teamGuardrailsTab.submitCallout")}
</div>
<Form
form={submitForm}
@ -985,7 +1032,7 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
litellm_params,
guardrail_info: values.guardrail_info ? JSON.parse(values.guardrail_info) : undefined,
});
NotificationsManager.success("Guardrail submitted for review");
NotificationsManager.success(t("guardrails.teamGuardrailsTab.guardrailSubmitted"));
setIsSubmitModalOpen(false);
submitForm.resetFields();
fetchSubmissions();
@ -994,37 +1041,45 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
}
}}
>
<Form.Item label="Team" name="team_id" rules={[{ required: true, message: "Select a team" }]}>
<Form.Item
label={t("guardrails.teamGuardrailsTab.formTeam")}
name="team_id"
rules={[{ required: true, message: t("guardrails.teamGuardrailsTab.formTeamRequired") }]}
>
<TeamDropdown />
</Form.Item>
<Form.Item
label="Guardrail Name"
label={t("guardrails.teamGuardrailsTab.formGuardrailName")}
name="guardrail_name"
rules={[{ required: true, message: "Enter a guardrail name" }]}
rules={[{ required: true, message: t("guardrails.teamGuardrailsTab.formGuardrailNameRequired") }]}
>
<Input placeholder="e.g. pii-detection" />
<Input placeholder={t("guardrails.teamGuardrailsTab.formGuardrailNamePlaceholder")} />
</Form.Item>
<Form.Item label="Mode" name="mode" rules={[{ required: true, message: "Select a mode" }]}>
<Form.Item
label={t("guardrails.teamGuardrailsTab.formMode")}
name="mode"
rules={[{ required: true, message: t("guardrails.teamGuardrailsTab.formModeRequired") }]}
>
<Select>
<Select.Option value="pre_call">Pre Call</Select.Option>
<Select.Option value="post_call">Post Call</Select.Option>
<Select.Option value="during_call">During Call</Select.Option>
<Select.Option value="pre_call">{t("guardrails.teamGuardrailsTab.modePreCall")}</Select.Option>
<Select.Option value="post_call">{t("guardrails.teamGuardrailsTab.modePostCall")}</Select.Option>
<Select.Option value="during_call">{t("guardrails.teamGuardrailsTab.modeDuringCall")}</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="API Base URL"
label={t("guardrails.teamGuardrailsTab.formApiBaseUrl")}
name="api_base"
rules={[
{ required: true, message: "Enter the API base URL" },
{ type: "url", message: "Must be a valid URL" },
{ required: true, message: t("guardrails.teamGuardrailsTab.formApiBaseUrlRequired") },
{ type: "url", message: t("guardrails.teamGuardrailsTab.formApiBaseUrlInvalid") },
]}
>
<Input placeholder="https://your-guardrail-api.com/v1/check" className="font-mono" />
<Input placeholder={t("guardrails.teamGuardrailsTab.formApiBaseUrlPlaceholder")} className="font-mono" />
</Form.Item>
<Form.Item
label="Additional litellm_params (optional)"
label={t("guardrails.teamGuardrailsTab.formExtraParams")}
name="extra_litellm_params"
tooltip="JSON object merged into litellm_params. e.g. forward_api_key, headers, model, unreachable_fallback"
tooltip={t("guardrails.teamGuardrailsTab.formExtraParamsTooltip")}
rules={[
{
validator: (_, value) => {
@ -1032,11 +1087,11 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
try {
const parsed = JSON.parse(value);
if (typeof parsed !== "object" || Array.isArray(parsed)) {
return Promise.reject("Must be a JSON object");
return Promise.reject(t("guardrails.teamGuardrailsTab.formMustBeJsonObject"));
}
return Promise.resolve();
} catch {
return Promise.reject("Invalid JSON");
return Promise.reject(t("guardrails.teamGuardrailsTab.formInvalidJson"));
}
},
},
@ -1045,11 +1100,11 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
<Input.TextArea
rows={3}
className="font-mono text-xs"
placeholder='{"forward_api_key": true, "headers": {"X-Custom": "value"}}'
placeholder={t("guardrails.teamGuardrailsTab.formExtraParamsPlaceholder")}
/>
</Form.Item>
<Form.Item
label="Guardrail Info (optional)"
label={t("guardrails.teamGuardrailsTab.formGuardrailInfo")}
name="guardrail_info"
rules={[
{
@ -1059,7 +1114,7 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
JSON.parse(value);
return Promise.resolve();
} catch {
return Promise.reject("Invalid JSON");
return Promise.reject(t("guardrails.teamGuardrailsTab.formInvalidJson"));
}
},
},
@ -1068,7 +1123,7 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
<Input.TextArea
rows={3}
className="font-mono text-xs"
placeholder='{"description": "Detects PII in requests"}'
placeholder={t("guardrails.teamGuardrailsTab.formGuardrailInfoPlaceholder")}
/>
</Form.Item>
</Form>

View file

@ -1222,7 +1222,85 @@
"labelMode": "Mode",
"labelDefaultOn": "Default On",
"deleteSuccess": "Guardrail \"{{name}}\" deleted successfully",
"deleteFailed": "Failed to delete guardrail"
"deleteFailed": "Failed to delete guardrail",
"teamGuardrailsTab": {
"statusActive": "Active",
"statusPendingReview": "Pending Review",
"statusRejected": "Rejected",
"teamLabel": "Team: {{team}}",
"modelLabel": "Model:",
"submittedLabel": "Submitted:",
"forwardApiKey": "Forward API Key",
"review": "Review",
"approve": "Approve",
"reject": "Reject",
"staticHeaders": "Static headers",
"noStaticHeaders": "No static headers configured.",
"submittedBy": "Submitted by {{by}} on {{at}}",
"closeDetailPanel": "Close detail panel",
"endpoint": "Endpoint",
"method": "Method",
"forwardLiteLLMApiKey": "Forward LiteLLM API Key",
"forwardKeyDesc": "When enabled, the caller's LiteLLM API key is forwarded as an <code>Authorization</code> header to your guardrail endpoint. This allows your guardrail to authenticate model calls using the original caller's credentials.",
"staticHeadersDesc": "Sent with every request to the guardrail.",
"removeHeader": "Remove {{name}}",
"headerNamePlaceholder": "Header name (e.g. X-API-Key)",
"headerValuePlaceholder": "Value",
"forwardClientHeaders": "Forward client headers",
"forwardClientHeadersDesc": "Allowed header names to forward from the client request to the guardrail (e.g. x-request-id).",
"noForwardClientHeaders": "No forward client headers configured.",
"extraHeaderPlaceholder": "e.g. x-request-id",
"equivalentConfig": "Equivalent config",
"guardrailInfoNote": "This guardrail runs on a separate instance. It receives the user request and forwards the result to the next step in the pipeline. See <docsLink>LiteLLM Generic Guardrail API docs</docsLink> for configuration details.",
"testEndpoint": "Test Endpoint",
"approveGuardrailTitle": "Approve Guardrail",
"rejectGuardrailTitle": "Reject Guardrail",
"approveGuardrailConfirm": "Are you sure you want to approve \"{{name}}\"? This will make it active and available for use.",
"rejectGuardrailConfirm": "Are you sure you want to reject \"{{name}}\"? This will mark it as rejected and notify the team.",
"totalSubmitted": "Total Submitted",
"searchPlaceholder": "Search guardrails...",
"filterAllStatus": "All Status",
"addGuardrail": "Add Guardrail",
"loadingSubmissions": "Loading submissions…",
"noGuardrailsMatch": "No guardrails match your filters.",
"failedToLoadSubmissions": "Failed to load submissions",
"forwardApiKeyEnabled": "Forward API key enabled",
"forwardApiKeyDisabled": "Forward API key disabled",
"failedToUpdateForwardApiKey": "Failed to update forward API key",
"staticHeadersUpdated": "Static headers updated",
"failedToUpdateStaticHeaders": "Failed to update static headers",
"forwardClientHeadersUpdated": "Forward client headers updated",
"failedToUpdateForwardClientHeaders": "Failed to update forward client headers",
"guardrailApproved": "Guardrail approved",
"failedToApproveGuardrail": "Failed to approve guardrail",
"guardrailRejected": "Guardrail rejected",
"failedToRejectGuardrail": "Failed to reject guardrail",
"submitModalTitle": "Submit Guardrail for Review",
"submitForReview": "Submit for Review",
"submitCallout": "Your guardrail will be sent for admin review before it becomes active.",
"guardrailSubmitted": "Guardrail submitted for review",
"formTeam": "Team",
"formTeamRequired": "Select a team",
"formGuardrailName": "Guardrail Name",
"formGuardrailNameRequired": "Enter a guardrail name",
"formGuardrailNamePlaceholder": "e.g. pii-detection",
"formMode": "Mode",
"formModeRequired": "Select a mode",
"modePreCall": "Pre Call",
"modePostCall": "Post Call",
"modeDuringCall": "During Call",
"formApiBaseUrl": "API Base URL",
"formApiBaseUrlRequired": "Enter the API base URL",
"formApiBaseUrlInvalid": "Must be a valid URL",
"formApiBaseUrlPlaceholder": "https://your-guardrail-api.com/v1/check",
"formExtraParams": "Additional litellm_params (optional)",
"formExtraParamsTooltip": "JSON object merged into litellm_params. e.g. forward_api_key, headers, model, unreachable_fallback",
"formExtraParamsPlaceholder": "{\"forward_api_key\": true, \"headers\": {\"X-Custom\": \"value\"}}",
"formMustBeJsonObject": "Must be a JSON object",
"formInvalidJson": "Invalid JSON",
"formGuardrailInfo": "Guardrail Info (optional)",
"formGuardrailInfoPlaceholder": "{\"description\": \"Detects PII in requests\"}"
}
},
"keyValueInput": {
"headerNamePlaceholder": "Header Name",

View file

@ -1208,6 +1208,84 @@
"statusNotSet": "未设置"
},
"guardrails": {
"teamGuardrailsTab": {
"statusActive": "已激活",
"statusPendingReview": "待审核",
"statusRejected": "已拒绝",
"teamLabel": "团队:{{team}}",
"modelLabel": "模型:",
"submittedLabel": "提交时间:",
"forwardApiKey": "转发 API 密钥",
"review": "审阅",
"approve": "批准",
"reject": "拒绝",
"staticHeaders": "静态请求头",
"noStaticHeaders": "未配置静态请求头。",
"submittedBy": "由 {{by}} 于 {{at}} 提交",
"closeDetailPanel": "关闭详情面板",
"endpoint": "端点",
"method": "方法",
"forwardLiteLLMApiKey": "转发 LiteLLM API 密钥",
"forwardKeyDesc": "启用后,调用方的 LiteLLM API 密钥将作为 <code>Authorization</code> 请求头转发到您的护栏端点,使护栏能够使用原始调用方凭据验证模型调用。",
"staticHeadersDesc": "随每次护栏请求一同发送。",
"removeHeader": "移除 {{name}}",
"headerNamePlaceholder": "请求头名称(如 X-API-Key)",
"headerValuePlaceholder": "值",
"forwardClientHeaders": "转发客户端请求头",
"forwardClientHeadersDesc": "允许从客户端请求转发到护栏的请求头名称(如 x-request-id)。",
"noForwardClientHeaders": "未配置转发客户端请求头。",
"extraHeaderPlaceholder": "如 x-request-id",
"equivalentConfig": "等效配置",
"guardrailInfoNote": "此护栏运行在独立实例上,接收用户请求并将结果转发到流水线下一步。请参阅 <docsLink>LiteLLM Generic Guardrail API 文档</docsLink>了解配置详情。",
"testEndpoint": "测试端点",
"approveGuardrailTitle": "批准护栏",
"rejectGuardrailTitle": "拒绝护栏",
"approveGuardrailConfirm": "确定要批准「{{name}}」吗?批准后将使其激活并可供使用。",
"rejectGuardrailConfirm": "确定要拒绝「{{name}}」吗?拒绝后将标记为已拒绝并通知团队。",
"totalSubmitted": "已提交总数",
"searchPlaceholder": "搜索护栏...",
"filterAllStatus": "所有状态",
"addGuardrail": "添加护栏",
"loadingSubmissions": "加载提交记录中…",
"noGuardrailsMatch": "没有护栏匹配您的筛选条件。",
"failedToLoadSubmissions": "加载提交记录失败",
"forwardApiKeyEnabled": "转发 API 密钥已启用",
"forwardApiKeyDisabled": "转发 API 密钥已禁用",
"failedToUpdateForwardApiKey": "更新转发 API 密钥失败",
"staticHeadersUpdated": "静态请求头已更新",
"failedToUpdateStaticHeaders": "更新静态请求头失败",
"forwardClientHeadersUpdated": "转发客户端请求头已更新",
"failedToUpdateForwardClientHeaders": "更新转发客户端请求头失败",
"guardrailApproved": "护栏已批准",
"failedToApproveGuardrail": "批准护栏失败",
"guardrailRejected": "护栏已拒绝",
"failedToRejectGuardrail": "拒绝护栏失败",
"submitModalTitle": "提交护栏审核",
"submitForReview": "提交审核",
"submitCallout": "您的护栏将在激活前发送给管理员审核。",
"guardrailSubmitted": "护栏已提交审核",
"formTeam": "团队",
"formTeamRequired": "请选择团队",
"formGuardrailName": "护栏名称",
"formGuardrailNameRequired": "请输入护栏名称",
"formGuardrailNamePlaceholder": "如 pii-detection",
"formMode": "模式",
"formModeRequired": "请选择模式",
"modePreCall": "调用前",
"modePostCall": "调用后",
"modeDuringCall": "调用中",
"formApiBaseUrl": "API 基础 URL",
"formApiBaseUrlRequired": "请输入 API 基础 URL",
"formApiBaseUrlInvalid": "请输入有效的 URL",
"formApiBaseUrlPlaceholder": "https://your-guardrail-api.com/v1/check",
"formExtraParams": "附加 litellm_params(可选)",
"formExtraParamsTooltip": "合并到 litellm_params 的 JSON 对象,如 forward_api_key、headers、model、unreachable_fallback",
"formExtraParamsPlaceholder": "{\"forward_api_key\": true, \"headers\": {\"X-Custom\": \"value\"}}",
"formMustBeJsonObject": "必须为 JSON 对象",
"formInvalidJson": "JSON 格式无效",
"formGuardrailInfo": "护栏信息(可选)",
"formGuardrailInfoPlaceholder": "{\"description\": \"Detects PII in requests\"}"
},
"guardrailGardenTab": "护栏库",
"guardrailsTab": "护栏",
"testPlaygroundTab": "测试 Playground",