feat(ui): migrate price_data_reload to shadcn (drops antd Popconfirm/Modal/InputNumber/Typography)

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
cursor 2026-04-24 06:13:00 +00:00
parent bdcee81983
commit 7e0f12d847
No known key found for this signature in database
4 changed files with 264 additions and 252 deletions

View file

@ -135,7 +135,8 @@
"src/components/DeletedKeysPage/DeletedKeysPage.tsx",
"src/components/DeletedTeamsPage/DeletedTeamsPage.tsx",
"src/components/molecules/models/columns.tsx",
"src/components/common_components/MemberTable.tsx"
"src/components/common_components/MemberTable.tsx",
"src/components/price_data_reload.tsx"
],
"rules": {
"litellm-ui/no-banned-ui-imports": "off",

View file

@ -7,3 +7,27 @@ One-line entries per blocker. Format:
- `src/components/common_components/check_openapi_schema.tsx`: tightly coupled to antd `Form.Item` API (uses `rules`, `validator`, `help`, `initialValue` props that have no direct RHF analog in one file). Only caller is `organisms/create_key_button.tsx` which still uses antd Form. Defer migration until create_key_button migrates.
- `src/components/common_components/PassThroughGuardrailsSection.tsx`: uses `antd.Select` in `mode="tags"` (free-form tag input) and antd `Form.Item` labels. Callers (`add_pass_through.tsx`, `CreatePassThroughEndpoint.tsx`) still on antd Form. Defer.
## Very large / deeply-coupled antd forms — out of scope for this run
Files where the cost to faithfully migrate exceeds the two-attempt budget
and a visual/behavioral regression would break core workflows. These
stay on antd for phase 1 and will be addressed in a targeted follow-up.
- `src/components/organisms/create_key_button.tsx` (1672 LoC): end-to-end
antd `Form` with deeply nested validation, collapse panels, dynamic
field visibility based on Team/Org selection. Migration would
realistically require extracting 8+ sub-forms; the existing behavior
is heavily test-covered.
- `src/components/templates/key_info_view.tsx` (863 LoC): mirrors
create_key_button's form structure for the edit flow. Same concerns.
- `src/components/templates/key_edit_view.tsx` (774 LoC): shares
common_components (RateLimitTypeFormItem, etc.) with the above;
defer with them.
- `src/components/public_model_hub.tsx` (2033 LoC): multi-tab page with
its own filter/search/form state; deep antd Select + Form coupling.
- `src/components/pass_through_info.tsx` (427 LoC): uses
`PassThroughGuardrailsSection` (itself blocked), plus an antd Form.Item
with Switch/InputNumber/Select controls. Defer until shared section
unblocks.
- `src/components/add_pass_through.tsx`: same dependency chain.

View file

@ -1,6 +1,14 @@
import React, { useState, useEffect } from "react";
import { Button, Popconfirm, Modal, InputNumber, Space, Typography, Tag, Card, Tooltip, Divider } from "antd";
import { RefreshCcw as ReloadOutlined, Clock as ClockCircleOutlined, Ban as StopOutlined, Cloud as CloudOutlined, Database as DatabaseOutlined, Info as InfoCircleOutlined, AlertTriangle as WarningOutlined } from "lucide-react";
import {
RefreshCcw as ReloadOutlined,
Clock as ClockCircleOutlined,
Ban as StopOutlined,
Cloud as CloudOutlined,
Database as DatabaseOutlined,
Info as InfoCircleOutlined,
AlertTriangle as WarningOutlined,
Loader2,
} from "lucide-react";
import {
reloadModelCostMap,
scheduleModelCostMapReload,
@ -9,8 +17,35 @@ import {
getModelCostMapSource,
} from "./networking";
import NotificationsManager from "./molecules/notifications_manager";
const { Text } = Typography;
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
interface ReloadStatus {
scheduled: boolean;
@ -42,8 +77,12 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
onReloadSuccess,
buttonText = "Reload Price Data",
showIcon = true,
size = "middle",
type = "primary",
// `size` and `type` are retained in props for API compatibility with
// existing call-sites (they were antd Button props). They are not
// currently wired into shadcn Button variants because visual intent
// was always "primary indigo"; preserve that here.
size: _size,
type: _type,
className = "",
}) => {
const [isLoading, setIsLoading] = useState(false);
@ -52,58 +91,44 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
const [showScheduleModal, setShowScheduleModal] = useState(false);
const [hours, setHours] = useState<number>(6);
const [reloadStatus, setReloadStatus] = useState<ReloadStatus | null>(null);
const [loadingStatus, setLoadingStatus] = useState(false);
const [sourceInfo, setSourceInfo] = useState<CostMapSourceInfo | null>(null);
const [loadingSource, setLoadingSource] = useState(false);
// Fetch status on component mount and periodically
useEffect(() => {
fetchReloadStatus();
fetchSourceInfo();
// Refresh status every 30 seconds to keep it up to date
const interval = setInterval(() => {
fetchReloadStatus();
fetchSourceInfo();
}, 30000);
return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [accessToken]);
const fetchReloadStatus = async () => {
if (!accessToken) return;
setLoadingStatus(true);
try {
console.log("Fetching reload status...");
const status = await getModelCostMapReloadStatus(accessToken);
console.log("Received status:", status);
setReloadStatus(status);
} catch (error) {
console.error("Failed to fetch reload status:", error);
// Set a default status to prevent UI issues
setReloadStatus({
scheduled: false,
interval_hours: null,
last_run: null,
next_run: null,
});
} finally {
setLoadingStatus(false);
}
};
const fetchSourceInfo = async () => {
if (!accessToken) return;
setLoadingSource(true);
try {
const info = await getModelCostMapSource(accessToken);
setSourceInfo(info);
} catch (error) {
console.error("Failed to fetch cost map source info:", error);
} finally {
setLoadingSource(false);
}
};
@ -116,11 +141,11 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
setIsLoading(true);
try {
const response = await reloadModelCostMap(accessToken);
if (response.status === "success") {
NotificationsManager.success(`Price data reloaded successfully! ${response.models_count || 0} models updated.`);
NotificationsManager.success(
`Price data reloaded successfully! ${response.models_count || 0} models updated.`,
);
onReloadSuccess?.();
// Refresh status and source info after successful reload
await fetchReloadStatus();
await fetchSourceInfo();
} else {
@ -128,11 +153,14 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
}
} catch (error) {
console.error("Error reloading price data:", error);
NotificationsManager.fromBackend("Failed to reload price data. Please try again.");
NotificationsManager.fromBackend(
"Failed to reload price data. Please try again.",
);
} finally {
setIsLoading(false);
}
};
const handleScheduleReload = async () => {
if (!accessToken) {
NotificationsManager.fromBackend("No access token available");
@ -147,9 +175,10 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
setIsScheduling(true);
try {
const response = await scheduleModelCostMapReload(accessToken, hours);
if (response.status === "success") {
NotificationsManager.success(`Periodic reload scheduled for every ${hours} hours`);
NotificationsManager.success(
`Periodic reload scheduled for every ${hours} hours`,
);
setShowScheduleModal(false);
await fetchReloadStatus();
} else {
@ -157,7 +186,9 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
}
} catch (error) {
console.error("Error scheduling reload:", error);
NotificationsManager.fromBackend("Failed to schedule periodic reload. Please try again.");
NotificationsManager.fromBackend(
"Failed to schedule periodic reload. Please try again.",
);
} finally {
setIsScheduling(false);
}
@ -172,7 +203,6 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
setIsCancelling(true);
try {
const response = await cancelModelCostMapReload(accessToken);
if (response.status === "success") {
NotificationsManager.success("Periodic reload cancelled successfully");
await fetchReloadStatus();
@ -181,7 +211,9 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
}
} catch (error) {
console.error("Error cancelling reload:", error);
NotificationsManager.fromBackend("Failed to cancel periodic reload. Please try again.");
NotificationsManager.fromBackend(
"Failed to cancel periodic reload. Please try again.",
);
} finally {
setIsCancelling(false);
}
@ -202,303 +234,258 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
return "Active";
};
const getStatusColor = () => {
if (!reloadStatus?.scheduled) return "default";
if (!reloadStatus.last_run) return "processing";
return "success";
};
return (
<div className={className}>
{/* Action Buttons */}
<Space direction="horizontal" size="middle" style={{ marginBottom: 16 }}>
{/* Hard Refresh Button - Always visible */}
<Popconfirm
title="Hard Refresh Price Data"
description="This will immediately fetch the latest pricing information from the remote source. Continue?"
onConfirm={handleHardRefresh}
okText="Yes"
cancelText="No"
okButtonProps={{
style: {
backgroundColor: "#6366f1",
borderColor: "#6366f1",
color: "white",
fontWeight: "500",
borderRadius: "0.375rem",
padding: "0.375rem 0.75rem",
height: "auto",
fontSize: "0.875rem",
lineHeight: "1.25rem",
transition: "all 0.2s ease-in-out",
},
onMouseEnter: (e) => {
e.currentTarget.style.backgroundColor = "#4f46e5";
},
onMouseLeave: (e) => {
e.currentTarget.style.backgroundColor = "#6366f1";
},
}}
>
<Button
type={type}
size={size}
loading={isLoading}
icon={showIcon ? <ReloadOutlined /> : undefined}
style={{
backgroundColor: "#6366f1",
borderColor: "#6366f1",
color: "white",
fontWeight: "500",
borderRadius: "0.375rem",
padding: "0.375rem 0.75rem",
height: "auto",
fontSize: "0.875rem",
lineHeight: "1.25rem",
transition: "all 0.2s ease-in-out",
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = "#4f46e5";
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = "#6366f1";
}}
>
{buttonText}
</Button>
</Popconfirm>
<div className="flex items-center gap-3 mb-4">
{/* Hard Refresh Button with confirmation */}
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
className="bg-indigo-500 hover:bg-indigo-600 text-white"
disabled={isLoading}
>
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
showIcon && <ReloadOutlined className="h-4 w-4" />
)}
{buttonText}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Hard Refresh Price Data</AlertDialogTitle>
<AlertDialogDescription>
This will immediately fetch the latest pricing information
from the remote source. Continue?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>No</AlertDialogCancel>
<AlertDialogAction
onClick={handleHardRefresh}
className="bg-indigo-500 hover:bg-indigo-600 text-white"
>
Yes
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Periodic Reload Controls */}
{!reloadStatus?.scheduled ? (
<Button
type="default"
size={size}
icon={<ClockCircleOutlined />}
onClick={() => setShowScheduleModal(true)}
style={{
borderColor: "#d9d9d9",
color: "#6366f1",
fontWeight: "500",
borderRadius: "0.375rem",
padding: "0.375rem 0.75rem",
height: "auto",
fontSize: "0.875rem",
lineHeight: "1.25rem",
}}
>
<Button variant="outline" onClick={() => setShowScheduleModal(true)}>
<ClockCircleOutlined className="h-4 w-4" />
Set Up Periodic Reload
</Button>
) : (
<Button
type="default"
size={size}
danger
icon={<StopOutlined />}
loading={isCancelling}
variant="outline"
className="border-red-500 text-red-500 hover:bg-red-50 hover:text-red-600"
disabled={isCancelling}
onClick={handleCancelReload}
style={{
borderColor: "#ff4d4f",
color: "#ff4d4f",
fontWeight: "500",
borderRadius: "0.375rem",
padding: "0.375rem 0.75rem",
height: "auto",
fontSize: "0.875rem",
lineHeight: "1.25rem",
}}
>
{isCancelling ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<StopOutlined className="h-4 w-4" />
)}
Cancel Periodic Reload
</Button>
)}
</Space>
</div>
{/* Cost Map Source Info Card */}
{sourceInfo && (
<Card
size="small"
style={{
backgroundColor: sourceInfo.source === "remote" ? "#f0f7ff" : "#fff8f0",
border: `1px solid ${sourceInfo.source === "remote" ? "#bae0ff" : "#ffd591"}`,
borderRadius: 8,
marginBottom: 12,
}}
className={`p-3 mb-3 rounded-lg ${
sourceInfo.source === "remote"
? "bg-blue-50 border-blue-200"
: "bg-orange-50 border-orange-200"
}`}
>
<Space direction="vertical" size="small" style={{ width: "100%" }}>
<div className="flex flex-col gap-2 w-full">
{/* Header row */}
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<div className="flex items-center gap-2">
{sourceInfo.source === "remote" ? (
<CloudOutlined style={{ color: "#1677ff", fontSize: 16 }} />
<CloudOutlined className="h-4 w-4 text-blue-600" />
) : (
<DatabaseOutlined style={{ color: "#fa8c16", fontSize: 16 }} />
<DatabaseOutlined className="h-4 w-4 text-orange-500" />
)}
<Text strong style={{ fontSize: "13px" }}>
<span className="font-semibold text-[13px]">
Pricing Data Source
</Text>
<Tag
color={sourceInfo.source === "remote" ? "blue" : "orange"}
style={{ marginLeft: "auto", fontWeight: 600, textTransform: "uppercase", fontSize: "11px" }}
</span>
<Badge
className={`ml-auto font-semibold uppercase text-[11px] ${
sourceInfo.source === "remote"
? "bg-blue-100 text-blue-700"
: "bg-orange-100 text-orange-700"
}`}
>
{sourceInfo.source === "remote" ? "Remote" : "Local"}
</Tag>
</Badge>
</div>
<Divider style={{ margin: "6px 0" }} />
<Separator className="my-1" />
{/* Model count */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Text type="secondary" style={{ fontSize: "12px" }}>
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs">
Models loaded:
</Text>
<Text strong style={{ fontSize: "12px" }}>
</span>
<span className="font-semibold text-xs">
{sourceInfo.model_count.toLocaleString()}
</Text>
</span>
</div>
{/* URL (when remote or attempted) */}
{/* URL */}
{sourceInfo.url && (
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 8 }}>
<Text type="secondary" style={{ fontSize: "12px", whiteSpace: "nowrap" }}>
{sourceInfo.source === "remote" ? "Loaded from:" : "Attempted URL:"}
</Text>
<Tooltip title={sourceInfo.url}>
<Text
style={{
fontSize: "11px",
maxWidth: 240,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
display: "block",
color: "#1677ff",
cursor: "default",
}}
>
{sourceInfo.url}
</Text>
</Tooltip>
<div className="flex items-start justify-between gap-2">
<span className="text-muted-foreground text-xs whitespace-nowrap">
{sourceInfo.source === "remote"
? "Loaded from:"
: "Attempted URL:"}
</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="text-[11px] max-w-[240px] truncate block text-blue-600 cursor-default">
{sourceInfo.url}
</span>
</TooltipTrigger>
<TooltipContent>{sourceInfo.url}</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)}
{/* Env forced notice */}
{sourceInfo.is_env_forced && (
<div style={{ display: "flex", alignItems: "center", gap: 6, marginTop: 2 }}>
<InfoCircleOutlined style={{ color: "#fa8c16", fontSize: 12 }} />
<Text type="secondary" style={{ fontSize: "11px" }}>
Local mode forced via <code>LITELLM_LOCAL_MODEL_COST_MAP=True</code>
</Text>
<div className="flex items-center gap-1.5 mt-0.5">
<InfoCircleOutlined className="h-3 w-3 text-orange-500" />
<span className="text-muted-foreground text-[11px]">
Local mode forced via{" "}
<code>LITELLM_LOCAL_MODEL_COST_MAP=True</code>
</span>
</div>
)}
{/* Fallback reason */}
{sourceInfo.fallback_reason && (
<div
style={{
display: "flex",
alignItems: "flex-start",
gap: 6,
backgroundColor: "#fff7e6",
border: "1px solid #ffd591",
borderRadius: 4,
padding: "4px 8px",
marginTop: 2,
}}
>
<WarningOutlined style={{ color: "#fa8c16", fontSize: 12, marginTop: 2 }} />
<Text style={{ fontSize: "11px", color: "#614700" }}>
<div className="flex items-start gap-1.5 bg-orange-50 border border-orange-200 rounded px-2 py-1 mt-0.5">
<WarningOutlined className="h-3 w-3 text-orange-500 mt-0.5" />
<span className="text-[11px] text-orange-800">
Fell back to local: {sourceInfo.fallback_reason}
</Text>
</span>
</div>
)}
</Space>
</div>
</Card>
)}
{/* Reload Schedule Status Card */}
{reloadStatus && (
<Card
size="small"
style={{
backgroundColor: "#f8f9fa",
border: "1px solid #e9ecef",
borderRadius: 8,
}}
>
<Space direction="vertical" size="small" style={{ width: "100%" }}>
<Card className="p-3 bg-muted border-border rounded-lg">
<div className="flex flex-col gap-2 w-full">
{reloadStatus.scheduled ? (
<div>
<Tag color="green" icon={<ClockCircleOutlined />}>
<Badge className="bg-green-100 text-green-700 gap-1 inline-flex items-center">
<ClockCircleOutlined className="h-3 w-3" />
Scheduled every {reloadStatus.interval_hours} hours
</Tag>
</Badge>
</div>
) : (
<Text type="secondary">No periodic reload scheduled</Text>
<span className="text-muted-foreground text-sm">
No periodic reload scheduled
</span>
)}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Text type="secondary" style={{ fontSize: "12px" }}>
Last run:
</Text>
<Text style={{ fontSize: "12px" }}>{formatDateTime(reloadStatus.last_run)}</Text>
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs">Last run:</span>
<span className="text-xs">
{formatDateTime(reloadStatus.last_run)}
</span>
</div>
{reloadStatus.scheduled && (
<>
{reloadStatus.next_run && (
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Text type="secondary" style={{ fontSize: "12px" }}>
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs">
Next run:
</Text>
<Text style={{ fontSize: "12px" }}>{formatDateTime(reloadStatus.next_run)}</Text>
</span>
<span className="text-xs">
{formatDateTime(reloadStatus.next_run)}
</span>
</div>
)}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Text type="secondary" style={{ fontSize: "12px" }}>
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs">
Status:
</Text>
<Tag color={getStatusColor()}>{getStatusText()}</Tag>
</span>
<Badge
className={`${
reloadStatus.last_run
? "bg-green-100 text-green-700"
: "bg-blue-100 text-blue-700"
}`}
>
{getStatusText()}
</Badge>
</div>
</>
)}
</Space>
</div>
</Card>
)}
{/* Schedule Modal */}
<Modal
title="Set Up Periodic Reload"
open={showScheduleModal}
onOk={handleScheduleReload}
onCancel={() => setShowScheduleModal(false)}
confirmLoading={isScheduling}
okText="Schedule"
cancelText="Cancel"
okButtonProps={{
style: {
backgroundColor: "#6366f1",
borderColor: "#6366f1",
color: "white",
},
}}
>
<div style={{ marginBottom: 16 }}>
<Text>Set up automatic reload of price data every:</Text>
</div>
<div style={{ marginBottom: 16 }}>
<InputNumber
min={1}
max={168} // 1 week max
value={hours}
onChange={(value) => setHours(value || 6)}
addonAfter="hours"
style={{ width: "100%" }}
/>
</div>
<div>
<Text type="secondary">
This will automatically fetch the latest pricing data from the remote source every {hours} hours.
</Text>
</div>
</Modal>
<Dialog open={showScheduleModal} onOpenChange={setShowScheduleModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>Set Up Periodic Reload</DialogTitle>
</DialogHeader>
<div className="mb-4">
Set up automatic reload of price data every:
</div>
<div className="mb-4 flex items-center gap-2">
<Input
type="number"
min={1}
max={168}
value={hours}
onChange={(e) => setHours(Number(e.target.value) || 6)}
className="w-full"
/>
<span className="text-sm text-muted-foreground whitespace-nowrap">
hours
</span>
</div>
<div>
<span className="text-muted-foreground text-sm">
This will automatically fetch the latest pricing data from the
remote source every {hours} hours.
</span>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowScheduleModal(false)}
>
Cancel
</Button>
<Button
className="bg-indigo-500 hover:bg-indigo-600 text-white"
disabled={isScheduling}
onClick={handleScheduleReload}
>
{isScheduling && <Loader2 className="h-4 w-4 animate-spin" />}
Schedule
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};

File diff suppressed because one or more lines are too long