refactor: 全局代码模块化重构 — 所有文件<700行
后端 handler 拆分: - automation.go(1067→4文件), server.go(1011→5文件), ldap.go(815→3文件), user.go(704→2文件) 前端组件拆分: - SystemManager(1782→569), AlertManager(1133→220), K8sClusters(1062→4文件) - Dashboard(941→4文件), ServerManager(905→640), ApprovalFlow(830→548) - AlertCenter(775→693), AIPredictiveAnalytics(760→670) - CloudBilling(728→663), GrafanaDashboard(717→643), sidebar(726→689) Mock 数据模块化: - mocks/data.ts(2552行→barrel+11个domain模块) 新增 CLAUDE.md 项目全局开发规范 修复: AlertCenter/LogManager ListResponse.data→.list 类型不匹配 验证: go build ✅ + tsc --noEmit ✅ + npm run build ✅
This commit is contained in:
1 parent
e8dbe65955
commit
73b5a48df9
37 files changed
+5148
-5237
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -24,8 +24,9 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { toast } from 'sonner';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { alertCenterApi, alertApi } from '@/services/api';
|
||||
import { AlertDetailDialog, AlertSilenceDialog } from './AlertCenterDetail';
|
||||
|
||||
interface Alert {
|
||||
export interface Alert {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
@@ -102,7 +103,7 @@ const AlertCenter: React.FC = () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await alertCenterApi.getAlerts();
|
||||
const data = res.data || [];
|
||||
const data = res.list || [];
|
||||
setAlerts(data);
|
||||
setStats({
|
||||
firing: data.filter((a: Alert) => a.status === 'firing').length,
|
||||
@@ -169,7 +170,7 @@ const AlertCenter: React.FC = () => {
|
||||
|
||||
const toggleRule = async (ruleId: string, enabled: boolean) => {
|
||||
try {
|
||||
await alertCenterApi.toggleRule(ruleId, enabled);
|
||||
await alertCenterApi.toggleRule(ruleId);
|
||||
setRules(rules.map(r => r.id === ruleId ? { ...r, enabled } : r));
|
||||
toast.success('规则状态已更新');
|
||||
} catch (error) {
|
||||
@@ -616,101 +617,18 @@ const AlertCenter: React.FC = () => {
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* 告警详情对话框 */}
|
||||
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
{selectedAlert?.title}
|
||||
{selectedAlert && getSeverityBadge(selectedAlert.severity)}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{selectedAlert && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-muted-foreground">描述</Label>
|
||||
<p className="mt-1">{selectedAlert.description}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-muted-foreground">来源</Label>
|
||||
<p className="font-medium">{selectedAlert.source}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground">服务</Label>
|
||||
<p className="font-medium">{selectedAlert.service}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground">指标</Label>
|
||||
<p className="font-medium">{selectedAlert.metric}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground">当前值 / 阈值</Label>
|
||||
<p className="font-medium">{selectedAlert.value} / {selectedAlert.threshold}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground">开始时间</Label>
|
||||
<p className="font-medium">{selectedAlert.startedAt}</p>
|
||||
</div>
|
||||
{selectedAlert.resolvedAt && (
|
||||
<div>
|
||||
<Label className="text-muted-foreground">解决时间</Label>
|
||||
<p className="font-medium">{selectedAlert.resolvedAt}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground">标签</Label>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{Object.entries(selectedAlert.labels).map(([k, v]) => (
|
||||
<Badge key={k} variant="outline">{k}={v}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{selectedAlert.runbookUrl && (
|
||||
<div>
|
||||
<Label className="text-muted-foreground">处理手册</Label>
|
||||
<a href={selectedAlert.runbookUrl} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline block mt-1">
|
||||
查看处理手册 →
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDetailOpen(false)}>关闭</Button>
|
||||
{selectedAlert?.status === 'firing' && (
|
||||
<Button onClick={() => { handleAcknowledge(selectedAlert.id); setDetailOpen(false); }}>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
确认告警
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 静默对话框 */}
|
||||
<Dialog open={silenceOpen} onOpenChange={setSilenceOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>静默告警</DialogTitle>
|
||||
<DialogDescription>选择静默持续时间</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
{['1h', '4h', '8h', '24h', '7d'].map((duration) => (
|
||||
<Button
|
||||
key={duration}
|
||||
variant="outline"
|
||||
className="w-full justify-between"
|
||||
onClick={() => selectedAlert && handleSilence(selectedAlert.id, duration)}
|
||||
>
|
||||
<span>静默 {duration}</span>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<AlertDetailDialog
|
||||
alert={selectedAlert}
|
||||
open={detailOpen}
|
||||
onOpenChange={setDetailOpen}
|
||||
onAcknowledge={handleAcknowledge}
|
||||
/>
|
||||
<AlertSilenceDialog
|
||||
alert={selectedAlert}
|
||||
open={silenceOpen}
|
||||
onOpenChange={setSilenceOpen}
|
||||
onSilence={handleSilence}
|
||||
/>
|
||||
|
||||
{/* 通知渠道创建/编辑对话框 */}
|
||||
<Dialog open={channelDialogOpen} onOpenChange={setChannelDialogOpen}>
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import {
|
||||
Check, ChevronRight, AlertCircle, AlertTriangle, Info,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import type { Alert } from './AlertCenter';
|
||||
|
||||
function getSeverityBadge(severity: string) {
|
||||
switch (severity) {
|
||||
case 'critical':
|
||||
return <Badge variant="destructive" className="gap-1"><AlertCircle className="w-3 h-3" />严重</Badge>;
|
||||
case 'warning':
|
||||
return <Badge variant="default" className="bg-orange-500 gap-1"><AlertTriangle className="w-3 h-3" />警告</Badge>;
|
||||
case 'info':
|
||||
return <Badge variant="default" className="bg-blue-500 gap-1"><Info className="w-3 h-3" />信息</Badge>;
|
||||
default:
|
||||
return <Badge variant="secondary">未知</Badge>;
|
||||
}
|
||||
}
|
||||
|
||||
export function AlertDetailDialog({
|
||||
alert,
|
||||
open,
|
||||
onOpenChange,
|
||||
onAcknowledge,
|
||||
}: {
|
||||
alert: Alert | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onAcknowledge: (id: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
{alert?.title}
|
||||
{alert && getSeverityBadge(alert.severity)}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{alert && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-muted-foreground">描述</Label>
|
||||
<p className="mt-1">{alert.description}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-muted-foreground">来源</Label>
|
||||
<p className="font-medium">{alert.source}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground">服务</Label>
|
||||
<p className="font-medium">{alert.service}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground">指标</Label>
|
||||
<p className="font-medium">{alert.metric}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground">当前值 / 阈值</Label>
|
||||
<p className="font-medium">{alert.value} / {alert.threshold}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground">开始时间</Label>
|
||||
<p className="font-medium">{alert.startedAt}</p>
|
||||
</div>
|
||||
{alert.resolvedAt && (
|
||||
<div>
|
||||
<Label className="text-muted-foreground">解决时间</Label>
|
||||
<p className="font-medium">{alert.resolvedAt}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground">标签</Label>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{Object.entries(alert.labels).map(([k, v]) => (
|
||||
<Badge key={k} variant="outline">{k}={v}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{alert.runbookUrl && (
|
||||
<div>
|
||||
<Label className="text-muted-foreground">处理手册</Label>
|
||||
<a href={alert.runbookUrl} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline block mt-1">
|
||||
查看处理手册 →
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>关闭</Button>
|
||||
{alert?.status === 'firing' && (
|
||||
<Button onClick={() => { onAcknowledge(alert.id); onOpenChange(false); }}>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
确认告警
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export function AlertSilenceDialog({
|
||||
alert,
|
||||
open,
|
||||
onOpenChange,
|
||||
onSilence,
|
||||
}: {
|
||||
alert: Alert | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSilence: (id: string, duration: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>静默告警</DialogTitle>
|
||||
<DialogDescription>选择静默持续时间</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
{['1h', '4h', '8h', '24h', '7d'].map((duration) => (
|
||||
<Button
|
||||
key={duration}
|
||||
variant="outline"
|
||||
className="w-full justify-between"
|
||||
onClick={() => alert && onSilence(alert.id, duration)}
|
||||
>
|
||||
<span>静默 {duration}</span>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import { Separator } from '@/components/ui/separator';
|
||||
import { toast } from 'sonner';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { dashboardApi } from '@/services/api';
|
||||
import { GrafanaPanelContent } from './GrafanaPanelContent';
|
||||
|
||||
interface Dashboard {
|
||||
id: string;
|
||||
@@ -36,7 +37,7 @@ interface Dashboard {
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
interface Panel {
|
||||
export interface Panel {
|
||||
id: string;
|
||||
title: string;
|
||||
type: 'graph' | 'stat' | 'gauge' | 'table' | 'pie' | 'heatmap' | 'text';
|
||||
@@ -250,81 +251,6 @@ const GrafanaDashboard: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 模拟图表数据
|
||||
const generateChartData = (panel: Panel) => {
|
||||
const dataPoints = 24;
|
||||
const data = [];
|
||||
for (let i = 0; i < dataPoints; i++) {
|
||||
data.push({
|
||||
time: new Date(Date.now() - (dataPoints - i) * 3600000).toLocaleTimeString(),
|
||||
value: Math.random() * 100
|
||||
});
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
// 渲染面板内容
|
||||
const renderPanelContent = (panel: Panel) => {
|
||||
const data = generateChartData(panel);
|
||||
|
||||
switch (panel.type) {
|
||||
case 'stat':
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<span className="text-4xl font-bold text-blue-500">{Math.floor(Math.random() * 1000)}</span>
|
||||
<span className="text-sm text-muted-foreground mt-2">{panel.options?.unit || 'requests/s'}</span>
|
||||
<div className="flex items-center gap-1 mt-2 text-green-500 text-sm">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
<span>+12.5%</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 'gauge':
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<div className="relative w-24 h-24">
|
||||
<svg className="w-full h-full" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="40" fill="none" stroke="#e5e7eb" strokeWidth="10" />
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="40"
|
||||
fill="none"
|
||||
stroke="#3b82f6"
|
||||
strokeWidth="10"
|
||||
strokeDasharray={`${Math.random() * 251} 251`}
|
||||
transform="rotate(-90 50 50)"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="text-xl font-bold">{Math.floor(Math.random() * 100)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground mt-2">CPU使用率</span>
|
||||
</div>
|
||||
);
|
||||
case 'graph':
|
||||
default:
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex-1 flex items-end gap-1 px-2">
|
||||
{data.map((d, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 bg-blue-500 rounded-t"
|
||||
style={{ height: `${d.value}%`, opacity: 0.3 + (i / data.length) * 0.7 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-muted-foreground px-2 pt-2">
|
||||
<span>{data[0].time}</span>
|
||||
<span>{data[data.length - 1].time}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`space-y-4 ${fullscreen ? 'fixed inset-0 z-50 bg-background p-4' : ''}`}>
|
||||
{/* 顶部工具栏 */}
|
||||
@@ -448,7 +374,7 @@ const GrafanaDashboard: React.FC = () => {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 pt-0 flex-1">
|
||||
{renderPanelContent(panel)}
|
||||
<GrafanaPanelContent panel={panel} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { TrendingUp } from 'lucide-react';
|
||||
import type { Panel } from './GrafanaDashboard';
|
||||
|
||||
/** 模拟图表数据 */
|
||||
function generateChartData(panel: Panel) {
|
||||
const dataPoints = 24;
|
||||
const data = [];
|
||||
for (let i = 0; i < dataPoints; i++) {
|
||||
data.push({
|
||||
time: new Date(Date.now() - (dataPoints - i) * 3600000).toLocaleTimeString(),
|
||||
value: Math.random() * 100
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 渲染面板内容 */
|
||||
export function GrafanaPanelContent({ panel }: { panel: Panel }) {
|
||||
const data = generateChartData(panel);
|
||||
|
||||
switch (panel.type) {
|
||||
case 'stat':
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<span className="text-4xl font-bold text-blue-500">{Math.floor(Math.random() * 1000)}</span>
|
||||
<span className="text-sm text-muted-foreground mt-2">{panel.options?.unit || 'requests/s'}</span>
|
||||
<div className="flex items-center gap-1 mt-2 text-green-500 text-sm">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
<span>+12.5%</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 'gauge':
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<div className="relative w-24 h-24">
|
||||
<svg className="w-full h-full" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="40" fill="none" stroke="#e5e7eb" strokeWidth="10" />
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="40"
|
||||
fill="none"
|
||||
stroke="#3b82f6"
|
||||
strokeWidth="10"
|
||||
strokeDasharray={`${Math.random() * 251} 251`}
|
||||
transform="rotate(-90 50 50)"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="text-xl font-bold">{Math.floor(Math.random() * 100)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground mt-2">CPU使用率</span>
|
||||
</div>
|
||||
);
|
||||
case 'graph':
|
||||
default:
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex-1 flex items-end gap-1 px-2">
|
||||
{data.map((d, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 bg-blue-500 rounded-t"
|
||||
style={{ height: `${d.value}%`, opacity: 0.3 + (i / data.length) * 0.7 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-muted-foreground px-2 pt-2">
|
||||
<span>{data[0].time}</span>
|
||||
<span>{data[data.length - 1].time}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user