mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): SpendByProvider + EditProjectModal + FormattedToolView
- SpendByProvider: @tremor Card/Col/Grid/Switch/Table family/Title + antd Tooltip + @ant-design InfoCircleOutlined \u2192 shadcn Card/Switch/ Table/Tooltip + lucide Info. Tremor DonutChart preserved (chart whitelist). - EditProjectModal: antd Modal/Button/Typography + @ant-design SaveOutlined \u2192 shadcn Dialog/Button + lucide Save. ProjectBaseForm (antd Form) preserved inside. - FormattedToolView: antd Typography/Table \u2192 shadcn Table + plain spans/codes. Preserves type-badge blue palette and called-with emerald success tint (in overrides). Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
d5df40b9c4
commit
d0e2b93d51
4 changed files with 196 additions and 188 deletions
|
|
@ -63,6 +63,8 @@
|
|||
"src/components/prompts/prompt_editor_view/conversation_panel/MessageBubble.tsx",
|
||||
"src/components/playground/chat_ui/SessionManagement.tsx",
|
||||
"src/components/playground/chat_ui/SearchResultsDisplay.tsx",
|
||||
"src/components/UsagePage/components/EntityUsage/SpendByProvider.tsx",
|
||||
"src/components/view_logs/ToolsSection/FormattedToolView.tsx",
|
||||
"src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable.tsx",
|
||||
"src/components/policies/impact_preview_alert.tsx",
|
||||
"src/components/policies/impact_popover.tsx",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
import { useEffect } from "react";
|
||||
import { Modal, Form, Button, Typography } from "antd";
|
||||
import { SaveOutlined } from "@ant-design/icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Form } from "antd";
|
||||
import { Save } from "lucide-react";
|
||||
import MessageManager from "@/components/molecules/message_manager";
|
||||
import { ProjectResponse } from "@/app/(dashboard)/hooks/projects/useProjects";
|
||||
import {
|
||||
|
|
@ -26,16 +34,20 @@ export function EditProjectModal({
|
|||
const [form] = Form.useForm<ProjectFormValues>();
|
||||
const updateMutation = useUpdateProject();
|
||||
|
||||
// Populate form with existing project data when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen && project) {
|
||||
// Model limits are stored inside metadata by the backend
|
||||
const metadataObj = (project.metadata ?? {}) as Record<string, unknown>;
|
||||
const rpmLimits = (metadataObj.model_rpm_limit ?? {}) as Record<string, number>;
|
||||
const tpmLimits = (metadataObj.model_tpm_limit ?? {}) as Record<string, number>;
|
||||
const guardrails = (Array.isArray(metadataObj.guardrails)
|
||||
? metadataObj.guardrails
|
||||
: []) as string[];
|
||||
const rpmLimits = (metadataObj.model_rpm_limit ?? {}) as Record<
|
||||
string,
|
||||
number
|
||||
>;
|
||||
const tpmLimits = (metadataObj.model_tpm_limit ?? {}) as Record<
|
||||
string,
|
||||
number
|
||||
>;
|
||||
const guardrails = (
|
||||
Array.isArray(metadataObj.guardrails) ? metadataObj.guardrails : []
|
||||
) as string[];
|
||||
|
||||
const modelLimits: ProjectFormValues["modelLimits"] = [];
|
||||
const allLimitModels = new Set([
|
||||
|
|
@ -50,8 +62,11 @@ export function EditProjectModal({
|
|||
});
|
||||
}
|
||||
|
||||
// Filter out internal keys from user-facing metadata
|
||||
const internalKeys = new Set(["model_rpm_limit", "model_tpm_limit", "guardrails"]);
|
||||
const internalKeys = new Set([
|
||||
"model_rpm_limit",
|
||||
"model_tpm_limit",
|
||||
"guardrails",
|
||||
]);
|
||||
const metadata: ProjectFormValues["metadata"] = [];
|
||||
for (const [key, value] of Object.entries(metadataObj)) {
|
||||
if (!internalKeys.has(key)) {
|
||||
|
|
@ -100,32 +115,30 @@ export function EditProjectModal({
|
|||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<Typography.Text strong style={{ fontSize: 18 }}>
|
||||
Edit Project
|
||||
</Typography.Text>
|
||||
}
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onCancel={onClose}
|
||||
width={720}
|
||||
destroyOnHidden
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
loading={updateMutation.isPending}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Save Changes
|
||||
</Button>,
|
||||
]}
|
||||
onOpenChange={(o) => (!o ? onClose() : undefined)}
|
||||
>
|
||||
<ProjectBaseForm form={form} />
|
||||
</Modal>
|
||||
<DialogContent className="max-w-[720px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-lg font-semibold">
|
||||
Edit Project
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<ProjectBaseForm form={form} />
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={updateMutation.isPending}
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
{updateMutation.isPending ? "Saving…" : "Save Changes"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Card,
|
||||
Col,
|
||||
DonutChart,
|
||||
Grid,
|
||||
Switch,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Title,
|
||||
} from "@tremor/react";
|
||||
import { Tooltip } from "antd";
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
|
||||
import { DonutChart } from "@tremor/react";
|
||||
import { Info } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { ProviderLogo } from "../../../molecules/models/ProviderLogo";
|
||||
import { ChartLoader } from "../../../shared/chart_loader";
|
||||
|
|
@ -34,20 +37,21 @@ interface SpendByProviderProps {
|
|||
providerSpend: ProviderSpendData[];
|
||||
}
|
||||
|
||||
const SpendByProvider: React.FC<SpendByProviderProps> = ({ loading, isDateChanging, providerSpend }) => {
|
||||
const SpendByProvider: React.FC<SpendByProviderProps> = ({
|
||||
loading,
|
||||
isDateChanging,
|
||||
providerSpend,
|
||||
}) => {
|
||||
const [includeZeroSpend, setIncludeZeroSpend] = useState(false);
|
||||
const [includeUnknown, setIncludeUnknown] = useState(false);
|
||||
|
||||
const filteredProviderSpend = providerSpend.filter((provider) => {
|
||||
const isUnknown = provider.provider?.toLowerCase() === "unknown";
|
||||
|
||||
// If includeUnknown is true, always include unknown provider
|
||||
if (isUnknown) {
|
||||
return includeUnknown;
|
||||
}
|
||||
|
||||
// If includeZeroSpend is true, include all providers (including those with 0 spend)
|
||||
// Otherwise, only include providers with spend > 0
|
||||
if (includeZeroSpend) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -56,73 +60,101 @@ const SpendByProvider: React.FC<SpendByProviderProps> = ({ loading, isDateChangi
|
|||
});
|
||||
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<Card className="h-full p-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>Spend by Provider</Title>
|
||||
<h3 className="text-lg font-semibold">Spend by Provider</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm text-gray-700">Show Zero Spend</label>
|
||||
<Switch checked={includeZeroSpend} onChange={setIncludeZeroSpend} />
|
||||
<label className="text-sm text-foreground">Show Zero Spend</label>
|
||||
<Switch
|
||||
checked={includeZeroSpend}
|
||||
onCheckedChange={setIncludeZeroSpend}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<label className="text-sm text-gray-700">Show Unknown</label>
|
||||
<Tooltip title="Requests that failed to route to a provider">
|
||||
<InfoCircleOutlined className="text-gray-400 hover:text-gray-600" />
|
||||
</Tooltip>
|
||||
<label className="text-sm text-foreground">Show Unknown</label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-3 w-3 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Requests that failed to route to a provider
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<Switch checked={includeUnknown} onChange={setIncludeUnknown} />
|
||||
<Switch
|
||||
checked={includeUnknown}
|
||||
onCheckedChange={setIncludeUnknown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{loading ? (
|
||||
<ChartLoader isDateChanging={isDateChanging} />
|
||||
) : (
|
||||
<Grid numItems={2}>
|
||||
<Col numColSpan={1}>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<DonutChart
|
||||
className="mt-4 h-40"
|
||||
data={filteredProviderSpend}
|
||||
index="provider"
|
||||
category="spend"
|
||||
valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`}
|
||||
valueFormatter={(value) =>
|
||||
`$${formatNumberWithCommas(value, 2)}`
|
||||
}
|
||||
colors={["cyan"]}
|
||||
/>
|
||||
</Col>
|
||||
<Col numColSpan={1}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Provider</TableHeaderCell>
|
||||
<TableHeaderCell>Spend</TableHeaderCell>
|
||||
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell>
|
||||
<TableHeaderCell className="text-red-600">Failed</TableHeaderCell>
|
||||
<TableHeaderCell>Tokens</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{filteredProviderSpend.map((provider) => (
|
||||
<TableRow key={provider.provider}>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-2">
|
||||
{provider.provider && <ProviderLogo provider={provider.provider} className="w-4 h-4" />}
|
||||
<span>{provider.provider}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>${formatNumberWithCommas(provider.spend, 2)}</TableCell>
|
||||
<TableCell className="text-green-600">
|
||||
{provider.successful_requests.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-red-600">
|
||||
{provider.failed_requests.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell>{provider.tokens.toLocaleString()}</TableCell>
|
||||
</div>
|
||||
<div>
|
||||
<div className="border border-border rounded-md overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Provider</TableHead>
|
||||
<TableHead>Spend</TableHead>
|
||||
<TableHead className="text-emerald-600 dark:text-emerald-400">
|
||||
Successful
|
||||
</TableHead>
|
||||
<TableHead className="text-destructive">Failed</TableHead>
|
||||
<TableHead>Tokens</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Col>
|
||||
</Grid>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredProviderSpend.map((provider) => (
|
||||
<TableRow key={provider.provider}>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-2">
|
||||
{provider.provider && (
|
||||
<ProviderLogo
|
||||
provider={provider.provider}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
)}
|
||||
<span>{provider.provider}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
${formatNumberWithCommas(provider.spend, 2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-emerald-600 dark:text-emerald-400">
|
||||
{provider.successful_requests.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-destructive">
|
||||
{provider.failed_requests.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{provider.tokens.toLocaleString()}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
/**
|
||||
* Formatted view of tool definition with parameters table and call data
|
||||
*/
|
||||
|
||||
import { Typography, Table } from "antd";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ParsedTool, ParameterRow } from "./types";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface FormattedToolViewProps {
|
||||
tool: ParsedTool;
|
||||
}
|
||||
|
||||
export function FormattedToolView({ tool }: FormattedToolViewProps) {
|
||||
// Parse parameters for table display
|
||||
const parameterRows: ParameterRow[] = Object.entries(
|
||||
tool.parameters?.properties || {}
|
||||
tool.parameters?.properties || {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
).map(([name, schema]: [string, any]) => ({
|
||||
key: name,
|
||||
name: name,
|
||||
|
|
@ -23,104 +24,64 @@ export function FormattedToolView({ tool }: FormattedToolViewProps) {
|
|||
required: tool.parameters?.required?.includes(name) || false,
|
||||
}));
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "Parameter",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
render: (name: string, record: ParameterRow) => (
|
||||
<Text code>
|
||||
{name}
|
||||
{record.required && <Text type="danger">*</Text>}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
render: (type: string) => (
|
||||
<Text code style={{ color: "#1890ff" }}>
|
||||
{type}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Description",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
render: (desc: string) => <Text type="secondary">{desc}</Text>,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Description */}
|
||||
{tool.description && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text
|
||||
style={{
|
||||
lineHeight: 1.6,
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<span className="leading-relaxed whitespace-pre-wrap">
|
||||
{tool.description}
|
||||
</Text>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Parameters Table */}
|
||||
{parameterRows.length > 0 && (
|
||||
<div>
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
display: "block",
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<span className="text-xs text-muted-foreground block mb-2">
|
||||
Parameters
|
||||
</Text>
|
||||
<Table
|
||||
dataSource={parameterRows}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
size="small"
|
||||
bordered
|
||||
/>
|
||||
</span>
|
||||
<div className="border border-border rounded-md overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Parameter</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{parameterRows.map((row) => (
|
||||
<TableRow key={row.key}>
|
||||
<TableCell>
|
||||
<code className="text-xs bg-muted px-1 py-0.5 rounded">
|
||||
{row.name}
|
||||
{row.required && (
|
||||
<span className="text-destructive">*</span>
|
||||
)}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-xs bg-muted px-1 py-0.5 rounded text-blue-600 dark:text-blue-400">
|
||||
{row.type}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{row.description}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* If tool was called, show the arguments used */}
|
||||
{tool.called && tool.callData && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
display: "block",
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<div className="mt-4">
|
||||
<span className="text-xs text-muted-foreground block mb-2">
|
||||
Called With
|
||||
</Text>
|
||||
<div
|
||||
style={{
|
||||
background: "#f6ffed",
|
||||
border: "1px solid #b7eb8f",
|
||||
borderRadius: 4,
|
||||
padding: 12,
|
||||
}}
|
||||
>
|
||||
<pre
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: 12,
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
</span>
|
||||
<div className="bg-emerald-50 border border-emerald-200 dark:bg-emerald-950/30 dark:border-emerald-900 rounded p-3">
|
||||
<pre className="m-0 text-xs whitespace-pre-wrap break-words">
|
||||
{JSON.stringify(tool.callData.arguments, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue