mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): policies/add_attachment_form to shadcn
- antd Modal/Radio/Divider/Typography + @tremor Button \u2192 shadcn Dialog/Button/RadioGroup + native section divider. - antd Form/Form.Item/Select preserved (mode='tags' + tokenSeparators + filterOption). loadTeamsKeysAndModels refactored into useCallback. Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
331a1fe752
commit
90f0a42743
1 changed files with 134 additions and 79 deletions
|
|
@ -1,21 +1,32 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Modal, Form, Select, Radio, Divider, Typography } from "antd";
|
||||
import { Button } from "@tremor/react";
|
||||
import React, { useCallback, useState, useEffect } from "react";
|
||||
import { Form, Select } from "antd";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Policy } from "./types";
|
||||
import { teamListCall, keyListCall, modelAvailableCall, estimateAttachmentImpactCall } from "../networking";
|
||||
import {
|
||||
teamListCall,
|
||||
keyListCall,
|
||||
modelAvailableCall,
|
||||
estimateAttachmentImpactCall,
|
||||
} from "../networking";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { buildAttachmentData } from "./build_attachment_data";
|
||||
import ImpactPreviewAlert from "./impact_preview_alert";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface AddAttachmentFormProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
accessToken: string | null;
|
||||
policies: Policy[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
createAttachment: (accessToken: string, attachmentData: any) => Promise<any>;
|
||||
}
|
||||
|
||||
|
|
@ -37,25 +48,21 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
|||
const [isLoadingKeys, setIsLoadingKeys] = useState(false);
|
||||
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
||||
const [isEstimating, setIsEstimating] = useState(false);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [impactResult, setImpactResult] = useState<any>(null);
|
||||
const { userId, userRole } = useAuthorized();
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && accessToken) {
|
||||
loadTeamsKeysAndModels();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visible, accessToken]);
|
||||
|
||||
const loadTeamsKeysAndModels = async () => {
|
||||
const loadTeamsKeysAndModels = useCallback(async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
// Load teams — teamListCall returns a plain array of team objects
|
||||
setIsLoadingTeams(true);
|
||||
try {
|
||||
const teamsResponse = await teamListCall(accessToken, null, userId);
|
||||
const teamsArray = Array.isArray(teamsResponse) ? teamsResponse : (teamsResponse?.data || []);
|
||||
const teamsArray = Array.isArray(teamsResponse)
|
||||
? teamsResponse
|
||||
: teamsResponse?.data || [];
|
||||
const teamAliases = teamsArray
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.map((t: any) => t.team_alias)
|
||||
.filter(Boolean);
|
||||
setAvailableTeams(teamAliases);
|
||||
|
|
@ -65,12 +72,21 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
|||
setIsLoadingTeams(false);
|
||||
}
|
||||
|
||||
// Load keys — keyListCall returns {keys: [...], total_count, ...}
|
||||
setIsLoadingKeys(true);
|
||||
try {
|
||||
const keysResponse = await keyListCall(accessToken, null, null, null, null, null, 1, 100);
|
||||
const keysResponse = await keyListCall(
|
||||
accessToken,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
100,
|
||||
);
|
||||
const keysArray = keysResponse?.keys || keysResponse?.data || [];
|
||||
const keyAliases = keysArray
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.map((k: any) => k.key_alias)
|
||||
.filter(Boolean);
|
||||
setAvailableKeys(keyAliases);
|
||||
|
|
@ -80,12 +96,18 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
|||
setIsLoadingKeys(false);
|
||||
}
|
||||
|
||||
// Load models
|
||||
setIsLoadingModels(true);
|
||||
try {
|
||||
const modelsResponse = await modelAvailableCall(accessToken, userId || "", userRole || "");
|
||||
const modelsArray = modelsResponse?.data || (Array.isArray(modelsResponse) ? modelsResponse : []);
|
||||
const modelsResponse = await modelAvailableCall(
|
||||
accessToken,
|
||||
userId || "",
|
||||
userRole || "",
|
||||
);
|
||||
const modelsArray =
|
||||
modelsResponse?.data ||
|
||||
(Array.isArray(modelsResponse) ? modelsResponse : []);
|
||||
const modelIds = modelsArray
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.map((m: any) => m.id || m.model_name)
|
||||
.filter(Boolean);
|
||||
setAvailableModels(modelIds);
|
||||
|
|
@ -94,7 +116,13 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
|||
} finally {
|
||||
setIsLoadingModels(false);
|
||||
}
|
||||
};
|
||||
}, [accessToken, userId, userRole]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && accessToken) {
|
||||
loadTeamsKeysAndModels();
|
||||
}
|
||||
}, [visible, accessToken, loadTeamsKeysAndModels]);
|
||||
|
||||
const resetForm = () => {
|
||||
form.resetFields();
|
||||
|
|
@ -200,50 +228,68 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
|||
}));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Create Policy Attachment"
|
||||
<Dialog
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width={600}
|
||||
onOpenChange={(o) => (!o ? handleClose() : undefined)}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
scope_type: "global",
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="policy_names"
|
||||
label="Policies"
|
||||
rules={[{ required: true, message: "Please select at least one policy" }]}
|
||||
<DialogContent className="max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Policy Attachment</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
scope_type: "global",
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="Select policies to attach"
|
||||
options={policyOptions}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Divider orientation="left">
|
||||
<Text strong>Scope</Text>
|
||||
</Divider>
|
||||
|
||||
<Form.Item label="Scope Type">
|
||||
<Radio.Group
|
||||
value={scopeType}
|
||||
onChange={(e) => setScopeType(e.target.value)}
|
||||
<Form.Item
|
||||
name="policy_names"
|
||||
label="Policies"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please select at least one policy",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Radio value="specific">Specific (teams, keys, models, or tags)</Radio>
|
||||
<Radio value="global">Global (applies to all requests)</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="Select policies to attach"
|
||||
options={policyOptions}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? "")
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="font-bold">Scope</span>
|
||||
<hr className="flex-1 border-border" />
|
||||
</div>
|
||||
|
||||
<Form.Item label="Scope Type">
|
||||
<RadioGroup
|
||||
value={scopeType}
|
||||
onValueChange={(v) =>
|
||||
setScopeType(v as "global" | "specific")
|
||||
}
|
||||
className="flex flex-col gap-2"
|
||||
>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<RadioGroupItem value="specific" />
|
||||
Specific (teams, keys, models, or tags)
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<RadioGroupItem value="global" />
|
||||
Global (applies to all requests)
|
||||
</label>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
|
||||
{scopeType === "specific" && (
|
||||
<>
|
||||
|
|
@ -318,9 +364,13 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
|||
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."
|
||||
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>).
|
||||
</Text>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
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>).
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
|
|
@ -338,21 +388,26 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
|||
|
||||
{impactResult && <ImpactPreviewAlert impactResult={impactResult} />}
|
||||
|
||||
<div className="flex justify-end space-x-2 mt-4">
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
{scopeType === "specific" && (
|
||||
<Button variant="secondary" onClick={handlePreviewImpact} loading={isEstimating}>
|
||||
Estimate Impact
|
||||
<div className="flex justify-end space-x-2 mt-4">
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleSubmit} loading={isSubmitting}>
|
||||
Create Attachment
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
{scopeType === "specific" && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handlePreviewImpact}
|
||||
disabled={isEstimating}
|
||||
>
|
||||
{isEstimating ? "Estimating..." : "Estimate Impact"}
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
||||
{isSubmitting ? "Creating..." : "Create Attachment"}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue