mirror of
https://github.com/tiennm99/litellm.git
synced 2026-06-29 13:12:53 +00:00
Display Error from Backend on the UI - Notification (#13427)
* fix sso logout - add a new login page with sso button * lint fix * lint fix * lint fix * fix tests * fix test * Revert "fix test" This reverts commit 74eb7345710892d5a9d02baec0ef389b98d0dde3. * Reapply "fix test" This reverts commit 72d0b2d4c62f6bb9351a7656ff88efc2ba91aef7. * add host to add modal * close modal after save is clicked. and auto-refresh * show old values in edit modal * send the whole payload on edit * Update settings.tsx * resolve conflict * fix conflict * merge main * first draft of notifications added to settings * add error compatibility by taking errors from the backend - db errors - auth errors * add support for different types of errors * minor * name change * email alerts page notifications modified * remove unused code
This commit is contained in:
@@ -2164,6 +2164,7 @@ class AllCallbacks(LiteLLMPydanticObjectBase):
|
||||
litellm_callback_params=[
|
||||
"LANGFUSE_PUBLIC_KEY",
|
||||
"LANGFUSE_SECRET_KEY",
|
||||
"LANGFUSE_HOST",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Card, Text, Grid, Button } from "@tremor/react";
|
||||
import { Typography, message, Divider, Spin, Checkbox } from "antd";
|
||||
import { Typography, Divider, Spin, Checkbox } from "antd";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import { getEmailEventSettings, updateEmailEventSettings, resetEmailEventSettings } from "../networking";
|
||||
import { EmailEvent } from "../../types";
|
||||
import { EmailEventSetting } from "./types";
|
||||
@@ -31,7 +32,7 @@ const EmailEventSettings: React.FC<EmailEventSettingsProps> = ({
|
||||
setEventSettings(response.settings);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch email event settings:", error);
|
||||
message.error("Failed to fetch email event settings");
|
||||
NotificationsManager.fromBackend(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -49,10 +50,10 @@ const EmailEventSettings: React.FC<EmailEventSettingsProps> = ({
|
||||
|
||||
try {
|
||||
await updateEmailEventSettings(accessToken, { settings: eventSettings });
|
||||
message.success("Email event settings updated successfully");
|
||||
NotificationsManager.success("Email event settings updated successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to update email event settings:", error);
|
||||
message.error("Failed to update email event settings");
|
||||
NotificationsManager.fromBackend(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -61,12 +62,12 @@ const EmailEventSettings: React.FC<EmailEventSettingsProps> = ({
|
||||
|
||||
try {
|
||||
await resetEmailEventSettings(accessToken);
|
||||
message.success("Email event settings reset to defaults");
|
||||
NotificationsManager.success("Email event settings reset to defaults");
|
||||
// Refresh settings after reset
|
||||
fetchEventSettings();
|
||||
} catch (error) {
|
||||
console.error("Failed to reset email event settings:", error);
|
||||
message.error("Failed to reset email event settings");
|
||||
NotificationsManager.fromBackend(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
TextInput,
|
||||
TableCell,
|
||||
} from "@tremor/react";
|
||||
import { Typography, message, Divider } from "antd";
|
||||
import { Typography } from "antd";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import { serviceHealthCheck, setCallbacksCall } from "./networking";
|
||||
import { EmailEventSettings } from "./email_events";
|
||||
|
||||
@@ -24,7 +25,7 @@ const EmailSettings: React.FC<EmailSettingsProps> = ({
|
||||
premiumUser,
|
||||
alerts,
|
||||
}) => {
|
||||
const handleSaveEmailSettings = () => {
|
||||
const handleSaveEmailSettings = async () => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
@@ -52,12 +53,11 @@ const EmailSettings: React.FC<EmailSettingsProps> = ({
|
||||
environment_variables: updatedVariables,
|
||||
};
|
||||
try {
|
||||
setCallbacksCall(accessToken, payload);
|
||||
await setCallbacksCall(accessToken, payload);
|
||||
NotificationsManager.success("Email settings updated successfully");
|
||||
} catch (error) {
|
||||
message.error("Failed to update alerts: " + error, 20);
|
||||
NotificationsManager.fromBackend(error);
|
||||
}
|
||||
|
||||
message.success("Email settings updated successfully");
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -191,9 +191,15 @@ const EmailSettings: React.FC<EmailSettingsProps> = ({
|
||||
Save Changes
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
accessToken && serviceHealthCheck(accessToken, "email")
|
||||
}
|
||||
onClick={async () => {
|
||||
if (!accessToken) return;
|
||||
try {
|
||||
await serviceHealthCheck(accessToken, "email");
|
||||
NotificationsManager.success("Email test triggered. Check your configured email inbox/logs.");
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend(error);
|
||||
}
|
||||
}}
|
||||
className="mx-2"
|
||||
>
|
||||
Test Email Alerts
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
import { notification } from "antd"
|
||||
import { parseErrorMessage } from "../shared/errorUtils"
|
||||
|
||||
type Placement = "top" | "topLeft" | "topRight" | "bottom" | "bottomLeft" | "bottomRight"
|
||||
|
||||
type NotificationConfig = {
|
||||
message?: string
|
||||
description?: string
|
||||
duration?: number
|
||||
placement?: Placement
|
||||
key?: string
|
||||
}
|
||||
|
||||
type NotificationConfigResolved = Omit<NotificationConfig, "message"> & { message: string }
|
||||
|
||||
function defaultPlacement(): Placement {
|
||||
return "topRight"
|
||||
}
|
||||
|
||||
function normalize(input: string | NotificationConfig, fallbackTitle: string): NotificationConfigResolved {
|
||||
if (typeof input === "string") return { message: fallbackTitle, description: input }
|
||||
return { message: input.message ?? fallbackTitle, ...input }
|
||||
}
|
||||
|
||||
function toIntMaybe(val: any): number | undefined {
|
||||
if (typeof val === "number") return val
|
||||
if (typeof val === "string" && /^\d+$/.test(val)) return parseInt(val, 10)
|
||||
return undefined
|
||||
}
|
||||
|
||||
const AUTH_MATCH = [
|
||||
"invalid api key",
|
||||
"invalid authorization header format",
|
||||
"authentication error",
|
||||
"invalid proxy server token",
|
||||
"invalid jwt token",
|
||||
"invalid jwt submitted",
|
||||
"unauthorized access to metrics endpoint",
|
||||
];
|
||||
|
||||
const FORBIDDEN_MATCH = [
|
||||
"admin-only endpoint",
|
||||
"not allowed to access model",
|
||||
"user does not have permission",
|
||||
"access forbidden",
|
||||
"invalid credentials used to access ui",
|
||||
"user not allowed to access proxy",
|
||||
];
|
||||
|
||||
const DB_MATCH = [
|
||||
"db not connected",
|
||||
"database not initialized",
|
||||
"no db connected",
|
||||
"prisma client not initialized",
|
||||
"service unhealthy",
|
||||
];
|
||||
|
||||
const ROUTER_MATCH = [
|
||||
"no models configured on proxy",
|
||||
"llm router not initialized",
|
||||
"no deployments available",
|
||||
"no healthy deployment available",
|
||||
"not allowed to access model due to tags configuration",
|
||||
"invalid model name passed in",
|
||||
];
|
||||
|
||||
const RATE_LIMIT_EXTRA = [
|
||||
"deployment over user-defined ratelimit",
|
||||
"crossed tpm / rpm / max parallel request limit",
|
||||
"max parallel request limit",
|
||||
];
|
||||
|
||||
const BUDGET_MATCH = [
|
||||
"budget exceeded",
|
||||
"crossed budget",
|
||||
"provider budget",
|
||||
];
|
||||
|
||||
const ENTERPRISE_MATCH = [
|
||||
"must be a litellm enterprise user",
|
||||
"only be available for liteLLM enterprise users",
|
||||
"missing litellm-enterprise package",
|
||||
"only available on the docker image",
|
||||
"enterprise feature",
|
||||
"premium user",
|
||||
];
|
||||
|
||||
const VALIDATION_MATCH = [
|
||||
"invalid json payload",
|
||||
"invalid request type",
|
||||
"invalid key format",
|
||||
"invalid hash key",
|
||||
"invalid sort column",
|
||||
"invalid sort order",
|
||||
"invalid limit",
|
||||
"invalid file type",
|
||||
"invalid field",
|
||||
"invalid date format",
|
||||
];
|
||||
|
||||
const NOT_FOUND_MATCH = [
|
||||
"model not found",
|
||||
"model with id",
|
||||
"credential not found",
|
||||
"user not found",
|
||||
"team not found",
|
||||
"organization not found",
|
||||
"mcp server with id",
|
||||
"tool '", // will combine with “not found” in message
|
||||
];
|
||||
|
||||
const EXISTS_MATCH = [
|
||||
"already exists",
|
||||
"team member is already in team",
|
||||
"user already exists",
|
||||
];
|
||||
|
||||
const GUARDRAIL_MATCH = [
|
||||
"violated openai moderation policy",
|
||||
"violated jailbreak threshold",
|
||||
"violated prompt_injection threshold",
|
||||
"violated content safety policy",
|
||||
"violated lasso guardrail policy",
|
||||
"blocked by pillar security guardrail",
|
||||
"violated azure prompt shield guardrail policy",
|
||||
"content blocked by model armor",
|
||||
"response blocked by model armor",
|
||||
"streaming response blocked by model armor",
|
||||
"guardrail",
|
||||
"moderation",
|
||||
];
|
||||
|
||||
const FILE_UPLOAD_MATCH = [
|
||||
"invalid purpose",
|
||||
"service must be specified",
|
||||
"invalid response - response.response is none",
|
||||
];
|
||||
|
||||
const CLOUDZERO_MATCH = [
|
||||
"cloudzero settings not configured",
|
||||
"failed to decrypt cloudzero api key",
|
||||
"cloudzero settings not found",
|
||||
];
|
||||
|
||||
function titleFor(status?: number, desc?: string): string {
|
||||
const d = (desc || "").toLowerCase();
|
||||
|
||||
if (AUTH_MATCH.some(s => d.includes(s))) return "Authentication Error";
|
||||
if (FORBIDDEN_MATCH.some(s => d.includes(s))) return "Access Denied";
|
||||
if (DB_MATCH?.some?.((s:string)=>d.includes(s)) || status === 503) return "Service Unavailable";
|
||||
if (BUDGET_MATCH?.some?.((s:string)=>d.includes(s))) return "Budget Exceeded";
|
||||
if (ENTERPRISE_MATCH?.some?.((s:string)=>d.includes(s))) return "Feature Unavailable";
|
||||
if (ROUTER_MATCH?.some?.((s:string)=>d.includes(s))) return "Routing Error";
|
||||
|
||||
if (EXISTS_MATCH.some(s => d.includes(s))) return "Already Exists";
|
||||
if (GUARDRAIL_MATCH.some(s => d.includes(s))) return "Content Blocked";
|
||||
|
||||
if (FILE_UPLOAD_MATCH.some(s => d.includes(s))) return "Validation Error";
|
||||
if (CLOUDZERO_MATCH.some(s => d.includes(s))) return "Integration Error";
|
||||
|
||||
if (VALIDATION_MATCH.some(s => d.includes(s))) return "Validation Error";
|
||||
if (status === 404 || d.includes("not found") || NOT_FOUND_MATCH.some(s => d.includes(s))) return "Not Found";
|
||||
if (status === 429 || d.includes("rate limit") || d.includes("tpm") || d.includes("rpm") || RATE_LIMIT_EXTRA?.some?.((s:string)=>d.includes(s))) return "Rate Limit Exceeded";
|
||||
if (status && status >= 500) return "Server Error";
|
||||
if (status === 401) return "Authentication Error";
|
||||
if (status === 403) return "Access Denied";
|
||||
if (d.includes("enterprise") || d.includes("premium")) return "Info";
|
||||
if (status && status >= 400) return "Request Error";
|
||||
return "Error";
|
||||
}
|
||||
|
||||
const SUCCESS_MATCH = [
|
||||
"created successfully",
|
||||
"updated successfully",
|
||||
"deleted successfully",
|
||||
"credential created successfully",
|
||||
"model added successfully",
|
||||
"team created successfully",
|
||||
"user created successfully",
|
||||
"organization created successfully",
|
||||
"cloudzero settings initialized successfully",
|
||||
"cloudzero settings updated successfully",
|
||||
"cloudzero export completed successfully",
|
||||
"mock llm request made",
|
||||
"mock slack alert sent",
|
||||
"mock email alert sent",
|
||||
"spend for all api keys and teams reset successfully",
|
||||
"monthlyglobalspend view refreshed",
|
||||
"cache cleared successfully",
|
||||
"cache set successfully",
|
||||
"ip ",
|
||||
"deleted successfully"
|
||||
];
|
||||
|
||||
const INFO_MATCH = [
|
||||
"rate limit reached for deployment",
|
||||
"deployment cooldown period active",
|
||||
];
|
||||
|
||||
const DEPRECATION_FEATURE_WARN_MATCH = [
|
||||
"this feature is only available for litellm enterprise users",
|
||||
"enterprise features are not available",
|
||||
"regenerating virtual keys is an enterprise feature",
|
||||
"trying to set allowed_routes. this is an enterprise feature",
|
||||
];
|
||||
|
||||
const CONFIG_WARN_MATCH = [
|
||||
"invalid maximum_spend_logs_retention_interval value",
|
||||
"error has invalid or non-convertible code",
|
||||
"failed to save health check to database",
|
||||
];
|
||||
|
||||
function classifyGeneralMessage(desc?: string): { kind: "success" | "info" | "warning"; title: string } | null {
|
||||
const d = (desc || "").toLowerCase();
|
||||
|
||||
if (SUCCESS_MATCH.some(s => d.includes(s))) return { kind: "success", title: "Success" };
|
||||
if (DEPRECATION_FEATURE_WARN_MATCH.some(s => d.includes(s))) return { kind: "warning", title: "Feature Notice" };
|
||||
if (CONFIG_WARN_MATCH.some(s => d.includes(s))) return { kind: "warning", title: "Configuration Warning" };
|
||||
if (INFO_MATCH.some(s => d.includes(s))) return { kind: "warning", title: "Rate Limit" }; // show as warning for visibility
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractStatus(input: any): number | undefined {
|
||||
return toIntMaybe(input?.response?.status) ?? toIntMaybe(input?.status_code) ?? toIntMaybe(input?.code);
|
||||
}
|
||||
|
||||
function extractDescription(input: any): string {
|
||||
if (typeof input === "string") return input; // raw error string
|
||||
const backendMsg =
|
||||
input?.response?.data?.error?.message ??
|
||||
input?.response?.data?.message ??
|
||||
input?.response?.data?.error ??
|
||||
input?.detail ??
|
||||
input?.message ??
|
||||
input;
|
||||
return parseErrorMessage(backendMsg);
|
||||
}
|
||||
|
||||
function looksErrorPayload(input: any, status?: number): boolean {
|
||||
if (status !== undefined) return true;
|
||||
if (input instanceof Error) return true;
|
||||
if (typeof input === "string") return true; // treat raw strings passed to fromBackend as errors
|
||||
if (input && typeof input === "object" && ("error" in input || "detail" in input)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const NotificationManager = {
|
||||
error(input: string | NotificationConfig) {
|
||||
const cfg = normalize(input, "Error")
|
||||
notification.error({
|
||||
...cfg,
|
||||
placement: cfg.placement ?? defaultPlacement(),
|
||||
duration: cfg.duration ?? 6,
|
||||
})
|
||||
},
|
||||
|
||||
warning(input: string | NotificationConfig) {
|
||||
const cfg = normalize(input, "Warning")
|
||||
notification.warning({
|
||||
...cfg,
|
||||
placement: cfg.placement ?? defaultPlacement(),
|
||||
duration: cfg.duration ?? 5,
|
||||
})
|
||||
},
|
||||
|
||||
info(input: string | NotificationConfig) {
|
||||
const cfg = normalize(input, "Info")
|
||||
notification.info({
|
||||
...cfg,
|
||||
placement: cfg.placement ?? defaultPlacement(),
|
||||
duration: cfg.duration ?? 4,
|
||||
})
|
||||
},
|
||||
|
||||
success(input: string | NotificationConfig) {
|
||||
const cfg = normalize(input, "Success")
|
||||
notification.success({
|
||||
...cfg,
|
||||
placement: cfg.placement ?? defaultPlacement(),
|
||||
duration: cfg.duration ?? 3.5,
|
||||
})
|
||||
},
|
||||
|
||||
fromBackend(input: any, extra?: Omit<NotificationConfig, "message" | "description">) {
|
||||
const status = extractStatus(input);
|
||||
const description = extractDescription(input);
|
||||
const base = { ...(extra ?? {}), description, placement: extra?.placement ?? defaultPlacement() };
|
||||
|
||||
if (looksErrorPayload(input, status)) {
|
||||
const title = titleFor(status, description);
|
||||
const payload = { ...base, message: title };
|
||||
|
||||
if (title === "Rate Limit Exceeded" || title === "Info" || title === "Budget Exceeded" || title === "Feature Unavailable" || title === "Content Blocked" || title === "Integration Error") {
|
||||
notification.warning({ ...payload, duration: extra?.duration ?? 7 }); return;
|
||||
}
|
||||
if (title === "Server Error") { notification.error({ ...payload, duration: extra?.duration ?? 8 }); return; }
|
||||
if (title === "Request Error" || title === "Authentication Error" || title === "Access Denied" || title === "Not Found" || title === "Error") {
|
||||
notification.error({ ...payload, duration: extra?.duration ?? 6 }); return;
|
||||
}
|
||||
notification.info({ ...payload, duration: extra?.duration ?? 4 }); return;
|
||||
}
|
||||
|
||||
// Non-error: success/info/warning classifier
|
||||
const cls = classifyGeneralMessage(description);
|
||||
const payload = { ...base, message: cls?.title ?? "Info" };
|
||||
|
||||
if (cls?.kind === "success") { notification.success({ ...payload, duration: extra?.duration ?? 3.5 }); return; }
|
||||
if (cls?.kind === "warning") { notification.warning({ ...payload, duration: extra?.duration ?? 6 }); return; }
|
||||
notification.info({ ...payload, duration: extra?.duration ?? 4 });
|
||||
},
|
||||
|
||||
clear() {
|
||||
notification.destroy()
|
||||
},
|
||||
}
|
||||
|
||||
export default NotificationManager
|
||||
@@ -4264,9 +4264,6 @@ export const serviceHealthCheck = async (
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
message.success(
|
||||
`Test request to ${service} made - check logs/alerts on ${service} to verify`
|
||||
);
|
||||
// You can add additional logic here based on the response if needed
|
||||
return data;
|
||||
} catch (error) {
|
||||
|
||||
@@ -30,8 +30,8 @@ import {
|
||||
Input,
|
||||
Select,
|
||||
Button as Button2,
|
||||
message,
|
||||
} from "antd";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import EmailSettings from "./email_settings";
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
callbackInfo,
|
||||
Callbacks,
|
||||
} from "./callback_info_helpers";
|
||||
import { parseErrorMessage } from "./shared/errorUtils";
|
||||
interface SettingsPageProps {
|
||||
accessToken: string | null;
|
||||
userRole: string | null;
|
||||
@@ -84,7 +85,8 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
const [callbacks, setCallbacks] = useState<AlertingObject[]>([]);
|
||||
const [alerts, setAlerts] = useState<any[]>([]);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [addForm] = Form.useForm();
|
||||
const [editForm] = Form.useForm();
|
||||
const [selectedCallback, setSelectedCallback] = useState<string | null>(null);
|
||||
const [catchAllWebhookURL, setCatchAllWebhookURL] = useState<string>("");
|
||||
const [alertToWebhooks, setAlertToWebhooks] = useState<
|
||||
@@ -106,6 +108,15 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false);
|
||||
const [callbackToDelete, setCallbackToDelete] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (showEditCallback && selectedEditCallback) {
|
||||
const normalized = Object.fromEntries(
|
||||
Object.entries(selectedEditCallback.variables || {}).map(([k, v]) => [k, v ?? ""])
|
||||
);
|
||||
editForm.setFieldsValue(normalized)
|
||||
}
|
||||
}, [showEditCallback, selectedEditCallback, editForm]);
|
||||
|
||||
const handleSwitchChange = (alertName: string) => {
|
||||
if (activeAlerts.includes(alertName)) {
|
||||
setActiveAlerts(activeAlerts.filter((alert) => alert !== alertName));
|
||||
@@ -155,7 +166,7 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
};
|
||||
|
||||
const updateCallbackCall = async (formValues: Record<string, any>) => {
|
||||
if (!accessToken) {
|
||||
if (!accessToken || !selectedEditCallback) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -167,17 +178,27 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
}
|
||||
});
|
||||
let payload = {
|
||||
environment_variables: env_vars,
|
||||
};
|
||||
environment_variables: formValues,
|
||||
litellm_settings: {
|
||||
"success_callback": [selectedEditCallback.name]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
await setCallbacksCall(accessToken, payload);
|
||||
message.success(`Callback added successfully`);
|
||||
setIsModalVisible(false);
|
||||
form.resetFields();
|
||||
setSelectedCallback(null);
|
||||
NotificationsManager.success("Callback updated successfully");
|
||||
setShowEditCallback(false);
|
||||
editForm.resetFields();
|
||||
setSelectedEditCallback(null);
|
||||
|
||||
// Refresh the callbacks list
|
||||
if (userID && userRole) {
|
||||
const updatedData = await getCallbacksCall(accessToken, userID, userRole);
|
||||
setCallbacks(updatedData.callbacks);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error("Failed to add callback: " + error, 20);
|
||||
NotificationsManager.fromBackend(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -196,7 +217,7 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
});
|
||||
|
||||
let payload = {
|
||||
environment_variables: env_vars,
|
||||
environment_variables: formValues,
|
||||
litellm_settings: {
|
||||
success_callback: [new_callback],
|
||||
},
|
||||
@@ -204,12 +225,17 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
|
||||
try {
|
||||
await setCallbacksCall(accessToken, payload);
|
||||
message.success(`Callback ${new_callback} added successfully`);
|
||||
setIsModalVisible(false);
|
||||
form.resetFields();
|
||||
NotificationsManager.success(`Callback ${new_callback} added successfully`);
|
||||
setShowAddCallbacksModal(false);
|
||||
addForm.resetFields();
|
||||
setSelectedCallback(null);
|
||||
setSelectedCallbackParams([]);
|
||||
|
||||
// Refresh the callbacks list
|
||||
const updatedData = await getCallbacksCall(accessToken, userID || "", userRole || "");
|
||||
setCallbacks(updatedData.callbacks);
|
||||
} catch (error) {
|
||||
message.error("Failed to add callback: " + error, 20);
|
||||
NotificationsManager.fromBackend(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -225,7 +251,7 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveAlerts = () => {
|
||||
const handleSaveAlerts = async () => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
@@ -247,12 +273,11 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
};
|
||||
|
||||
try {
|
||||
setCallbacksCall(accessToken, payload);
|
||||
await setCallbacksCall(accessToken, payload);
|
||||
} catch (error) {
|
||||
message.error("Failed to update alerts: " + error, 20);
|
||||
NotificationsManager.fromBackend(error);
|
||||
}
|
||||
|
||||
message.success("Alerts updated successfully");
|
||||
NotificationsManager.success("Alerts updated successfully");
|
||||
};
|
||||
const handleSaveChanges = (callback: any) => {
|
||||
if (!accessToken) {
|
||||
@@ -277,10 +302,9 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
try {
|
||||
setCallbacksCall(accessToken, payload);
|
||||
} catch (error) {
|
||||
message.error("Failed to update callback: " + error, 20);
|
||||
NotificationsManager.fromBackend(error);
|
||||
}
|
||||
|
||||
message.success("Callback updated successfully");
|
||||
NotificationsManager.success("Callback updated successfully");
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
@@ -288,7 +312,7 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
return;
|
||||
}
|
||||
// Handle form submission
|
||||
form.validateFields().then((values) => {
|
||||
addForm.validateFields().then((values) => {
|
||||
// Call API to add the callback
|
||||
let payload;
|
||||
if (values.callback === "langfuse") {
|
||||
@@ -365,7 +389,7 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
};
|
||||
}
|
||||
setIsModalVisible(false);
|
||||
form.resetFields();
|
||||
addForm.resetFields();
|
||||
setSelectedCallback(null);
|
||||
});
|
||||
};
|
||||
@@ -382,7 +406,7 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
|
||||
try {
|
||||
await deleteCallback(accessToken, callbackToDelete);
|
||||
message.success(`Callback ${callbackToDelete} deleted successfully`);
|
||||
NotificationsManager.success(`Callback ${callbackToDelete} deleted successfully`);
|
||||
|
||||
// Refresh the callbacks list
|
||||
if (userID && userRole) {
|
||||
@@ -394,7 +418,7 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
setCallbackToDelete(null);
|
||||
} catch (error) {
|
||||
console.error("Failed to delete callback:", error);
|
||||
message.error(`Failed to delete callback: ${error}`);
|
||||
NotificationsManager.fromBackend(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -450,9 +474,14 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
className="text-red-500 hover:text-red-700 cursor-pointer"
|
||||
/>
|
||||
<Button
|
||||
onClick={() =>
|
||||
serviceHealthCheck(accessToken, callback.name)
|
||||
}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await serviceHealthCheck(accessToken, callback.name);
|
||||
NotificationsManager.success("Health check triggered");
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend(parseErrorMessage(error));
|
||||
}
|
||||
}}
|
||||
className="ml-2"
|
||||
variant="secondary"
|
||||
>
|
||||
@@ -551,7 +580,14 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => serviceHealthCheck(accessToken, "slack")}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await serviceHealthCheck(accessToken, "slack");
|
||||
NotificationsManager.success("Alert test triggered. Test request to slack made - check logs/alerts on slack to verify");
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend(parseErrorMessage(error));
|
||||
}
|
||||
}}
|
||||
className="mx-2"
|
||||
>
|
||||
Test Alerts
|
||||
@@ -579,7 +615,11 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
title="Add Logging Callback"
|
||||
visible={showAddCallbacksModal}
|
||||
width={800}
|
||||
onCancel={() => setShowAddCallbacksModal(false)}
|
||||
onCancel= {() => {
|
||||
setShowAddCallbacksModal(false)
|
||||
setSelectedCallback(null);
|
||||
setSelectedCallbackParams([]);
|
||||
}}
|
||||
footer={null}
|
||||
>
|
||||
<a
|
||||
@@ -593,7 +633,7 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
</a>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
form={addForm}
|
||||
onFinish={addNewCallbackCall}
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
@@ -659,7 +699,7 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TextInput type="password" />
|
||||
<Input.Password />
|
||||
</FormItem>
|
||||
))}
|
||||
|
||||
@@ -674,11 +714,14 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
visible={showEditCallback}
|
||||
width={800}
|
||||
title={`Edit ${selectedEditCallback?.name} Settings`}
|
||||
onCancel={() => setShowEditCallback(false)}
|
||||
onCancel={() => {
|
||||
setShowEditCallback(false)
|
||||
setSelectedEditCallback(null);
|
||||
}}
|
||||
footer={null}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
form={editForm}
|
||||
onFinish={updateCallbackCall}
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
@@ -687,13 +730,21 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||
<>
|
||||
{selectedEditCallback &&
|
||||
selectedEditCallback.variables &&
|
||||
Object.entries(selectedEditCallback.variables).map(
|
||||
([param, value]) => (
|
||||
<FormItem label={param} name={param} key={param}>
|
||||
<TextInput type="password" defaultValue={value as string} />
|
||||
</FormItem>
|
||||
)
|
||||
)}
|
||||
Object.entries(selectedEditCallback.variables).map(([param]) => (
|
||||
<FormItem
|
||||
label={param}
|
||||
name={param}
|
||||
key={param}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `Please enter the value for ${param}`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.Password />
|
||||
</FormItem>
|
||||
))}
|
||||
</>
|
||||
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
|
||||
Reference in New Issue
Block a user