forked from 77media/video-flow
- 新增任务统计数据接口和消息队列状态接口 - 添加任务监控页面 (app/task-monitor/page.tsx) - 实现任务监控相关组件: - 项目选择器 (project-selector.tsx) - 任务统计卡片 (task-statistics-cards.tsx) - 任务状态图表 (task-status-chart.tsx) - 任务时间线图表 (task-timeline-chart.tsx) - 任务详情表格 (task-detail-table.tsx) - 消息队列面板 (message-queue-panel.tsx) - 扩展 video_flow.ts API 接口,支持项目任务统计和消息队列管理
268 lines
10 KiB
TypeScript
268 lines
10 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState } from 'react';
|
|
import { MessageQueueStatus } from '@/api/video_flow';
|
|
import { cn } from '@/public/lib/utils';
|
|
|
|
interface MessageQueuePanelProps {
|
|
queueStatus: MessageQueueStatus[];
|
|
onRestartQueue: (queueName: string) => Promise<void>;
|
|
}
|
|
|
|
export default function MessageQueuePanel({ queueStatus, onRestartQueue }: MessageQueuePanelProps) {
|
|
const [restartingQueues, setRestartingQueues] = useState<Set<string>>(new Set());
|
|
|
|
const handleRestart = async (queueName: string) => {
|
|
if (restartingQueues.has(queueName)) return;
|
|
|
|
setRestartingQueues(prev => new Set(prev).add(queueName));
|
|
|
|
try {
|
|
await onRestartQueue(queueName);
|
|
} finally {
|
|
setRestartingQueues(prev => {
|
|
const newSet = new Set(prev);
|
|
newSet.delete(queueName);
|
|
return newSet;
|
|
});
|
|
}
|
|
};
|
|
|
|
const getStatusColor = (status: MessageQueueStatus['status']) => {
|
|
switch (status) {
|
|
case 'healthy':
|
|
return 'text-emerald-400 bg-emerald-500/10 border-emerald-500/20';
|
|
case 'warning':
|
|
return 'text-yellow-400 bg-yellow-500/10 border-yellow-500/20';
|
|
case 'error':
|
|
return 'text-red-400 bg-red-500/10 border-red-500/20';
|
|
default:
|
|
return 'text-gray-400 bg-gray-500/10 border-gray-500/20';
|
|
}
|
|
};
|
|
|
|
const getStatusIcon = (status: MessageQueueStatus['status']) => {
|
|
switch (status) {
|
|
case 'healthy':
|
|
return '✅';
|
|
case 'warning':
|
|
return '⚠️';
|
|
case 'error':
|
|
return '❌';
|
|
default:
|
|
return '❓';
|
|
}
|
|
};
|
|
|
|
const getStatusText = (status: MessageQueueStatus['status']) => {
|
|
switch (status) {
|
|
case 'healthy':
|
|
return '健康';
|
|
case 'warning':
|
|
return '警告';
|
|
case 'error':
|
|
return '错误';
|
|
default:
|
|
return '未知';
|
|
}
|
|
};
|
|
|
|
// 计算整体健康度
|
|
const overallHealth = React.useMemo(() => {
|
|
if (queueStatus.length === 0) return { status: 'unknown', percentage: 0 };
|
|
|
|
const healthyCount = queueStatus.filter(q => q.status === 'healthy').length;
|
|
const warningCount = queueStatus.filter(q => q.status === 'warning').length;
|
|
const errorCount = queueStatus.filter(q => q.status === 'error').length;
|
|
|
|
const percentage = (healthyCount / queueStatus.length) * 100;
|
|
|
|
if (errorCount > 0) return { status: 'error', percentage };
|
|
if (warningCount > 0) return { status: 'warning', percentage };
|
|
return { status: 'healthy', percentage };
|
|
}, [queueStatus]);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* 消息队列总览 */}
|
|
<div className="bg-gray-800/50 border border-gray-700 rounded-lg p-6 backdrop-blur-sm">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold text-white">消息队列监控</h3>
|
|
<div className="flex items-center gap-2">
|
|
<div className={cn(
|
|
'w-2 h-2 rounded-full',
|
|
overallHealth.status === 'healthy' ? 'bg-emerald-400' :
|
|
overallHealth.status === 'warning' ? 'bg-yellow-400' :
|
|
overallHealth.status === 'error' ? 'bg-red-400' : 'bg-gray-400'
|
|
)}></div>
|
|
<span className="text-sm text-gray-400">
|
|
{queueStatus.length} 个队列
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 整体健康度指示器 */}
|
|
<div className="mb-6">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-sm text-gray-400">系统健康度</span>
|
|
<span className="text-sm font-medium text-white">
|
|
{overallHealth.percentage.toFixed(0)}%
|
|
</span>
|
|
</div>
|
|
<div className="h-2 bg-gray-700 rounded-full overflow-hidden">
|
|
<div
|
|
className={cn(
|
|
'h-full transition-all duration-500',
|
|
overallHealth.status === 'healthy' ? 'bg-emerald-400' :
|
|
overallHealth.status === 'warning' ? 'bg-yellow-400' :
|
|
'bg-red-400'
|
|
)}
|
|
style={{ width: `${overallHealth.percentage}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 队列列表 */}
|
|
{queueStatus.length === 0 ? (
|
|
<div className="text-center py-8 text-gray-400">
|
|
<div className="text-4xl mb-2">🔄</div>
|
|
<div>暂无队列数据</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{queueStatus.map((queue, index) => (
|
|
<div
|
|
key={index}
|
|
className={cn(
|
|
'p-4 rounded-lg border transition-all duration-200',
|
|
getStatusColor(queue.status)
|
|
)}
|
|
>
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-lg">{getStatusIcon(queue.status)}</span>
|
|
<div>
|
|
<div className="font-medium">{queue.queue_name}</div>
|
|
<div className="text-xs opacity-80">
|
|
{getStatusText(queue.status)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => handleRestart(queue.queue_name)}
|
|
disabled={restartingQueues.has(queue.queue_name) || queue.status === 'healthy'}
|
|
className={cn(
|
|
'px-3 py-1 rounded text-xs font-medium transition-colors',
|
|
queue.status === 'healthy'
|
|
? 'bg-gray-600 text-gray-400 cursor-not-allowed'
|
|
: 'bg-blue-500 hover:bg-blue-600 text-white disabled:opacity-50'
|
|
)}
|
|
>
|
|
{restartingQueues.has(queue.queue_name) ? (
|
|
<div className="flex items-center gap-1">
|
|
<div className="w-3 h-3 animate-spin rounded-full border border-white border-t-transparent"></div>
|
|
<span>重启中</span>
|
|
</div>
|
|
) : (
|
|
'重启'
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<div className="opacity-80">消息数量</div>
|
|
<div className="font-medium">{queue.message_count.toLocaleString()}</div>
|
|
</div>
|
|
<div>
|
|
<div className="opacity-80">消费者</div>
|
|
<div className="font-medium">{queue.consumer_count}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-3 text-xs opacity-60">
|
|
最后心跳: {new Date(queue.last_heartbeat).toLocaleString()}
|
|
</div>
|
|
|
|
{queue.error_message && (
|
|
<div className="mt-2 p-2 bg-red-500/10 border border-red-500/20 rounded text-xs text-red-300">
|
|
{queue.error_message}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 快速操作面板 */}
|
|
<div className="bg-gray-800/50 border border-gray-700 rounded-lg p-6 backdrop-blur-sm">
|
|
<h4 className="text-md font-semibold text-white mb-4">快速操作</h4>
|
|
|
|
<div className="space-y-3">
|
|
<button
|
|
onClick={() => {
|
|
queueStatus
|
|
.filter(q => q.status === 'error')
|
|
.forEach(q => handleRestart(q.queue_name));
|
|
}}
|
|
disabled={queueStatus.filter(q => q.status === 'error').length === 0}
|
|
className="w-full px-4 py-2 bg-red-500 hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg transition-colors text-sm"
|
|
>
|
|
重启所有错误队列
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => {
|
|
queueStatus
|
|
.filter(q => q.status !== 'healthy')
|
|
.forEach(q => handleRestart(q.queue_name));
|
|
}}
|
|
disabled={queueStatus.filter(q => q.status !== 'healthy').length === 0}
|
|
className="w-full px-4 py-2 bg-yellow-500 hover:bg-yellow-600 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg transition-colors text-sm"
|
|
>
|
|
重启所有异常队列
|
|
</button>
|
|
|
|
<div className="pt-3 border-t border-gray-700">
|
|
<div className="text-xs text-gray-400 space-y-1">
|
|
<div>• 健康队列无需重启</div>
|
|
<div>• 重启操作会中断当前处理</div>
|
|
<div>• 建议在低峰期执行批量重启</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 队列统计 */}
|
|
<div className="bg-gray-800/50 border border-gray-700 rounded-lg p-6 backdrop-blur-sm">
|
|
<h4 className="text-md font-semibold text-white mb-4">队列统计</h4>
|
|
|
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
<div className="text-center p-3 bg-emerald-500/10 border border-emerald-500/20 rounded-lg">
|
|
<div className="text-lg font-bold text-emerald-400">
|
|
{queueStatus.filter(q => q.status === 'healthy').length}
|
|
</div>
|
|
<div className="text-emerald-300">健康队列</div>
|
|
</div>
|
|
|
|
<div className="text-center p-3 bg-red-500/10 border border-red-500/20 rounded-lg">
|
|
<div className="text-lg font-bold text-red-400">
|
|
{queueStatus.filter(q => q.status === 'error').length}
|
|
</div>
|
|
<div className="text-red-300">异常队列</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 text-center">
|
|
<div className="text-2xl font-bold text-white">
|
|
{queueStatus.reduce((sum, q) => sum + q.message_count, 0).toLocaleString()}
|
|
</div>
|
|
<div className="text-sm text-gray-400">总消息数</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|