Files
aiops/frontend/src/components/monitor/AlertChannelDialog.tsx
T
attacker a7e2351ad2 feat: 定时任务编辑入口全栈打通 + 修复ApprovalFlow编译错误 + 完成大文件拆分重构
- CronJobManager: 编辑按钮接通 onClick → openEdit(job),弹出编辑对话框预填数据,
  复用新建/编辑共用 Dialog(editingId 区分模式),标题/按钮文案动态切换,
  保存时根据 editingId 调用 createJob 或 updateJob — 消除占位按钮
- ApprovalFlow: 修复 API 方法名不匹配导致 tsc -b 编译失败
  (getInstances→getProcesses, getMyApprovals→getProcesses({status:'pending'}),
   approveInstance→approveProcess, rejectInstance→rejectProcess)
  + 添加 ApprovalFlowManager 导出别名兼容 App.tsx 导入
- 合并已完成的大文件拆分重构: automation.go→automation_cron/script/playbook,
  ldap.go→ldap_ou/ldap_users, server.go→cloud_accounts/domain/platform/region,
  user.go→user_admin; 前端 Dashboard/K8s/Server/Approval/Monitor 子组件拆分
  // Logan.Li
2026-06-15 01:09:49 +08:00

275 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge';
import { Switch } from '@/components/ui/switch';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Plus, Trash2, Loader2 } from 'lucide-react';
import { alertApi } from '@/services/api';
import { CHANNEL_TYPES, getChannelTypeLabel } from './alertShared';
const defaultChannelForm = { name: '', type: 'dingtalk', config: '', enabled: true };
export function AlertChannelDialog({
open,
onOpenChange,
notifyChannels,
onChannelsChanged,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
notifyChannels: any[];
onChannelsChanged: () => void;
}) {
const _confirm = useConfirm();
const [editingChannel, setEditingChannel] = useState<any>(null);
const [channelSaving, setChannelSaving] = useState(false);
const [channelTesting, setChannelTesting] = useState<number | null>(null);
const [channelForm, setChannelForm] = useState({ ...defaultChannelForm });
// 打开时重置为新建模式
useEffect(() => {
if (open) {
setEditingChannel(null);
setChannelForm({ ...defaultChannelForm });
}
}, [open]);
const openEditChannel = (ch: any) => {
setEditingChannel(ch);
setChannelForm({
name: ch.name || '',
type: ch.type || 'dingtalk',
config: ch.config || '',
enabled: ch.enabled !== false,
});
};
const resetChannelForm = () => {
setEditingChannel(null);
setChannelForm({ ...defaultChannelForm });
};
const handleSaveChannel = async () => {
if (!channelForm.name.trim()) return;
setChannelSaving(true);
try {
const payload = {
name: channelForm.name.trim(),
type: channelForm.type,
config: channelForm.config.trim(),
};
if (editingChannel) {
await alertApi.updateNotifyChannel(editingChannel.id, { ...payload, enabled: channelForm.enabled });
} else {
await alertApi.createNotifyChannel(payload);
}
resetChannelForm();
onChannelsChanged();
} catch (error) {
console.error('Failed to save notify channel:', error);
} finally {
setChannelSaving(false);
}
};
const handleDeleteChannel = async (id: any) => {
{ const ok = await _confirm({ title: '删除通知渠道', description: '确定删除此通知渠道?', confirmText: '删除', variant: 'destructive' }); if (!ok) return; }
try {
await alertApi.deleteNotifyChannel(id);
if (editingChannel && editingChannel.id === id) resetChannelForm();
onChannelsChanged();
} catch (error) {
console.error('Failed to delete notify channel:', error);
}
};
const handleToggleChannel = async (ch: any) => {
try {
await alertApi.updateNotifyChannel(ch.id, { enabled: !ch.enabled });
onChannelsChanged();
} catch (error) {
console.error('Failed to toggle notify channel:', error);
}
};
const handleTestChannel = async (id: any) => {
setChannelTesting(id);
try {
await alertApi.testNotifyChannel(id);
onChannelsChanged();
} catch (error) {
console.error('Failed to test notify channel:', error);
} finally {
setChannelTesting(null);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="bg-slate-900 border-slate-700 text-slate-100 max-w-2xl">
<DialogHeader>
<DialogTitle className="text-slate-100">通知渠道配置</DialogTitle>
<DialogDescription className="text-slate-500">
管理告警通知渠道(钉钉/企业微信/邮件/短信/Webhook),配置后可在告警规则中引用
</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 max-h-[65vh] overflow-y-auto pr-1">
{/* 渠道列表 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-slate-300 text-sm">已配置渠道</Label>
<Button
size="sm"
variant="outline"
className="h-7 border-slate-700 text-slate-300 hover:bg-slate-800 text-xs"
onClick={resetChannelForm}
>
<Plus className="h-3 w-3 mr-1" />
新建
</Button>
</div>
{notifyChannels.length === 0 ? (
<div className="text-center py-8 text-sm text-slate-600 border border-dashed border-slate-800 rounded-lg">
暂无通知渠道,请在右侧添加
</div>
) : (
<div className="space-y-2">
{notifyChannels.map((ch) => (
<div
key={ch.id}
className={`p-3 rounded-lg border cursor-pointer transition-colors ${
editingChannel && editingChannel.id === ch.id
? 'border-blue-500/50 bg-blue-500/5'
: 'border-slate-800 bg-slate-800/30 hover:border-slate-700'
}`}
onClick={() => openEditChannel(ch)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 min-w-0">
<span className="text-sm text-slate-200 truncate">{ch.name}</span>
<Badge variant="outline" className="border-slate-600 text-slate-400 text-[10px] py-0">
{getChannelTypeLabel(ch.type)}
</Badge>
</div>
<Switch
checked={ch.enabled !== false}
onCheckedChange={() => handleToggleChannel(ch)}
onClick={(e) => e.stopPropagation()}
className="data-[state=checked]:bg-emerald-600 scale-90"
/>
</div>
<div className="flex items-center justify-between mt-2">
{ch.test_status === 'success' ? (
<span className="text-[11px] text-emerald-400">测试通过</span>
) : (
<span className="text-[11px] text-slate-600">未测试</span>
)}
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
<Button
size="sm"
variant="ghost"
className="h-6 px-2 text-slate-400 hover:text-slate-200 hover:bg-slate-700 text-xs"
disabled={channelTesting === ch.id}
onClick={() => handleTestChannel(ch.id)}
>
{channelTesting === ch.id ? <Loader2 className="h-3 w-3 animate-spin" /> : '测试'}
</Button>
<Button
size="sm"
variant="ghost"
className="h-6 px-2 text-red-400/70 hover:text-red-400 hover:bg-red-500/10 text-xs"
onClick={() => handleDeleteChannel(ch.id)}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* 渠道表单 */}
<div className="space-y-4">
<Label className="text-slate-300 text-sm">
{editingChannel ? '编辑渠道' : '新建渠道'}
</Label>
<div className="space-y-1.5">
<Label className="text-slate-400 text-xs">渠道名称 <span className="text-red-400">*</span></Label>
<Input
value={channelForm.name}
onChange={(e) => setChannelForm({ ...channelForm, name: e.target.value })}
placeholder="如:运维告警群"
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600 h-9"
/>
</div>
<div className="space-y-1.5">
<Label className="text-slate-400 text-xs">渠道类型</Label>
<select
value={channelForm.type}
onChange={(e) => setChannelForm({ ...channelForm, type: e.target.value })}
className="w-full h-9 px-3 rounded-md bg-slate-800/50 border border-slate-700 text-slate-200 text-sm"
>
{CHANNEL_TYPES.map(({ key, label }) => (
<option key={key} value={key}>{label}</option>
))}
</select>
</div>
<div className="space-y-1.5">
<Label className="text-slate-400 text-xs">配置(JSON)</Label>
<Textarea
value={channelForm.config}
onChange={(e) => setChannelForm({ ...channelForm, config: e.target.value })}
placeholder={CHANNEL_TYPES.find((t) => t.key === channelForm.type)?.placeholder || ''}
rows={5}
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600 font-mono text-xs resize-none"
/>
</div>
{editingChannel && (
<div className="flex items-center gap-2">
<Switch
checked={channelForm.enabled}
onCheckedChange={(v) => setChannelForm({ ...channelForm, enabled: v })}
className="data-[state=checked]:bg-emerald-600"
/>
<Label className="text-slate-400 text-xs">启用此渠道</Label>
</div>
)}
</div>
</div>
<DialogFooter>
<Button
variant="outline"
className="border-slate-700 text-slate-300 hover:bg-slate-800"
onClick={() => onOpenChange(false)}
>
关闭
</Button>
<Button
className="bg-blue-600 hover:bg-blue-700 text-white"
disabled={!channelForm.name.trim() || channelSaving}
onClick={handleSaveChannel}
>
{channelSaving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
{editingChannel ? '保存修改' : '添加渠道'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}