mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-05 12:23:28 +00:00
[Feat] UI QA: Pass through endpoints (#11939)
* use ID for pass through management * use id for pass through * fix columns * fix PassThroughInfoView * cleanup * working edit and delete pass through * fix rendering id for pt row * fixes for pt info view * working delete pass through * fix use NumericalInput * fix alignment * qa - creating pt * show route preview * fix show just 1 msg * test_create_pass_through_endpoint * fix ui linting
This commit is contained in:
@@ -1431,6 +1431,10 @@ class DynamoDBArgs(LiteLLMPydanticObjectBase):
|
||||
|
||||
|
||||
class PassThroughGenericEndpoint(LiteLLMPydanticObjectBase):
|
||||
id: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Optional unique identifier for the pass-through endpoint. If not provided, endpoints will be identified by path for backwards compatibility.",
|
||||
)
|
||||
path: str = Field(description="The route to be added to the LiteLLM Proxy Server.")
|
||||
target: str = Field(
|
||||
description="The URL to which requests for this path should be forwarded."
|
||||
|
||||
@@ -1023,6 +1023,7 @@ async def initialize_pass_through_endpoints(
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
import uuid
|
||||
verbose_proxy_logger.debug("initializing pass through endpoints")
|
||||
from litellm.proxy._types import CommonProxyErrors, LiteLLMRoutes
|
||||
from litellm.proxy.proxy_server import app, premium_user
|
||||
@@ -1030,6 +1031,11 @@ async def initialize_pass_through_endpoints(
|
||||
for endpoint in pass_through_endpoints:
|
||||
if isinstance(endpoint, PassThroughGenericEndpoint):
|
||||
endpoint = endpoint.model_dump()
|
||||
|
||||
# Auto-generate ID for backwards compatibility if not present
|
||||
if endpoint.get("id") is None:
|
||||
endpoint["id"] = str(uuid.uuid4())
|
||||
|
||||
_target = endpoint.get("target", None)
|
||||
_path: Optional[str] = endpoint.get("path", None)
|
||||
if _path is None:
|
||||
@@ -1056,7 +1062,7 @@ async def initialize_pass_through_endpoints(
|
||||
continue
|
||||
|
||||
# Add exact path route
|
||||
verbose_proxy_logger.debug("Initializing pass through endpoint: %s", _path)
|
||||
verbose_proxy_logger.debug("Initializing pass through endpoint: %s (ID: %s)", _path, endpoint.get("id"))
|
||||
InitPassThroughEndpointHelpers.add_exact_path_route(
|
||||
app=app,
|
||||
path=_path,
|
||||
@@ -1081,7 +1087,7 @@ async def initialize_pass_through_endpoints(
|
||||
cost_per_request=endpoint.get("cost_per_request", None),
|
||||
)
|
||||
|
||||
verbose_proxy_logger.debug("Added new pass through endpoint: %s", _path)
|
||||
verbose_proxy_logger.debug("Added new pass through endpoint: %s (ID: %s)", _path, endpoint.get("id"))
|
||||
|
||||
|
||||
async def _get_pass_through_endpoints_from_db(
|
||||
@@ -1103,21 +1109,20 @@ async def _get_pass_through_endpoints_from_db(
|
||||
|
||||
returned_endpoints: List[PassThroughGenericEndpoint] = []
|
||||
if endpoint_id is None:
|
||||
# Return all endpoints
|
||||
for endpoint in pass_through_endpoint_data:
|
||||
if isinstance(endpoint, dict):
|
||||
returned_endpoints.append(PassThroughGenericEndpoint(**endpoint))
|
||||
elif isinstance(endpoint, PassThroughGenericEndpoint):
|
||||
returned_endpoints.append(endpoint)
|
||||
elif endpoint_id is not None:
|
||||
for endpoint in pass_through_endpoint_data:
|
||||
_endpoint: Optional[PassThroughGenericEndpoint] = None
|
||||
if isinstance(endpoint, dict):
|
||||
_endpoint = PassThroughGenericEndpoint(**endpoint)
|
||||
elif isinstance(endpoint, PassThroughGenericEndpoint):
|
||||
_endpoint = endpoint
|
||||
|
||||
if _endpoint is not None and _endpoint.path == endpoint_id:
|
||||
returned_endpoints.append(_endpoint)
|
||||
else:
|
||||
# Find specific endpoint by ID
|
||||
found_endpoint = _find_endpoint_by_id(
|
||||
pass_through_endpoint_data, endpoint_id
|
||||
)
|
||||
if found_endpoint is not None:
|
||||
returned_endpoints.append(found_endpoint)
|
||||
|
||||
return returned_endpoints
|
||||
|
||||
|
||||
@@ -1151,7 +1156,7 @@ async def update_pass_through_endpoints(
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Update a pass-through endpoint
|
||||
Update a pass-through endpoint by ID.
|
||||
"""
|
||||
from litellm.proxy.proxy_server import (
|
||||
get_config_general_settings,
|
||||
@@ -1176,46 +1181,54 @@ async def update_pass_through_endpoints(
|
||||
detail={"error": "No pass-through endpoints found"},
|
||||
)
|
||||
|
||||
# Find and update the endpoint
|
||||
updated_endpoint: Optional[PassThroughGenericEndpoint] = None
|
||||
endpoint_found = False
|
||||
# Find the endpoint to update
|
||||
found_endpoint = _find_endpoint_by_id(
|
||||
pass_through_endpoint_data, endpoint_id
|
||||
)
|
||||
|
||||
for idx, endpoint in enumerate(pass_through_endpoint_data):
|
||||
_endpoint: Optional[PassThroughGenericEndpoint] = None
|
||||
if isinstance(endpoint, dict):
|
||||
_endpoint = PassThroughGenericEndpoint(**endpoint)
|
||||
elif isinstance(endpoint, PassThroughGenericEndpoint):
|
||||
_endpoint = endpoint
|
||||
|
||||
if _endpoint is not None and _endpoint.path == endpoint_id:
|
||||
endpoint_found = True
|
||||
# Get the update data as dict, excluding None values for partial updates
|
||||
update_data = data.model_dump(exclude_none=True)
|
||||
|
||||
# Start with existing endpoint data
|
||||
endpoint_dict = _endpoint.model_dump()
|
||||
|
||||
# Update with new data (only non-None values)
|
||||
endpoint_dict.update(update_data)
|
||||
|
||||
# Ensure the path stays the same (can't change the endpoint_id)
|
||||
endpoint_dict["path"] = endpoint_id
|
||||
|
||||
# Create updated endpoint object
|
||||
updated_endpoint = PassThroughGenericEndpoint(**endpoint_dict)
|
||||
|
||||
# Update the list
|
||||
pass_through_endpoint_data[idx] = endpoint_dict
|
||||
break
|
||||
|
||||
if not endpoint_found:
|
||||
if found_endpoint is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail={
|
||||
"error": f"Endpoint with path '{endpoint_id}' not found"
|
||||
"error": f"Endpoint with ID '{endpoint_id}' not found"
|
||||
},
|
||||
)
|
||||
|
||||
# Find the index for updating the list
|
||||
endpoint_index = None
|
||||
for idx, endpoint in enumerate(pass_through_endpoint_data):
|
||||
_endpoint = PassThroughGenericEndpoint(**endpoint) if isinstance(endpoint, dict) else endpoint
|
||||
if _endpoint.id == endpoint_id:
|
||||
endpoint_index = idx
|
||||
break
|
||||
|
||||
if endpoint_index is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail={
|
||||
"error": f"Could not find index for endpoint with ID '{endpoint_id}'"
|
||||
},
|
||||
)
|
||||
|
||||
# Get the update data as dict, excluding None values for partial updates
|
||||
update_data = data.model_dump(exclude_none=True)
|
||||
|
||||
# Start with existing endpoint data
|
||||
endpoint_dict = found_endpoint.model_dump()
|
||||
|
||||
# Update with new data (only non-None values)
|
||||
endpoint_dict.update(update_data)
|
||||
|
||||
# Preserve existing ID if not provided in update and endpoint has ID
|
||||
if "id" not in update_data and found_endpoint.id is not None:
|
||||
endpoint_dict["id"] = found_endpoint.id
|
||||
|
||||
# Create updated endpoint object
|
||||
updated_endpoint = PassThroughGenericEndpoint(**endpoint_dict)
|
||||
|
||||
# Update the list
|
||||
pass_through_endpoint_data[endpoint_index] = endpoint_dict
|
||||
|
||||
## Update db
|
||||
updated_data = ConfigFieldUpdate(
|
||||
field_name="pass_through_endpoints",
|
||||
@@ -1240,6 +1253,8 @@ async def create_pass_through_endpoints(
|
||||
"""
|
||||
Create new pass-through endpoint
|
||||
"""
|
||||
import uuid
|
||||
|
||||
from litellm.proxy.proxy_server import (
|
||||
get_config_general_settings,
|
||||
update_config_general_settings,
|
||||
@@ -1256,8 +1271,11 @@ async def create_pass_through_endpoints(
|
||||
field_name="pass_through_endpoints", field_value=None
|
||||
)
|
||||
|
||||
## Update field with new endpoint
|
||||
## Auto-generate ID if not provided
|
||||
data_dict = data.model_dump()
|
||||
if data_dict.get("id") is None:
|
||||
data_dict["id"] = str(uuid.uuid4())
|
||||
|
||||
if response.field_value is None:
|
||||
response.field_value = [data_dict]
|
||||
elif isinstance(response.field_value, List):
|
||||
@@ -1273,6 +1291,10 @@ async def create_pass_through_endpoints(
|
||||
data=updated_data, user_api_key_dict=user_api_key_dict
|
||||
)
|
||||
|
||||
# Return the created endpoint with the generated ID
|
||||
created_endpoint = PassThroughGenericEndpoint(**data_dict)
|
||||
return PassThroughEndpointResponse(endpoints=[created_endpoint])
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/config/pass_through_endpoint",
|
||||
@@ -1284,7 +1306,7 @@ async def delete_pass_through_endpoints(
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Delete a pass-through endpoint
|
||||
Delete a pass-through endpoint by ID.
|
||||
|
||||
Returns - the deleted endpoint
|
||||
"""
|
||||
@@ -1306,27 +1328,46 @@ async def delete_pass_through_endpoints(
|
||||
|
||||
## Update field by removing endpoint
|
||||
pass_through_endpoint_data: Optional[List] = response.field_value
|
||||
response_obj: Optional[PassThroughGenericEndpoint] = None
|
||||
if response.field_value is None or pass_through_endpoint_data is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={"error": "There are no pass-through endpoints setup."},
|
||||
)
|
||||
elif isinstance(response.field_value, List):
|
||||
invalid_idx: Optional[int] = None
|
||||
for idx, endpoint in enumerate(pass_through_endpoint_data):
|
||||
_endpoint: Optional[PassThroughGenericEndpoint] = None
|
||||
if isinstance(endpoint, dict):
|
||||
_endpoint = PassThroughGenericEndpoint(**endpoint)
|
||||
elif isinstance(endpoint, PassThroughGenericEndpoint):
|
||||
_endpoint = endpoint
|
||||
|
||||
if _endpoint is not None and _endpoint.path == endpoint_id:
|
||||
invalid_idx = idx
|
||||
response_obj = _endpoint
|
||||
# Find the endpoint to delete
|
||||
found_endpoint = _find_endpoint_by_id(
|
||||
pass_through_endpoint_data, endpoint_id
|
||||
)
|
||||
|
||||
if found_endpoint is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "Endpoint with ID '{}' was not found in pass-through endpoint list.".format(
|
||||
endpoint_id
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
# Find the index for deleting from the list
|
||||
endpoint_index = None
|
||||
for idx, endpoint in enumerate(pass_through_endpoint_data):
|
||||
_endpoint = PassThroughGenericEndpoint(**endpoint) if isinstance(endpoint, dict) else endpoint
|
||||
if _endpoint.id == endpoint_id:
|
||||
endpoint_index = idx
|
||||
break
|
||||
|
||||
if invalid_idx is not None:
|
||||
pass_through_endpoint_data.pop(invalid_idx)
|
||||
if endpoint_index is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": f"Could not find index for endpoint with ID '{endpoint_id}'"
|
||||
},
|
||||
)
|
||||
|
||||
# Remove the endpoint
|
||||
pass_through_endpoint_data.pop(endpoint_index)
|
||||
response_obj = found_endpoint
|
||||
|
||||
## Update db
|
||||
updated_data = ConfigFieldUpdate(
|
||||
@@ -1338,18 +1379,37 @@ async def delete_pass_through_endpoints(
|
||||
data=updated_data, user_api_key_dict=user_api_key_dict
|
||||
)
|
||||
|
||||
if response_obj is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "Endpoint={} was not found in pass-through endpoint list.".format(
|
||||
endpoint_id
|
||||
)
|
||||
},
|
||||
)
|
||||
return PassThroughEndpointResponse(endpoints=[response_obj])
|
||||
|
||||
|
||||
def _find_endpoint_by_id(
|
||||
endpoints_data: List,
|
||||
endpoint_id: str,
|
||||
) -> Optional[PassThroughGenericEndpoint]:
|
||||
"""
|
||||
Find an endpoint by ID.
|
||||
|
||||
Args:
|
||||
endpoints_data: List of endpoint data (dicts or PassThroughGenericEndpoint objects)
|
||||
endpoint_id: ID to search for
|
||||
|
||||
Returns:
|
||||
Found endpoint or None if not found
|
||||
"""
|
||||
for endpoint in endpoints_data:
|
||||
_endpoint: Optional[PassThroughGenericEndpoint] = None
|
||||
if isinstance(endpoint, dict):
|
||||
_endpoint = PassThroughGenericEndpoint(**endpoint)
|
||||
elif isinstance(endpoint, PassThroughGenericEndpoint):
|
||||
_endpoint = endpoint
|
||||
|
||||
# Only compare IDs to IDs
|
||||
if _endpoint is not None and _endpoint.id == endpoint_id:
|
||||
return _endpoint
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def initialize_pass_through_endpoints_in_db():
|
||||
"""
|
||||
Gets all pass-through endpoints from db and initializes them in the proxy server.
|
||||
|
||||
@@ -852,3 +852,396 @@ async def test_pass_through_request_contains_proxy_server_request_in_kwargs():
|
||||
assert metadata["user_api_key_alias"] == "test-alias"
|
||||
assert metadata["user_api_key_user_email"] == "test@example.com"
|
||||
assert metadata["user_api_key_user_id"] == "test-user-id"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_pass_through_endpoint():
|
||||
"""
|
||||
Test creating a new pass-through endpoint
|
||||
|
||||
This test verifies that the create_pass_through_endpoints function:
|
||||
1. Accepts a PassThroughGenericEndpoint object
|
||||
2. Auto-generates an ID if not provided
|
||||
3. Adds the endpoint to the database
|
||||
4. Returns the created endpoint with the generated ID
|
||||
"""
|
||||
from litellm.proxy._types import (
|
||||
ConfigFieldInfo,
|
||||
ConfigFieldUpdate,
|
||||
PassThroughEndpointResponse,
|
||||
PassThroughGenericEndpoint,
|
||||
UserAPIKeyAuth,
|
||||
)
|
||||
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
|
||||
create_pass_through_endpoints,
|
||||
)
|
||||
|
||||
# Mock the database functions
|
||||
with patch("litellm.proxy.proxy_server.get_config_general_settings") as mock_get_config:
|
||||
with patch("litellm.proxy.proxy_server.update_config_general_settings") as mock_update_config:
|
||||
# Mock existing config (empty list)
|
||||
mock_get_config.return_value = ConfigFieldInfo(
|
||||
field_name="pass_through_endpoints",
|
||||
field_value=[]
|
||||
)
|
||||
|
||||
# Create test endpoint data
|
||||
test_endpoint = PassThroughGenericEndpoint(
|
||||
path="/test/endpoint",
|
||||
target="http://example.com/api",
|
||||
headers={"Authorization": "Bearer test-token"},
|
||||
include_subpath=True,
|
||||
cost_per_request=0.50
|
||||
)
|
||||
|
||||
# Mock user API key dict
|
||||
mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth)
|
||||
|
||||
# Call the create function
|
||||
result = await create_pass_through_endpoints(
|
||||
data=test_endpoint,
|
||||
user_api_key_dict=mock_user_api_key_dict
|
||||
)
|
||||
|
||||
# Verify the result
|
||||
assert isinstance(result, PassThroughEndpointResponse)
|
||||
assert len(result.endpoints) == 1
|
||||
|
||||
created_endpoint = result.endpoints[0]
|
||||
assert created_endpoint.path == "/test/endpoint"
|
||||
assert created_endpoint.target == "http://example.com/api"
|
||||
assert created_endpoint.headers == {"Authorization": "Bearer test-token"}
|
||||
assert created_endpoint.include_subpath is True
|
||||
assert created_endpoint.cost_per_request == 0.50
|
||||
assert created_endpoint.id is not None # Should be auto-generated
|
||||
|
||||
# Verify database calls
|
||||
mock_get_config.assert_called_once_with(
|
||||
field_name="pass_through_endpoints",
|
||||
user_api_key_dict=mock_user_api_key_dict
|
||||
)
|
||||
|
||||
mock_update_config.assert_called_once()
|
||||
update_call_args = mock_update_config.call_args[1]
|
||||
assert update_call_args["data"].field_name == "pass_through_endpoints"
|
||||
assert len(update_call_args["data"].field_value) == 1
|
||||
assert update_call_args["data"].field_value[0]["path"] == "/test/endpoint"
|
||||
assert update_call_args["data"].field_value[0]["id"] is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_pass_through_endpoint():
|
||||
"""
|
||||
Test updating an existing pass-through endpoint
|
||||
|
||||
This test verifies that the update_pass_through_endpoints function:
|
||||
1. Finds the existing endpoint by ID
|
||||
2. Updates only the provided fields (partial updates)
|
||||
3. Preserves the existing ID
|
||||
4. Updates the database with the modified endpoint
|
||||
5. Returns the updated endpoint
|
||||
"""
|
||||
from litellm.proxy._types import (
|
||||
ConfigFieldInfo,
|
||||
ConfigFieldUpdate,
|
||||
PassThroughEndpointResponse,
|
||||
PassThroughGenericEndpoint,
|
||||
UserAPIKeyAuth,
|
||||
)
|
||||
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
|
||||
update_pass_through_endpoints,
|
||||
)
|
||||
|
||||
# Mock the database functions
|
||||
with patch("litellm.proxy.proxy_server.get_config_general_settings") as mock_get_config:
|
||||
with patch("litellm.proxy.proxy_server.update_config_general_settings") as mock_update_config:
|
||||
# Create existing endpoint data
|
||||
existing_endpoint_id = "test-endpoint-123"
|
||||
existing_endpoints = [
|
||||
{
|
||||
"id": existing_endpoint_id,
|
||||
"path": "/test/endpoint",
|
||||
"target": "http://example.com/api",
|
||||
"headers": {"Authorization": "Bearer old-token"},
|
||||
"include_subpath": False,
|
||||
"cost_per_request": 0.25
|
||||
}
|
||||
]
|
||||
|
||||
# Mock existing config
|
||||
mock_get_config.return_value = ConfigFieldInfo(
|
||||
field_name="pass_through_endpoints",
|
||||
field_value=existing_endpoints
|
||||
)
|
||||
|
||||
# Create update data (partial update)
|
||||
update_data = PassThroughGenericEndpoint(
|
||||
path="/test/endpoint", # Keep same path
|
||||
target="http://newapi.com/v2", # Update target
|
||||
headers={"Authorization": "Bearer new-token", "X-Custom": "header"}, # Update headers
|
||||
cost_per_request=0.75 # Update cost
|
||||
# include_subpath not provided - should preserve existing value
|
||||
)
|
||||
|
||||
# Mock user API key dict
|
||||
mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth)
|
||||
|
||||
# Call the update function
|
||||
result = await update_pass_through_endpoints(
|
||||
endpoint_id=existing_endpoint_id,
|
||||
data=update_data,
|
||||
user_api_key_dict=mock_user_api_key_dict
|
||||
)
|
||||
|
||||
# Verify the result
|
||||
assert isinstance(result, PassThroughEndpointResponse)
|
||||
assert len(result.endpoints) == 1
|
||||
|
||||
updated_endpoint = result.endpoints[0]
|
||||
assert updated_endpoint.id == existing_endpoint_id # ID preserved
|
||||
assert updated_endpoint.path == "/test/endpoint"
|
||||
assert updated_endpoint.target == "http://newapi.com/v2" # Updated
|
||||
assert updated_endpoint.headers == {"Authorization": "Bearer new-token", "X-Custom": "header"} # Updated
|
||||
assert updated_endpoint.include_subpath is False # Preserved existing value
|
||||
assert updated_endpoint.cost_per_request == 0.75 # Updated
|
||||
|
||||
# Verify database calls
|
||||
mock_get_config.assert_called_once_with(
|
||||
field_name="pass_through_endpoints",
|
||||
user_api_key_dict=mock_user_api_key_dict
|
||||
)
|
||||
|
||||
mock_update_config.assert_called_once()
|
||||
update_call_args = mock_update_config.call_args[1]
|
||||
assert update_call_args["data"].field_name == "pass_through_endpoints"
|
||||
assert len(update_call_args["data"].field_value) == 1
|
||||
updated_data = update_call_args["data"].field_value[0]
|
||||
assert updated_data["id"] == existing_endpoint_id
|
||||
assert updated_data["target"] == "http://newapi.com/v2"
|
||||
assert updated_data["cost_per_request"] == 0.75
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_pass_through_endpoint_not_found():
|
||||
"""
|
||||
Test updating a non-existent pass-through endpoint raises HTTPException
|
||||
"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
from litellm.proxy._types import (
|
||||
ConfigFieldInfo,
|
||||
PassThroughGenericEndpoint,
|
||||
UserAPIKeyAuth,
|
||||
)
|
||||
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
|
||||
update_pass_through_endpoints,
|
||||
)
|
||||
|
||||
# Mock the database functions
|
||||
with patch("litellm.proxy.proxy_server.get_config_general_settings") as mock_get_config:
|
||||
# Mock existing config with different endpoint
|
||||
existing_endpoints = [
|
||||
{
|
||||
"id": "different-endpoint-456",
|
||||
"path": "/different/endpoint",
|
||||
"target": "http://different.com/api",
|
||||
"headers": {},
|
||||
"include_subpath": False,
|
||||
"cost_per_request": 0.0
|
||||
}
|
||||
]
|
||||
|
||||
mock_get_config.return_value = ConfigFieldInfo(
|
||||
field_name="pass_through_endpoints",
|
||||
field_value=existing_endpoints
|
||||
)
|
||||
|
||||
# Create update data
|
||||
update_data = PassThroughGenericEndpoint(
|
||||
path="/test/endpoint",
|
||||
target="http://newapi.com/v2"
|
||||
)
|
||||
|
||||
# Mock user API key dict
|
||||
mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth)
|
||||
|
||||
# Call the update function with non-existent ID
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await update_pass_through_endpoints(
|
||||
endpoint_id="non-existent-endpoint-123",
|
||||
data=update_data,
|
||||
user_api_key_dict=mock_user_api_key_dict
|
||||
)
|
||||
|
||||
# Verify the exception
|
||||
assert exc_info.value.status_code == 404
|
||||
assert "not found" in str(exc_info.value.detail).lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_pass_through_endpoint():
|
||||
"""
|
||||
Test deleting an existing pass-through endpoint
|
||||
|
||||
This test verifies that the delete_pass_through_endpoints function:
|
||||
1. Finds the existing endpoint by ID
|
||||
2. Removes it from the database
|
||||
3. Returns the deleted endpoint
|
||||
"""
|
||||
from litellm.proxy._types import (
|
||||
ConfigFieldInfo,
|
||||
ConfigFieldUpdate,
|
||||
PassThroughEndpointResponse,
|
||||
UserAPIKeyAuth,
|
||||
)
|
||||
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
|
||||
delete_pass_through_endpoints,
|
||||
)
|
||||
|
||||
# Mock the database functions
|
||||
with patch("litellm.proxy.proxy_server.get_config_general_settings") as mock_get_config:
|
||||
with patch("litellm.proxy.proxy_server.update_config_general_settings") as mock_update_config:
|
||||
# Create existing endpoint data
|
||||
endpoint_to_delete_id = "test-endpoint-123"
|
||||
other_endpoint_id = "other-endpoint-456"
|
||||
existing_endpoints = [
|
||||
{
|
||||
"id": endpoint_to_delete_id,
|
||||
"path": "/test/endpoint",
|
||||
"target": "http://example.com/api",
|
||||
"headers": {"Authorization": "Bearer test-token"},
|
||||
"include_subpath": True,
|
||||
"cost_per_request": 0.50
|
||||
},
|
||||
{
|
||||
"id": other_endpoint_id,
|
||||
"path": "/other/endpoint",
|
||||
"target": "http://other.com/api",
|
||||
"headers": {},
|
||||
"include_subpath": False,
|
||||
"cost_per_request": 0.25
|
||||
}
|
||||
]
|
||||
|
||||
# Mock existing config
|
||||
mock_get_config.return_value = ConfigFieldInfo(
|
||||
field_name="pass_through_endpoints",
|
||||
field_value=existing_endpoints
|
||||
)
|
||||
|
||||
# Mock user API key dict
|
||||
mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth)
|
||||
|
||||
# Call the delete function
|
||||
result = await delete_pass_through_endpoints(
|
||||
endpoint_id=endpoint_to_delete_id,
|
||||
user_api_key_dict=mock_user_api_key_dict
|
||||
)
|
||||
|
||||
# Verify the result
|
||||
assert isinstance(result, PassThroughEndpointResponse)
|
||||
assert len(result.endpoints) == 1
|
||||
|
||||
deleted_endpoint = result.endpoints[0]
|
||||
assert deleted_endpoint.id == endpoint_to_delete_id
|
||||
assert deleted_endpoint.path == "/test/endpoint"
|
||||
assert deleted_endpoint.target == "http://example.com/api"
|
||||
assert deleted_endpoint.headers == {"Authorization": "Bearer test-token"}
|
||||
assert deleted_endpoint.include_subpath is True
|
||||
assert deleted_endpoint.cost_per_request == 0.50
|
||||
|
||||
# Verify database calls
|
||||
mock_get_config.assert_called_once_with(
|
||||
field_name="pass_through_endpoints",
|
||||
user_api_key_dict=mock_user_api_key_dict
|
||||
)
|
||||
|
||||
mock_update_config.assert_called_once()
|
||||
update_call_args = mock_update_config.call_args[1]
|
||||
assert update_call_args["data"].field_name == "pass_through_endpoints"
|
||||
# Should only have the other endpoint remaining
|
||||
assert len(update_call_args["data"].field_value) == 1
|
||||
remaining_endpoint = update_call_args["data"].field_value[0]
|
||||
assert remaining_endpoint["id"] == other_endpoint_id
|
||||
assert remaining_endpoint["path"] == "/other/endpoint"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_pass_through_endpoint_not_found():
|
||||
"""
|
||||
Test deleting a non-existent pass-through endpoint raises HTTPException
|
||||
"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
from litellm.proxy._types import ConfigFieldInfo, UserAPIKeyAuth
|
||||
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
|
||||
delete_pass_through_endpoints,
|
||||
)
|
||||
|
||||
# Mock the database functions
|
||||
with patch("litellm.proxy.proxy_server.get_config_general_settings") as mock_get_config:
|
||||
# Mock existing config with different endpoint
|
||||
existing_endpoints = [
|
||||
{
|
||||
"id": "different-endpoint-456",
|
||||
"path": "/different/endpoint",
|
||||
"target": "http://different.com/api",
|
||||
"headers": {},
|
||||
"include_subpath": False,
|
||||
"cost_per_request": 0.0
|
||||
}
|
||||
]
|
||||
|
||||
mock_get_config.return_value = ConfigFieldInfo(
|
||||
field_name="pass_through_endpoints",
|
||||
field_value=existing_endpoints
|
||||
)
|
||||
|
||||
# Mock user API key dict
|
||||
mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth)
|
||||
|
||||
# Call the delete function with non-existent ID
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await delete_pass_through_endpoints(
|
||||
endpoint_id="non-existent-endpoint-123",
|
||||
user_api_key_dict=mock_user_api_key_dict
|
||||
)
|
||||
|
||||
# Verify the exception
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "not found" in str(exc_info.value.detail).lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_pass_through_endpoint_empty_list():
|
||||
"""
|
||||
Test deleting from an empty endpoint list raises HTTPException
|
||||
"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
from litellm.proxy._types import ConfigFieldInfo, UserAPIKeyAuth
|
||||
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
|
||||
delete_pass_through_endpoints,
|
||||
)
|
||||
|
||||
# Mock the database functions
|
||||
with patch("litellm.proxy.proxy_server.get_config_general_settings") as mock_get_config:
|
||||
# Mock empty config
|
||||
mock_get_config.return_value = ConfigFieldInfo(
|
||||
field_name="pass_through_endpoints",
|
||||
field_value=None
|
||||
)
|
||||
|
||||
# Mock user API key dict
|
||||
mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth)
|
||||
|
||||
# Call the delete function
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await delete_pass_through_endpoints(
|
||||
endpoint_id="any-endpoint-123",
|
||||
user_api_key_dict=mock_user_api_key_dict
|
||||
)
|
||||
|
||||
# Verify the exception
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "no pass-through endpoints setup" in str(exc_info.value.detail).lower()
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
Divider,
|
||||
Collapse,
|
||||
} from "antd";
|
||||
import NumericalInput from "./shared/numerical_input";
|
||||
import { InfoCircleOutlined, ApiOutlined, ExclamationCircleOutlined, CheckCircleOutlined, CopyOutlined } from "@ant-design/icons";
|
||||
import { keyCreateCall, slackBudgetAlertsHealthCheck, modelAvailableCall } from "./networking";
|
||||
import { list } from "postcss";
|
||||
@@ -71,17 +72,12 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
||||
try {
|
||||
console.log(`formValues: ${JSON.stringify(formValues)}`);
|
||||
|
||||
const newPassThroughItem: passThroughItem = {
|
||||
"headers": formValues["headers"],
|
||||
"path": formValues["path"],
|
||||
"target": formValues["target"],
|
||||
"include_subpath": formValues["include_subpath"] || false,
|
||||
"cost_per_request": formValues["cost_per_request"] || 0
|
||||
}
|
||||
const response = await createPassThroughEndpoint(accessToken, formValues);
|
||||
|
||||
await createPassThroughEndpoint(accessToken, formValues);
|
||||
// Use the created endpoint from the API response (includes the generated ID)
|
||||
const createdEndpoint = response.endpoints[0];
|
||||
|
||||
const updatedPassThroughSettings = [...passThroughItems, newPassThroughItem]
|
||||
const updatedPassThroughSettings = [...passThroughItems, createdEndpoint]
|
||||
setPassThroughItems(updatedPassThroughSettings)
|
||||
|
||||
message.success("Pass-through endpoint created successfully");
|
||||
@@ -284,13 +280,12 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<InputNumber
|
||||
<NumericalInput
|
||||
min={0}
|
||||
step={0.001}
|
||||
precision={6}
|
||||
placeholder="2.000000"
|
||||
precision={4}
|
||||
placeholder="2.0000"
|
||||
size="large"
|
||||
className="rounded-lg w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
@@ -32,7 +32,7 @@ const KeyValueInput: React.FC<KeyValueInputProps> = ({ value = {}, onChange }) =
|
||||
return (
|
||||
<div>
|
||||
{pairs.map(([key, val], index) => (
|
||||
<Space key={index} style={{ display: 'flex', marginBottom: 8 }} align="start">
|
||||
<Space key={index} style={{ display: 'flex', marginBottom: 8 }} align="center">
|
||||
<TextInput
|
||||
placeholder="Header Name"
|
||||
value={key}
|
||||
@@ -43,7 +43,12 @@ const KeyValueInput: React.FC<KeyValueInputProps> = ({ value = {}, onChange }) =
|
||||
value={val}
|
||||
onChange={(e) => handleChange(index, key, e.target.value)}
|
||||
/>
|
||||
<MinusCircleOutlined onClick={() => handleRemove(index)} />
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
||||
<MinusCircleOutlined
|
||||
onClick={() => handleRemove(index)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
))}
|
||||
<Button type="dashed" onClick={handleAdd} icon={<PlusOutlined />}>
|
||||
|
||||
@@ -15,14 +15,14 @@ import {
|
||||
} from "@tremor/react";
|
||||
import { Button, Form, Input, Switch, message, InputNumber } from "antd";
|
||||
import {
|
||||
getPassThroughEndpointInfo,
|
||||
updatePassThroughEndpoint,
|
||||
deletePassThroughEndpointsCall
|
||||
} from "./networking";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import RoutePreview from "./route_preview";
|
||||
|
||||
export interface PassThroughInfoProps {
|
||||
endpointPath: string;
|
||||
endpointData: PassThroughEndpoint;
|
||||
onClose: () => void;
|
||||
accessToken: string | null;
|
||||
isAdmin: boolean;
|
||||
@@ -30,6 +30,7 @@ export interface PassThroughInfoProps {
|
||||
}
|
||||
|
||||
interface PassThroughEndpoint {
|
||||
id?: string;
|
||||
path: string;
|
||||
target: string;
|
||||
headers: Record<string, any>;
|
||||
@@ -63,38 +64,20 @@ const PasswordField: React.FC<{ value: Record<string, any> }> = ({ value }) => {
|
||||
};
|
||||
|
||||
const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
|
||||
endpointPath,
|
||||
endpointData: initialEndpointData,
|
||||
onClose,
|
||||
accessToken,
|
||||
isAdmin,
|
||||
onEndpointUpdated
|
||||
}) => {
|
||||
const [endpointData, setEndpointData] = useState<PassThroughEndpoint | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [endpointData, setEndpointData] = useState<PassThroughEndpoint | null>(initialEndpointData);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const fetchEndpointInfo = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if (!accessToken) return;
|
||||
const response = await getPassThroughEndpointInfo(accessToken, endpointPath);
|
||||
setEndpointData(response);
|
||||
} catch (error) {
|
||||
message.error("Failed to load pass through endpoint information");
|
||||
console.error("Error fetching endpoint info:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchEndpointInfo();
|
||||
}, [endpointPath, accessToken]);
|
||||
|
||||
const handleEndpointUpdate = async (values: any) => {
|
||||
try {
|
||||
if (!accessToken) return;
|
||||
if (!accessToken || !endpointData?.id) return;
|
||||
|
||||
// Parse headers if provided as string
|
||||
let headers = {};
|
||||
@@ -110,15 +93,21 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
path: endpointData.path,
|
||||
target: values.target,
|
||||
headers: headers,
|
||||
include_subpath: values.include_subpath,
|
||||
cost_per_request: values.cost_per_request,
|
||||
};
|
||||
|
||||
await updatePassThroughEndpoint(accessToken, endpointPath, updateData);
|
||||
message.success("Pass through endpoint updated successfully");
|
||||
fetchEndpointInfo();
|
||||
await updatePassThroughEndpoint(accessToken, endpointData.id, updateData);
|
||||
|
||||
// Update local state with the new values
|
||||
setEndpointData({
|
||||
...endpointData,
|
||||
...updateData,
|
||||
});
|
||||
|
||||
setIsEditing(false);
|
||||
if (onEndpointUpdated) {
|
||||
onEndpointUpdated();
|
||||
@@ -131,9 +120,9 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
|
||||
|
||||
const handleDeleteEndpoint = async () => {
|
||||
try {
|
||||
if (!accessToken) return;
|
||||
if (!accessToken || !endpointData?.id) return;
|
||||
|
||||
await deletePassThroughEndpointsCall(accessToken, endpointPath);
|
||||
await deletePassThroughEndpointsCall(accessToken, endpointData.id);
|
||||
message.success("Pass through endpoint deleted successfully");
|
||||
onClose();
|
||||
if (onEndpointUpdated) {
|
||||
@@ -158,8 +147,8 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<Button onClick={onClose} className="mb-4">← Back</Button>
|
||||
<Title>Pass Through Endpoint</Title>
|
||||
<Text className="text-gray-500 font-mono">{endpointData.path}</Text>
|
||||
<Title>Pass Through Endpoint: {endpointData.path}</Title>
|
||||
<Text className="text-gray-500 font-mono">{endpointData.id}</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -204,6 +193,15 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* Route Preview Section */}
|
||||
<div className="mt-6">
|
||||
<RoutePreview
|
||||
pathValue={endpointData.path}
|
||||
targetValue={endpointData.target}
|
||||
includeSubpath={endpointData.include_subpath || false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{endpointData.headers && Object.keys(endpointData.headers).length > 0 && (
|
||||
<Card className="mt-6">
|
||||
<div className="flex justify-between items-center">
|
||||
|
||||
@@ -34,23 +34,14 @@ import {
|
||||
Button as Button2,
|
||||
message,
|
||||
InputNumber,
|
||||
Tooltip,
|
||||
} from "antd";
|
||||
import {
|
||||
InformationCircleIcon,
|
||||
PencilAltIcon,
|
||||
PencilIcon,
|
||||
StatusOnlineIcon,
|
||||
TrashIcon,
|
||||
RefreshIcon,
|
||||
CheckCircleIcon,
|
||||
XCircleIcon,
|
||||
QuestionMarkCircleIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
import AddFallbacks from "./add_fallbacks";
|
||||
import AddPassThroughEndpoint from "./add_pass_through";
|
||||
import PassThroughInfoView from "./pass_through_info";
|
||||
import openai from "openai";
|
||||
import Paragraph from "antd/es/skeleton/Paragraph";
|
||||
import { DataTable } from "./view_logs/table";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
@@ -76,6 +67,7 @@ interface nestedFieldItem {
|
||||
}
|
||||
|
||||
export interface passThroughItem {
|
||||
id?: string
|
||||
path: string
|
||||
target: string
|
||||
headers: object
|
||||
@@ -117,7 +109,9 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
|
||||
const [generalSettings, setGeneralSettings] = useState<passThroughItem[]>(
|
||||
[]
|
||||
);
|
||||
const [selectedEndpointPath, setSelectedEndpointPath] = useState<string | null>(null);
|
||||
const [selectedEndpointId, setSelectedEndpointId] = useState<string | null>(null);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [endpointToDelete, setEndpointToDelete] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!accessToken || !userRole || !userID) {
|
||||
@@ -139,41 +133,65 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetField = (fieldName: string, idx: number) => {
|
||||
if (!accessToken) {
|
||||
const handleDelete = async (endpointId: string) => {
|
||||
// Set the endpoint to delete and open the confirmation modal
|
||||
setEndpointToDelete(endpointId);
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (endpointToDelete == null || !accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
deletePassThroughEndpointsCall(accessToken, fieldName);
|
||||
await deletePassThroughEndpointsCall(accessToken, endpointToDelete);
|
||||
|
||||
const updatedSettings = generalSettings.filter((setting) => setting.path !== fieldName);
|
||||
const updatedSettings = generalSettings.filter((setting) => setting.id !== endpointToDelete);
|
||||
setGeneralSettings(updatedSettings);
|
||||
|
||||
message.success("Endpoint deleted successfully.");
|
||||
} catch (error) {
|
||||
// do something
|
||||
console.error("Error deleting the endpoint:", error);
|
||||
message.error("Error deleting the endpoint: " + error);
|
||||
}
|
||||
|
||||
// Close the confirmation modal and reset the endpointToDelete
|
||||
setIsDeleteModalOpen(false);
|
||||
setEndpointToDelete(null);
|
||||
};
|
||||
|
||||
const cancelDelete = () => {
|
||||
// Close the confirmation modal and reset the endpointToDelete
|
||||
setIsDeleteModalOpen(false);
|
||||
setEndpointToDelete(null);
|
||||
};
|
||||
|
||||
const handleResetField = (endpointId: string, idx: number) => {
|
||||
// Use handleDelete instead of direct deletion
|
||||
handleDelete(endpointId);
|
||||
};
|
||||
|
||||
// Define columns for the DataTable
|
||||
const columns: ColumnDef<passThroughItem>[] = [
|
||||
{
|
||||
header: "Path",
|
||||
accessorKey: "path",
|
||||
header: "ID",
|
||||
accessorKey: "id",
|
||||
cell: (info: any) => (
|
||||
<div className="overflow-hidden">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
|
||||
onClick={() => setSelectedEndpointPath(info.getValue())}
|
||||
<Tooltip title={info.row.original.id}>
|
||||
<div
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left w-full truncate whitespace-nowrap cursor-pointer max-w-[15ch]"
|
||||
onClick={() => info.row.original.id && setSelectedEndpointId(info.row.original.id)}
|
||||
>
|
||||
{info.getValue()}
|
||||
</Button>
|
||||
</div>
|
||||
{info.row.original.id}
|
||||
</div>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Path",
|
||||
accessorKey: "path"
|
||||
},
|
||||
{
|
||||
header: "Target",
|
||||
accessorKey: "target",
|
||||
@@ -196,13 +214,13 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
|
||||
<Icon
|
||||
icon={PencilAltIcon}
|
||||
size="sm"
|
||||
onClick={() => setSelectedEndpointPath(row.original.path)}
|
||||
onClick={() => row.original.id && setSelectedEndpointId(row.original.id)}
|
||||
title="Edit"
|
||||
/>
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
size="sm"
|
||||
onClick={() => handleResetField(row.original.path, row.index)}
|
||||
onClick={() => handleResetField(row.original.id!, row.index)}
|
||||
title="Delete"
|
||||
/>
|
||||
</div>
|
||||
@@ -215,11 +233,20 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
|
||||
}
|
||||
|
||||
// If a specific endpoint is selected, show the info view
|
||||
if (selectedEndpointPath) {
|
||||
if (selectedEndpointId) {
|
||||
// Find the endpoint by ID to get the endpoint data for the info view
|
||||
console.log("selectedEndpointId", selectedEndpointId);
|
||||
console.log("generalSettings", generalSettings);
|
||||
const selectedEndpoint = generalSettings.find(endpoint => endpoint.id === selectedEndpointId);
|
||||
|
||||
if (!selectedEndpoint) {
|
||||
return <div>Endpoint not found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<PassThroughInfoView
|
||||
endpointPath={selectedEndpointPath}
|
||||
onClose={() => setSelectedEndpointPath(null)}
|
||||
endpointData={selectedEndpoint}
|
||||
onClose={() => setSelectedEndpointId(null)}
|
||||
accessToken={accessToken}
|
||||
isAdmin={userRole === "Admin" || userRole === "admin"}
|
||||
onEndpointUpdated={handleEndpointUpdated}
|
||||
@@ -250,6 +277,55 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
|
||||
isLoading={false}
|
||||
noDataMessage="No pass-through endpoints configured"
|
||||
/>
|
||||
|
||||
{isDeleteModalOpen && (
|
||||
<div className="fixed z-10 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>
|
||||
|
||||
{/* Modal Panel */}
|
||||
<span
|
||||
className="hidden sm:inline-block sm:align-middle sm:h-screen"
|
||||
aria-hidden="true"
|
||||
>
|
||||
​
|
||||
</span>
|
||||
|
||||
{/* Confirmation Modal Content */}
|
||||
<div className="inline-block align-bottom bg-white 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-white 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-gray-900">
|
||||
Delete Pass-Through Endpoint
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
Are you sure you want to delete this pass-through endpoint? This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<Button
|
||||
onClick={confirmDelete}
|
||||
color="red"
|
||||
className="ml-2"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button onClick={cancelDelete}>Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -783,7 +783,7 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<p>
|
||||
Are you sure you want to delete the callback "{callbackToDelete}"?
|
||||
Are you sure you want to delete the callback - {callbackToDelete}?
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user