From 47804f2df3bfcb35fe50a839caed7ec9efb4b0cb Mon Sep 17 00:00:00 2001 From: sinohqb Date: Mon, 17 Aug 2026 23:51:40 +0800 Subject: [PATCH] refactor(ui): extract SectionHeader + drop deprecated CronPoolMonitor (audit P2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI/UX 盘点 P2 + 清理: - 新增 SectionHeader 共享组件,替换 DecisionProcess/ConfigSnapshots(主列表+快照 对比)/EvalReport 自管 header 的重复(返回+标题+右侧操作区) - 删除已 DEPRECATED 的 CronPoolMonitor 页面及其测试(导航早已移除,监控职责 已由 TaskQueueMonitor 承担) tsc 0 错误 vitest 16 passed(CronPoolMonitor 3 测试随之删除) --- frontend/web/src/components/SectionHeader.tsx | 29 +++ .../intelligent_eval/ConfigSnapshots.tsx | 35 +-- .../intelligent_eval/DecisionProcess.tsx | 42 ++-- .../intelligent_eval/EvalReport.tsx | 18 +- .../web/src/pages/CronPoolMonitor.test.tsx | 126 ---------- frontend/web/src/pages/CronPoolMonitor.tsx | 222 ------------------ 6 files changed, 78 insertions(+), 394 deletions(-) create mode 100644 frontend/web/src/components/SectionHeader.tsx delete mode 100644 frontend/web/src/pages/CronPoolMonitor.test.tsx delete mode 100644 frontend/web/src/pages/CronPoolMonitor.tsx diff --git a/frontend/web/src/components/SectionHeader.tsx b/frontend/web/src/components/SectionHeader.tsx new file mode 100644 index 0000000..58a9ec1 --- /dev/null +++ b/frontend/web/src/components/SectionHeader.tsx @@ -0,0 +1,29 @@ +import { Button } from 'antd' +import { ArrowLeftOutlined } from '@ant-design/icons' +import type { ReactNode } from 'react' +import { colors } from '../tokens' + +interface SectionHeaderProps { + title: ReactNode + /** 提供时显示"返回"按钮(子视图/抽屉 tab 环境可省)。 */ + onBack?: () => void + /** 返回按钮文案(默认"返回")。 */ + backLabel?: string + /** 右侧操作区(筛选/刷新/导出等)。 */ + actions?: ReactNode +} + +/** + * 内容区子视图的标准横排头部:可选返回按钮 + 标题 + 右侧操作区。 + * 供智能评估等 Drawer/Tabs 内容组件复用,消除各自手写 header 的重复。 + */ +export default function SectionHeader({ title, onBack, backLabel = '返回', actions }: SectionHeaderProps) { + return ( +
+ {onBack && } + {title} +
+ {actions} +
+ ) +} diff --git a/frontend/web/src/components/intelligent_eval/ConfigSnapshots.tsx b/frontend/web/src/components/intelligent_eval/ConfigSnapshots.tsx index 4a5aa80..8337b3f 100644 --- a/frontend/web/src/components/intelligent_eval/ConfigSnapshots.tsx +++ b/frontend/web/src/components/intelligent_eval/ConfigSnapshots.tsx @@ -4,6 +4,7 @@ import { } from 'antd' import type { ColumnsType } from 'antd/es/table' import { ArrowLeftOutlined, DiffOutlined, ReloadOutlined } from '@ant-design/icons' +import SectionHeader from '../SectionHeader' import { intelligentEvalsApi, type ConfigSnapshot, type ConfigSnapshotComparison } from '../../api' import { colors } from '../../tokens' import { formatDateTime } from '../../utils/date' @@ -182,10 +183,7 @@ export default function ConfigSnapshots({ evalId, onBack }: ConfigSnapshotsProps if (compareMode && comparison) { return (
-
- - 快照对比 -
+ setCompareMode(false)} /> @@ -241,19 +239,22 @@ export default function ConfigSnapshots({ evalId, onBack }: ConfigSnapshotsProps return (
-
- {onBack && } - 配置历史 -
- - -
+ + + + + )} + /> -
- {onBack && } - 决策过程 -
- setFilterType(val ?? null)} + options={[ + { label: '执行会话', value: 'execute_session' }, + { label: '等待', value: 'wait' }, + { label: '开始分析', value: 'start_analysis' }, + ]} + /> + + + + )} + /> {loading ? ( diff --git a/frontend/web/src/components/intelligent_eval/EvalReport.tsx b/frontend/web/src/components/intelligent_eval/EvalReport.tsx index b37e644..21a5ea1 100644 --- a/frontend/web/src/components/intelligent_eval/EvalReport.tsx +++ b/frontend/web/src/components/intelligent_eval/EvalReport.tsx @@ -3,11 +3,11 @@ import { Button, Card, Col, Collapse, Empty, Row, Space, Spin, Tag, message, } from 'antd' import { - ArrowLeftOutlined, BulbOutlined, DownloadOutlined, MessageOutlined, - StarOutlined, WarningOutlined, + BulbOutlined, DownloadOutlined, MessageOutlined, StarOutlined, WarningOutlined, } from '@ant-design/icons' import { Bar } from '@ant-design/charts' import ChatBubble from '../ChatBubble' +import SectionHeader from '../SectionHeader' import { intelligentEvalsApi, type IntelligentEval, @@ -175,18 +175,16 @@ export default function EvalReport({ ev, onBack }: EvalReportProps) { return (
-
- {onBack && } - - {ev.name} · 评估报告 - -
- {report && ( + } loading={exporting} onClick={exportMarkdown}> 导出 Markdown )} -
+ /> {reportLoading && } diff --git a/frontend/web/src/pages/CronPoolMonitor.test.tsx b/frontend/web/src/pages/CronPoolMonitor.test.tsx deleted file mode 100644 index f0abb26..0000000 --- a/frontend/web/src/pages/CronPoolMonitor.test.tsx +++ /dev/null @@ -1,126 +0,0 @@ -// @vitest-environment jsdom -import { act, cleanup, render } from '@testing-library/react' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -vi.mock('../api', () => ({ - openclawCronPoolApi: { - getStatus: vi.fn(), - getMetrics: vi.fn(), - getAlerts: vi.fn(), - scale: vi.fn(), - resolveAlert: vi.fn(), - }, -})) - -vi.mock('antd', async () => { - const actual = await vi.importActual('antd') - return { - ...actual, - message: { error: vi.fn(), success: vi.fn(), warning: vi.fn(), info: vi.fn() }, - } -}) - -import { openclawCronPoolApi } from '../api' -import CronPoolMonitor from './CronPoolMonitor' - -const statusResp = { - data: { pool: { total: 5, idle: 3, busy: 2, stuck: 0, min_size: 5, max_size: 20 } }, -} -const metricsResp = { data: { metrics: { pool_utilization: 0.4, task_backlog: 0, stuck_rate: 0 } } } -const alertsResp = { data: { alerts: [] } } - -beforeEach(() => { - Object.defineProperty(window, 'matchMedia', { - writable: true, - value: vi.fn().mockImplementation((query: string) => ({ - matches: false, - media: query, - onchange: null, - addListener: vi.fn(), - removeListener: vi.fn(), - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - dispatchEvent: vi.fn(), - })), - }) - vi.useFakeTimers() - vi.mocked(openclawCronPoolApi.getStatus).mockResolvedValue(statusResp as never) - vi.mocked(openclawCronPoolApi.getMetrics).mockResolvedValue(metricsResp as never) - vi.mocked(openclawCronPoolApi.getAlerts).mockResolvedValue(alertsResp as never) -}) - -afterEach(() => { - vi.useRealTimers() - cleanup() -}) - -async function settle() { - await act(async () => { - await Promise.resolve() - }) -} - -describe('CronPoolMonitor polling lifecycle', () => { - it('polls every 5 seconds while mounted', async () => { - render() - await settle() - expect(openclawCronPoolApi.getStatus).toHaveBeenCalledTimes(1) - await act(async () => { - await vi.advanceTimersByTimeAsync(5000) - }) - await settle() - expect(openclawCronPoolApi.getStatus).toHaveBeenCalledTimes(2) - await act(async () => { - await vi.advanceTimersByTimeAsync(5000) - }) - await settle() - expect(openclawCronPoolApi.getStatus).toHaveBeenCalledTimes(3) - }) - - it('clears the polling interval on unmount', async () => { - const { unmount } = render() - await settle() - await act(async () => { - await vi.advanceTimersByTimeAsync(5000) - }) - await settle() - const before = vi.mocked(openclawCronPoolApi.getStatus).mock.calls.length - unmount() - await act(async () => { - await Promise.resolve() - await Promise.resolve() - }) - await act(async () => { - await vi.advanceTimersByTimeAsync(30000) - }) - await settle() - const after = vi.mocked(openclawCronPoolApi.getStatus).mock.calls.length - expect(after).toBe(before) - }) - it('pauses polling while the document is hidden and resumes on visible', async () => { - // Default starting state is visible. - vi.spyOn(document, 'visibilityState', 'get').mockReturnValue('visible') - - render() - await settle() - const initial = vi.mocked(openclawCronPoolApi.getStatus).mock.calls.length - expect(initial).toBeGreaterThanOrEqual(1) - - // While hidden, advancing the clock must NOT trigger another poll. - vi.spyOn(document, 'visibilityState', 'get').mockReturnValue('hidden') - document.dispatchEvent(new Event('visibilitychange')) - await act(async () => { - await vi.advanceTimersByTimeAsync(15000) - }) - await settle() - const afterHidden = vi.mocked(openclawCronPoolApi.getStatus).mock.calls.length - expect(afterHidden).toBe(initial) - - // Returning to visible resumes polling and triggers an immediate reload. - vi.spyOn(document, 'visibilityState', 'get').mockReturnValue('visible') - document.dispatchEvent(new Event('visibilitychange')) - await settle() - const afterVisible = vi.mocked(openclawCronPoolApi.getStatus).mock.calls.length - expect(afterVisible).toBeGreaterThan(initial) - }) -}) diff --git a/frontend/web/src/pages/CronPoolMonitor.tsx b/frontend/web/src/pages/CronPoolMonitor.tsx deleted file mode 100644 index 9f6d861..0000000 --- a/frontend/web/src/pages/CronPoolMonitor.tsx +++ /dev/null @@ -1,222 +0,0 @@ -// DEPRECATED (ADR-0009): 智能评估已改为触发式执行,cron 池不再使用(worker cron 已禁用)。 -// 本页已从导航移除,遗留保留仅供回溯。监控职责由 TaskQueueMonitor(任务队列页)承担。 -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 }} - /> - - - -
- ) : ( - - )} -
- - -
}} - /> - - - ) -}