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 -