mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
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:
parent
bdcee81983
commit
7e0f12d847
4 changed files with 264 additions and 252 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue