mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for policy attachment and creation forms
This commit is contained in:
parent
3b7cdc1246
commit
d2c6db823e
4 changed files with 350 additions and 169 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Modal, Form, Select, Radio, Divider, Typography } from "antd";
|
||||
import { Button } from "@tremor/react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { Policy } from "./types";
|
||||
import { teamListCall, keyListCall, modelAvailableCall, estimateAttachmentImpactCall } from "../networking";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
|
|
@ -27,6 +28,7 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
|||
policies,
|
||||
createAttachment,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [scopeType, setScopeType] = useState<"global" | "specific">("global");
|
||||
|
|
@ -158,11 +160,11 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
|||
const failed = results.filter((r) => r.status === "rejected") as PromiseRejectedResult[];
|
||||
|
||||
if (successCount > 0 && failed.length === 0) {
|
||||
NotificationsManager.success(
|
||||
successCount === 1 ? "Attachment created successfully" : `${successCount} attachments created successfully`,
|
||||
);
|
||||
NotificationsManager.success(t("policies.addAttachmentForm.attachmentCreatedSuccess", { count: successCount }));
|
||||
} else if (successCount > 0 && failed.length > 0) {
|
||||
NotificationsManager.fromBackend(`${successCount} attachments created, ${failed.length} failed`);
|
||||
NotificationsManager.fromBackend(
|
||||
t("policies.addAttachmentForm.attachmentsPartialSuccess", { successCount, failedCount: failed.length }),
|
||||
);
|
||||
} else {
|
||||
throw new Error(failed[0]?.reason instanceof Error ? failed[0].reason.message : "Failed to create attachments");
|
||||
}
|
||||
|
|
@ -173,7 +175,9 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
|||
} catch (error) {
|
||||
console.error("Failed to create attachment:", error);
|
||||
NotificationsManager.fromBackend(
|
||||
"Failed to create attachment: " + (error instanceof Error ? error.message : String(error)),
|
||||
t("policies.addAttachmentForm.createAttachmentFailed", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
|
|
@ -186,7 +190,13 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
|||
}));
|
||||
|
||||
return (
|
||||
<Modal title="Create Policy Attachment" open={visible} onCancel={handleClose} footer={null} width={600}>
|
||||
<Modal
|
||||
title={t("policies.addAttachmentForm.modalTitle")}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
|
|
@ -196,12 +206,12 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
|||
>
|
||||
<Form.Item
|
||||
name="policy_names"
|
||||
label="Policies"
|
||||
rules={[{ required: true, message: "Please select at least one policy" }]}
|
||||
label={t("policies.addAttachmentForm.policiesLabel")}
|
||||
rules={[{ required: true, message: t("policies.addAttachmentForm.policiesRequired") }]}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="Select policies to attach"
|
||||
placeholder={t("policies.addAttachmentForm.policiesPlaceholder")}
|
||||
options={policyOptions}
|
||||
showSearch
|
||||
filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())}
|
||||
|
|
@ -210,13 +220,13 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
|||
</Form.Item>
|
||||
|
||||
<Divider orientation="left">
|
||||
<Text strong>Scope</Text>
|
||||
<Text strong>{t("policies.addAttachmentForm.scopeDivider")}</Text>
|
||||
</Divider>
|
||||
|
||||
<Form.Item label="Scope Type">
|
||||
<Form.Item label={t("policies.addAttachmentForm.scopeTypeLabel")}>
|
||||
<Radio.Group value={scopeType} onChange={(e) => setScopeType(e.target.value)}>
|
||||
<Radio value="specific">Specific (teams, keys, models, or tags)</Radio>
|
||||
<Radio value="global">Global (applies to all requests)</Radio>
|
||||
<Radio value="specific">{t("policies.addAttachmentForm.scopeSpecific")}</Radio>
|
||||
<Radio value="global">{t("policies.addAttachmentForm.scopeGlobal")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
|
|
@ -224,12 +234,12 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
|||
<>
|
||||
<Form.Item
|
||||
name="teams"
|
||||
label="Teams"
|
||||
tooltip="Select team aliases or enter custom patterns. Supports wildcards (e.g., healthcare-*)"
|
||||
label={t("policies.addAttachmentForm.teamsLabel")}
|
||||
tooltip={t("policies.addAttachmentForm.teamsTooltip")}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder={isLoadingTeams ? "Loading teams..." : "Select or enter team aliases"}
|
||||
placeholder={isLoadingTeams ? t("common.loading") : t("policies.addAttachmentForm.teamsPlaceholder")}
|
||||
loading={isLoadingTeams}
|
||||
options={availableTeams.map((team) => ({
|
||||
label: team,
|
||||
|
|
@ -244,12 +254,12 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
|||
|
||||
<Form.Item
|
||||
name="keys"
|
||||
label="Keys"
|
||||
tooltip="Select key aliases or enter custom patterns. Supports wildcards (e.g., dev-*)"
|
||||
label={t("policies.addAttachmentForm.keysLabel")}
|
||||
tooltip={t("policies.addAttachmentForm.keysTooltip")}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder={isLoadingKeys ? "Loading keys..." : "Select or enter key aliases"}
|
||||
placeholder={isLoadingKeys ? t("common.loading") : t("policies.addAttachmentForm.keysPlaceholder")}
|
||||
loading={isLoadingKeys}
|
||||
options={availableKeys.map((key) => ({
|
||||
label: key,
|
||||
|
|
@ -264,14 +274,12 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
|||
|
||||
<Form.Item
|
||||
name="models"
|
||||
label="Models"
|
||||
tooltip="Model names this attachment applies to. Supports wildcards (e.g., gpt-4*). Leave empty to apply to all models."
|
||||
label={t("policies.addAttachmentForm.modelsLabel")}
|
||||
tooltip={t("policies.addAttachmentForm.modelsTooltip")}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder={
|
||||
isLoadingModels ? "Loading models..." : "Select or enter model names (e.g., gpt-4, bedrock/*)"
|
||||
}
|
||||
placeholder={isLoadingModels ? t("common.loading") : t("policies.addAttachmentForm.modelsPlaceholder")}
|
||||
loading={isLoadingModels}
|
||||
options={availableModels.map((model) => ({
|
||||
label: model,
|
||||
|
|
@ -286,19 +294,17 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
|||
|
||||
<Form.Item
|
||||
name="tags"
|
||||
label="Tags"
|
||||
tooltip="Match against tags set in key or team metadata. Use exact values (e.g., healthcare) or wildcard patterns (e.g., health-*) where * matches any suffix."
|
||||
label={t("policies.addAttachmentForm.tagsLabel")}
|
||||
tooltip={t("policies.addAttachmentForm.tagsTooltip")}
|
||||
extra={
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
Matches tags from key/team <code>metadata.tags</code> or tags passed dynamically in the request body.
|
||||
Use <code>*</code> as a suffix wildcard (e.g., <code>prod-*</code> matches <code>prod-us</code>,{" "}
|
||||
<code>prod-eu</code>).
|
||||
<Trans i18nKey="policies.addAttachmentForm.tagsExtra" components={{ code: <code /> }} />
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder="Type a tag and press Enter (e.g. healthcare, prod-*)"
|
||||
placeholder={t("policies.addAttachmentForm.tagsPlaceholder")}
|
||||
tokenSeparators={[",", " "]}
|
||||
notFoundContent={null}
|
||||
suffixIcon={null}
|
||||
|
|
@ -313,15 +319,15 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
|||
|
||||
<div className="flex justify-end space-x-2 mt-4">
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
{scopeType === "specific" && (
|
||||
<Button variant="secondary" onClick={handlePreviewImpact} loading={isEstimating}>
|
||||
Estimate Impact
|
||||
{t("policies.addAttachmentForm.estimateImpact")}
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleSubmit} loading={isSubmitting}>
|
||||
Create Attachment
|
||||
{t("policies.addAttachmentForm.createAttachment")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Form, Select, Modal, Divider, Typography, Tag, Alert, Radio } from "antd";
|
||||
import { Button, TextInput, Textarea } from "@tremor/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Policy, PolicyCreateRequest, PolicyUpdateRequest } from "./types";
|
||||
import { Guardrail } from "../guardrails/types";
|
||||
import { getResolvedGuardrails, modelAvailableCall } from "../networking";
|
||||
|
|
@ -32,116 +33,119 @@ interface ModePicker {
|
|||
onSelect: (mode: "simple" | "flow_builder") => void;
|
||||
}
|
||||
|
||||
const ModePicker: React.FC<ModePicker> = ({ selected, onSelect }) => (
|
||||
<div className="flex gap-4" style={{ padding: "8px 0" }}>
|
||||
{/* Simple Mode Card */}
|
||||
<div
|
||||
onClick={() => onSelect("simple")}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "24px 20px",
|
||||
border: `2px solid ${selected === "simple" ? "#4f46e5" : "#e5e7eb"}`,
|
||||
borderRadius: 12,
|
||||
cursor: "pointer",
|
||||
backgroundColor: selected === "simple" ? "#eef2ff" : "#fff",
|
||||
transition: "all 0.15s ease",
|
||||
}}
|
||||
>
|
||||
const ModePicker: React.FC<ModePicker> = ({ selected, onSelect }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex gap-4" style={{ padding: "8px 0" }}>
|
||||
{/* Simple Mode Card */}
|
||||
<div
|
||||
onClick={() => onSelect("simple")}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
backgroundColor: selected === "simple" ? "#e0e7ff" : "#f3f4f6",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: 16,
|
||||
flex: 1,
|
||||
padding: "24px 20px",
|
||||
border: `2px solid ${selected === "simple" ? "#4f46e5" : "#e5e7eb"}`,
|
||||
borderRadius: 12,
|
||||
cursor: "pointer",
|
||||
backgroundColor: selected === "simple" ? "#eef2ff" : "#fff",
|
||||
transition: "all 0.15s ease",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={selected === "simple" ? "#4f46e5" : "#6b7280"}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
backgroundColor: selected === "simple" ? "#e0e7ff" : "#f3f4f6",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||
<path d="M8 7h8M8 12h8M8 17h5" />
|
||||
</svg>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={selected === "simple" ? "#4f46e5" : "#6b7280"}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||
<path d="M8 7h8M8 12h8M8 17h5" />
|
||||
</svg>
|
||||
</div>
|
||||
<Text strong style={{ fontSize: 15, display: "block", marginBottom: 4 }}>
|
||||
{t("policies.addPolicyForm.simpleModeTitle")}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
{t("policies.addPolicyForm.simpleModeSubtitle")}
|
||||
</Text>
|
||||
</div>
|
||||
<Text strong style={{ fontSize: 15, display: "block", marginBottom: 4 }}>
|
||||
Simple Mode
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
Pick guardrails from a list. All run in parallel.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Flow Builder Card */}
|
||||
<div
|
||||
onClick={() => onSelect("flow_builder")}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "24px 20px",
|
||||
border: `2px solid ${selected === "flow_builder" ? "#4f46e5" : "#e5e7eb"}`,
|
||||
borderRadius: 12,
|
||||
cursor: "pointer",
|
||||
backgroundColor: selected === "flow_builder" ? "#eef2ff" : "#fff",
|
||||
transition: "all 0.15s ease",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Tag
|
||||
color="purple"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 12,
|
||||
right: 12,
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
NEW
|
||||
</Tag>
|
||||
{/* Flow Builder Card */}
|
||||
<div
|
||||
onClick={() => onSelect("flow_builder")}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
backgroundColor: selected === "flow_builder" ? "#e0e7ff" : "#f3f4f6",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: 16,
|
||||
flex: 1,
|
||||
padding: "24px 20px",
|
||||
border: `2px solid ${selected === "flow_builder" ? "#4f46e5" : "#e5e7eb"}`,
|
||||
borderRadius: 12,
|
||||
cursor: "pointer",
|
||||
backgroundColor: selected === "flow_builder" ? "#eef2ff" : "#fff",
|
||||
transition: "all 0.15s ease",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={selected === "flow_builder" ? "#4f46e5" : "#6b7280"}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
<Tag
|
||||
color="purple"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 12,
|
||||
right: 12,
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
|
||||
</svg>
|
||||
{t("policies.addPolicyForm.newBadge")}
|
||||
</Tag>
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
backgroundColor: selected === "flow_builder" ? "#e0e7ff" : "#f3f4f6",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={selected === "flow_builder" ? "#4f46e5" : "#6b7280"}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<Text strong style={{ fontSize: 15, display: "block", marginBottom: 4 }}>
|
||||
{t("policies.addPolicyForm.flowBuilderTitle")}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
{t("policies.addPolicyForm.flowBuilderSubtitle")}
|
||||
</Text>
|
||||
</div>
|
||||
<Text strong style={{ fontSize: 15, display: "block", marginBottom: 4 }}>
|
||||
Flow Builder
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
Define steps, conditions, and error responses.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Main Component
|
||||
|
|
@ -159,6 +163,7 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
|||
createPolicy,
|
||||
updatePolicy,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [resolvedGuardrails, setResolvedGuardrails] = useState<string[]>([]);
|
||||
|
|
@ -327,10 +332,10 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
|||
|
||||
if (isEditing && editingPolicy) {
|
||||
await updatePolicy(accessToken, editingPolicy.policy_id, data as PolicyUpdateRequest);
|
||||
NotificationsManager.success("Policy updated successfully");
|
||||
NotificationsManager.success(t("policies.addPolicyForm.policyUpdatedSuccess"));
|
||||
} else {
|
||||
await createPolicy(accessToken, data as PolicyCreateRequest);
|
||||
NotificationsManager.success("Policy created successfully");
|
||||
NotificationsManager.success(t("policies.addPolicyForm.policyCreatedSuccess"));
|
||||
}
|
||||
|
||||
resetForm();
|
||||
|
|
@ -339,7 +344,9 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
|||
} catch (error) {
|
||||
console.error("Failed to save policy:", error);
|
||||
NotificationsManager.fromBackend(
|
||||
"Failed to save policy: " + (error instanceof Error ? error.message : String(error)),
|
||||
t("policies.addPolicyForm.savePolicyFailed", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
|
|
@ -361,12 +368,18 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
|||
// ── Mode Picker Step ──────────────────────────────────────────────────────
|
||||
if (step === "pick_mode") {
|
||||
return (
|
||||
<Modal title="Create New Policy" open={visible} onCancel={handleClose} footer={null} width={620}>
|
||||
<Modal
|
||||
title={t("policies.addPolicyForm.createNewPolicyTitle")}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width={620}
|
||||
>
|
||||
<ModePicker selected={selectedMode} onSelect={setSelectedMode} />
|
||||
|
||||
{selectedMode === "flow_builder" && (
|
||||
<Alert
|
||||
message="You'll be redirected to the full-screen Flow Builder to design your policy logic visually."
|
||||
message={t("policies.addPolicyForm.flowBuilderRedirectAlert")}
|
||||
type="info"
|
||||
style={{
|
||||
marginTop: 16,
|
||||
|
|
@ -378,7 +391,7 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
|||
|
||||
<div className="flex justify-end gap-2" style={{ marginTop: 24 }}>
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleModeConfirm}
|
||||
|
|
@ -388,7 +401,9 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
|||
border: "none",
|
||||
}}
|
||||
>
|
||||
{selectedMode === "flow_builder" ? "Continue to Builder" : "Create Policy"}
|
||||
{selectedMode === "flow_builder"
|
||||
? t("policies.addPolicyForm.continueToBuilder")
|
||||
: t("policies.addPolicyForm.createPolicyButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
@ -398,7 +413,7 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
|||
// ── Simple Form Step ──────────────────────────────────────────────────────
|
||||
return (
|
||||
<Modal
|
||||
title={isEditing ? "Edit Policy" : "Create New Policy"}
|
||||
title={isEditing ? t("policies.addPolicyForm.editPolicyTitle") : t("policies.addPolicyForm.createNewPolicyTitle")}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
|
|
@ -415,52 +430,52 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
|||
>
|
||||
<Form.Item
|
||||
name="policy_name"
|
||||
label="Policy Name"
|
||||
label={t("policies.addPolicyForm.policyNameLabel")}
|
||||
rules={[
|
||||
{ required: true, message: "Please enter a policy name" },
|
||||
{ required: true, message: t("policies.addPolicyForm.policyNameRequired") },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_-]+$/,
|
||||
message: "Policy name can only contain letters, numbers, hyphens, and underscores",
|
||||
message: t("policies.addPolicyForm.policyNamePattern"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TextInput placeholder="e.g., global-baseline, healthcare-compliance" disabled={isEditing} />
|
||||
<TextInput placeholder={t("policies.addPolicyForm.policyNamePlaceholder")} disabled={isEditing} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="description" label="Description">
|
||||
<Textarea rows={2} placeholder="Describe what this policy does..." />
|
||||
<Form.Item name="description" label={t("common.description")}>
|
||||
<Textarea rows={2} placeholder={t("policies.addPolicyForm.descriptionPlaceholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Divider orientation="left">
|
||||
<Text strong>Inheritance</Text>
|
||||
<Text strong>{t("policies.addPolicyForm.inheritanceDivider")}</Text>
|
||||
</Divider>
|
||||
|
||||
<Form.Item
|
||||
name="inherit"
|
||||
label="Inherit From"
|
||||
tooltip="Inherit guardrails from another policy. The child policy will include all guardrails from the parent."
|
||||
label={t("policies.addPolicyForm.inheritFromLabel")}
|
||||
tooltip={t("policies.addPolicyForm.inheritFromTooltip")}
|
||||
>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="Select a parent policy (optional)"
|
||||
placeholder={t("policies.addPolicyForm.inheritFromPlaceholder")}
|
||||
options={policyOptions}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Divider orientation="left">
|
||||
<Text strong>Guardrails</Text>
|
||||
<Text strong>{t("policies.addPolicyForm.guardrailsDivider")}</Text>
|
||||
</Divider>
|
||||
|
||||
<Form.Item
|
||||
name="guardrails_add"
|
||||
label="Guardrails to Add"
|
||||
tooltip="These guardrails will be added to requests matching this policy"
|
||||
label={t("policies.addPolicyForm.guardrailsAddLabel")}
|
||||
tooltip={t("policies.addPolicyForm.guardrailsAddTooltip")}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
placeholder="Select guardrails to add"
|
||||
placeholder={t("policies.addPolicyForm.guardrailsAddPlaceholder")}
|
||||
options={guardrailOptions}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
|
|
@ -468,13 +483,13 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
|||
|
||||
<Form.Item
|
||||
name="guardrails_remove"
|
||||
label="Guardrails to Remove"
|
||||
tooltip="These guardrails will be removed from inherited guardrails"
|
||||
label={t("policies.addPolicyForm.guardrailsRemoveLabel")}
|
||||
tooltip={t("policies.addPolicyForm.guardrailsRemoveTooltip")}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
placeholder="Select guardrails to remove (from inherited)"
|
||||
placeholder={t("policies.addPolicyForm.guardrailsRemovePlaceholder")}
|
||||
options={guardrailOptions}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
|
|
@ -482,11 +497,11 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
|||
|
||||
{resolvedGuardrails.length > 0 && (
|
||||
<Alert
|
||||
message="Resolved Guardrails"
|
||||
message={t("policies.addPolicyForm.resolvedGuardrailsTitle")}
|
||||
description={
|
||||
<div>
|
||||
<Text type="secondary" style={{ display: "block", marginBottom: 8 }}>
|
||||
These are the final guardrails that will be applied (including inheritance):
|
||||
{t("policies.addPolicyForm.resolvedGuardrailsDesc")}
|
||||
</Text>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{resolvedGuardrails.map((g) => (
|
||||
|
|
@ -504,18 +519,18 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
|||
)}
|
||||
|
||||
<Divider orientation="left">
|
||||
<Text strong>Conditions (Optional)</Text>
|
||||
<Text strong>{t("policies.addPolicyForm.conditionsDivider")}</Text>
|
||||
</Divider>
|
||||
|
||||
<Alert
|
||||
message="Model Scope"
|
||||
description="By default, this policy will run on all models. You can optionally restrict it to specific models below."
|
||||
message={t("policies.addPolicyForm.modelScopeAlertTitle")}
|
||||
description={t("policies.addPolicyForm.modelScopeAlertDesc")}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<Form.Item label="Model Condition Type">
|
||||
<Form.Item label={t("policies.addPolicyForm.modelConditionTypeLabel")}>
|
||||
<Radio.Group
|
||||
value={modelConditionType}
|
||||
onChange={(e) => {
|
||||
|
|
@ -523,25 +538,29 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
|||
form.setFieldValue("model_condition", undefined);
|
||||
}}
|
||||
>
|
||||
<Radio value="model">Select Model</Radio>
|
||||
<Radio value="regex">Custom Regex Pattern</Radio>
|
||||
<Radio value="model">{t("policies.addPolicyForm.modelConditionSelectModel")}</Radio>
|
||||
<Radio value="regex">{t("policies.addPolicyForm.modelConditionRegex")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="model_condition"
|
||||
label={modelConditionType === "model" ? "Model (Optional)" : "Regex Pattern (Optional)"}
|
||||
label={
|
||||
modelConditionType === "model"
|
||||
? t("policies.addPolicyForm.modelConditionLabelModel")
|
||||
: t("policies.addPolicyForm.modelConditionLabelRegex")
|
||||
}
|
||||
tooltip={
|
||||
modelConditionType === "model"
|
||||
? "Select a specific model to apply this policy to. Leave empty to apply to all models."
|
||||
: "Enter a regex pattern to match models (e.g., gpt-4.* or bedrock/.*). Leave empty to apply to all models."
|
||||
? t("policies.addPolicyForm.modelConditionTooltipModel")
|
||||
: t("policies.addPolicyForm.modelConditionTooltipRegex")
|
||||
}
|
||||
>
|
||||
{modelConditionType === "model" ? (
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder="Leave empty to apply to all models"
|
||||
placeholder={t("policies.addPolicyForm.modelConditionPlaceholderModel")}
|
||||
options={availableModels.map((model) => ({
|
||||
label: model,
|
||||
value: model,
|
||||
|
|
@ -550,16 +569,18 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
|||
style={{ width: "100%" }}
|
||||
/>
|
||||
) : (
|
||||
<TextInput placeholder="Leave empty to apply to all models (e.g., gpt-4.* or bedrock/claude-.*)" />
|
||||
<TextInput placeholder={t("policies.addPolicyForm.modelConditionPlaceholderRegex")} />
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<div className="flex justify-end space-x-2 mt-4">
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} loading={isSubmitting}>
|
||||
{isEditing ? "Update Policy" : "Create Policy"}
|
||||
{isEditing
|
||||
? t("policies.addPolicyForm.updatePolicyButton")
|
||||
: t("policies.addPolicyForm.createPolicyButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -4654,5 +4654,82 @@
|
|||
"editVariableName": "Edit variable name",
|
||||
"variableNamePlaceholder": "Variable name"
|
||||
}
|
||||
},
|
||||
"policies": {
|
||||
"addAttachmentForm": {
|
||||
"modalTitle": "Create Policy Attachment",
|
||||
"policiesLabel": "Policies",
|
||||
"policiesRequired": "Please select at least one policy",
|
||||
"policiesPlaceholder": "Select policies to attach",
|
||||
"scopeDivider": "Scope",
|
||||
"scopeTypeLabel": "Scope Type",
|
||||
"scopeSpecific": "Specific (teams, keys, models, or tags)",
|
||||
"scopeGlobal": "Global (applies to all requests)",
|
||||
"teamsLabel": "Teams",
|
||||
"teamsTooltip": "Select team aliases or enter custom patterns. Supports wildcards (e.g., healthcare-*)",
|
||||
"teamsPlaceholder": "Select or enter team aliases",
|
||||
"keysLabel": "Keys",
|
||||
"keysTooltip": "Select key aliases or enter custom patterns. Supports wildcards (e.g., dev-*)",
|
||||
"keysPlaceholder": "Select or enter key aliases",
|
||||
"modelsLabel": "Models",
|
||||
"modelsTooltip": "Model names this attachment applies to. Supports wildcards (e.g., gpt-4*). Leave empty to apply to all models.",
|
||||
"modelsPlaceholder": "Select or enter model names (e.g., gpt-4, bedrock/*)",
|
||||
"tagsLabel": "Tags",
|
||||
"tagsTooltip": "Match against tags set in key or team metadata. Use exact values (e.g., healthcare) or wildcard patterns (e.g., health-*) where * matches any suffix.",
|
||||
"tagsExtra": "Matches tags from key/team <code>metadata.tags</code> or tags passed dynamically in the request body. Use <code>*</code> as a suffix wildcard (e.g., <code>prod-*</code> matches <code>prod-us</code>, <code>prod-eu</code>).",
|
||||
"tagsPlaceholder": "Type a tag and press Enter (e.g. healthcare, prod-*)",
|
||||
"estimateImpact": "Estimate Impact",
|
||||
"createAttachment": "Create Attachment",
|
||||
"attachmentCreatedSuccess_one": "Attachment created successfully",
|
||||
"attachmentCreatedSuccess_other": "{{count}} attachments created successfully",
|
||||
"attachmentsPartialSuccess": "{{successCount}} attachments created, {{failedCount}} failed",
|
||||
"createAttachmentFailed": "Failed to create attachment: {{error}}"
|
||||
},
|
||||
"addPolicyForm": {
|
||||
"simpleModeTitle": "Simple Mode",
|
||||
"simpleModeSubtitle": "Pick guardrails from a list. All run in parallel.",
|
||||
"flowBuilderTitle": "Flow Builder",
|
||||
"flowBuilderSubtitle": "Define steps, conditions, and error responses.",
|
||||
"newBadge": "NEW",
|
||||
"createNewPolicyTitle": "Create New Policy",
|
||||
"editPolicyTitle": "Edit Policy",
|
||||
"flowBuilderRedirectAlert": "You'll be redirected to the full-screen Flow Builder to design your policy logic visually.",
|
||||
"continueToBuilder": "Continue to Builder",
|
||||
"createPolicyButton": "Create Policy",
|
||||
"updatePolicyButton": "Update Policy",
|
||||
"policyNameLabel": "Policy Name",
|
||||
"policyNameRequired": "Please enter a policy name",
|
||||
"policyNamePattern": "Policy name can only contain letters, numbers, hyphens, and underscores",
|
||||
"policyNamePlaceholder": "e.g., global-baseline, healthcare-compliance",
|
||||
"descriptionPlaceholder": "Describe what this policy does...",
|
||||
"inheritanceDivider": "Inheritance",
|
||||
"inheritFromLabel": "Inherit From",
|
||||
"inheritFromTooltip": "Inherit guardrails from another policy. The child policy will include all guardrails from the parent.",
|
||||
"inheritFromPlaceholder": "Select a parent policy (optional)",
|
||||
"guardrailsDivider": "Guardrails",
|
||||
"guardrailsAddLabel": "Guardrails to Add",
|
||||
"guardrailsAddTooltip": "These guardrails will be added to requests matching this policy",
|
||||
"guardrailsAddPlaceholder": "Select guardrails to add",
|
||||
"guardrailsRemoveLabel": "Guardrails to Remove",
|
||||
"guardrailsRemoveTooltip": "These guardrails will be removed from inherited guardrails",
|
||||
"guardrailsRemovePlaceholder": "Select guardrails to remove (from inherited)",
|
||||
"resolvedGuardrailsTitle": "Resolved Guardrails",
|
||||
"resolvedGuardrailsDesc": "These are the final guardrails that will be applied (including inheritance):",
|
||||
"conditionsDivider": "Conditions (Optional)",
|
||||
"modelScopeAlertTitle": "Model Scope",
|
||||
"modelScopeAlertDesc": "By default, this policy will run on all models. You can optionally restrict it to specific models below.",
|
||||
"modelConditionTypeLabel": "Model Condition Type",
|
||||
"modelConditionSelectModel": "Select Model",
|
||||
"modelConditionRegex": "Custom Regex Pattern",
|
||||
"modelConditionLabelModel": "Model (Optional)",
|
||||
"modelConditionLabelRegex": "Regex Pattern (Optional)",
|
||||
"modelConditionTooltipModel": "Select a specific model to apply this policy to. Leave empty to apply to all models.",
|
||||
"modelConditionTooltipRegex": "Enter a regex pattern to match models (e.g., gpt-4.* or bedrock/.*). Leave empty to apply to all models.",
|
||||
"modelConditionPlaceholderModel": "Leave empty to apply to all models",
|
||||
"modelConditionPlaceholderRegex": "Leave empty to apply to all models (e.g., gpt-4.* or bedrock/claude-.*)",
|
||||
"policyUpdatedSuccess": "Policy updated successfully",
|
||||
"policyCreatedSuccess": "Policy created successfully",
|
||||
"savePolicyFailed": "Failed to save policy: {{error}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4654,5 +4654,82 @@
|
|||
"editVariableName": "编辑变量名",
|
||||
"variableNamePlaceholder": "变量名"
|
||||
}
|
||||
},
|
||||
"policies": {
|
||||
"addAttachmentForm": {
|
||||
"modalTitle": "创建策略附加",
|
||||
"policiesLabel": "策略",
|
||||
"policiesRequired": "请至少选择一个策略",
|
||||
"policiesPlaceholder": "选择要附加的策略",
|
||||
"scopeDivider": "范围",
|
||||
"scopeTypeLabel": "范围类型",
|
||||
"scopeSpecific": "特定(团队、密钥、模型或标签)",
|
||||
"scopeGlobal": "全局(适用于所有请求)",
|
||||
"teamsLabel": "团队",
|
||||
"teamsTooltip": "选择团队别名或输入自定义模式,支持通配符(如 healthcare-*)",
|
||||
"teamsPlaceholder": "选择或输入团队别名",
|
||||
"keysLabel": "密钥",
|
||||
"keysTooltip": "选择密钥别名或输入自定义模式,支持通配符(如 dev-*)",
|
||||
"keysPlaceholder": "选择或输入密钥别名",
|
||||
"modelsLabel": "模型",
|
||||
"modelsTooltip": "此附加适用的模型名称,支持通配符(如 gpt-4*)。留空则适用于所有模型。",
|
||||
"modelsPlaceholder": "选择或输入模型名称(如 gpt-4、bedrock/*)",
|
||||
"tagsLabel": "标签",
|
||||
"tagsTooltip": "匹配密钥或团队元数据中设置的标签。使用精确值(如 healthcare)或通配符模式(如 health-*),* 匹配任意后缀。",
|
||||
"tagsExtra": "匹配密钥/团队 <code>metadata.tags</code> 中的标签或请求体中动态传入的标签。使用 <code>*</code> 作为后缀通配符(如 <code>prod-*</code> 匹配 <code>prod-us</code>、<code>prod-eu</code>)。",
|
||||
"tagsPlaceholder": "输入标签后按 Enter(如 healthcare、prod-*)",
|
||||
"estimateImpact": "预估影响",
|
||||
"createAttachment": "创建附加",
|
||||
"attachmentCreatedSuccess_one": "附加创建成功",
|
||||
"attachmentCreatedSuccess_other": "成功创建 {{count}} 个附加",
|
||||
"attachmentsPartialSuccess": "成功创建 {{successCount}} 个附加,{{failedCount}} 个失败",
|
||||
"createAttachmentFailed": "创建附加失败:{{error}}"
|
||||
},
|
||||
"addPolicyForm": {
|
||||
"simpleModeTitle": "简单模式",
|
||||
"simpleModeSubtitle": "从列表中选择护栏,所有护栏并行运行。",
|
||||
"flowBuilderTitle": "流程构建器",
|
||||
"flowBuilderSubtitle": "定义步骤、条件和错误响应。",
|
||||
"newBadge": "新",
|
||||
"createNewPolicyTitle": "创建新策略",
|
||||
"editPolicyTitle": "编辑策略",
|
||||
"flowBuilderRedirectAlert": "您将被重定向到全屏流程构建器,以可视化方式设计策略逻辑。",
|
||||
"continueToBuilder": "前往构建器",
|
||||
"createPolicyButton": "创建策略",
|
||||
"updatePolicyButton": "更新策略",
|
||||
"policyNameLabel": "策略名称",
|
||||
"policyNameRequired": "请输入策略名称",
|
||||
"policyNamePattern": "策略名称只能包含字母、数字、连字符和下划线",
|
||||
"policyNamePlaceholder": "如:global-baseline、healthcare-compliance",
|
||||
"descriptionPlaceholder": "描述此策略的用途...",
|
||||
"inheritanceDivider": "继承",
|
||||
"inheritFromLabel": "继承自",
|
||||
"inheritFromTooltip": "从另一个策略继承护栏,子策略将包含父策略的所有护栏。",
|
||||
"inheritFromPlaceholder": "选择父策略(可选)",
|
||||
"guardrailsDivider": "护栏",
|
||||
"guardrailsAddLabel": "要添加的护栏",
|
||||
"guardrailsAddTooltip": "这些护栏将被添加到匹配此策略的请求中",
|
||||
"guardrailsAddPlaceholder": "选择要添加的护栏",
|
||||
"guardrailsRemoveLabel": "要移除的护栏",
|
||||
"guardrailsRemoveTooltip": "这些护栏将从继承的护栏中移除",
|
||||
"guardrailsRemovePlaceholder": "选择要移除的护栏(来自继承)",
|
||||
"resolvedGuardrailsTitle": "已解析护栏",
|
||||
"resolvedGuardrailsDesc": "以下是最终将应用的护栏(含继承):",
|
||||
"conditionsDivider": "条件(可选)",
|
||||
"modelScopeAlertTitle": "模型范围",
|
||||
"modelScopeAlertDesc": "默认情况下,此策略将在所有模型上运行。您可以在下方选择性限制为特定模型。",
|
||||
"modelConditionTypeLabel": "模型条件类型",
|
||||
"modelConditionSelectModel": "选择模型",
|
||||
"modelConditionRegex": "自定义正则表达式",
|
||||
"modelConditionLabelModel": "模型(可选)",
|
||||
"modelConditionLabelRegex": "正则表达式(可选)",
|
||||
"modelConditionTooltipModel": "选择将此策略应用于的特定模型,留空则应用于所有模型。",
|
||||
"modelConditionTooltipRegex": "输入匹配模型的正则表达式(如 gpt-4.* 或 bedrock/.*),留空则应用于所有模型。",
|
||||
"modelConditionPlaceholderModel": "留空则应用于所有模型",
|
||||
"modelConditionPlaceholderRegex": "留空则应用于所有模型(如 gpt-4.* 或 bedrock/claude-.*)",
|
||||
"policyUpdatedSuccess": "策略更新成功",
|
||||
"policyCreatedSuccess": "策略创建成功",
|
||||
"savePolicyFailed": "保存策略失败:{{error}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue