Show all callbacks on UI

This commit is contained in:
yuneng-jiang 2025-11-06 12:38:47 -08:00
parent b762493ec5
commit 2d5ae35a85
4 changed files with 548 additions and 222 deletions

View File

@ -9572,8 +9572,61 @@ async def get_config(): # noqa: PLR0915
_general_settings = config_data.get("general_settings", {})
environment_variables = config_data.get("environment_variables", {})
# check if "langfuse" in litellm_settings
# Helper function to process callbacks and get environment variables
def process_callback(_callback: str, callback_type: str) -> dict:
"""Process a single callback and return its data with environment variables"""
if _callback == "langfuse" or _callback == "langfuse_otel":
env_vars = [
"LANGFUSE_PUBLIC_KEY",
"LANGFUSE_SECRET_KEY",
"LANGFUSE_HOST",
]
elif _callback == "openmeter":
env_vars = [
"OPENMETER_API_KEY",
]
elif _callback == "braintrust":
env_vars = [
"BRAINTRUST_API_KEY",
"BRAINTRUST_API_BASE",
]
elif _callback == "traceloop":
env_vars = ["TRACELOOP_API_KEY"]
elif _callback == "custom_callback_api":
env_vars = ["GENERIC_LOGGER_ENDPOINT"]
elif _callback == "otel":
env_vars = ["OTEL_EXPORTER", "OTEL_ENDPOINT", "OTEL_HEADERS"]
elif _callback == "langsmith":
env_vars = [
"LANGSMITH_API_KEY",
"LANGSMITH_PROJECT",
"LANGSMITH_DEFAULT_RUN_NAME",
]
else:
env_vars = []
env_vars_dict = {}
for _var in env_vars:
env_variable = environment_variables.get(_var, None)
if env_variable is None:
env_vars_dict[_var] = None
else:
# decode + decrypt the value
decrypted_value = decrypt_value_helper(
value=env_variable, key=_var
)
env_vars_dict[_var] = decrypted_value
return {
"name": _callback,
"variables": env_vars_dict,
"type": callback_type
}
_success_callbacks = _litellm_settings.get("success_callback", [])
_failure_callbacks = _litellm_settings.get("failure_callback", [])
_generic_callbacks = _litellm_settings.get("callbacks", [])
_data_to_return = []
"""
[
@ -9584,70 +9637,20 @@ async def get_config(): # noqa: PLR0915
"LANGFUSE_SECRET_KEY": "value",
"LANGFUSE_HOST": "value"
},
"type": "success"
}
]
"""
for _callback in _success_callbacks:
if _callback != "langfuse":
if _callback == "openmeter":
env_vars = [
"OPENMETER_API_KEY",
]
elif _callback == "braintrust":
env_vars = [
"BRAINTRUST_API_KEY",
"BRAINTRUST_API_BASE",
]
elif _callback == "traceloop":
env_vars = ["TRACELOOP_API_KEY"]
elif _callback == "custom_callback_api":
env_vars = ["GENERIC_LOGGER_ENDPOINT"]
elif _callback == "otel":
env_vars = ["OTEL_EXPORTER", "OTEL_ENDPOINT", "OTEL_HEADERS"]
elif _callback == "langsmith":
env_vars = [
"LANGSMITH_API_KEY",
"LANGSMITH_PROJECT",
"LANGSMITH_DEFAULT_RUN_NAME",
]
else:
env_vars = []
env_vars_dict = {}
for _var in env_vars:
env_variable = environment_variables.get(_var, None)
if env_variable is None:
env_vars_dict[_var] = None
else:
# decode + decrypt the value
decrypted_value = decrypt_value_helper(
value=env_variable, key=_var
)
env_vars_dict[_var] = decrypted_value
_data_to_return.append({"name": _callback, "variables": env_vars_dict})
elif _callback == "langfuse":
_langfuse_vars = [
"LANGFUSE_PUBLIC_KEY",
"LANGFUSE_SECRET_KEY",
"LANGFUSE_HOST",
]
_langfuse_env_vars = {}
for _var in _langfuse_vars:
env_variable = environment_variables.get(_var, None)
if env_variable is None:
_langfuse_env_vars[_var] = None
else:
# decode + decrypt the value
decrypted_value = decrypt_value_helper(
value=env_variable, key=_var
)
_langfuse_env_vars[_var] = decrypted_value
_data_to_return.append(
{"name": _callback, "variables": _langfuse_env_vars}
)
_data_to_return.append(process_callback(_callback, "success"))
for _callback in _failure_callbacks:
_data_to_return.append(process_callback(_callback, "failure"))
for _callback in _generic_callbacks:
_data_to_return.append(process_callback(_callback, "generic"))
# Check if slack alerting is on
_alerting = _general_settings.get("alerting", [])

View File

@ -2371,3 +2371,166 @@ def test_non_root_ui_path_logic(monkeypatch, tmp_path, ui_exists, ui_has_content
error_calls = [call[0][0] for call in mock_logger.error.call_args_list]
assert any("Path exists:" in call for call in error_calls)
assert mock_logger.info.call_count == 0
@pytest.mark.asyncio
async def test_get_config_callbacks_with_all_types(client_no_auth):
"""
Test that /get/config/callbacks returns all three callback types:
- success_callback with type="success"
- failure_callback with type="failure"
- callbacks (generic) with type="generic"
"""
from litellm.proxy.proxy_server import ProxyConfig
# Create a mock config with all three callback types
mock_config_data = {
"litellm_settings": {
"success_callback": ["langfuse", "braintrust"],
"failure_callback": ["sentry"],
"callbacks": ["otel", "langsmith"]
},
"environment_variables": {
"LANGFUSE_PUBLIC_KEY": "test-public-key",
"LANGFUSE_SECRET_KEY": "test-secret-key",
"LANGFUSE_HOST": "https://test.langfuse.com",
"BRAINTRUST_API_KEY": "test-braintrust-key",
"OTEL_EXPORTER": "otlp",
"OTEL_ENDPOINT": "http://localhost:4317",
"LANGSMITH_API_KEY": "test-langsmith-key",
},
"general_settings": {}
}
proxy_config = getattr(litellm.proxy.proxy_server, "proxy_config")
with patch.object(
proxy_config, "get_config", new=AsyncMock(return_value=mock_config_data)
), patch(
"litellm.proxy.proxy_server.decrypt_value_helper",
side_effect=lambda value, key=None: value
):
response = client_no_auth.get("/get/config/callbacks")
assert response.status_code == 200
result = response.json()
# Verify response structure
assert "status" in result
assert result["status"] == "success"
assert "callbacks" in result
callbacks = result["callbacks"]
# Verify we have all 5 callbacks (2 success + 1 failure + 2 generic)
assert len(callbacks) == 5
# Group callbacks by type
success_callbacks = [cb for cb in callbacks if cb.get("type") == "success"]
failure_callbacks = [cb for cb in callbacks if cb.get("type") == "failure"]
generic_callbacks = [cb for cb in callbacks if cb.get("type") == "generic"]
# Verify all callbacks have required fields
for callback in callbacks:
assert "name" in callback
assert "variables" in callback
assert "type" in callback
assert callback["type"] in ["success", "failure", "generic"]
# Verify success callbacks
assert len(success_callbacks) == 2
success_names = [cb["name"] for cb in success_callbacks]
assert "langfuse" in success_names
assert "braintrust" in success_names
# Verify failure callbacks
assert len(failure_callbacks) == 1
assert failure_callbacks[0]["name"] == "sentry"
# Verify generic callbacks
assert len(generic_callbacks) == 2
generic_names = [cb["name"] for cb in generic_callbacks]
assert "otel" in generic_names
assert "langsmith" in generic_names
@pytest.mark.asyncio
async def test_get_config_callbacks_environment_variables(client_no_auth):
"""
Test that /get/config/callbacks correctly includes environment variables
for each callback type with proper decryption.
"""
from litellm.proxy.proxy_server import ProxyConfig
# Create a mock config with callbacks and their env vars
mock_config_data = {
"litellm_settings": {
"success_callback": ["langfuse"],
"failure_callback": [],
"callbacks": ["otel"]
},
"environment_variables": {
"LANGFUSE_PUBLIC_KEY": "encrypted-public-key",
"LANGFUSE_SECRET_KEY": "encrypted-secret-key",
"LANGFUSE_HOST": "https://cloud.langfuse.com",
"OTEL_EXPORTER": "otlp",
"OTEL_ENDPOINT": "http://localhost:4317",
"OTEL_HEADERS": "key=value",
},
"general_settings": {}
}
# Mock decrypt to prepend "decrypted-" to values
def mock_decrypt(value, key=None):
if value and isinstance(value, str) and "encrypted" in value:
return f"decrypted-{value}"
return value
proxy_config = getattr(litellm.proxy.proxy_server, "proxy_config")
with patch.object(
proxy_config, "get_config", new=AsyncMock(return_value=mock_config_data)
), patch(
"litellm.proxy.proxy_server.decrypt_value_helper",
side_effect=mock_decrypt
):
response = client_no_auth.get("/get/config/callbacks")
assert response.status_code == 200
result = response.json()
callbacks = result["callbacks"]
# Find langfuse callback (success type)
langfuse_callback = next(
(cb for cb in callbacks if cb["name"] == "langfuse"), None
)
assert langfuse_callback is not None
assert langfuse_callback["type"] == "success"
assert "variables" in langfuse_callback
# Verify langfuse env vars are present and decrypted
langfuse_vars = langfuse_callback["variables"]
assert "LANGFUSE_PUBLIC_KEY" in langfuse_vars
assert langfuse_vars["LANGFUSE_PUBLIC_KEY"] == "decrypted-encrypted-public-key"
assert "LANGFUSE_SECRET_KEY" in langfuse_vars
assert langfuse_vars["LANGFUSE_SECRET_KEY"] == "decrypted-encrypted-secret-key"
assert "LANGFUSE_HOST" in langfuse_vars
assert langfuse_vars["LANGFUSE_HOST"] == "https://cloud.langfuse.com"
# Find otel callback (generic type)
otel_callback = next(
(cb for cb in callbacks if cb["name"] == "otel"), None
)
assert otel_callback is not None
assert otel_callback["type"] == "generic"
assert "variables" in otel_callback
# Verify otel env vars are present
otel_vars = otel_callback["variables"]
assert "OTEL_EXPORTER" in otel_vars
assert otel_vars["OTEL_EXPORTER"] == "otlp"
assert "OTEL_ENDPOINT" in otel_vars
assert otel_vars["OTEL_ENDPOINT"] == "http://localhost:4317"
assert "OTEL_HEADERS" in otel_vars
assert otel_vars["OTEL_HEADERS"] == "key=value"

View File

@ -0,0 +1,136 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { describe, expect, it, beforeAll, beforeEach, vi } from "vitest";
import Settings from "./settings";
import * as networking from "./networking";
beforeAll(() => {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: (query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => true,
}),
});
});
const mockCallbacksData = {
callbacks: [
{
name: "langfuse",
type: "success",
variables: {
LANGFUSE_PUBLIC_KEY: "test_key",
LANGFUSE_SECRET_KEY: "test_secret",
},
},
{
name: "datadog",
type: "success",
variables: {
DD_API_KEY: "test_dd_key",
},
},
],
available_callbacks: [
{
litellm_callback_name: "langfuse",
ui_callback_name: "Langfuse",
litellm_callback_params: ["LANGFUSE_PUBLIC_KEY", "LANGFUSE_SECRET_KEY"],
},
{
litellm_callback_name: "datadog",
ui_callback_name: "Datadog",
litellm_callback_params: ["DD_API_KEY"],
},
],
alerts: [],
};
describe("Settings", () => {
it("should render the settings page", () => {
render(<Settings accessToken="test-token" userRole="admin" userID="test-user" premiumUser={false} />);
});
});
describe("Logging Callbacks Section", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should display the list of active callbacks", async () => {
vi.spyOn(networking, "getCallbacksCall").mockResolvedValue(mockCallbacksData);
render(<Settings accessToken="test-token" userRole="admin" userID="test-user" premiumUser={false} />);
await waitFor(
() => {
expect(screen.getByText("langfuse")).toBeInTheDocument();
expect(screen.getByText("datadog")).toBeInTheDocument();
},
{ timeout: 3000 },
);
});
it("should open add callback modal and display form", async () => {
vi.spyOn(networking, "getCallbacksCall").mockResolvedValue(mockCallbacksData);
render(<Settings accessToken="test-token" userRole="admin" userID="test-user" premiumUser={false} />);
await waitFor(() => {
expect(screen.getByText("langfuse")).toBeInTheDocument();
});
const addButton = screen.getByText("Add Callback");
fireEvent.click(addButton);
await waitFor(() => {
expect(screen.getByText("Add Logging Callback")).toBeInTheDocument();
expect(screen.getByText("LiteLLM Docs: Logging")).toBeInTheDocument();
});
});
it("should successfully delete a callback", async () => {
const getCallbacksSpy = vi.spyOn(networking, "getCallbacksCall").mockResolvedValue(mockCallbacksData);
const deleteCallbackSpy = vi.spyOn(networking, "deleteCallback").mockResolvedValue(undefined);
const { container } = render(
<Settings accessToken="test-token" userRole="admin" userID="test-user" premiumUser={false} />,
);
await waitFor(() => {
expect(screen.getByText("langfuse")).toBeInTheDocument();
});
const trashIcons = container.querySelectorAll("svg");
const trashIcon = Array.from(trashIcons).find((svg) => {
const parentElement = svg.parentElement;
return parentElement?.className.includes("text-red") || parentElement?.outerHTML.includes("red");
});
expect(trashIcon).toBeDefined();
if (trashIcon && trashIcon.parentElement) {
fireEvent.click(trashIcon.parentElement);
}
await waitFor(() => {
const modalText = screen.getByText((content, element) => {
return element?.tagName.toLowerCase() === "p" && content.includes("Are you sure you want to delete");
});
expect(modalText).toBeInTheDocument();
});
const deleteButton = screen.getByRole("button", { name: "Delete" });
fireEvent.click(deleteButton);
await waitFor(() => {
expect(deleteCallbackSpy).toHaveBeenCalledWith("test-token", "langfuse");
expect(getCallbacksSpy).toHaveBeenCalledTimes(2);
});
});
});

