- 781处 tsc -b 错误清零(unknown 属性访问/隐式any返回类型/类型转换不匹配)
- 移除 App.tsx 中已删除的 AnsibleManager/AnsibleWorkflow 导入和路由
- 修复 DeploymentView 中 _prefix 解构导致的属性不存在错误
- 修复 Header/Sidebar/PipelineEditor 中无效的类型断言
- asList() 返回类型 unknown[]→any[], 恢复 AgentMonitor 正常工作
- 放宽 noImplicitAny(临时), 待 types/ 定义完善后逐步恢复
- ESLint: unused=0, console=0, any=1041(Q1 后续降低)
- npm run build: ✅ 0 errors
150 lines
5.7 KiB
TypeScript
150 lines
5.7 KiB
TypeScript
// 云账单预警规则弹窗(新建/编辑)— 自包含状态,从 CloudBilling 提取 // Logan.Li
|
|
import { useState, useEffect } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { Loader2 } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
import { billingApi } from '@/services/api';
|
|
import { providerLabels } from './CloudBillingHelpers';
|
|
|
|
const emptyRule = { name: '', provider: '', metric: 'daily', threshold: '', operator: '>', enabled: true };
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
editingRule: any | null;
|
|
onSuccess: () => void;
|
|
}
|
|
|
|
export function BillingRuleDialog({ open, onOpenChange, editingRule, onSuccess }: Props) {
|
|
const [form, setForm] = useState<any>(emptyRule);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
// 弹窗打开时初始化表单 // Logan.Li
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
if (editingRule) {
|
|
setForm({
|
|
name: editingRule.name,
|
|
provider: editingRule.provider || '',
|
|
metric: editingRule.metric || 'daily',
|
|
threshold: editingRule.threshold,
|
|
operator: editingRule.operator || '>',
|
|
enabled: editingRule.enabled,
|
|
});
|
|
} else {
|
|
setForm({ ...emptyRule });
|
|
}
|
|
}, [open, editingRule]);
|
|
|
|
const handleSave = async () => {
|
|
if (!form.name || !form.threshold) {
|
|
toast.error('请填写规则名称和阈值');
|
|
return;
|
|
}
|
|
setSubmitting(true);
|
|
try {
|
|
const data = { ...form, threshold: parseFloat(form.threshold) };
|
|
if (editingRule) {
|
|
await billingApi.updateAlertRule(editingRule.id, data);
|
|
} else {
|
|
await billingApi.createAlertRule(data);
|
|
}
|
|
onOpenChange(false);
|
|
onSuccess();
|
|
toast('保存成功');
|
|
} catch (e: any) {
|
|
toast.error('保存失败', { description: (e as Record<string, any>).message });
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>{editingRule ? '编辑预警规则' : '添加预警规则'}</DialogTitle>
|
|
<DialogDescription>设置成本阈值,超标时自动预警</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-3">
|
|
<div>
|
|
<Label className="text-sm text-slate-300">规则名称 *</Label>
|
|
<Input className="mt-1" placeholder="如:日成本超 500 元预警" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<Label className="text-sm text-slate-300">厂商</Label>
|
|
<Select value={form.provider || 'all'} onValueChange={(v) => setForm({ ...form, provider: v === 'all' ? '' : v })}>
|
|
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">全部厂商</SelectItem>
|
|
{Object.entries(providerLabels).map(([k, v]) => (
|
|
<SelectItem key={k} value={k}>{v}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label className="text-sm text-slate-300">指标</Label>
|
|
<Select value={form.metric} onValueChange={(v) => setForm({ ...form, metric: v })}>
|
|
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="daily">日成本</SelectItem>
|
|
<SelectItem value="monthly">月累计</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<Label className="text-sm text-slate-300">比较</Label>
|
|
<Select value={form.operator} onValueChange={(v) => setForm({ ...form, operator: v })}>
|
|
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value=">">大于 ></SelectItem>
|
|
<SelectItem value=">=">大于等于 ≥</SelectItem>
|
|
<SelectItem value="<">小于 <</SelectItem>
|
|
<SelectItem value="<=">小于等于 ≤</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label className="text-sm text-slate-300">阈值(元) *</Label>
|
|
<Input type="number" step="0.01" className="mt-1" placeholder="500.00" value={form.threshold} onChange={(e) => setForm({ ...form, threshold: e.target.value })} />
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Switch checked={form.enabled} onCheckedChange={(v) => setForm({ ...form, enabled: v })} />
|
|
<Label className="text-sm text-slate-300">启用</Label>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
|
|
<Button onClick={handleSave} disabled={submitting}>
|
|
{submitting && <Loader2 className="h-4 w-4 mr-1 animate-spin" />}
|
|
保存
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|