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:
Cursor Agent 2026-04-23 12:44:35 +00:00
parent d5df40b9c4
commit d0e2b93d51
No known key found for this signature in database
4 changed files with 196 additions and 188 deletions

View file

@ -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",

View file

@ -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>
);
}

View file

@ -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>
);

View file

@ -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>