feat(web): CopilotAlert 告警组件 + 告警 API 扩展
- CopilotAlert: 分级告警列表,30秒轮询刷新,危急 banner - copilot.ts 新增 listAlerts 函数
This commit is contained in:
@@ -54,6 +54,10 @@ export function dismissInsight(id: string) {
|
|||||||
return client.post(`/copilot/insights/${id}/dismiss`);
|
return client.post(`/copilot/insights/${id}/dismiss`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listAlerts(params?: { severity?: string; page?: number; page_size?: number }) {
|
||||||
|
return listInsights({ insight_type: 'anomaly', ...params });
|
||||||
|
}
|
||||||
|
|
||||||
export function listRules(params?: { page?: number; page_size?: number }) {
|
export function listRules(params?: { page?: number; page_size?: number }) {
|
||||||
return client.get<PaginatedResponse<Record<string, unknown>>>('/copilot/rules', { params });
|
return client.get<PaginatedResponse<Record<string, unknown>>>('/copilot/rules', { params });
|
||||||
}
|
}
|
||||||
|
|||||||
106
apps/web/src/components/Copilot/CopilotAlert.tsx
Normal file
106
apps/web/src/components/Copilot/CopilotAlert.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { Alert, Badge, List, Button, Space, Typography, Spin } from 'antd';
|
||||||
|
import { CheckOutlined } from '@ant-design/icons';
|
||||||
|
import { listAlerts, dismissInsight } from '../../api/copilot';
|
||||||
|
import type { CopilotInsight } from '../../api/copilot';
|
||||||
|
|
||||||
|
const severityConfig: Record<string, { type: 'success' | 'info' | 'warning' | 'error'; label: string }> = {
|
||||||
|
critical: { type: 'error', label: '危急' },
|
||||||
|
warning: { type: 'warning', label: '警告' },
|
||||||
|
info: { type: 'info', label: '提示' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CopilotAlert() {
|
||||||
|
const [alerts, setAlerts] = useState<CopilotInsight[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [dismissing, setDismissing] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchAlerts = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await listAlerts({ page_size: 50 });
|
||||||
|
const result = res.data as unknown as { items: CopilotInsight[]; total: number };
|
||||||
|
setAlerts(result.items ?? []);
|
||||||
|
} catch {
|
||||||
|
// 静默
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAlerts();
|
||||||
|
const timer = setInterval(fetchAlerts, 30_000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [fetchAlerts]);
|
||||||
|
|
||||||
|
const handleDismiss = async (id: string) => {
|
||||||
|
setDismissing(id);
|
||||||
|
try {
|
||||||
|
await dismissInsight(id);
|
||||||
|
setAlerts((prev) => prev.filter((a) => a.id !== id));
|
||||||
|
} finally {
|
||||||
|
setDismissing(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!alerts.length && !loading) return null;
|
||||||
|
|
||||||
|
const criticalCount = alerts.filter((a) => a.severity === 'critical').length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{criticalCount > 0 && (
|
||||||
|
<Alert
|
||||||
|
type="error"
|
||||||
|
showIcon
|
||||||
|
message={`${criticalCount} 条危急告警`}
|
||||||
|
banner
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{loading && alerts.length === 0 ? (
|
||||||
|
<Spin />
|
||||||
|
) : (
|
||||||
|
<List
|
||||||
|
size="small"
|
||||||
|
dataSource={alerts}
|
||||||
|
renderItem={(item) => {
|
||||||
|
const config = severityConfig[item.severity] ?? severityConfig.info;
|
||||||
|
return (
|
||||||
|
<List.Item
|
||||||
|
actions={[
|
||||||
|
<Button
|
||||||
|
key="dismiss"
|
||||||
|
size="small"
|
||||||
|
icon={<CheckOutlined />}
|
||||||
|
loading={dismissing === item.id}
|
||||||
|
onClick={() => handleDismiss(item.id)}
|
||||||
|
>
|
||||||
|
已知悉
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<List.Item.Meta
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<Badge status={config.type} />
|
||||||
|
<Typography.Text>{item.title}</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
item.content?.suggestion ? (
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
{item.content.suggestion as string}
|
||||||
|
</Typography.Text>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export { CopilotBadge } from './CopilotBadge';
|
export { CopilotBadge } from './CopilotBadge';
|
||||||
export { CopilotCard } from './CopilotCard';
|
export { CopilotCard } from './CopilotCard';
|
||||||
|
export { CopilotAlert } from './CopilotAlert';
|
||||||
export { useCopilotRisk } from './useCopilotRisk';
|
export { useCopilotRisk } from './useCopilotRisk';
|
||||||
export { useCopilotInsights } from './useCopilotInsights';
|
export { useCopilotInsights } from './useCopilotInsights';
|
||||||
|
|||||||
Reference in New Issue
Block a user