- 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
275 lines
11 KiB
TypeScript
275 lines
11 KiB
TypeScript
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>
|
||
);
|
||
}
|