video-flow-b/components/task-monitor/message-queue-panel.tsx
qikongjian 9258f50f8e feat: 添加任务监控功能
- 新增任务统计数据接口和消息队列状态接口
- 添加任务监控页面 (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 接口,支持项目任务统计和消息队列管理
2025-08-26 22:03:57 +08:00

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>
);
}