Merge pull request #40303 from mubashir1osmani/litellm_fix_delete_passthrough_ui

fix(ui): repair pass-through delete confirm dialog and disable delete for config endpoints
This commit is contained in:
ryan-crabbe-berri 2026-09-09 10:15:38 -07:00 committed by GitHub
commit 1763aeff62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 112 additions and 41 deletions

View file

@ -89,6 +89,65 @@ describe("PassThroughEndpointsTable", () => {
expect(onDeleteClick).toHaveBeenCalledWith("ep-1");
});
it("should disable edit and delete for config-defined endpoints", async () => {
const user = userEvent.setup();
const onEndpointClick = vi.fn();
const onDeleteClick = vi.fn();
const configEndpoint: passThroughItem = {
id: "ep-config",
path: "/from-config",
target: "https://config.example.com",
headers: {},
is_from_config: true,
};
render(
<PassThroughEndpointsTable
{...defaultProps}
endpoints={[configEndpoint]}
onEndpointClick={onEndpointClick}
onDeleteClick={onDeleteClick}
/>,
);
await user.click(screen.getByTestId("endpoint-actions-ep-config"));
const editItem = await screen.findByTestId("endpoint-action-edit");
const deleteItem = await screen.findByTestId("endpoint-action-delete");
expect(editItem).toHaveAttribute("data-disabled");
expect(deleteItem).toHaveAttribute("data-disabled");
expect(screen.getByTestId("endpoint-config-hint")).toHaveTextContent(
"This endpoint is defined in the config file and cannot be edited or deleted on the dashboard.",
);
await user.click(editItem);
await user.click(deleteItem);
expect(onEndpointClick).not.toHaveBeenCalled();
expect(onDeleteClick).not.toHaveBeenCalled();
});
it("should not show the config hint for DB endpoints", async () => {
const user = userEvent.setup();
render(<PassThroughEndpointsTable {...defaultProps} />);
await user.click(screen.getByTestId("endpoint-actions-ep-1"));
await screen.findByTestId("endpoint-action-delete");
expect(screen.queryByTestId("endpoint-config-hint")).not.toBeInTheDocument();
});
it("should label endpoint source as Config or DB", () => {
const configEndpoint: passThroughItem = {
id: "ep-config",
path: "/from-config",
target: "https://config.example.com",
headers: {},
is_from_config: true,
};
render(<PassThroughEndpointsTable {...defaultProps} endpoints={[...endpoints, configEndpoint]} />);
expect(screen.getByText("Config")).toBeInTheDocument();
expect(screen.getAllByText("DB")).toHaveLength(2);
});
it("should disable edit and delete for endpoints without an id", async () => {
const user = userEvent.setup();
const onEndpointClick = vi.fn();

View file

@ -18,6 +18,9 @@ import { cn } from "@/lib/cva.config";
import type { passThroughItem } from "./PassThroughSettings";
const CONFIG_ENDPOINT_HINT =
"This endpoint is defined in the config file and cannot be edited or deleted on the dashboard.";
function HeaderWithTooltip({ title, tooltip }: { title: string; tooltip: string }) {
return (
<div className="flex items-center gap-1">
@ -73,6 +76,7 @@ interface EndpointRowActionsProps {
function EndpointRowActions({ endpoint, onEndpointClick, onDeleteClick }: EndpointRowActionsProps) {
const endpointId = endpoint.id;
const isFromConfig = endpoint.is_from_config ?? false;
return (
<DropdownMenu>
<DropdownMenuTrigger
@ -85,8 +89,8 @@ function EndpointRowActions({ endpoint, onEndpointClick, onDeleteClick }: Endpoi
<DropdownMenuContent align="end" className="w-52">
<DropdownMenuItem
data-testid="endpoint-action-edit"
disabled={!endpointId}
onClick={() => endpointId && onEndpointClick(endpointId)}
disabled={isFromConfig || !endpointId}
onClick={() => !isFromConfig && endpointId && onEndpointClick(endpointId)}
>
<Pencil />
Edit
@ -95,12 +99,17 @@ function EndpointRowActions({ endpoint, onEndpointClick, onDeleteClick }: Endpoi
<DropdownMenuItem
variant="destructive"
data-testid="endpoint-action-delete"
disabled={!endpointId}
onClick={() => endpointId && onDeleteClick(endpointId)}
disabled={isFromConfig || !endpointId}
onClick={() => !isFromConfig && endpointId && onDeleteClick(endpointId)}
>
<Trash2 />
Delete
</DropdownMenuItem>
{isFromConfig && (
<div data-testid="endpoint-config-hint" className="px-2 py-1.5 text-xs text-muted-foreground">
{CONFIG_ENDPOINT_HINT}
</div>
)}
</DropdownMenuContent>
</DropdownMenu>
);
@ -124,7 +133,9 @@ export const getPassThroughEndpointsTableColumns = ({
enableSorting: false,
cell: ({ row }) => {
const endpointId = row.original.id;
if (!endpointId) return <span className="font-mono text-xs text-muted-foreground"></span>;
if (!endpointId || row.original.is_from_config) {
return <span className="font-mono text-xs text-muted-foreground"></span>;
}
return (
<IdentityCell
title={endpointId}
@ -134,6 +145,17 @@ export const getPassThroughEndpointsTableColumns = ({
);
},
},
{
id: "source",
meta: { title: "Source", skeleton: "badge" },
header: "Source",
size: 100,
enableSorting: false,
cell: ({ row }) => {
const isFromConfig = row.original.is_from_config ?? false;
return <StatusBadge tone={isFromConfig ? "neutral" : "info"} label={isFromConfig ? "Config" : "DB"} />;
},
},
{
id: "path",
accessorKey: "path",

View file

@ -1,4 +1,13 @@
import React, { useState, useEffect } from "react";
import {
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { deletePassThroughEndpointsCall, getPassThroughEndpointsCall } from "../networking";
import AddPassThroughEndpoint from "../add_pass_through";
@ -25,6 +34,7 @@ export interface passThroughItem {
methods?: string[];
guardrails?: Record<string, { request_fields?: string[]; response_fields?: string[] } | null>;
default_query_params?: Record<string, string>;
is_from_config?: boolean;
}
const PassThroughSettings: React.FC<PassThroughSettingsProps> = ({ accessToken, userRole, userID, premiumUser }) => {
@ -133,42 +143,22 @@ const PassThroughSettings: React.FC<PassThroughSettingsProps> = ({ accessToken,
onDeleteClick={handleDelete}
/>
{isDeleteModalOpen && (
<div className="fixed z-overlay inset-0 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 transition-opacity" aria-hidden="true">
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<div className="inline-block align-bottom bg-card rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-card px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-foreground">Delete Pass-Through Endpoint</h3>
<div className="mt-2">
<p className="text-sm text-muted-foreground">
Are you sure you want to delete this pass-through endpoint? This action cannot be undone.
</p>
</div>
</div>
</div>
</div>
<div className="bg-muted px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<Button variant="destructive" onClick={confirmDelete} className="ml-2">
Delete
</Button>
<Button variant="outline" onClick={cancelDelete}>
Cancel
</Button>
</div>
</div>
</div>
</div>
)}
<AlertDialog open={isDeleteModalOpen} onOpenChange={(open) => !open && cancelDelete()}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Pass-Through Endpoint</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this pass-through endpoint? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<Button variant="destructive" onClick={confirmDelete}>
Delete
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
};