mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
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:
commit
1763aeff62
3 changed files with 112 additions and 41 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
​
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue