import { useEffect, useState } from 'react' import { Alert, Button, Card, Descriptions, Empty, InputNumber, Space, Statistic, Table, Tag, message, } from 'antd' import type { ColumnsType } from 'antd/es/table' import { ReloadOutlined, WarningOutlined } from '@ant-design/icons' import { openclawCronPoolApi, type CronPoolAlert, type CronPoolMetrics, type CronPoolStatus } from '../api' import { usePolling } from '../hooks/usePolling' import { colors } from '../tokens' import { formatDateTime } from '../utils/date' export default function CronPoolMonitor() { const [status, setStatus] = useState(null) const [metrics, setMetrics] = useState(null) const [alerts, setAlerts] = useState([]) const [loading, setLoading] = useState(false) const [scaleTarget, setScaleTarget] = useState(5) const [scaleBusy, setScaleBusy] = useState(false) const loadData = async () => { setLoading(true) try { const [statusRes, metricsRes, alertsRes] = await Promise.all([ openclawCronPoolApi.getStatus(), openclawCronPoolApi.getMetrics(), openclawCronPoolApi.getAlerts(50), ]) setStatus(statusRes.data.pool) setMetrics(metricsRes.data.metrics) setAlerts(alertsRes.data.alerts) } catch { message.error('加载数据失败') } finally { setLoading(false) } } // Initial fetch (usePolling owns the 5s interval + visibility pause). useEffect(() => { void loadData() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // S6: defer the recurring 5s poll to the shared `usePolling` hook. The // default pauses the timer while the document is hidden (background tab / // minimised window) and re-fetches once on the way back to `visible`. usePolling(() => { void loadData() }, 5000, true) const handleScale = async () => { setScaleBusy(true) try { await openclawCronPoolApi.scale(scaleTarget) message.success('扩缩容成功') await loadData() } catch { message.error('扩缩容失败') } finally { setScaleBusy(false) } } const handleResolveAlert = async (alertId: string) => { try { await openclawCronPoolApi.resolveAlert(alertId) message.success('已解决告警') await loadData() } catch { message.error('解决告警失败') } } const alertColumns: ColumnsType = [ { title: '时间', dataIndex: 'created_at', key: 'created_at', width: 180, render: (val: string) => formatDateTime(val), }, { title: '级别', dataIndex: 'severity', key: 'severity', width: 100, render: (val: string) => ( {val === 'critical' ? '严重' : val === 'warning' ? '警告' : val} ), }, { title: '类型', dataIndex: 'alert_type', key: 'alert_type', width: 150, }, { title: '消息', dataIndex: 'message', key: 'message', ellipsis: true, }, { title: '状态', key: 'status', width: 100, render: (_, record) => ( record.resolved_at ? ( 已解决 ) : ( ) ), }, ] const unresolvedAlerts = alerts.filter((a) => !a.resolved_at) return (
Cron 池监控
{unresolvedAlerts.length > 0 && ( } message={`有 ${unresolvedAlerts.length} 个未解决的告警`} /> )} {status ? (
0 ? '#ff4d4f' : undefined }} />
{status.min_size} {status.max_size}
val && setScaleTarget(val)} />
) : ( )}
{metrics ? (
0.9 ? '#ff4d4f' : metrics.pool_utilization > 0.7 ? colors.warning : undefined, }} /> 0.1 ? '#ff4d4f' : undefined }} />
) : ( )}
}} /> ) }