feat: harden downstream requeue tasks
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { AlertTriangle, BarChart3, CheckCircle2, Eye, RefreshCw, Search, TimerReset } from 'lucide-react';
|
||||
import { adminApi, type DownstreamDeliveryDashboard, type DownstreamDeliveryRecord, type DownstreamRequeuePreview, type DownstreamRequeueTask, type EnterpriseApplication } from '@/api/adminApi';
|
||||
import { adminApi, type DownstreamDeliveryDashboard, type DownstreamDeliveryRecord, type DownstreamRequeuePreview, type DownstreamRequeueTask, type DownstreamRequeueTaskItem, type EnterpriseApplication, type TenantOption } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Select, Tag, type DateRangeValue } from '@/components/ui';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
|
||||
@@ -42,6 +42,24 @@ const attemptStatusLabel: Record<string, string> = {
|
||||
failed: '投递失败',
|
||||
};
|
||||
|
||||
const requeueTaskStatusLabel: Record<string, string> = {
|
||||
queued: '排队中', running: '执行中', paused: '已暂停', completed: '已完成',
|
||||
partial_completed: '部分完成', terminated: '已终止',
|
||||
};
|
||||
|
||||
const requeueItemStatusLabel: Record<string, string> = {
|
||||
queued: '排队中', processing: '处理中', waiting_connection: '等待连接',
|
||||
waiting_external_ack: '等待其他链路确认', waiting_ack: '等待客户确认', success: '成功',
|
||||
failed: '失败', skipped: '跳过', unprocessed: '未处理',
|
||||
};
|
||||
|
||||
function requeueTone(status: string) {
|
||||
if (status === 'completed' || status === 'success') return 'success' as const;
|
||||
if (status === 'partial_completed' || status === 'failed') return 'danger' as const;
|
||||
if (status === 'paused' || status === 'skipped' || status === 'unprocessed') return 'warning' as const;
|
||||
return 'info' as const;
|
||||
}
|
||||
|
||||
type RequeueTarget =
|
||||
| { kind: 'single'; record: DownstreamDeliveryRecord }
|
||||
| { kind: 'batch'; ids: string[] };
|
||||
@@ -148,10 +166,12 @@ export function AdminDownstreamDeliveriesPage() {
|
||||
const [records, setRecords] = useState<DownstreamDeliveryRecord[]>([]);
|
||||
const [dashboard, setDashboard] = useState<DownstreamDeliveryDashboard | null>(null);
|
||||
const [applications, setApplications] = useState<EnterpriseApplication[]>([]);
|
||||
const [tenants, setTenants] = useState<TenantOption[]>([]);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [status, setStatus] = useState('all');
|
||||
const [deliveryType, setDeliveryType] = useState('all');
|
||||
const [applicationId, setApplicationId] = useState('all');
|
||||
const [tenantId, setTenantId] = useState('all');
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [total, setTotal] = useState(0);
|
||||
@@ -170,28 +190,39 @@ export function AdminDownstreamDeliveriesPage() {
|
||||
const [taskRate, setTaskRate] = useState(10);
|
||||
const [taskCreateBusy, setTaskCreateBusy] = useState(false);
|
||||
const [requeueTasks, setRequeueTasks] = useState<DownstreamRequeueTask[]>([]);
|
||||
const [requeueTaskStatus, setRequeueTaskStatus] = useState('all');
|
||||
const [requeueTaskPage, setRequeueTaskPage] = useState(1);
|
||||
const [requeueTaskTotal, setRequeueTaskTotal] = useState(0);
|
||||
const [selectedTask, setSelectedTask] = useState<DownstreamRequeueTask | null>(null);
|
||||
const [taskItems, setTaskItems] = useState<DownstreamRequeueTaskItem[]>([]);
|
||||
const [taskItemStatus, setTaskItemStatus] = useState('all');
|
||||
const [taskItemKeyword, setTaskItemKeyword] = useState('');
|
||||
const [taskItemAppliedKeyword, setTaskItemAppliedKeyword] = useState('');
|
||||
const [taskItemPage, setTaskItemPage] = useState(1);
|
||||
const [taskItemTotal, setTaskItemTotal] = useState(0);
|
||||
|
||||
const currentTaskFilter = useCallback(() => ({
|
||||
keyword: keyword || undefined,
|
||||
status,
|
||||
deliveryType,
|
||||
tenantId,
|
||||
applicationId,
|
||||
createdAtFrom: dateRange.start,
|
||||
createdAtTo: dateRange.end,
|
||||
}), [applicationId, dateRange.end, dateRange.start, deliveryType, keyword, status]);
|
||||
}), [applicationId, dateRange.end, dateRange.start, deliveryType, keyword, status, tenantId]);
|
||||
|
||||
const loadRequeueTasks = useCallback(() => {
|
||||
adminApi.listDownstreamRequeueTasks({ page: 1, pageSize: 10 })
|
||||
.then((response) => setRequeueTasks(response.items))
|
||||
.catch(() => undefined);
|
||||
}, []);
|
||||
adminApi.listDownstreamRequeueTasks({ status: requeueTaskStatus, page: requeueTaskPage, pageSize: 10 })
|
||||
.then((response) => { setRequeueTasks(response.items); setRequeueTaskTotal(response.total); })
|
||||
.catch((failure: Error) => setError(failure.message || '后台重投任务加载失败'));
|
||||
}, [requeueTaskPage, requeueTaskStatus]);
|
||||
|
||||
const loadData = useCallback(() => {
|
||||
setLoading(true);
|
||||
Promise.all([
|
||||
adminApi.getDownstreamDeliveryDashboard({
|
||||
applicationId,
|
||||
tenantId,
|
||||
deliveryType,
|
||||
createdAtFrom: dateRange.start,
|
||||
createdAtTo: dateRange.end,
|
||||
@@ -201,24 +232,27 @@ export function AdminDownstreamDeliveriesPage() {
|
||||
status,
|
||||
deliveryType,
|
||||
applicationId,
|
||||
tenantId,
|
||||
page,
|
||||
pageSize,
|
||||
createdAtFrom: dateRange.start,
|
||||
createdAtTo: dateRange.end,
|
||||
}),
|
||||
adminApi.listEnterpriseApplications(),
|
||||
adminApi.listTenants(),
|
||||
])
|
||||
.then(([dashboardResponse, response, apps]) => {
|
||||
.then(([dashboardResponse, response, apps, tenantOptions]) => {
|
||||
setDashboard(dashboardResponse);
|
||||
setRecords(response.items);
|
||||
setTotal(response.total);
|
||||
setApplications(apps);
|
||||
setTenants(tenantOptions);
|
||||
setSelectedIds((current) => current.filter((id) => response.items.some((item) => item.id === id)));
|
||||
setError('');
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '下游投递记录加载失败'))
|
||||
.finally(() => setLoading(false));
|
||||
}, [applicationId, dateRange.end, dateRange.start, deliveryType, keyword, page, pageSize, status]);
|
||||
}, [applicationId, dateRange.end, dateRange.start, deliveryType, keyword, page, pageSize, status, tenantId]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
@@ -247,7 +281,7 @@ export function AdminDownstreamDeliveriesPage() {
|
||||
if (!taskPreview || taskReason.trim().length < 5) return;
|
||||
setTaskCreateBusy(true);
|
||||
try {
|
||||
await adminApi.createDownstreamRequeueTask({ filter: taskPreview.filter, snapshotAt: taskPreview.snapshotAt, reason: taskReason.trim(), ratePerSecond: taskRate, consecutiveFailureLimit: 10 });
|
||||
await adminApi.createDownstreamRequeueTask({ previewToken: taskPreview.previewToken, reason: taskReason.trim(), ratePerSecond: taskRate, consecutiveFailureLimit: 10 });
|
||||
setTaskPreview(null);
|
||||
loadRequeueTasks();
|
||||
} catch (failure) {
|
||||
@@ -257,6 +291,28 @@ export function AdminDownstreamDeliveriesPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const filteredApplications = useMemo(
|
||||
() => applications.filter((item) => tenantId === 'all' || item.tenantId === tenantId),
|
||||
[applications, tenantId],
|
||||
);
|
||||
|
||||
const openTaskDetail = async (id: string) => {
|
||||
try {
|
||||
const task = await adminApi.getDownstreamRequeueTask(id);
|
||||
setSelectedTask(task);
|
||||
setTaskItemStatus('all'); setTaskItemKeyword(''); setTaskItemAppliedKeyword(''); setTaskItemPage(1);
|
||||
} catch (failure) { setError(failure instanceof Error ? failure.message : '任务详情加载失败'); }
|
||||
};
|
||||
|
||||
const loadTaskItems = useCallback(() => {
|
||||
if (!selectedTask) return;
|
||||
adminApi.listDownstreamRequeueTaskItems(selectedTask.id, { status: taskItemStatus, keyword: taskItemAppliedKeyword || undefined, page: taskItemPage, pageSize: 20 })
|
||||
.then((response) => { setTaskItems(response.items); setTaskItemTotal(response.total); })
|
||||
.catch((failure: Error) => setError(failure.message || '任务明细加载失败'));
|
||||
}, [selectedTask, taskItemAppliedKeyword, taskItemPage, taskItemStatus]);
|
||||
|
||||
useEffect(() => { loadTaskItems(); }, [loadTaskItems]);
|
||||
|
||||
const replayableStatuses = useMemo(() => new Set(['pending', 'delivered', 'failed', 'unconfirmed', 'rejected']), []);
|
||||
const selectableIds = useMemo(
|
||||
() => records.filter((item) => replayableStatuses.has(item.status)).map((item) => item.id),
|
||||
@@ -375,6 +431,12 @@ export function AdminDownstreamDeliveriesPage() {
|
||||
<div className="surface ui-filter-row">
|
||||
<Input label="消息ID / 账号 / 手机号 / 错误" onChange={(event) => setKeyword(event.target.value)} placeholder="请输入关键字" value={keyword} />
|
||||
<DateRangeInput label="创建日期" onChange={(value) => { setDateRange(value); setPage(1); }} value={dateRange} />
|
||||
<Select
|
||||
label="企业"
|
||||
options={[{ label: '全部企业', value: 'all' }, ...tenants.map((item) => ({ label: item.name, value: item.id }))]}
|
||||
value={tenantId}
|
||||
onChange={(event) => { setTenantId(event.target.value); setApplicationId('all'); setPage(1); }}
|
||||
/>
|
||||
<Select
|
||||
label="状态"
|
||||
options={[
|
||||
@@ -409,7 +471,7 @@ export function AdminDownstreamDeliveriesPage() {
|
||||
label="应用"
|
||||
options={[
|
||||
{ label: '全部应用', value: 'all' },
|
||||
...applications.map((item) => ({ label: item.name, value: item.id })),
|
||||
...filteredApplications.map((item) => ({ label: item.name, value: item.id })),
|
||||
]}
|
||||
value={applicationId}
|
||||
onChange={(event) => {
|
||||
@@ -424,6 +486,7 @@ export function AdminDownstreamDeliveriesPage() {
|
||||
setKeyword('');
|
||||
setStatus('all');
|
||||
setDeliveryType('all');
|
||||
setTenantId('all');
|
||||
setApplicationId('all');
|
||||
setDateRange(recentSevenDays());
|
||||
setPage(1);
|
||||
@@ -615,18 +678,33 @@ export function AdminDownstreamDeliveriesPage() {
|
||||
</div>
|
||||
|
||||
<div className="surface admin-task-table-card report-task-table-card">
|
||||
<div className="section-heading">
|
||||
<div><h2>后台重投任务</h2><p className="muted">大批量事故恢复按筛选快照分批执行,不受投递记录分页影响。</p></div>
|
||||
<Button onClick={loadRequeueTasks} variant="ghost">刷新</Button>
|
||||
<div className="section-heading downstream-requeue-task-heading">
|
||||
<div><h2>后台重投任务</h2><p className="muted">按筛选快照安全恢复,支持暂停、继续、终止和完整结果追踪。</p></div>
|
||||
<div className="downstream-requeue-task-heading__actions">
|
||||
<Select
|
||||
aria-label="任务状态"
|
||||
options={[
|
||||
{ label: '全部状态', value: 'all' }, { label: '排队中', value: 'queued' }, { label: '执行中', value: 'running' },
|
||||
{ label: '已暂停', value: 'paused' }, { label: '已完成', value: 'completed' }, { label: '部分完成', value: 'partial_completed' }, { label: '已终止', value: 'terminated' },
|
||||
]}
|
||||
value={requeueTaskStatus}
|
||||
onChange={(event) => { setRequeueTaskStatus(event.target.value); setRequeueTaskPage(1); }}
|
||||
/>
|
||||
<Button icon={<RefreshCw size={15} />} onClick={loadRequeueTasks} variant="ghost">刷新</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="downstream-requeue-task-list">
|
||||
{requeueTasks.map((task) => (
|
||||
<article key={task.id}>
|
||||
<div><strong>{task.taskNo}</strong><span>{formatDateTime(task.createdAt)}</span></div>
|
||||
<div><span>{task.application?.name ?? '多个应用'}</span><small>{task.reason}</small></div>
|
||||
<div><Tag tone={task.status === 'completed' ? 'success' : task.status === 'paused' ? 'warning' : task.status === 'partial_completed' ? 'danger' : 'info'}>{task.status}</Tag><span>{task.successCount + task.failedCount + task.skippedCount}/{task.totalCount}</span></div>
|
||||
<div className="downstream-requeue-task-list__identity"><strong>{task.taskNo}</strong><span>{formatDateTime(task.createdAt)}</span><small>{task.createdBy?.displayName || task.createdBy?.username || '系统创建'}</small></div>
|
||||
<div className="downstream-requeue-task-list__scope"><span>{task.tenant?.name ?? '全部企业'} · {task.application?.name ?? '多个应用'}</span><strong>{task.reason}</strong></div>
|
||||
<div className="downstream-requeue-task-list__progress">
|
||||
<div><Tag tone={requeueTone(task.status)}>{requeueTaskStatusLabel[task.status] ?? task.status}</Tag><strong>{task.successCount + task.failedCount + task.skippedCount}/{task.totalCount}</strong></div>
|
||||
<div className="downstream-requeue-task-list__meter"><span style={{ width: `${task.totalCount ? Math.min(100, ((task.successCount + task.failedCount + task.skippedCount) / task.totalCount) * 100) : 0}%` }} /></div>
|
||||
<small>成功 {task.successCount} · 失败 {task.failedCount} · 跳过 {task.skippedCount} · 等待 {task.waitingCount}</small>
|
||||
</div>
|
||||
<div className="downstream-requeue-task-list__actions">
|
||||
<Button onClick={() => void adminApi.getDownstreamRequeueTask(task.id).then(setSelectedTask)} size="sm" variant="ghost">详情</Button>
|
||||
<Button icon={<Eye size={14} />} onClick={() => void openTaskDetail(task.id)} size="sm" variant="ghost">详情</Button>
|
||||
{task.status === 'running' || task.status === 'queued' ? <Button onClick={() => void adminApi.changeDownstreamRequeueTaskStatus(task.id, 'pause').then(loadRequeueTasks)} size="sm" variant="secondary">暂停</Button> : null}
|
||||
{task.status === 'paused' ? <Button onClick={() => void adminApi.changeDownstreamRequeueTaskStatus(task.id, 'resume').then(loadRequeueTasks)} size="sm" variant="secondary">继续</Button> : null}
|
||||
{['queued', 'running', 'paused'].includes(task.status) ? <Button onClick={() => void adminApi.changeDownstreamRequeueTaskStatus(task.id, 'terminate').then(loadRequeueTasks)} size="sm" variant="warning">终止</Button> : null}
|
||||
@@ -635,38 +713,76 @@ export function AdminDownstreamDeliveriesPage() {
|
||||
))}
|
||||
{requeueTasks.length === 0 ? <p className="muted">暂无后台重投任务</p> : null}
|
||||
</div>
|
||||
<Pagination
|
||||
total={requeueTaskTotal}
|
||||
page={requeueTaskPage}
|
||||
totalPages={Math.max(1, Math.ceil(requeueTaskTotal / 10))}
|
||||
onPageChange={setRequeueTaskPage}
|
||||
previousDisabled={requeueTaskPage <= 1}
|
||||
nextDisabled={requeueTaskPage >= Math.max(1, Math.ceil(requeueTaskTotal / 10))}
|
||||
onPrevious={() => setRequeueTaskPage((current) => Math.max(1, current - 1))}
|
||||
onNext={() => setRequeueTaskPage((current) => Math.min(Math.max(1, Math.ceil(requeueTaskTotal / 10)), current + 1))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{detail ? <DeliveryDetailModal record={detail} onClose={() => setDetail(null)} /> : null}
|
||||
{taskPreview ? (
|
||||
<Modal open onClose={() => !taskCreateBusy && setTaskPreview(null)} title="创建下游后台重投任务" size="xl" footer={<><Button disabled={taskCreateBusy} onClick={() => setTaskPreview(null)} variant="ghost">取消</Button><Button disabled={taskCreateBusy || taskPreview.replayableCount === 0 || taskReason.trim().length < 5} onClick={() => void createRequeueTask()} variant="warning">{taskCreateBusy ? '创建中…' : '确认创建任务'}</Button></>}>
|
||||
<Modal open onClose={() => !taskCreateBusy && setTaskPreview(null)} title={<div className="template-modal-title"><h2>创建下游后台重投任务</h2><p>确认范围、执行速度和安全边界后进入后台排队</p></div>} size="xl" footer={<><Button disabled={taskCreateBusy} onClick={() => setTaskPreview(null)} variant="ghost">取消</Button><Button disabled={taskCreateBusy || taskPreview.replayableCount === 0 || taskReason.trim().length < 5} onClick={() => void createRequeueTask()} variant="warning">{taskCreateBusy ? '创建中…' : `创建任务 · ${taskPreview.replayableCount} 条`}</Button></>}>
|
||||
<div className="downstream-requeue-preview">
|
||||
<div className="detail-grid">
|
||||
<div><span>筛选命中</span><strong>{taskPreview.matchedCount}</strong></div>
|
||||
<div><span>可重投</span><strong>{taskPreview.replayableCount}</strong></div>
|
||||
<div><span>规则跳过</span><strong>{taskPreview.skippedCount}</strong></div>
|
||||
<div><span>涉及应用</span><strong>{taskPreview.applicationCount}</strong></div>
|
||||
<div className="downstream-requeue-preview__summary">
|
||||
<div><span>筛选命中</span><strong>{taskPreview.matchedCount}</strong><small>严格按当前筛选快照</small></div>
|
||||
<div className="is-primary"><span>可安全重投</span><strong>{taskPreview.replayableCount}</strong><small>将物化为任务项</small></div>
|
||||
<div><span>规则跳过</span><strong>{taskPreview.skippedCount}</strong><small>不调用 Gateway</small></div>
|
||||
<div><span>涉及应用</span><strong>{taskPreview.applicationCount}</strong><small>按应用独立限速</small></div>
|
||||
</div>
|
||||
<div className="detail-grid downstream-requeue-preview__meta">
|
||||
<div><span>最早记录</span><strong>{taskPreview.oldestCreatedAt ? formatDateTime(taskPreview.oldestCreatedAt) : '-'}</strong></div>
|
||||
<div><span>快照时间</span><strong>{formatDateTime(taskPreview.snapshotAt)}</strong></div>
|
||||
</div>
|
||||
<p className="muted">状态分布:{Object.entries(taskPreview.statusCounts).map(([key, value]) => `${statusLabel[key] ?? key} ${value}`).join(';') || '无'}</p>
|
||||
<div className="downstream-requeue-preview__distribution"><span>状态分布</span><div>{Object.entries(taskPreview.statusCounts).map(([key, value]) => <Tag key={key} tone={statusTone[key] ?? 'neutral'}>{statusLabel[key] ?? key} {value}</Tag>)}</div></div>
|
||||
<Select label="执行速度" value={String(taskRate)} onChange={(event) => setTaskRate(Number(event.target.value))} options={[{ label: '平稳(每应用10条/秒)', value: '10' }, { label: '快速(每应用20条/秒)', value: '20' }, { label: '低速(每应用5条/秒)', value: '5' }]} />
|
||||
<label className="field"><span>任务原因 *</span><textarea value={taskReason} onChange={(event) => setTaskReason(event.target.value)} placeholder="请填写事故原因、工单号或处理说明(至少5个字)" rows={3} /></label>
|
||||
<div className="downstream-requeue-warning"><AlertTriangle size={20} /><strong>第一版仅处理待投递、失败、未确认和拒绝记录;不会批量重投客户端已确认记录。</strong></div>
|
||||
<div className="downstream-requeue-warning"><AlertTriangle size={20} /><div><strong>安全边界</strong><p>仅处理待投递、失败、未确认和拒绝记录;不会批量重投客户端已确认或正在等待 ACK 的记录。客户离线时进入等待,不计失败或跳过。</p></div></div>
|
||||
</div>
|
||||
</Modal>
|
||||
) : null}
|
||||
{selectedTask ? (
|
||||
<Modal open onClose={() => setSelectedTask(null)} title={`后台重投任务 ${selectedTask.taskNo}`} size="xl" footer={<Button onClick={() => setSelectedTask(null)}>关闭</Button>}>
|
||||
<div className="report-record-detail">
|
||||
<Modal open onClose={() => setSelectedTask(null)} title={<div className="template-modal-title"><h2>后台重投任务详情</h2><p>{selectedTask.taskNo}</p></div>} size="xl" footer={<Button onClick={() => setSelectedTask(null)}>关闭</Button>}>
|
||||
<div className="report-record-detail downstream-requeue-task-detail">
|
||||
<div className="detail-grid">
|
||||
<div><span>状态</span><strong>{selectedTask.status}</strong></div><div><span>原因</span><strong>{selectedTask.reason}</strong></div>
|
||||
<div><span>状态</span><strong><Tag tone={requeueTone(selectedTask.status)}>{requeueTaskStatusLabel[selectedTask.status] ?? selectedTask.status}</Tag></strong></div><div className="detail-grid__wide"><span>任务原因</span><strong>{selectedTask.reason}</strong></div>
|
||||
<div><span>企业 / 应用</span><strong>{selectedTask.tenant?.name ?? '全部企业'} / {selectedTask.application?.name ?? '多个应用'}</strong></div>
|
||||
<div><span>创建人</span><strong>{selectedTask.createdBy?.displayName || selectedTask.createdBy?.username || '系统创建'}</strong></div>
|
||||
<div><span>快照时间</span><strong>{formatDateTime(selectedTask.snapshotAt)}</strong></div><div><span>执行速度</span><strong>每应用 {selectedTask.ratePerSecond} 条/秒</strong></div>
|
||||
<div><span>总数</span><strong>{selectedTask.totalCount}</strong></div><div><span>成功</span><strong>{selectedTask.successCount}</strong></div>
|
||||
<div><span>失败</span><strong>{selectedTask.failedCount}</strong></div><div><span>跳过</span><strong>{selectedTask.skippedCount}</strong></div>
|
||||
<div><span>等待</span><strong>{selectedTask.waitingCount}</strong></div><div><span>速度</span><strong>{selectedTask.ratePerSecond}条/秒</strong></div>
|
||||
<div><span>等待</span><strong>{selectedTask.waitingCount}</strong></div><div><span>未处理</span><strong>{selectedTask.itemCounts?.unprocessed ?? 0}</strong></div>
|
||||
</div>
|
||||
<h3>最近任务明细</h3>
|
||||
<div className="downstream-requeue-detail-items">{(selectedTask.recentItems ?? []).map((item) => <div key={item.id}><span>{item.delivery.messageId ?? '-'}</span><Tag tone={item.status === 'success' ? 'success' : item.status === 'failed' ? 'danger' : item.status === 'skipped' ? 'warning' : 'info'}>{item.status}</Tag><span>{item.skipReason ?? item.errorMessage ?? '-'}</span></div>)}</div>
|
||||
{selectedTask.lastError ? <div className="downstream-requeue-detail-alert"><AlertTriangle size={17} /><span>{selectedTask.lastError}</span></div> : null}
|
||||
<section className="downstream-requeue-detail-section">
|
||||
<div className="section-heading"><div><h3>完整任务明细</h3><p className="muted">共 {taskItemTotal} 条,可按结果和原因查询全部历史项。</p></div></div>
|
||||
<div className="downstream-requeue-detail-filter">
|
||||
<Select options={[{ label: '全部结果', value: 'all' }, ...Object.entries(requeueItemStatusLabel).map(([value, label]) => ({ value, label }))]} value={taskItemStatus} onChange={(event) => { setTaskItemStatus(event.target.value); setTaskItemPage(1); }} />
|
||||
<Input placeholder="消息 ID / 错误 / 跳过原因" value={taskItemKeyword} onChange={(event) => setTaskItemKeyword(event.target.value)} />
|
||||
<Button icon={<Search size={14} />} onClick={() => {
|
||||
const nextKeyword = taskItemKeyword.trim();
|
||||
if (taskItemPage !== 1) setTaskItemPage(1);
|
||||
if (nextKeyword !== taskItemAppliedKeyword) setTaskItemAppliedKeyword(nextKeyword);
|
||||
else if (taskItemPage === 1) loadTaskItems();
|
||||
}} variant="secondary">查询</Button>
|
||||
</div>
|
||||
<div className="downstream-requeue-detail-items">
|
||||
<div className="downstream-requeue-detail-items__head"><span>消息 ID</span><span>结果</span><span>原状态</span><span>原因</span><span>更新时间</span></div>
|
||||
{taskItems.map((item) => <div key={item.id}><strong>{item.delivery.messageId ?? '-'}</strong><Tag tone={requeueTone(item.status)}>{requeueItemStatusLabel[item.status] ?? item.status}</Tag><span>{statusLabel[item.previousStatus] ?? item.previousStatus}</span><span>{item.skipReason ?? item.errorMessage ?? '—'}</span><time>{formatDateTime(item.updatedAt)}</time></div>)}
|
||||
{taskItems.length === 0 ? <p className="muted">没有符合条件的任务明细</p> : null}
|
||||
</div>
|
||||
<Pagination
|
||||
total={taskItemTotal} page={taskItemPage} totalPages={Math.max(1, Math.ceil(taskItemTotal / 20))} onPageChange={setTaskItemPage}
|
||||
previousDisabled={taskItemPage <= 1} nextDisabled={taskItemPage >= Math.max(1, Math.ceil(taskItemTotal / 20))}
|
||||
onPrevious={() => setTaskItemPage((current) => Math.max(1, current - 1))}
|
||||
onNext={() => setTaskItemPage((current) => Math.min(Math.max(1, Math.ceil(taskItemTotal / 20)), current + 1))}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</Modal>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user