View File

@ -19,11 +19,12 @@ import {
Tab,
SelectItem,
Icon,
Badge,
} from "@tremor/react";
import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline";
import { Modal, Typography, Form, Input, Select, Button as Button2 } from "antd";
import { Modal, Typography, Form, Input, Select, Button as Button2, Tooltip } from "antd";
import NotificationsManager from "./molecules/notifications_manager";
import EmailSettings from "./email_settings";
@ -32,10 +33,7 @@ const { Title, Paragraph } = Typography;
import { getCallbacksCall, setCallbacksCall, serviceHealthCheck, deleteCallback } from "./networking";
import AlertingSettings from "./alerting/alerting_settings";
import FormItem from "antd/es/form/FormItem";
import {
CALLBACK_CONFIGS,
getCallbackById,
} from "./callback_info_helpers";
import { CALLBACK_CONFIGS, getCallbackById } from "./callback_info_helpers";
import { parseErrorMessage } from "./shared/errorUtils";
interface SettingsPageProps {
accessToken: string | null;
@ -60,6 +58,7 @@ interface AlertingVariables {
interface AlertingObject {
name: string;
type?: "success" | "failure" | "generic";
variables: AlertingVariables;
}
@ -216,10 +215,10 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
const handleSelectedCallbackChange = (callbackName: string) => {
setSelectedCallback(callbackName);
// Get the callback configuration using the new clean structure
const callbackConfig = getCallbackById(callbackName);
// Get the parameters from the callback configuration
if (callbackConfig?.dynamic_params) {
const params = Object.keys(callbackConfig.dynamic_params);
@ -228,7 +227,7 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
setSelectedCallbackParams([]);
}
};
const handleSaveAlerts = async () => {
if (!accessToken) {
return;
@ -416,54 +415,94 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
<Title level={4}>Active Logging Callbacks</Title>
<Grid numItems={2}>
<Card className="max-h-[50vh]">
<Card className="h-[calc(100vh-300px)] overflow-auto">
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Callback Name</TableHeaderCell>
{/* <TableHeaderCell>Callback Env Vars</TableHeaderCell> */}
<TableHeaderCell>Callback Type</TableHeaderCell>
<TableHeaderCell>Actions</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{callbacks.map((callback, index) => (
<TableRow key={index} className="flex justify-between">
<TableCell>
<Text>{callback.name}</Text>
</TableCell>
<TableCell>
<Grid numItems={2} className="flex justify-between">
<Icon
icon={PencilAltIcon}
size="sm"
onClick={() => {
setSelectedEditCallback(callback);
setShowEditCallback(true);
}}
/>
<Icon
icon={TrashIcon}
size="sm"
onClick={() => handleDeleteCallback(callback.name)}
className="text-red-500 hover:text-red-700 cursor-pointer"
/>
<Button
onClick={async () => {
try {
await serviceHealthCheck(accessToken, callback.name);
NotificationsManager.success("Health check triggered");
} catch (error) {
NotificationsManager.fromBackend(parseErrorMessage(error));
}
}}
className="ml-2"
variant="secondary"
>
Test Callback
</Button>
</Grid>
</TableCell>
</TableRow>
))}
{callbacks.map((callback, index) => {
const canEdit = !callback.type || callback.type === "success";
const tooltipMessage =
callback.type === "failure"
? "Modifications and deletion of failure type callbacks are not yet supported in the UI"
: callback.type === "generic"
? "Modifications and deletion of generic type callbacks are not yet supported in the UI"
: "";
const getBadgeColor = (type?: string) => {
if (type === "success") return "green";
if (type === "failure") return "red";
if (type === "generic") return "blue";
return "gray";
};
return (
<TableRow key={index}>
<TableCell>
<Text>{callback.name}</Text>
</TableCell>
<TableCell>
{callback.type ? (
<Badge color={getBadgeColor(callback.type)}>{callback.type}</Badge>
) : (
<Badge color="gray">success</Badge>
)}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Tooltip title={tooltipMessage}>
<Icon
icon={PencilAltIcon}
size="sm"
onClick={() => {
if (canEdit) {
setSelectedEditCallback(callback);
setShowEditCallback(true);
}
}}
className={canEdit ? "cursor-pointer" : "opacity-40 cursor-not-allowed"}
/>
</Tooltip>
<Tooltip title={tooltipMessage}>
<Icon
icon={TrashIcon}
size="sm"
onClick={() => {
if (canEdit) {
handleDeleteCallback(callback.name);
}
}}
className={
canEdit
? "text-red-500 hover:text-red-700 cursor-pointer"
: "text-red-300 opacity-40 cursor-not-allowed"
}
/>
</Tooltip>
<Button
onClick={async () => {
try {
await serviceHealthCheck(accessToken, callback.name);
NotificationsManager.success("Health check triggered");
} catch (error) {
NotificationsManager.fromBackend(parseErrorMessage(error));
}
}}
variant="secondary"
size="xs"
>
Test Callback
</Button>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</Card>
@ -594,124 +633,109 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
wrapperCol={{ span: 16 }}
labelAlign="left"
>
<FormItem
label="Callback"
name="callback"
rules={[{ required: true, message: "Please select a callback" }]}
<FormItem label="Callback" name="callback" rules={[{ required: true, message: "Please select a callback" }]}>
<Select
placeholder="Choose a logging callback..."
size="large"
className="w-full"
showSearch
filterOption={(input, option) =>
(option?.children?.toString() ?? "").toLowerCase().includes(input.toLowerCase())
}
onChange={(value) => {
handleSelectedCallbackChange(value);
}}
>
<Select
placeholder="Choose a logging callback..."
size="large"
className="w-full"
showSearch
filterOption={(input, option) =>
(option?.children?.toString() ?? "")
.toLowerCase()
.includes(input.toLowerCase())
}
onChange={(value) => {
handleSelectedCallbackChange(value);
}}
>
{CALLBACK_CONFIGS.map((callbackConfig) => (
<SelectItem
key={callbackConfig.id}
value={callbackConfig.id}
>
<div className="flex items-center space-x-3 py-1">
<div className="w-6 h-6 flex items-center justify-center">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={callbackConfig.logo}
alt={`${callbackConfig.displayName} logo`}
className="w-6 h-6 rounded object-contain"
onError={(e) => {
e.currentTarget.style.display = 'none';
}}
/>
</div>
<span className="font-medium text-gray-900">
{callbackConfig.displayName}
</span>
{CALLBACK_CONFIGS.map((callbackConfig) => (
<SelectItem key={callbackConfig.id} value={callbackConfig.id}>
<div className="flex items-center space-x-3 py-1">
<div className="w-6 h-6 flex items-center justify-center">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={callbackConfig.logo}
alt={`${callbackConfig.displayName} logo`}
className="w-6 h-6 rounded object-contain"
onError={(e) => {
e.currentTarget.style.display = "none";
}}
/>
</div>
</SelectItem>
))}
</Select>
</FormItem>
<span className="font-medium text-gray-900">{callbackConfig.displayName}</span>
</div>
</SelectItem>
))}
</Select>
</FormItem>
{selectedCallbackParams && selectedCallbackParams.length > 0 && (
<div className="space-y-4 mt-6 p-4 bg-gray-50 rounded-lg border">
{selectedCallbackParams.map((param) => {
// Get the callback configuration to look up parameter types
const callbackConfig = getCallbackById(selectedCallback || '');
const paramType = callbackConfig?.dynamic_params[param] || "text";
const fieldLabel = param.replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase());
return (
<FormItem
label={
<span className="text-sm font-medium text-gray-700">
{fieldLabel}
<span className="text-red-500 ml-1">*</span>
</span>
}
name={param}
key={param}
className="mb-4"
rules={[
{
required: true,
message: `Please enter the ${fieldLabel.toLowerCase()}`,
},
]}
>
{paramType === "password" ? (
<Input.Password
size="large"
placeholder={`Enter your ${fieldLabel.toLowerCase()}`}
className="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
/>
) : paramType === "number" ? (
<Input
type="number"
size="large"
placeholder={`Enter ${fieldLabel.toLowerCase()}`}
className="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
min={0}
max={1}
step={0.1}
/>
) : (
<Input
size="large"
placeholder={`Enter your ${fieldLabel.toLowerCase()}`}
className="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
/>
)}
</FormItem>
);
})}
</div>
)}
{selectedCallbackParams && selectedCallbackParams.length > 0 && (
<div className="space-y-4 mt-6 p-4 bg-gray-50 rounded-lg border">
{selectedCallbackParams.map((param) => {
// Get the callback configuration to look up parameter types
const callbackConfig = getCallbackById(selectedCallback || "");
const paramType = callbackConfig?.dynamic_params[param] || "text";
<div className="flex justify-end space-x-3 pt-6 mt-6 border-t border-gray-200">
<Button
onClick={() => {
setShowAddCallbacksModal(false);
setSelectedCallback(null);
setSelectedCallbackParams([]);
addForm.resetFields();
}}
>
Cancel
</Button>
<Button2
htmlType="submit"
>
Add Callback
</Button2>
const fieldLabel = param.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
return (
<FormItem
label={
<span className="text-sm font-medium text-gray-700">
{fieldLabel}
<span className="text-red-500 ml-1">*</span>
</span>
}
name={param}
key={param}
className="mb-4"
rules={[
{
required: true,
message: `Please enter the ${fieldLabel.toLowerCase()}`,
},
]}
>
{paramType === "password" ? (
<Input.Password
size="large"
placeholder={`Enter your ${fieldLabel.toLowerCase()}`}
className="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
/>
) : paramType === "number" ? (
<Input
type="number"
size="large"
placeholder={`Enter ${fieldLabel.toLowerCase()}`}
className="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
min={0}
max={1}
step={0.1}
/>
) : (
<Input
size="large"
placeholder={`Enter your ${fieldLabel.toLowerCase()}`}
className="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
/>
)}
</FormItem>
);
})}
</div>
)}
<div className="flex justify-end space-x-3 pt-6 mt-6 border-t border-gray-200">
<Button
onClick={() => {
setShowAddCallbacksModal(false);
setSelectedCallback(null);
setSelectedCallbackParams([]);
addForm.resetFields();
}}
>
Cancel
</Button>
<Button2 htmlType="submit">Add Callback</Button2>
</div>
</Form>
</Modal>