From bfa5d59dd25c8228e661133e6e6305007cd11152 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Fri, 23 Jan 2026 09:24:36 -0800 Subject: [PATCH 01/11] init UI --- .../app/(dashboard)/components/Sidebar2.tsx | 10 + .../src/app/(dashboard)/policies/page.tsx | 37 +++ .../src/components/leftnav.tsx | 8 + .../src/components/networking.tsx | 217 +++++++++++++ .../policies/add_attachment_form.tsx | 171 ++++++++++ .../components/policies/add_policy_form.tsx | 213 +++++++++++++ .../components/policies/attachment_table.tsx | 153 +++++++++ .../src/components/policies/index.tsx | 301 ++++++++++++++++++ .../src/components/policies/policy_info.tsx | 168 ++++++++++ .../src/components/policies/policy_table.tsx | 160 ++++++++++ .../src/components/policies/types.ts | 66 ++++ 11 files changed, 1504 insertions(+) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/policies/page.tsx create mode 100644 ui/litellm-dashboard/src/components/policies/add_attachment_form.tsx create mode 100644 ui/litellm-dashboard/src/components/policies/add_policy_form.tsx create mode 100644 ui/litellm-dashboard/src/components/policies/attachment_table.tsx create mode 100644 ui/litellm-dashboard/src/components/policies/index.tsx create mode 100644 ui/litellm-dashboard/src/components/policies/policy_info.tsx create mode 100644 ui/litellm-dashboard/src/components/policies/policy_table.tsx create mode 100644 ui/litellm-dashboard/src/components/policies/types.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx index 260cac16e02..405f8329b67 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx @@ -19,6 +19,7 @@ import { ExperimentOutlined, ToolOutlined, TagsOutlined, + AuditOutlined, } from "@ant-design/icons"; // import { // all_admin_roles, @@ -102,6 +103,8 @@ const routeFor = (slug: string): string => { return "logs"; case "guardrails": return "guardrails"; + case "policies": + return "policies"; // tools case "mcp-servers": @@ -202,6 +205,13 @@ const menuItems: MenuItemCfg[] = [ icon: , roles: all_admin_roles, }, + { + key: "28", + page: "policies", + label: "Policies", + icon: , + roles: all_admin_roles, + }, { key: "26", page: "tools", diff --git a/ui/litellm-dashboard/src/app/(dashboard)/policies/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/policies/page.tsx new file mode 100644 index 00000000000..86e063c7358 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/policies/page.tsx @@ -0,0 +1,37 @@ +"use client"; + +import PoliciesPanel from "@/components/policies"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { + getPoliciesList, + createPolicyCall, + updatePolicyCall, + deletePolicyCall, + getPolicyInfo, + getPolicyAttachmentsList, + createPolicyAttachmentCall, + deletePolicyAttachmentCall, + getGuardrailsList, +} from "@/components/networking"; + +const PoliciesPage = () => { + const { accessToken, userRole } = useAuthorized(); + + return ( + + ); +}; + +export default PoliciesPage; diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index 1aa1df14a10..543d95d2ccd 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -3,6 +3,7 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { ApiOutlined, AppstoreOutlined, + AuditOutlined, BankOutlined, BarChartOutlined, BgColorsOutlined, @@ -123,6 +124,13 @@ const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapse icon: , roles: all_admin_roles, }, + { + key: "policies", + page: "policies", + label: "Policies", + icon: , + roles: all_admin_roles, + }, { key: "tools", page: "tools", diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index a4f2dfce3bd..f3b23ababbc 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -5347,6 +5347,223 @@ export const getGuardrailsList = async (accessToken: string) => { } }; +// ───────────────────────────────────────────────────────────────────────────── +// Policy CRUD API Calls +// ───────────────────────────────────────────────────────────────────────────── + +export const getPoliciesList = async (accessToken: string) => { + try { + const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/list` : `/policies/list`; + const response = await fetch(url, { + method: "GET", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Failed to get policies list:", error); + throw error; + } +}; + +export const createPolicyCall = async (accessToken: string, policyData: any) => { + try { + const url = proxyBaseUrl ? `${proxyBaseUrl}/policies` : `/policies`; + const response = await fetch(url, { + method: "POST", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(policyData), + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Failed to create policy:", error); + throw error; + } +}; + +export const updatePolicyCall = async (accessToken: string, policyId: string, policyData: any) => { + try { + const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/${policyId}` : `/policies/${policyId}`; + const response = await fetch(url, { + method: "PUT", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(policyData), + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Failed to update policy:", error); + throw error; + } +}; + +export const deletePolicyCall = async (accessToken: string, policyId: string) => { + try { + const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/${policyId}` : `/policies/${policyId}`; + const response = await fetch(url, { + method: "DELETE", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Failed to delete policy:", error); + throw error; + } +}; + +export const getPolicyInfo = async (accessToken: string, policyId: string) => { + try { + const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/${policyId}` : `/policies/${policyId}`; + const response = await fetch(url, { + method: "GET", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Failed to get policy info:", error); + throw error; + } +}; + +// Policy Attachments API Calls + +export const getPolicyAttachmentsList = async (accessToken: string) => { + try { + const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/attachments/list` : `/policies/attachments/list`; + const response = await fetch(url, { + method: "GET", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Failed to get policy attachments list:", error); + throw error; + } +}; + +export const createPolicyAttachmentCall = async (accessToken: string, attachmentData: any) => { + try { + const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/attachments` : `/policies/attachments`; + const response = await fetch(url, { + method: "POST", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(attachmentData), + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Failed to create policy attachment:", error); + throw error; + } +}; + +export const deletePolicyAttachmentCall = async (accessToken: string, attachmentId: string) => { + try { + const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/attachments/${attachmentId}` : `/policies/attachments/${attachmentId}`; + const response = await fetch(url, { + method: "DELETE", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Failed to delete policy attachment:", error); + throw error; + } +}; + export const getPromptsList = async (accessToken: string): Promise => { try { const url = proxyBaseUrl ? `${proxyBaseUrl}/prompts/list` : `/prompts/list`; diff --git a/ui/litellm-dashboard/src/components/policies/add_attachment_form.tsx b/ui/litellm-dashboard/src/components/policies/add_attachment_form.tsx new file mode 100644 index 00000000000..37428c03d77 --- /dev/null +++ b/ui/litellm-dashboard/src/components/policies/add_attachment_form.tsx @@ -0,0 +1,171 @@ +import React, { useState } from "react"; +import { + Modal, + Form, + Input, + Select, + Button, + Space, + Radio, + Divider, +} from "antd"; +import { Policy, PolicyAttachmentCreateRequest } from "./types"; + +interface AddAttachmentFormProps { + visible: boolean; + onClose: () => void; + onSuccess: () => void; + accessToken: string | null; + policies: Policy[]; + onCreateAttachment: (data: PolicyAttachmentCreateRequest) => Promise; +} + +const AddAttachmentForm: React.FC = ({ + visible, + onClose, + onSuccess, + accessToken, + policies, + onCreateAttachment, +}) => { + const [form] = Form.useForm(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [scopeType, setScopeType] = useState<"global" | "specific">("global"); + + const handleSubmit = async (values: any) => { + if (!accessToken) return; + + setIsSubmitting(true); + try { + const data: PolicyAttachmentCreateRequest = { + policy_name: values.policy_name, + }; + + if (scopeType === "global") { + data.scope = "*"; + } else { + if (values.teams && values.teams.length > 0) { + data.teams = values.teams; + } + if (values.keys && values.keys.length > 0) { + data.keys = values.keys; + } + if (values.models && values.models.length > 0) { + data.models = values.models; + } + } + + await onCreateAttachment(data); + onSuccess(); + onClose(); + form.resetFields(); + setScopeType("global"); + } catch (error) { + console.error("Error creating attachment:", error); + } finally { + setIsSubmitting(false); + } + }; + + const policyOptions = policies.map((p) => ({ + label: p.policy_name, + value: p.policy_name, + })); + + return ( + +
+ + + + + + + + + )} + + + + + + + +
+
+ ); +}; + +export default AddAttachmentForm; diff --git a/ui/litellm-dashboard/src/components/policies/add_policy_form.tsx b/ui/litellm-dashboard/src/components/policies/add_policy_form.tsx new file mode 100644 index 00000000000..c49754bc8ef --- /dev/null +++ b/ui/litellm-dashboard/src/components/policies/add_policy_form.tsx @@ -0,0 +1,213 @@ +import React, { useState, useEffect } from "react"; +import { + Modal, + Form, + Input, + Select, + Button, + Space, + Typography, + Divider, +} from "antd"; +import { PlusOutlined, MinusCircleOutlined } from "@ant-design/icons"; +import { Policy, PolicyCreateRequest, PolicyUpdateRequest } from "./types"; +import { Guardrail } from "../guardrails/types"; + +const { TextArea } = Input; +const { Text } = Typography; + +interface AddPolicyFormProps { + visible: boolean; + onClose: () => void; + onSuccess: () => void; + accessToken: string | null; + editingPolicy?: Policy | null; + existingPolicies: Policy[]; + availableGuardrails: Guardrail[]; + onCreatePolicy: (data: PolicyCreateRequest) => Promise; + onUpdatePolicy: (policyId: string, data: PolicyUpdateRequest) => Promise; +} + +const AddPolicyForm: React.FC = ({ + visible, + onClose, + onSuccess, + accessToken, + editingPolicy, + existingPolicies, + availableGuardrails, + onCreatePolicy, + onUpdatePolicy, +}) => { + const [form] = Form.useForm(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const isEditing = !!editingPolicy; + + useEffect(() => { + if (visible && editingPolicy) { + form.setFieldsValue({ + policy_name: editingPolicy.policy_name, + description: editingPolicy.description, + inherit: editingPolicy.inherit, + guardrails_add: editingPolicy.guardrails_add || [], + guardrails_remove: editingPolicy.guardrails_remove || [], + model_condition: editingPolicy.condition?.model, + }); + } else if (visible) { + form.resetFields(); + } + }, [visible, editingPolicy, form]); + + const handleSubmit = async (values: any) => { + if (!accessToken) return; + + setIsSubmitting(true); + try { + const data: PolicyCreateRequest | PolicyUpdateRequest = { + policy_name: values.policy_name, + description: values.description || undefined, + inherit: values.inherit || undefined, + guardrails_add: values.guardrails_add || [], + guardrails_remove: values.guardrails_remove || [], + condition: values.model_condition + ? { model: values.model_condition } + : undefined, + }; + + if (isEditing && editingPolicy) { + await onUpdatePolicy(editingPolicy.policy_id, data as PolicyUpdateRequest); + } else { + await onCreatePolicy(data as PolicyCreateRequest); + } + + onSuccess(); + onClose(); + form.resetFields(); + } catch (error) { + console.error("Error saving policy:", error); + } finally { + setIsSubmitting(false); + } + }; + + const guardrailOptions = availableGuardrails.map((g) => ({ + label: g.guardrail_name || g.guardrail_id, + value: g.guardrail_name || g.guardrail_id, + })); + + const policyOptions = existingPolicies + .filter((p) => !editingPolicy || p.policy_id !== editingPolicy.policy_id) + .map((p) => ({ + label: p.policy_name, + value: p.policy_name, + })); + + return ( + +
+ + + + + +