mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
208 lines
7.5 KiB
TypeScript
208 lines
7.5 KiB
TypeScript
import { Button } from "@tremor/react";
|
|
import type { TableProps } from "antd";
|
|
import { Table, Tag } from "antd";
|
|
import Title from "antd/es/typography/Title";
|
|
import React from "react";
|
|
import TableIconActionButton from "../../../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
|
|
import { AlertingObject } from "./types";
|
|
|
|
type LoggingCallbacksProps = {
|
|
callbacks: AlertingObject[];
|
|
availableCallbacks?: Record<
|
|
string,
|
|
{
|
|
litellm_callback_name: string;
|
|
litellm_callback_params: string[];
|
|
ui_callback_name: string;
|
|
}
|
|
>;
|
|
onTest?: (callback: AlertingObject) => void | Promise<void>;
|
|
onEdit?: (callback: AlertingObject) => void;
|
|
onDelete?: (callback: AlertingObject) => void;
|
|
onEditAccess?: (callback: AlertingObject) => void;
|
|
onAdd?: () => void;
|
|
};
|
|
|
|
const isDestination = (record: AlertingObject): boolean => record.credentialName != null;
|
|
|
|
const SCOPE_BADGES_LIMIT = 4;
|
|
|
|
// Renders the union of identities that route to this destination, with each team/org
|
|
// labeled by its alias. Global supersedes everything. Pulls from record.resolvedScope
|
|
// (computed at the page level from BOTH directions: destination-side access AND
|
|
// identity-side metadata.logging_exporters).
|
|
const ScopeCell: React.FC<{ record: AlertingObject }> = ({ record }) => {
|
|
const scope = record.resolvedScope;
|
|
if (!scope || (!scope.global && scope.teams.length === 0 && scope.orgs.length === 0)) {
|
|
return <span className="text-gray-400">—</span>;
|
|
}
|
|
if (scope.global) {
|
|
return <Tag color="blue">Global</Tag>;
|
|
}
|
|
const items = [
|
|
...scope.teams.map((label) => ({ kind: "team" as const, label })),
|
|
...scope.orgs.map((label) => ({ kind: "org" as const, label })),
|
|
];
|
|
const shown = items.slice(0, SCOPE_BADGES_LIMIT);
|
|
const remainder = items.length - shown.length;
|
|
return (
|
|
<div className="flex flex-wrap gap-1">
|
|
{shown.map((item, i) => (
|
|
<Tag key={`${item.kind}-${item.label}-${i}`} color={item.kind === "team" ? "blue" : "geekblue"}>
|
|
{item.kind}: {item.label}
|
|
</Tag>
|
|
))}
|
|
{remainder > 0 && <Tag>+{remainder} more</Tag>}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
type CallbackRow = AlertingObject & {
|
|
id?: string;
|
|
mode?: "success" | "failure" | "info" | string;
|
|
};
|
|
|
|
const CALLBACK_MODES: { value: string; label: string }[] = [
|
|
{ value: "success", label: "Success" },
|
|
{ value: "failure", label: "Failure" },
|
|
{ value: "success_and_failure", label: "Success & Failure" },
|
|
];
|
|
|
|
export const LoggingCallbacksTable: React.FC<LoggingCallbacksProps> = ({
|
|
callbacks,
|
|
availableCallbacks = {},
|
|
onTest = () => {},
|
|
onEdit = () => {},
|
|
onDelete = () => {},
|
|
onEditAccess = () => {},
|
|
onAdd = () => {},
|
|
}) => {
|
|
const columns: TableProps<CallbackRow>["columns"] = [
|
|
{
|
|
title: <span className="font-medium text-gray-700">Callback Name</span>,
|
|
dataIndex: "name",
|
|
key: "name",
|
|
render: (_: string, record: CallbackRow) => {
|
|
const id = record.name;
|
|
const displayName = availableCallbacks[id]?.ui_callback_name || id;
|
|
return (
|
|
<div>
|
|
<div className="font-medium text-gray-800">{displayName}</div>
|
|
{record.destinationLabel && <div className="text-xs text-gray-500">{record.destinationLabel}</div>}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
title: <span className="font-medium text-gray-700">Mode</span>,
|
|
key: "mode",
|
|
render: (_: unknown, record: CallbackRow) => {
|
|
// Destination rows fan out on every span, so the success/failure split
|
|
// does not apply -- only config callbacks carry a mode.
|
|
if (isDestination(record)) return <span className="text-gray-400">—</span>;
|
|
// Backend sends `type` (success | failure); legacy in-memory rows
|
|
// from add-callback flow set `mode`. Read both so newly-added rows
|
|
// and server-fetched rows both render correctly.
|
|
const mode = record.type || record.mode || "success";
|
|
const label = CALLBACK_MODES.find((m) => m.value === mode)?.label || mode;
|
|
const badgeClass =
|
|
mode === "success"
|
|
? "bg-green-100 text-green-800"
|
|
: mode === "failure"
|
|
? "bg-red-100 text-red-800"
|
|
: "bg-blue-100 text-blue-800";
|
|
return (
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${badgeClass}`}>
|
|
{label}
|
|
</span>
|
|
);
|
|
},
|
|
width: 200,
|
|
},
|
|
{
|
|
title: <span className="font-medium text-gray-700">Scope</span>,
|
|
key: "access",
|
|
render: (_: unknown, record: CallbackRow) =>
|
|
isDestination(record) ? <ScopeCell record={record} /> : <span className="text-gray-400">—</span>,
|
|
width: 280,
|
|
},
|
|
{
|
|
title: <span className="font-medium text-gray-700 text-right w-full block">Actions</span>,
|
|
key: "actions",
|
|
align: "right",
|
|
render: (_: unknown, record: CallbackRow) =>
|
|
isDestination(record) ? (
|
|
<div className="flex justify-end gap-2">
|
|
<TableIconActionButton
|
|
variant="Edit"
|
|
tooltipText="Edit scope"
|
|
dataTestId="edit-access"
|
|
onClick={() => onEditAccess(record)}
|
|
/>
|
|
<TableIconActionButton
|
|
variant="Delete"
|
|
tooltipText="Delete destination"
|
|
dataTestId="delete-destination"
|
|
onClick={() => onDelete(record)}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="flex justify-end gap-2">
|
|
<TableIconActionButton
|
|
variant="Test"
|
|
tooltipText="Test Callback"
|
|
dataTestId="test-callback"
|
|
onClick={() => onTest(record)}
|
|
/>
|
|
<TableIconActionButton
|
|
variant="Edit"
|
|
tooltipText="Edit Callback"
|
|
dataTestId="edit-callback"
|
|
onClick={() => onEdit(record)}
|
|
/>
|
|
<TableIconActionButton
|
|
variant="Delete"
|
|
tooltipText="Delete Callback"
|
|
dataTestId="delete-callback"
|
|
onClick={() => onDelete(record)}
|
|
/>
|
|
</div>
|
|
),
|
|
width: 200,
|
|
},
|
|
];
|
|
return (
|
|
<>
|
|
<div className="w-full mt-4">
|
|
<Button onClick={onAdd} className="mx-auto">
|
|
+ Add Callback
|
|
</Button>
|
|
<div className="flex justify-between items-center my-2">
|
|
<Title level={4}>Active Logging Callbacks</Title>
|
|
</div>
|
|
{/* Empty state */}
|
|
{callbacks.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center p-8 bg-gray-50 border border-gray-200 rounded-lg">
|
|
<div className="text-center">
|
|
<h3 className="text-lg font-medium text-gray-700 mb-2">No callbacks configured</h3>
|
|
<p className="text-gray-500">Add your first callback to start logging data to external services.</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
|
|
<Table
|
|
columns={columns}
|
|
dataSource={callbacks as CallbackRow[]}
|
|
// `generic_api` can appear as both a success and a failure
|
|
// callback simultaneously — keying by `name` alone produced
|
|
// duplicate React keys. Compose with type to keep keys unique.
|
|
rowKey={(record) => `${record.name}-${record.type || record.mode || "success"}`}
|
|
pagination={false}
|
|
rowClassName={() => "hover:bg-gray-50"}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
};
|