diff --git a/ui/litellm-dashboard/src/components/policies/pipeline_flow_builder.tsx b/ui/litellm-dashboard/src/components/policies/pipeline_flow_builder.tsx index 9df2a33fa66..8d3b386c0f6 100644 --- a/ui/litellm-dashboard/src/components/policies/pipeline_flow_builder.tsx +++ b/ui/litellm-dashboard/src/components/policies/pipeline_flow_builder.tsx @@ -1,9 +1,10 @@ -import React from "react"; -import { Select, Typography, Tag } from "antd"; -import { TextInput } from "@tremor/react"; -import { PlusIcon, TrashIcon } from "@heroicons/react/outline"; -import { GuardrailPipeline, PipelineStep } from "./types"; +import React, { useState } from "react"; +import { Select, Typography, Tag, message } from "antd"; +import { Button, TextInput } from "@tremor/react"; +import { ArrowLeftIcon, PlusIcon, TrashIcon } from "@heroicons/react/outline"; +import { GuardrailPipeline, PipelineStep, PolicyCreateRequest, PolicyUpdateRequest, Policy } from "./types"; import { Guardrail } from "../guardrails/types"; +import NotificationsManager from "../molecules/notifications_manager"; const { Text } = Typography; @@ -120,7 +121,7 @@ const StepCard: React.FC = ({ padding: 20, backgroundColor: "#fff", boxShadow: "0 1px 3px rgba(0,0,0,0.06)", - maxWidth: 520, + maxWidth: 680, width: "100%", }} > @@ -272,7 +273,7 @@ const PipelineFlowBuilder: React.FC = ({ borderRadius: 12, padding: "16px 24px", backgroundColor: "#fafafa", - maxWidth: 520, + maxWidth: 680, width: "100%", }} > @@ -355,7 +356,7 @@ export const PipelineInfoDisplay: React.FC = ({ pipeli borderRadius: 12, padding: "12px 20px", backgroundColor: "#fafafa", - maxWidth: 520, + maxWidth: 680, width: "100%", }} > @@ -382,7 +383,7 @@ export const PipelineInfoDisplay: React.FC = ({ pipeli borderRadius: 12, padding: "12px 20px", backgroundColor: "#fff", - maxWidth: 520, + maxWidth: 680, width: "100%", }} > @@ -415,5 +416,198 @@ export const PipelineInfoDisplay: React.FC = ({ pipeli ); +// ───────────────────────────────────────────────────────────────────────────── +// Full-screen Flow Builder Page +// ───────────────────────────────────────────────────────────────────────────── + +interface FlowBuilderPageProps { + onBack: () => void; + onSuccess: () => void; + accessToken: string | null; + editingPolicy?: Policy | null; + availableGuardrails: Guardrail[]; + createPolicy: (accessToken: string, policyData: any) => Promise; + updatePolicy: (accessToken: string, policyId: string, policyData: any) => Promise; +} + +export const FlowBuilderPage: React.FC = ({ + onBack, + onSuccess, + accessToken, + editingPolicy, + availableGuardrails, + createPolicy, + updatePolicy, +}) => { + const isEditing = !!editingPolicy?.policy_id; + + const [policyName, setPolicyName] = useState(editingPolicy?.policy_name || ""); + const [description, setDescription] = useState(editingPolicy?.description || ""); + const [isSubmitting, setIsSubmitting] = useState(false); + const [pipeline, setPipeline] = useState( + editingPolicy?.pipeline || { mode: "pre_call", steps: [createDefaultStep()] } + ); + + const handleSave = async () => { + if (!policyName.trim()) { + message.error("Please enter a policy name"); + return; + } + if (!accessToken) { + message.error("No access token available"); + return; + } + + const emptySteps = pipeline.steps.filter((s) => !s.guardrail); + if (emptySteps.length > 0) { + message.error("Please select a guardrail for all steps"); + return; + } + + setIsSubmitting(true); + try { + const guardrailsFromPipeline = pipeline.steps + .map((s) => s.guardrail) + .filter(Boolean); + + const data: PolicyCreateRequest | PolicyUpdateRequest = { + policy_name: policyName, + description: description || undefined, + guardrails_add: guardrailsFromPipeline, + guardrails_remove: [], + pipeline: pipeline, + }; + + if (isEditing && editingPolicy) { + await updatePolicy(accessToken, editingPolicy.policy_id, data as PolicyUpdateRequest); + NotificationsManager.success("Policy updated successfully"); + } else { + await createPolicy(accessToken, data as PolicyCreateRequest); + NotificationsManager.success("Policy created successfully"); + } + + onSuccess(); + onBack(); + } catch (error) { + console.error("Failed to save policy:", error); + NotificationsManager.fromBackend( + "Failed to save policy: " + (error instanceof Error ? error.message : String(error)) + ); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+ {/* Header bar */} +
+
+ + Policies + / + setPolicyName(e.target.value)} + disabled={isEditing} + style={{ width: 240 }} + /> + + Flow + +
+
+ + +
+
+ + {/* Description bar */} +
+ setDescription(e.target.value)} + style={{ maxWidth: 500 }} + /> +
+ + {/* Flow builder canvas - scrollable */} +
+
+ +
+
+
+ ); +}; + export { createDefaultStep }; export default PipelineFlowBuilder;