;
isAdmin: boolean;
- onPromptClick?: (id: string) => void;
- onDeleteClick?: (id: string, name: string) => void;
+ onPromptClick?: (id: string, environment: string) => void;
+ onDeleteClick?: (id: string, name: string, environment: string) => void;
}
export const getPromptTableColumns = ({
@@ -128,7 +134,11 @@ export const getPromptTableColumns = ({
title={row.original.prompt_id}
titleClassName="font-mono text-xs font-normal"
className="max-w-60"
- onClick={onPromptClick ? () => onPromptClick(row.original.prompt_id) : undefined}
+ onClick={
+ onPromptClick
+ ? () => onPromptClick(row.original.prompt_id, row.original.environment || "development")
+ : undefined
+ }
/>
),
},
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.test.tsx
index d6a4aaea2e1..334339e6f1d 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.test.tsx
@@ -15,21 +15,31 @@ vi.mock("./PromptTable", () => ({
__esModule: true,
default: ({
isLoading,
+ onPromptClick,
onDeleteClick,
}: {
isLoading: boolean;
- onDeleteClick: (id: string, name: string) => void;
+ onPromptClick: (id: string, environment: string) => void;
+ onDeleteClick: (id: string, name: string, environment: string) => void;
}) => (
{isLoading ? "table-loading" : "table-loaded"}
-
),
}));
-vi.mock("./prompt_info", () => ({ __esModule: true, default: () => prompt-info-view
}));
+vi.mock("./prompt_info", () => ({
+ __esModule: true,
+ default: ({ initialEnvironment }: { initialEnvironment?: string }) => (
+ prompt-info-view:{initialEnvironment ?? "none"}
+ ),
+}));
vi.mock("./add_prompt_form", () => ({
__esModule: true,
default: ({ visible }: { visible: boolean }) => (visible ? add-prompt-form
: null),
@@ -143,6 +153,22 @@ describe("PromptsPanel toolbar", () => {
});
});
+describe("PromptsPanel row navigation", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockGetPromptsList.mockResolvedValue({ prompts: [] } as never);
+ });
+
+ it("should open the info view preselected to the clicked row's environment", async () => {
+ const user = userEvent.setup();
+ renderPanel("Admin");
+
+ await user.click(await screen.findByRole("button", { name: "row-open" }));
+
+ expect(screen.getByText("prompt-info-view:staging")).toBeInTheDocument();
+ });
+});
+
describe("PromptsPanel delete confirmation", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -156,13 +182,13 @@ describe("PromptsPanel delete confirmation", () => {
await user.click(await screen.findByRole("button", { name: "row-delete" }));
- expect(await screen.findByText(/delete prompt: my-prompt/i)).toBeInTheDocument();
+ expect(await screen.findByText(/the staging copy of prompt: my-prompt/i)).toBeInTheDocument();
expect(screen.getByText(/cannot be undone/i)).toBeInTheDocument();
expect(mockDeletePromptCall).not.toHaveBeenCalled();
await user.click(screen.getByRole("button", { name: /^delete$/i }));
- await waitFor(() => expect(mockDeletePromptCall).toHaveBeenCalledWith("sk-test", "prompt-1"));
+ await waitFor(() => expect(mockDeletePromptCall).toHaveBeenCalledWith("sk-test", "prompt-1", "staging"));
});
it("should abandon the delete when the confirmation is dismissed", async () => {
@@ -170,11 +196,11 @@ describe("PromptsPanel delete confirmation", () => {
renderPanel("Admin");
await user.click(await screen.findByRole("button", { name: "row-delete" }));
- await screen.findByText(/delete prompt: my-prompt/i);
+ await screen.findByText(/the staging copy of prompt: my-prompt/i);
await user.click(screen.getByRole("button", { name: /cancel/i }));
- await waitFor(() => expect(screen.queryByText(/delete prompt: my-prompt/i)).not.toBeInTheDocument());
+ await waitFor(() => expect(screen.queryByText(/the staging copy of prompt: my-prompt/i)).not.toBeInTheDocument());
expect(mockDeletePromptCall).not.toHaveBeenCalled();
});
@@ -189,14 +215,14 @@ describe("PromptsPanel delete confirmation", () => {
renderPanel("Admin");
await user.click(await screen.findByRole("button", { name: "row-delete" }));
- await screen.findByText(/delete prompt: my-prompt/i);
+ await screen.findByText(/the staging copy of prompt: my-prompt/i);
await user.click(screen.getByRole("button", { name: /^delete$/i }));
- await waitFor(() => expect(mockDeletePromptCall).toHaveBeenCalledWith("sk-test", "prompt-1"));
+ await waitFor(() => expect(mockDeletePromptCall).toHaveBeenCalledWith("sk-test", "prompt-1", "staging"));
await user.keyboard("{Escape}");
- expect(screen.getByText(/delete prompt: my-prompt/i)).toBeInTheDocument();
+ expect(screen.getByText(/the staging copy of prompt: my-prompt/i)).toBeInTheDocument();
finishDelete();
- await waitFor(() => expect(screen.queryByText(/delete prompt: my-prompt/i)).not.toBeInTheDocument());
+ await waitFor(() => expect(screen.queryByText(/the staging copy of prompt: my-prompt/i)).not.toBeInTheDocument());
});
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.tsx
index 3594e933704..2d8d905c480 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.tsx
@@ -41,11 +41,12 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => {
const [isLoading, setIsLoading] = useState(true);
const [selectedEnvironment, setSelectedEnvironment] = useState(undefined);
const [selectedPromptId, setSelectedPromptId] = useState(null);
+ const [selectedPromptEnvironment, setSelectedPromptEnvironment] = useState(undefined);
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
const [showEditorView, setShowEditorView] = useState(false);
const [editPromptData, setEditPromptData] = useState(null);
const [isDeleting, setIsDeleting] = useState(false);
- const [promptToDelete, setPromptToDelete] = useState<{ id: string; name: string } | null>(null);
+ const [promptToDelete, setPromptToDelete] = useState<{ id: string; name: string; environment: string } | null>(null);
// Admin Viewer follows the read-parity rule: see prompts, no writes.
const canModify = userRole ? isProxyAdminRole(userRole) : false;
@@ -71,8 +72,9 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => {
fetchPrompts();
}, [accessToken, selectedEnvironment]);
- const handlePromptClick = (promptId: string) => {
+ const handlePromptClick = (promptId: string, environment: string) => {
setSelectedPromptId(promptId);
+ setSelectedPromptEnvironment(environment);
};
const handleAddPrompt = () => {
@@ -111,8 +113,8 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => {
setSelectedPromptId(null);
};
- const handleDeleteClick = (promptId: string, promptName: string) => {
- setPromptToDelete({ id: promptId, name: promptName });
+ const handleDeleteClick = (promptId: string, promptName: string, environment: string) => {
+ setPromptToDelete({ id: promptId, name: promptName, environment });
};
const handleDeleteConfirm = async () => {
@@ -120,8 +122,8 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => {
setIsDeleting(true);
try {
- await deletePromptCall(accessToken, promptToDelete.id);
- toast.success(`Prompt "${promptToDelete.name}" deleted successfully`);
+ await deletePromptCall(accessToken, promptToDelete.id, promptToDelete.environment);
+ toast.success(`Prompt "${promptToDelete.name}" deleted successfully from ${promptToDelete.environment}`);
fetchPrompts(); // Refresh the list
} catch (error) {
console.error("Error deleting prompt:", error);
@@ -148,6 +150,7 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => {
) : selectedPromptId ? (
setSelectedPromptId(null)}
accessToken={accessToken}
isAdmin={canModify}
@@ -219,7 +222,8 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => {
Delete Prompt
- Are you sure you want to delete prompt: {promptToDelete.name} ? This action cannot be undone.
+ Are you sure you want to delete the {promptToDelete.environment} copy of prompt: {promptToDelete.name}?
+ This action cannot be undone.
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
index bb29f12ac42..a5b195e5057 100644
--- 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
@@ -29,6 +29,35 @@ const promptWithoutTemplate = {
environments: [],
};
+describe("PromptInfoView environment scoping", () => {
+ beforeEach(() => {
+ vi.mocked(networking.getPromptInfo).mockReset().mockResolvedValue(promptWithoutTemplate);
+ vi.mocked(networking.getPromptVersions).mockReset().mockResolvedValue({ prompts: [] });
+ });
+
+ it("fetches the initial environment it was opened with", async () => {
+ render(
+ ,
+ );
+
+ await screen.findByRole("tab", { name: "Raw JSON" });
+ expect(networking.getPromptInfo).toHaveBeenCalledWith("sk-test", "support-reply", "staging");
+ });
+
+ it("fetches the serve default when opened without an environment", async () => {
+ render();
+
+ await screen.findByRole("tab", { name: "Raw JSON" });
+ expect(networking.getPromptInfo).toHaveBeenCalledWith("sk-test", "support-reply", undefined);
+ });
+});
+
describe("PromptInfoView tabs", () => {
beforeEach(() => {
vi.mocked(networking.getPromptInfo).mockReset().mockResolvedValue(promptWithoutTemplate);
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 c6a6f09fcaa..af4e3bf2121 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
@@ -20,6 +20,7 @@ import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "
export interface PromptInfoProps {
promptId: string;
+ initialEnvironment?: string;
onClose: () => void;
accessToken: string | null;
isAdmin: boolean;
@@ -27,7 +28,15 @@ export interface PromptInfoProps {
onEdit?: (promptData: any) => void;
}
-const PromptInfoView: React.FC = ({ promptId, onClose, accessToken, isAdmin, onDelete, onEdit }) => {
+const PromptInfoView: React.FC = ({
+ promptId,
+ initialEnvironment,
+ onClose,
+ accessToken,
+ isAdmin,
+ onDelete,
+ onEdit,
+}) => {
const [promptData, setPromptData] = useState(null);
const [promptTemplate, setPromptTemplate] = useState(null);
const [rawApiResponse, setRawApiResponse] = useState(null);
@@ -43,7 +52,7 @@ const PromptInfoView: React.FC = ({ promptId, onClose, accessTo
const [selectedVersion, setSelectedVersion] = useState(null);
const [loadingVersions, setLoadingVersions] = useState(false);
- // Initial fetch — no environment filter, gets default + all environments list
+ // Fetches the requested environment (or the serve-time default when omitted) plus the environments list
const fetchPromptInfo = async (environment?: string) => {
try {
setLoading(true);
@@ -89,7 +98,7 @@ const PromptInfoView: React.FC = ({ promptId, onClose, accessTo
setSelectedEnv(null);
setEnvironments([]);
setVersionHistory([]);
- fetchPromptInfo();
+ fetchPromptInfo(initialEnvironment);
}, [promptId, accessToken]);
// When environment changes (user clicks tab), re-fetch — skip initial mount
@@ -493,7 +502,7 @@ const PromptInfoView: React.FC = ({ promptId, onClose, accessTo
Delete Prompt
- Are you sure you want to delete prompt: {basePromptId}?
+ Are you sure you want to delete prompt: {basePromptId} from every environment?
This action cannot be undone.
diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx
index 032429ba8ed..086851383cf 100644
--- a/ui/litellm-dashboard/src/components/networking.tsx
+++ b/ui/litellm-dashboard/src/components/networking.tsx
@@ -4629,9 +4629,12 @@ export const updatePromptCall = async (accessToken: string, promptId: string, pr
}
};
-export const deletePromptCall = async (accessToken: string, promptId: string) => {
+export const deletePromptCall = async (accessToken: string, promptId: string, environment?: string) => {
try {
- const data = await apiClient.delete(`/prompts/${promptId}`, { accessToken });
+ const data = await apiClient.delete(`/prompts/${promptId}`, {
+ accessToken,
+ query: { environment: environment || undefined },
+ });
return data;
} catch (error) {
console.error("Failed to delete prompt:", error);
diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx
index e1e6dcfa442..d0da4ddfb2b 100644
--- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx
+++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx
@@ -349,7 +349,7 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp
const fetchPrompts = async () => {
try {
const response = await getPromptsList(accessToken);
- setPromptsList(response.prompts.map((prompt) => prompt.prompt_id));
+ setPromptsList(Array.from(new Set(response.prompts.map((prompt) => prompt.prompt_id))));
} catch (error) {
console.error("Failed to fetch prompts:", error);
}
diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx
index bf8f43b8b5f..08edce6eda1 100644
--- a/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx
+++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx
@@ -425,6 +425,22 @@ describe("KeyEditView", () => {
expect(screen.getByText("Policies")).toBeInTheDocument();
});
+ it("lists a prompt existing in several environments once in the dropdown", async () => {
+ vi.mocked(getPromptsList).mockResolvedValueOnce({
+ prompts: [
+ { prompt_id: "envgreet", litellm_params: {}, prompt_info: { prompt_type: "db" }, environment: "development" },
+ { prompt_id: "envgreet", litellm_params: {}, prompt_info: { prompt_type: "db" }, environment: "production" },
+ ],
+ });
+
+ renderAs("Admin");
+
+ const prompts = await screen.findByLabelText(/Prompts/);
+ await userEvent.type(prompts, "envgreet");
+
+ expect(await screen.findAllByRole("option", { name: "envgreet" })).toHaveLength(1);
+ });
+
it("should omit both fields and fire neither admin-only request for an internal user", async () => {
renderAs("Internal User");
diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx
index df1af2ca8e9..d4a8a804f29 100644
--- a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx
+++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx
@@ -165,7 +165,7 @@ export function KeyEditView({
if (!accessToken) return;
try {
const response = await getPromptsList(accessToken);
- setPromptsList(response.prompts.map((prompt) => prompt.prompt_id));
+ setPromptsList(Array.from(new Set(response.prompts.map((prompt) => prompt.prompt_id))));
} catch (error) {
console.error("Failed to fetch prompts:", error);
}