在前后端开发联调前 的提交20260223
This commit is contained in:
@@ -2,22 +2,27 @@
|
||||
* 任务管理页面。
|
||||
*
|
||||
* 三个 Tab:队列、调度、历史
|
||||
* 队列 Tab:running 状态的任务可点击查看实时 WebSocket 日志流
|
||||
* 历史 Tab:点击记录可查看执行详情和历史日志
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import {
|
||||
Tabs, Table, Tag, Button, Popconfirm, Space, message, Drawer,
|
||||
Typography, Descriptions, Empty,
|
||||
Typography, Descriptions, Empty, Spin,
|
||||
} from 'antd';
|
||||
import {
|
||||
ReloadOutlined, DeleteOutlined, StopOutlined,
|
||||
UnorderedListOutlined, ClockCircleOutlined, HistoryOutlined,
|
||||
FileTextOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import type { QueuedTask, ExecutionLog } from '../types';
|
||||
import {
|
||||
fetchQueue, fetchHistory, deleteFromQueue, cancelExecution,
|
||||
} from '../api/execution';
|
||||
import { apiClient } from '../api/client';
|
||||
import LogStream from '../components/LogStream';
|
||||
import ScheduleTab from '../components/ScheduleTab';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
@@ -61,6 +66,13 @@ const QueueTab: React.FC = () => {
|
||||
const [data, setData] = useState<QueuedTask[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
/* WebSocket 日志流状态 */
|
||||
const [logDrawerOpen, setLogDrawerOpen] = useState(false);
|
||||
const [logLines, setLogLines] = useState<string[]>([]);
|
||||
const [logTaskId, setLogTaskId] = useState<string | null>(null);
|
||||
const [wsConnected, setWsConnected] = useState(false);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try { setData(await fetchQueue()); }
|
||||
@@ -70,6 +82,51 @@ const QueueTab: React.FC = () => {
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
/* 自动轮询队列状态(5 秒间隔),保持状态实时 */
|
||||
useEffect(() => {
|
||||
const timer = setInterval(load, 5_000);
|
||||
return () => clearInterval(timer);
|
||||
}, [load]);
|
||||
|
||||
/* 组件卸载时关闭 WebSocket */
|
||||
useEffect(() => {
|
||||
return () => { wsRef.current?.close(); };
|
||||
}, []);
|
||||
|
||||
/** 打开日志抽屉并建立 WebSocket 连接 */
|
||||
const handleViewLogs = useCallback((taskId: string) => {
|
||||
setLogTaskId(taskId);
|
||||
setLogLines([]);
|
||||
setLogDrawerOpen(true);
|
||||
|
||||
// 关闭旧连接
|
||||
wsRef.current?.close();
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const host = window.location.host;
|
||||
const ws = new WebSocket(`${protocol}//${host}/ws/logs/${taskId}`);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => { setWsConnected(true); };
|
||||
ws.onmessage = (event) => {
|
||||
setLogLines((prev) => [...prev, event.data]);
|
||||
};
|
||||
ws.onclose = () => { setWsConnected(false); };
|
||||
ws.onerror = () => {
|
||||
message.error('WebSocket 连接失败');
|
||||
setWsConnected(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
/** 关闭日志抽屉 */
|
||||
const handleCloseLogDrawer = useCallback(() => {
|
||||
setLogDrawerOpen(false);
|
||||
wsRef.current?.close();
|
||||
wsRef.current = null;
|
||||
setWsConnected(false);
|
||||
setLogTaskId(null);
|
||||
}, []);
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try { await deleteFromQueue(id); message.success('已删除'); load(); }
|
||||
catch { message.error('删除失败'); }
|
||||
@@ -90,7 +147,7 @@ const QueueTab: React.FC = () => {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Flow', dataIndex: ['config', 'pipeline'], key: 'pipeline', width: 120,
|
||||
title: 'Flow', dataIndex: ['config', 'flow'], key: 'flow', width: 120,
|
||||
render: (v: string) => <Tag>{v}</Tag>,
|
||||
},
|
||||
{
|
||||
@@ -100,7 +157,7 @@ const QueueTab: React.FC = () => {
|
||||
{ title: '位置', dataIndex: 'position', key: 'position', width: 60, align: 'center' },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 170, render: fmtTime },
|
||||
{
|
||||
title: '操作', key: 'action', width: 100, align: 'center',
|
||||
title: '操作', key: 'action', width: 160, align: 'center',
|
||||
render: (_: unknown, record: QueuedTask) => {
|
||||
if (record.status === 'pending') {
|
||||
return (
|
||||
@@ -111,9 +168,17 @@ const QueueTab: React.FC = () => {
|
||||
}
|
||||
if (record.status === 'running') {
|
||||
return (
|
||||
<Popconfirm title="确认取消执行?" onConfirm={() => handleCancel(record.id)}>
|
||||
<Button type="link" danger icon={<StopOutlined />} size="small">取消</Button>
|
||||
</Popconfirm>
|
||||
<Space size={4}>
|
||||
<Button
|
||||
type="link" icon={<FileTextOutlined />} size="small"
|
||||
onClick={() => handleViewLogs(record.id)}
|
||||
>
|
||||
日志
|
||||
</Button>
|
||||
<Popconfirm title="确认取消执行?" onConfirm={() => handleCancel(record.id)}>
|
||||
<Button type="link" danger icon={<StopOutlined />} size="small">取消</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -132,6 +197,29 @@ const QueueTab: React.FC = () => {
|
||||
loading={loading} pagination={false} size="small"
|
||||
locale={{ emptyText: <Empty description="队列为空" /> }}
|
||||
/>
|
||||
|
||||
{/* 实时日志抽屉 */}
|
||||
<Drawer
|
||||
title={
|
||||
<Space>
|
||||
<FileTextOutlined />
|
||||
<span>执行日志</span>
|
||||
{wsConnected
|
||||
? <Tag color="processing">实时连接中</Tag>
|
||||
: <Tag>未连接</Tag>}
|
||||
</Space>
|
||||
}
|
||||
open={logDrawerOpen}
|
||||
onClose={handleCloseLogDrawer}
|
||||
width={720}
|
||||
styles={{ body: { padding: 12, display: 'flex', flexDirection: 'column', height: '100%' } }}
|
||||
>
|
||||
{logTaskId && (
|
||||
<div style={{ flex: 1, minHeight: 0 }}>
|
||||
<LogStream executionId={logTaskId} lines={logLines} />
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -144,6 +232,8 @@ const HistoryTab: React.FC = () => {
|
||||
const [data, setData] = useState<ExecutionLog[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [detail, setDetail] = useState<ExecutionLog | null>(null);
|
||||
const [historyLogLines, setHistoryLogLines] = useState<string[]>([]);
|
||||
const [logLoading, setLogLoading] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -154,6 +244,28 @@ const HistoryTab: React.FC = () => {
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
/** 点击行时加载详情和日志 */
|
||||
const handleRowClick = useCallback(async (record: ExecutionLog) => {
|
||||
setDetail(record);
|
||||
setHistoryLogLines([]);
|
||||
setLogLoading(true);
|
||||
try {
|
||||
const { data: logData } = await apiClient.get<{
|
||||
execution_id: string;
|
||||
output_log: string | null;
|
||||
error_log: string | null;
|
||||
}>(`/execution/${record.id}/logs`);
|
||||
const parts: string[] = [];
|
||||
if (logData.output_log) parts.push(logData.output_log);
|
||||
if (logData.error_log) parts.push(logData.error_log);
|
||||
setHistoryLogLines(parts.join('\n').split('\n').filter(Boolean));
|
||||
} catch {
|
||||
/* 日志可能不存在,静默处理 */
|
||||
} finally {
|
||||
setLogLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const columns: ColumnsType<ExecutionLog> = [
|
||||
{
|
||||
title: '任务', dataIndex: 'task_codes', key: 'task_codes',
|
||||
@@ -187,31 +299,44 @@ const HistoryTab: React.FC = () => {
|
||||
rowKey="id" columns={columns} dataSource={data}
|
||||
loading={loading} pagination={{ pageSize: 20, showTotal: (t) => `共 ${t} 条` }}
|
||||
size="small"
|
||||
onRow={(record) => ({ onClick: () => setDetail(record), style: { cursor: 'pointer' } })}
|
||||
onRow={(record) => ({ onClick: () => handleRowClick(record), style: { cursor: 'pointer' } })}
|
||||
/>
|
||||
|
||||
<Drawer
|
||||
title="执行详情" open={!!detail} onClose={() => setDetail(null)}
|
||||
width={520}
|
||||
width={720}
|
||||
styles={{ body: { padding: 12 } }}
|
||||
>
|
||||
{detail && (
|
||||
<Descriptions column={1} bordered size="small">
|
||||
<Descriptions.Item label="任务">{detail.task_codes?.join(', ')}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
<Tag color={STATUS_COLOR[detail.status] ?? 'default'}>{detail.status}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="开始时间">{fmtTime(detail.started_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="结束时间">{fmtTime(detail.finished_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="时长">{fmtDuration(detail.duration_ms)}</Descriptions.Item>
|
||||
<Descriptions.Item label="退出码">
|
||||
{detail.exit_code != null ? (
|
||||
<Tag color={detail.exit_code === 0 ? 'success' : 'error'}>{detail.exit_code}</Tag>
|
||||
) : '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="命令">
|
||||
<code style={{ wordBreak: 'break-all', fontSize: 12 }}>{detail.command || '—'}</code>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<>
|
||||
<Descriptions column={1} bordered size="small" style={{ marginBottom: 16 }}>
|
||||
<Descriptions.Item label="任务">{detail.task_codes?.join(', ')}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
<Tag color={STATUS_COLOR[detail.status] ?? 'default'}>{detail.status}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="开始时间">{fmtTime(detail.started_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="结束时间">{fmtTime(detail.finished_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="时长">{fmtDuration(detail.duration_ms)}</Descriptions.Item>
|
||||
<Descriptions.Item label="退出码">
|
||||
{detail.exit_code != null ? (
|
||||
<Tag color={detail.exit_code === 0 ? 'success' : 'error'}>{detail.exit_code}</Tag>
|
||||
) : '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="命令">
|
||||
<code style={{ wordBreak: 'break-all', fontSize: 12 }}>{detail.command || '—'}</code>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
{/* 历史日志展示 */}
|
||||
<div style={{ marginBottom: 8, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<FileTextOutlined />
|
||||
<Text strong>执行日志</Text>
|
||||
{logLoading && <Spin size="small" />}
|
||||
</div>
|
||||
<div style={{ height: 400 }}>
|
||||
<LogStream executionId={detail.id} lines={historyLogLines} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user