在准备环境前提交次全部更改。
This commit is contained in:
255
apps/admin-web/src/pages/TaskManager.tsx
Normal file
255
apps/admin-web/src/pages/TaskManager.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* 任务管理页面。
|
||||
*
|
||||
* 三个 Tab:队列、调度、历史
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import {
|
||||
Tabs, Table, Tag, Button, Popconfirm, Space, message, Drawer,
|
||||
Typography, Descriptions, Empty,
|
||||
} from 'antd';
|
||||
import {
|
||||
ReloadOutlined, DeleteOutlined, StopOutlined,
|
||||
UnorderedListOutlined, ClockCircleOutlined, HistoryOutlined,
|
||||
} 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 ScheduleTab from '../components/ScheduleTab';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 状态颜色映射 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
pending: 'default',
|
||||
running: 'processing',
|
||||
success: 'success',
|
||||
failed: 'error',
|
||||
cancelled: 'warning',
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 工具函数 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function fmtTime(iso: string | null | undefined): string {
|
||||
if (!iso) return '—';
|
||||
return new Date(iso).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function fmtDuration(ms: number | null | undefined): string {
|
||||
if (ms == null) return '—';
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
const sec = ms / 1000;
|
||||
if (sec < 60) return `${sec.toFixed(1)}s`;
|
||||
const min = Math.floor(sec / 60);
|
||||
const remainSec = Math.round(sec % 60);
|
||||
return `${min}m${remainSec}s`;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 队列 Tab */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const QueueTab: React.FC = () => {
|
||||
const [data, setData] = useState<QueuedTask[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try { setData(await fetchQueue()); }
|
||||
catch { message.error('加载队列失败'); }
|
||||
finally { setLoading(false); }
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try { await deleteFromQueue(id); message.success('已删除'); load(); }
|
||||
catch { message.error('删除失败'); }
|
||||
};
|
||||
|
||||
const handleCancel = async (id: string) => {
|
||||
try { await cancelExecution(id); message.success('已取消'); load(); }
|
||||
catch { message.error('取消失败'); }
|
||||
};
|
||||
|
||||
const columns: ColumnsType<QueuedTask> = [
|
||||
{
|
||||
title: '任务', dataIndex: ['config', 'tasks'], key: 'tasks',
|
||||
render: (tasks: string[]) => (
|
||||
<Text style={{ maxWidth: 300 }} ellipsis={{ tooltip: tasks?.join(', ') }}>
|
||||
{tasks?.join(', ') ?? '—'}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Flow', dataIndex: ['config', 'pipeline'], key: 'pipeline', width: 120,
|
||||
render: (v: string) => <Tag>{v}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '状态', dataIndex: 'status', key: 'status', width: 90,
|
||||
render: (s: string) => <Tag color={STATUS_COLOR[s] ?? 'default'}>{s}</Tag>,
|
||||
},
|
||||
{ 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',
|
||||
render: (_: unknown, record: QueuedTask) => {
|
||||
if (record.status === 'pending') {
|
||||
return (
|
||||
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
|
||||
<Button type="link" danger icon={<DeleteOutlined />} size="small">删除</Button>
|
||||
</Popconfirm>
|
||||
);
|
||||
}
|
||||
if (record.status === 'running') {
|
||||
return (
|
||||
<Popconfirm title="确认取消执行?" onConfirm={() => handleCancel(record.id)}>
|
||||
<Button type="link" danger icon={<StopOutlined />} size="small">取消</Button>
|
||||
</Popconfirm>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ marginBottom: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text type="secondary">共 {data.length} 个任务</Text>
|
||||
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}>刷新</Button>
|
||||
</div>
|
||||
<Table<QueuedTask>
|
||||
rowKey="id" columns={columns} dataSource={data}
|
||||
loading={loading} pagination={false} size="small"
|
||||
locale={{ emptyText: <Empty description="队列为空" /> }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 历史 Tab */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const HistoryTab: React.FC = () => {
|
||||
const [data, setData] = useState<ExecutionLog[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [detail, setDetail] = useState<ExecutionLog | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try { setData(await fetchHistory()); }
|
||||
catch { message.error('加载历史记录失败'); }
|
||||
finally { setLoading(false); }
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const columns: ColumnsType<ExecutionLog> = [
|
||||
{
|
||||
title: '任务', dataIndex: 'task_codes', key: 'task_codes',
|
||||
render: (codes: string[]) => (
|
||||
<Text style={{ maxWidth: 300 }} ellipsis={{ tooltip: codes?.join(', ') }}>
|
||||
{codes?.join(', ') ?? '—'}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态', dataIndex: 'status', key: 'status', width: 90,
|
||||
render: (s: string) => <Tag color={STATUS_COLOR[s] ?? 'default'}>{s}</Tag>,
|
||||
},
|
||||
{ title: '开始时间', dataIndex: 'started_at', key: 'started_at', width: 170, render: fmtTime },
|
||||
{ title: '时长', dataIndex: 'duration_ms', key: 'duration_ms', width: 90, render: fmtDuration },
|
||||
{
|
||||
title: '退出码', dataIndex: 'exit_code', key: 'exit_code', width: 70, align: 'center',
|
||||
render: (v: number | null) => v != null ? (
|
||||
<Tag color={v === 0 ? 'success' : 'error'}>{v}</Tag>
|
||||
) : '—',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ marginBottom: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text type="secondary">最近 {data.length} 条记录</Text>
|
||||
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}>刷新</Button>
|
||||
</div>
|
||||
<Table<ExecutionLog>
|
||||
rowKey="id" columns={columns} dataSource={data}
|
||||
loading={loading} pagination={{ pageSize: 20, showTotal: (t) => `共 ${t} 条` }}
|
||||
size="small"
|
||||
onRow={(record) => ({ onClick: () => setDetail(record), style: { cursor: 'pointer' } })}
|
||||
/>
|
||||
|
||||
<Drawer
|
||||
title="执行详情" open={!!detail} onClose={() => setDetail(null)}
|
||||
width={520}
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 主组件 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const TaskManager: React.FC = () => {
|
||||
const items = [
|
||||
{
|
||||
key: 'queue',
|
||||
label: <Space><UnorderedListOutlined />队列</Space>,
|
||||
children: <QueueTab />,
|
||||
},
|
||||
{
|
||||
key: 'schedule',
|
||||
label: <Space><ClockCircleOutlined />调度</Space>,
|
||||
children: <ScheduleTab />,
|
||||
},
|
||||
{
|
||||
key: 'history',
|
||||
label: <Space><HistoryOutlined />历史</Space>,
|
||||
children: <HistoryTab />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title level={4} style={{ marginBottom: 16 }}>
|
||||
<UnorderedListOutlined style={{ marginRight: 8 }} />
|
||||
任务管理
|
||||
</Title>
|
||||
<Tabs defaultActiveKey="queue" items={items} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskManager;
|
||||
Reference in New Issue
Block a user