From 669c1334b4ba9a02b743412efe9fb1064d80f8a4 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Tue, 18 Aug 2026 16:40:44 -0700 Subject: [PATCH] refactor(ui): move the agent, guardrail, prompt, policy and skill forms off tremor (#37320) * refactor(ui): move the agent, guardrail, prompt, policy and skill forms off tremor The agent info Save Changes button used to rely on tremor's implicit submit inside the antd Form, so it now carries an explicit type="submit". Every converted TabsContent is keepMounted to keep tremor's always-mounted panel semantics, pinned by a new guardrail info test. Prunes the tremor no-restricted-imports suppressions these nine files no longer need. * fix(ui): keep the line tab strip on the agent, guardrail and prompt info views tremor's TabList defaulted to the line variant while shadcn's TabsList defaults to the filled pill, so the bare conversion turned three underlined tab strips into segmented pills. Restores the line variant plus the bottom border and the tab padding the strips used to have. * test(ui): pin the agent settings submit and the prompt raw json tab Agent Settings only saves because Save Changes carries an explicit type="submit" now that the button is a Base UI button, so a test drives the edit and asserts the patch call fires. The prompt info tabs are keyed by slug now, which also makes Raw JSON render for prompts with no template, so a second test renders that case and asserts the serialized response is visible. --- ui/litellm-dashboard/eslint-suppressions.json | 101 +-------- .../agents/_components/add_agent_form.tsx | 23 +- .../agents/_components/agent_info.test.tsx | 66 ++++++ .../agents/_components/agent_info.tsx | 69 +++--- .../_components/guardrail_info.test.tsx | 26 +++ .../guardrails/_components/guardrail_info.tsx | 119 ++++++----- .../_components/add_attachment_form.tsx | 15 +- .../policies/_components/add_policy_form.tsx | 7 +- .../_components/policy_test_panel.tsx | 2 +- .../prompts/_components/prompt_info.test.tsx | 50 +++++ .../prompts/_components/prompt_info.tsx | 196 ++++++++---------- .../skills/_components/add_plugin_form.tsx | 2 +- 12 files changed, 373 insertions(+), 303 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_info.test.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.test.tsx diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index be7da7c2b42..ed72e784a35 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -4,11 +4,6 @@ "count": 1 } }, - "src/app/(dashboard)/access-groups/_components/AccessGroupsModal/AccessGroupBaseForm.tsx": { - "no-restricted-imports": { - "count": 2 - } - }, "src/app/(dashboard)/access-groups/_components/AccessGroupsModal/AccessGroupEditModal.tsx": { "no-restricted-imports": { "count": 1 @@ -33,7 +28,7 @@ "count": 3 }, "no-restricted-imports": { - "count": 2 + "count": 1 }, "react-hooks/set-state-in-effect": { "count": 2 @@ -58,12 +53,6 @@ "src/app/(dashboard)/agents/_components/agent_form_fields.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "no-nested-ternary": { - "count": 1 - }, - "no-restricted-imports": { - "count": 2 } }, "src/app/(dashboard)/agents/_components/agent_info.tsx": { @@ -73,11 +62,8 @@ "local/no-complex-jsx-arrow": { "count": 1 }, - "no-nested-ternary": { - "count": 1 - }, "no-restricted-imports": { - "count": 2 + "count": 1 }, "react-hooks/immutability": { "count": 1 @@ -94,20 +80,11 @@ "src/app/(dashboard)/agents/_components/cost_config_fields.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "src/app/(dashboard)/agents/_components/dynamic_agent_form_fields.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "no-nested-ternary": { - "count": 2 - }, - "no-restricted-imports": { - "count": 1 } }, "src/app/(dashboard)/budgets/_components/budget_modal.tsx": { @@ -115,7 +92,7 @@ "count": 1 }, "no-restricted-imports": { - "count": 2 + "count": 1 } }, "src/app/(dashboard)/budgets/_components/budget_panel.tsx": { @@ -128,7 +105,7 @@ "count": 1 }, "no-restricted-imports": { - "count": 2 + "count": 1 } }, "src/app/(dashboard)/caching/_components/cache_dashboard.tsx": { @@ -147,16 +124,6 @@ "count": 1 } }, - "src/app/(dashboard)/caching/_components/cache_settings/CacheFormField.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/caching/_components/cache_settings/cacheSettingsFields.ts": { - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/caching/_components/cache_settings/index.tsx": { "local/filename-pascal-case": { "count": 1 @@ -165,22 +132,9 @@ "count": 1 } }, - "src/app/(dashboard)/caching/_components/coordination_redis_settings/CoordinationRedisFormField.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/caching/_components/coordination_redis_settings/coordinationRedisFields.ts": { - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/caching/_components/coordination_redis_settings/index.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "src/app/(dashboard)/caching/_components/response_time_indicator.tsx": { @@ -383,7 +337,7 @@ "count": 1 }, "no-restricted-imports": { - "count": 2 + "count": 1 }, "react-hooks/set-state-in-effect": { "count": 3 @@ -1204,18 +1158,7 @@ "count": 1 } }, - "src/app/(dashboard)/projects/_components/ProjectModals/ProjectBaseForm.test.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/projects/_components/ProjectModals/ProjectBaseForm.tsx": { - "local/no-complex-jsx-arrow": { - "count": 1 - }, - "no-restricted-imports": { - "count": 2 - }, "react-hooks/set-state-in-effect": { "count": 2 } @@ -1286,7 +1229,7 @@ "count": 3 }, "no-restricted-imports": { - "count": 2 + "count": 1 }, "react-hooks/set-state-in-effect": { "count": 2 @@ -1354,12 +1297,12 @@ "count": 1 }, "no-restricted-imports": { - "count": 2 + "count": 1 } }, "src/app/(dashboard)/tag-management/_components/components/CreateTagModal.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 } }, "src/app/(dashboard)/tag-management/_components/index.tsx": { @@ -1475,9 +1418,6 @@ "local/filename-pascal-case": { "count": 1 }, - "local/no-complex-jsx-arrow": { - "count": 1 - }, "no-restricted-imports": { "count": 1 }, @@ -1645,11 +1585,6 @@ "count": 1 } }, - "src/components/SSOModals.test.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/SSOModals.tsx": { "no-restricted-imports": { "count": 1 @@ -1691,11 +1626,6 @@ "count": 1 } }, - "src/components/Settings/AdminSettings/SSOSettings/Modals/BaseSSOSettingsForm.test.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/Settings/AdminSettings/SSOSettings/Modals/EditSSOSettingsModal.test.tsx": { "max-nested-callbacks": { "count": 1 @@ -2098,14 +2028,6 @@ "count": 1 } }, - "src/components/common_components/KeyLifecycleSettings.tsx": { - "local/no-complex-jsx-arrow": { - "count": 1 - }, - "no-restricted-imports": { - "count": 1 - } - }, "src/components/common_components/MetadataKeyValueFields.test.tsx": { "no-restricted-imports": { "count": 1 @@ -2136,11 +2058,6 @@ "count": 1 } }, - "src/components/common_components/RateLimitTypeFormItem.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/common_components/budget_duration_dropdown.tsx": { "local/filename-pascal-case": { "count": 1 @@ -3150,4 +3067,4 @@ "count": 1 } } -} +} \ No newline at end of file diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/add_agent_form.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/add_agent_form.tsx index 34f6a879b1e..5e828537475 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/add_agent_form.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/add_agent_form.tsx @@ -3,10 +3,10 @@ import { Modal, Select, Steps, Tag } from "antd"; import { FormProvider, useForm, useWatch } from "react-hook-form"; import { toast } from "@/lib/toast"; import { Logo } from "@/components/molecules/logo/Logo"; -import { Button } from "@tremor/react"; import { CheckCircleFilled, KeyOutlined, RobotOutlined, AppstoreOutlined } from "@ant-design/icons"; import CreatedKeyDisplay from "@/components/shared/CreatedKeyDisplay"; -import { Button as ShadButton } from "@/components/ui/button"; +import { Button } from "@/components/ui/button"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { Input } from "@/components/ui/input"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Separator } from "@/components/ui/separator"; @@ -1025,9 +1025,9 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok
{currentStep > 0 && currentStep < 4 && ( - + )}
@@ -1036,21 +1036,14 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok Cancel )} - {currentStep < 3 && ( - - )} + {currentStep < 3 && } {currentStep === 3 && ( - )} - {currentStep === 4 && ( - - )} + {currentStep === 4 && }
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_info.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_info.test.tsx new file mode 100644 index 00000000000..0936b8e13db --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_info.test.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import AgentInfoView from "./agent_info"; +import * as networking from "@/components/networking"; +import type { Agent } from "@/components/agents/types"; + +vi.mock("@/components/networking", () => ({ + getAgentInfo: vi.fn(), + getAgentCreateMetadata: vi.fn(), + patchAgentCall: vi.fn(), +})); + +vi.mock("@/app/(dashboard)/hooks/keys/useKeys", () => ({ + useKeys: () => ({ data: { keys: [] }, isLoading: false, refetch: vi.fn() }), +})); + +vi.mock("./agent_card_discovery", () => ({ + default: () =>
, +})); + +vi.mock("./agent_form_fields", () => ({ + default: () =>
, + unmountedA2AFieldNames: () => [], +})); + +const agent = { + agent_id: "agent-1", + agent_name: "support-agent", + agent_card_params: { + name: "Support Agent", + description: "Answers support questions", + url: "http://localhost:9999/", + version: "1.0.0", + protocolVersion: "1.0", + capabilities: { streaming: false }, + skills: [], + }, + tpm_limit: 100, +} as unknown as Agent; + +describe("AgentInfoView settings", () => { + beforeEach(() => { + vi.mocked(networking.getAgentInfo).mockReset().mockResolvedValue(agent); + vi.mocked(networking.getAgentCreateMetadata).mockReset().mockResolvedValue([]); + vi.mocked(networking.patchAgentCall).mockReset().mockResolvedValue({}); + }); + + it("submits the edited agent when Save Changes is pressed", async () => { + render(); + + fireEvent.click(await screen.findByRole("tab", { name: "Settings" })); + fireEvent.click(screen.getByRole("button", { name: "Edit Settings" })); + + const tpmLimit = await screen.findByLabelText("TPM Limit"); + fireEvent.change(tpmLimit, { target: { value: "42" } }); + + fireEvent.click(screen.getByRole("button", { name: /Save Changes/ })); + + await waitFor(() => expect(networking.patchAgentCall).toHaveBeenCalledTimes(1)); + const [token, agentId, payload] = vi.mocked(networking.patchAgentCall).mock.calls[0]; + expect(token).toBe("sk-test"); + expect(agentId).toBe("agent-1"); + expect(payload.tpm_limit).toBe(42); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_info.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_info.tsx index 88b4c3238fc..d37065317c2 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_info.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_info.tsx @@ -1,15 +1,16 @@ import React, { useState, useEffect, useMemo } from "react"; -import { Card, Title, Text, Button as TremorButton, Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react"; import { Spin, Descriptions } from "antd"; import { FormProvider, useForm, useWatch } from "react-hook-form"; import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { TooltipProvider } from "@/components/ui/tooltip"; import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { Field, FieldGroup, FieldLabel } from "@/components/shared/form/field"; import { toast } from "@/lib/toast"; -import { ArrowLeftIcon } from "@heroicons/react/outline"; +import { ArrowLeft } from "lucide-react"; import { getAgentInfo, patchAgentCall, getAgentCreateMetadata, AgentCreateInfo } from "@/components/networking"; import { Agent } from "@/components/agents/types"; import { KeyResponse } from "@/components/key_team_helpers/key_list"; @@ -46,6 +47,7 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT const agentKeys = keysData?.keys ?? []; const [isLoading, setIsLoading] = useState(true); const [isEditing, setIsEditing] = useState(false); + const [activeTab, setActiveTab] = useState("overview"); const [isSaving, setIsSaving] = useState(false); const form = useForm({ defaultValues: {} }); const panels = useCollapsiblePanels([AGENT_FORM_CONFIG.basic.key]); @@ -203,9 +205,9 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT return (
Agent not found
- +
); } @@ -251,22 +253,29 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT return (
- + +

{agent.agent_name || "Unnamed Agent"}

+

{agent.agent_id}

- - - Overview - {isAdmin ? Settings : <>} - + + + + Overview + + {isAdmin && ( + + Settings + + )} + - +
{/* Overview Panel */} - + {agent.agent_id} {agent.agent_name} @@ -321,7 +330,7 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT (agent.object_permission.mcp_tool_permissions && Object.keys(agent.object_permission.mcp_tool_permissions).length > 0)) && (
- MCP Tool Permissions +

MCP Tool Permissions

{agent.object_permission.mcp_servers && agent.object_permission.mcp_servers.length > 0 && ( @@ -355,7 +364,7 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT {agent.agent_card_params?.skills && agent.agent_card_params.skills.length > 0 && (
- Skills +

Skills

{agent.agent_card_params.skills.map((skill: any, index: number) => ( @@ -381,23 +390,23 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT
)} - + {/* Settings Panel (only for admins) */} {isAdmin && ( - - + +
- Agent Settings +

Agent Settings

{!isEditing && ( - { setAppliedDiscoveredSelection(null); setIsEditing(true); }} > Edit Settings - + )}
@@ -430,7 +439,7 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT )} - Rate Limits +

Rate Limits

{rateLimitField("tpm_limit", "TPM Limit")} {rateLimitField("rpm_limit", "RPM Limit")} @@ -452,8 +461,8 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT > Cancel -
@@ -461,13 +470,13 @@ const AgentInfoView: React.FC = ({ agentId, onClose, accessT ) : ( - Click "Edit Settings" to modify agent configuration. +

Click "Edit Settings" to modify agent configuration.

)}
-
+ )} - - +
+
); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.test.tsx index eca1cf4df8a..320e0b5baef 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.test.tsx @@ -299,4 +299,30 @@ describe("Guardrail Info", () => { expect(secondCallArgs.litellm_params.patterns).toEqual(["new_pattern"]); expect(secondCallArgs.litellm_params.blocked_words).toEqual(["new_word"]); }); + + it("keeps the settings panel mounted while the overview tab is active", async () => { + vi.mocked(networking.getGuardrailInfo).mockResolvedValue({ + guardrail_id: "123", + guardrail_name: "Test Guardrail", + litellm_params: { guardrail: "presidio", mode: "pre_call", default_on: true }, + created_at: "2024-01-01T00:00:00Z", + updated_at: "2024-01-01T00:00:00Z", + guardrail_definition_location: "database", + }); + vi.mocked(networking.getGuardrailUISettings).mockResolvedValue({ + supported_entities: [], + supported_actions: [], + pii_entity_categories: [], + supported_modes: ["pre_call"], + }); + vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue({}); + + const { findByRole, getByRole, getByText } = render( + {}} accessToken="123" isAdmin={true} />, + ); + + expect(await findByRole("tab", { name: "Overview" })).toHaveAttribute("aria-selected", "true"); + expect(getByRole("tab", { name: "Settings" })).toHaveAttribute("aria-selected", "false"); + expect(getByText("Guardrail Settings")).toBeInTheDocument(); + }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.tsx index f8d7bede021..caac09a1c83 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.tsx @@ -6,10 +6,11 @@ import { } from "@/components/networking"; import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils"; import { CodeOutlined, EyeInvisibleOutlined, InfoCircleOutlined, StopOutlined } from "@ant-design/icons"; -import { ArrowLeftIcon } from "@heroicons/react/outline"; -import { Badge, Card, Grid, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title } from "@tremor/react"; import { Button as AntdButton, Tooltip } from "antd"; -import { CheckIcon, CopyIcon } from "lucide-react"; +import { ArrowLeft, CheckIcon, CopyIcon } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Card } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import React, { useCallback, useEffect, useLayoutEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "@/lib/toast"; @@ -509,12 +510,12 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose, return (
- } onClick={onClose} className="mb-4"> + } onClick={onClose} className="mb-4"> Back to Guardrails - {guardrailData.guardrail_name || "Unnamed Guardrail"} +

{guardrailData.guardrail_name || "Unnamed Guardrail"}

- {guardrailData.guardrail_id} +

{guardrailData.guardrail_id}

= ({ guardrailId, onClose,
- - - Overview - {isAdmin ? Settings : <>} - + + + + Overview + + {isAdmin && ( + + Settings + + )} + - +
{/* Overview Panel */} - - - - Provider + +
+ +

Provider

- {displayName} +

{displayName}

- - Mode + +

Mode

- {guardrailData.litellm_params?.mode || "-"} - +

{guardrailData.litellm_params?.mode || "-"}

+ {guardrailData.litellm_params?.default_on ? "Default On" : "Default Off"}
- - Created At + +

Created At

- {formatDate(guardrailData.created_at)} - Last Updated: {formatDate(guardrailData.updated_at)} +

{formatDate(guardrailData.created_at)}

+

Last Updated: {formatDate(guardrailData.updated_at)}

- +
{guardrailData.litellm_params?.pii_entities_config && Object.keys(guardrailData.litellm_params.pii_entities_config).length > 0 && ( - +
- PII Protection - +

PII Protection

+ {Object.keys(guardrailData.litellm_params.pii_entities_config).length} PII entities configured
@@ -581,18 +588,18 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose, {guardrailData.litellm_params?.pii_entities_config && Object.keys(guardrailData.litellm_params.pii_entities_config).length > 0 && ( - - PII Entity Configuration + +

PII Entity Configuration

- Entity Type - Configuration +

Entity Type

+

Configuration

{Object.entries(guardrailData.litellm_params?.pii_entities_config).map(([key, value]) => (
- {key} - +

{key}

+

= ({ guardrailId, onClose, {value === "MASK" ? : } {String(value)} - +

))}
@@ -610,18 +617,18 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose, )} {guardrailData.litellm_params?.guardrail === "tool_permission" && ( - + )} {/* Custom Code Display */} {guardrailData.litellm_params?.guardrail === "custom_code" && guardrailData.litellm_params?.custom_code && ( - +
- Custom Code +

Custom Code

{isAdmin && !isConfigGuardrail && ( } onClick={() => setCustomCodeModalVisible(true)}> @@ -647,14 +654,14 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose, isEditing={false} accessToken={accessToken} /> - + {/* Settings Panel (only for admins) */} {isAdmin && ( - - + +
- Guardrail Settings +

Guardrail Settings

{isConfigGuardrail && ( @@ -831,24 +838,24 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose, ) : (
- Guardrail ID +

Guardrail ID

{guardrailData.guardrail_id}
- Guardrail Name +

Guardrail Name

{guardrailData.guardrail_name || "Unnamed Guardrail"}
- Provider +

Provider

{displayName}
- Mode +

Mode

{guardrailData.litellm_params?.mode || "-"}
- Default On - +

Default On

+ {guardrailData.litellm_params?.default_on ? "Yes" : "No"}
@@ -856,9 +863,9 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose, {guardrailData.litellm_params?.pii_entities_config && Object.keys(guardrailData.litellm_params.pii_entities_config).length > 0 && (
- PII Protection +

PII Protection

- + {Object.keys(guardrailData.litellm_params.pii_entities_config).length} PII entities configured @@ -867,11 +874,11 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose, )}
- Created At +

Created At

{formatDate(guardrailData.created_at)}
- Last Updated +

Last Updated

{formatDate(guardrailData.updated_at)}
@@ -881,10 +888,10 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose,
)} - + )} - - +
+ {/* Custom Code Editor Modal */} = ({ Cancel {scopeType === "specific" && ( - )} - diff --git a/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/add_policy_form.tsx b/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/add_policy_form.tsx index a08ae1d9f98..cc0a85eedb9 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/add_policy_form.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/add_policy_form.tsx @@ -546,7 +546,12 @@ const AddPolicyForm: React.FC = ({ - diff --git a/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/policy_test_panel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/policy_test_panel.tsx index 82cb3932e21..90642d23a46 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/policy_test_panel.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/policy_test_panel.tsx @@ -218,7 +218,7 @@ const PolicyTestPanel: React.FC = ({ accessToken }) => {
- diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.test.tsx new file mode 100644 index 00000000000..bb29f12ac42 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.test.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import PromptInfoView from "./prompt_info"; +import * as networking from "@/components/networking"; + +vi.mock("@/components/networking", () => ({ + getPromptInfo: vi.fn(), + getPromptVersions: vi.fn(), + deletePromptCall: vi.fn(), +})); + +vi.mock("./prompt_editor_view/PromptCodeSnippets", () => ({ + default: () =>
, +})); + +const promptWithoutTemplate = { + prompt_spec: { + prompt_id: "support-reply", + version: 1, + litellm_params: { prompt_id: "support-reply" }, + prompt_info: { prompt_type: "dotprompt" }, + created_by: "admin", + created_at: "2024-01-01T00:00:00Z", + updated_at: "2024-01-01T00:00:00Z", + }, + raw_prompt_template: null, + environments: [], +}; + +describe("PromptInfoView tabs", () => { + beforeEach(() => { + vi.mocked(networking.getPromptInfo).mockReset().mockResolvedValue(promptWithoutTemplate); + vi.mocked(networking.getPromptVersions).mockReset().mockResolvedValue({ prompts: [] }); + }); + + it("shows the raw API response for a prompt that has no template", async () => { + const user = userEvent.setup(); + render(); + + expect(await screen.findByRole("tab", { name: "Raw JSON" })).toBeInTheDocument(); + expect(screen.queryByRole("tab", { name: "Prompt Template" })).not.toBeInTheDocument(); + + await user.click(screen.getByRole("tab", { name: "Raw JSON" })); + + expect(screen.getByText("Raw API Response")).toBeVisible(); + expect(screen.getByText(/"prompt_id": "support-reply"/)).toBeVisible(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.tsx index 5335ba06060..cf29280ea51 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.tsx @@ -1,25 +1,10 @@ import React, { useState, useEffect, useRef } from "react"; -import { - Card, - Title, - Text, - Grid, - Badge, - Button as TremorButton, - Tab, - TabGroup, - TabList, - TabPanel, - TabPanels, - Table, - TableBody, - TableCell, - TableHead, - TableHeaderCell, - TableRow, -} from "@tremor/react"; -import { Button, Modal } from "antd"; -import { ArrowLeftIcon, TrashIcon, PencilIcon } from "@heroicons/react/outline"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Button as AntButton, Modal } from "antd"; import { getPromptInfo, getPromptVersions, @@ -28,7 +13,7 @@ import { deletePromptCall, } from "@/components/networking"; import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils"; -import { CheckIcon, CopyIcon } from "lucide-react"; +import { ArrowLeft, CheckIcon, CopyIcon, Pencil, Trash2 } from "lucide-react"; import { toast } from "@/lib/toast"; import PromptCodeSnippets from "./prompt_editor_view/PromptCodeSnippets"; import { extractModel, extractTemplateVariables, getBasePromptId, getCurrentVersion } from "./prompt_utils"; @@ -197,15 +182,16 @@ const PromptInfoView: React.FC = ({ promptId, onClose, accessTo return (
- +
- Prompt Details +

Prompt Details

- {basePromptId} - {isAdmin && ( - + )}
@@ -285,81 +263,89 @@ const PromptInfoView: React.FC = ({ promptId, onClose, accessTo {/* Old version banner */} {isViewingOldVersion && (
- +

Viewing v{selectedVersion} — not the latest version (v{latestVersion}) - - +

)} - - - Overview - {promptTemplate ? Prompt Template : <>} - Raw JSON - + + + + Overview + + {promptTemplate && ( + + Prompt Template + + )} + + Raw JSON + + - +
{/* Overview Panel */} - - - - Version + +
+ +

Version

- {currentVersion} - +

{currentVersion}

+ v{currentVersion}
- - Prompt Type + +

Prompt Type

- {promptData.prompt_info?.prompt_type || "-"} +

{promptData.prompt_info?.prompt_type || "-"}

- - Created By + +

Created By

- {promptData.created_by || "-"} +

{promptData.created_by || "-"}

- - Created At + +

Created At

- {formatDate(promptData.created_at)} - Updated: {formatDate(promptData.updated_at)} +

{formatDate(promptData.created_at)}

+

Updated: {formatDate(promptData.updated_at)}

- +
{/* Version History Table */} - - Version History — {selectedEnv} + +

Version History — {selectedEnv}

{loadingVersions ? ( - Loading versions... +

Loading versions...

) : versionHistory.length > 0 ? ( - + - Version - Created By - Date - Actions + Version + Created By + Date + Actions - + {versionHistory.map((v) => { const vNum = v.version || 1; @@ -376,7 +362,7 @@ const PromptInfoView: React.FC = ({ promptId, onClose, accessTo v{vNum} {isLatest && ( - + latest )} @@ -388,10 +374,9 @@ const PromptInfoView: React.FC = ({ promptId, onClose, accessTo {formatDate(v.created_at)} - { e.stopPropagation(); // Build a response-like object for the editor @@ -406,8 +391,9 @@ const PromptInfoView: React.FC = ({ promptId, onClose, accessTo onEdit?.(editData); }} > + Edit - + ); @@ -415,18 +401,18 @@ const PromptInfoView: React.FC = ({ promptId, onClose, accessTo
) : ( - No versions found in {selectedEnv} +

No versions found in {selectedEnv}

)}
-
+ {/* Prompt Template Panel */} {promptTemplate && ( - - + +
- Prompt Template - +
- Template ID +

Template ID

{promptTemplate.litellm_prompt_id}
- Content +

Content

{promptTemplate.content}
@@ -458,7 +444,7 @@ const PromptInfoView: React.FC = ({ promptId, onClose, accessTo {promptTemplate.metadata && Object.keys(promptTemplate.metadata).length > 0 && (
- Template Metadata +

Template Metadata

                           {JSON.stringify(promptTemplate.metadata, null, 2)}
@@ -468,15 +454,15 @@ const PromptInfoView: React.FC = ({ promptId, onClose, accessTo
                   )}
                 
- + )} {/* Raw JSON Panel */} - - + +
- Raw API Response - +
@@ -497,9 +483,9 @@ const PromptInfoView: React.FC = ({ promptId, onClose, accessTo
-
- - + +
+ {/* Delete Confirmation Modal */} = ({ visible, onClose, accessT -