Primakov Alexandr Alexandrovich 6ae2d0d8ec Add organization and task queue features
- Introduced new models for `Organization` and `ReviewTask` to manage organizations and review tasks.
- Implemented API endpoints for CRUD operations on organizations and tasks, including scanning organizations for repositories and PRs.
- Developed a background worker for sequential processing of review tasks with priority handling and automatic retries.
- Created frontend components for managing organizations and monitoring task queues, including real-time updates and filtering options.
- Added comprehensive documentation for organization features and quick start guides.
- Fixed UI issues and improved navigation for better user experience.
2025-10-13 00:10:04 +03:00

324 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Tasks page - Task Queue monitoring
*/
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getTasks, getWorkerStatus, retryTask, deleteTask } from '../api/organizations';
import { TaskStatus } from '../types/organization';
import { Modal, ConfirmModal } from '../components/Modal';
import { formatDistanceToNow } from 'date-fns';
import { ru } from 'date-fns/locale';
export default function Tasks() {
const queryClient = useQueryClient();
const [statusFilter, setStatusFilter] = useState<TaskStatus | undefined>();
// Modal states
const [modalMessage, setModalMessage] = useState('');
const [showModal, setShowModal] = useState(false);
const [confirmAction, setConfirmAction] = useState<(() => void) | null>(null);
const [confirmMessage, setConfirmMessage] = useState('');
const [showConfirm, setShowConfirm] = useState(false);
const { data: tasksData, isLoading } = useQuery({
queryKey: ['tasks', statusFilter],
queryFn: () => getTasks(statusFilter),
refetchInterval: 5000, // Обновление каждые 5 секунд
});
const { data: workerStatus } = useQuery({
queryKey: ['workerStatus'],
queryFn: getWorkerStatus,
refetchInterval: 5000,
});
const retryMutation = useMutation({
mutationFn: retryTask,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
setModalMessage('✅ Задача поставлена в очередь повторно');
setShowModal(true);
},
onError: (error: Error) => {
setModalMessage(`❌ Ошибка: ${error.message}`);
setShowModal(true);
},
});
const deleteMutation = useMutation({
mutationFn: deleteTask,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
setModalMessage('✅ Задача удалена');
setShowModal(true);
},
onError: (error: Error) => {
setModalMessage(`❌ Ошибка: ${error.message}`);
setShowModal(true);
},
});
const handleRetry = (taskId: number) => {
setConfirmMessage('Повторить выполнение задачи?');
setConfirmAction(() => () => retryMutation.mutate(taskId));
setShowConfirm(true);
};
const handleDelete = (taskId: number) => {
setConfirmMessage('Удалить задачу из очереди?');
setConfirmAction(() => () => deleteMutation.mutate(taskId));
setShowConfirm(true);
};
const getStatusColor = (status: TaskStatus) => {
switch (status) {
case 'pending':
return 'bg-yellow-100 text-yellow-800';
case 'in_progress':
return 'bg-blue-100 text-blue-800';
case 'completed':
return 'bg-green-100 text-green-800';
case 'failed':
return 'bg-red-100 text-red-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const getStatusLabel = (status: TaskStatus) => {
switch (status) {
case 'pending':
return '⏳ Ожидает';
case 'in_progress':
return '⚙️ Выполняется';
case 'completed':
return '✅ Завершено';
case 'failed':
return '❌ Ошибка';
default:
return status;
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high':
return 'bg-red-100 text-red-800';
case 'normal':
return 'bg-gray-100 text-gray-800';
case 'low':
return 'bg-green-100 text-green-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Загрузка...</div>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-3xl font-bold text-gray-900">Очередь задач</h1>
<p className="text-gray-600 mt-1">
Мониторинг и управление задачами на review
</p>
</div>
{/* Worker Status */}
{workerStatus && (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`w-3 h-3 rounded-full ${workerStatus.running ? 'bg-green-500 animate-pulse' : 'bg-gray-400'}`} />
<span className="font-medium">
{workerStatus.running ? '🚀 Worker активен' : '⏹️ Worker остановлен'}
</span>
</div>
<div className="text-sm text-gray-600">
{workerStatus.current_task_id && (
<span>Обрабатывается задача #{workerStatus.current_task_id}</span>
)}
{!workerStatus.current_task_id && workerStatus.running && (
<span>Ожидание задач...</span>
)}
</div>
</div>
</div>
)}
{/* Stats */}
{tasksData && (
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<div
className={`bg-white rounded-lg shadow-sm border border-gray-200 p-4 cursor-pointer hover:shadow-md transition-shadow ${
statusFilter === undefined ? 'ring-2 ring-indigo-500' : ''
}`}
onClick={() => setStatusFilter(undefined)}
>
<div className="text-2xl font-bold text-gray-900">{tasksData.total}</div>
<div className="text-sm text-gray-600">Всего</div>
</div>
<div
className={`bg-white rounded-lg shadow-sm border border-gray-200 p-4 cursor-pointer hover:shadow-md transition-shadow ${
statusFilter === 'pending' ? 'ring-2 ring-yellow-500' : ''
}`}
onClick={() => setStatusFilter('pending')}
>
<div className="text-2xl font-bold text-yellow-600">{tasksData.pending}</div>
<div className="text-sm text-gray-600">Ожидает</div>
</div>
<div
className={`bg-white rounded-lg shadow-sm border border-gray-200 p-4 cursor-pointer hover:shadow-md transition-shadow ${
statusFilter === 'in_progress' ? 'ring-2 ring-blue-500' : ''
}`}
onClick={() => setStatusFilter('in_progress')}
>
<div className="text-2xl font-bold text-blue-600">{tasksData.in_progress}</div>
<div className="text-sm text-gray-600">Выполняется</div>
</div>
<div
className={`bg-white rounded-lg shadow-sm border border-gray-200 p-4 cursor-pointer hover:shadow-md transition-shadow ${
statusFilter === 'completed' ? 'ring-2 ring-green-500' : ''
}`}
onClick={() => setStatusFilter('completed')}
>
<div className="text-2xl font-bold text-green-600">{tasksData.completed}</div>
<div className="text-sm text-gray-600">Завершено</div>
</div>
<div
className={`bg-white rounded-lg shadow-sm border border-gray-200 p-4 cursor-pointer hover:shadow-md transition-shadow ${
statusFilter === 'failed' ? 'ring-2 ring-red-500' : ''
}`}
onClick={() => setStatusFilter('failed')}
>
<div className="text-2xl font-bold text-red-600">{tasksData.failed}</div>
<div className="text-sm text-gray-600">Ошибок</div>
</div>
</div>
)}
{/* Tasks List */}
<div className="space-y-3">
{tasksData?.items.map((task) => (
<div
key={task.id}
className="bg-white rounded-lg shadow-sm border border-gray-200 p-4"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3">
<span className="text-lg font-mono font-semibold text-gray-900">
#{task.id}
</span>
<span className={`px-2 py-1 rounded text-xs font-medium ${getStatusColor(task.status)}`}>
{getStatusLabel(task.status)}
</span>
<span className={`px-2 py-1 rounded text-xs font-medium ${getPriorityColor(task.priority)}`}>
{task.priority === 'high' && '🔴 Высокий'}
{task.priority === 'normal' && '⚪ Обычный'}
{task.priority === 'low' && '🟢 Низкий'}
</span>
</div>
<div className="mt-2 space-y-1">
<div className="text-sm">
<span className="text-gray-600">PR:</span>{' '}
<span className="font-medium">#{task.pr_number}</span>{' '}
<span className="text-gray-700">{task.pr_title}</span>
</div>
<div className="flex gap-4 text-xs text-gray-500">
<span>
Создано: {formatDistanceToNow(new Date(task.created_at), { addSuffix: true, locale: ru })}
</span>
{task.started_at && (
<span>
Начато: {formatDistanceToNow(new Date(task.started_at), { addSuffix: true, locale: ru })}
</span>
)}
{task.completed_at && (
<span>
Завершено: {formatDistanceToNow(new Date(task.completed_at), { addSuffix: true, locale: ru })}
</span>
)}
</div>
{task.error_message && (
<div className="mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-700">
<strong>Ошибка:</strong> {task.error_message}
</div>
)}
{task.retry_count > 0 && (
<div className="text-xs text-gray-500">
Попыток: {task.retry_count} / {task.max_retries}
</div>
)}
</div>
</div>
<div className="flex gap-2 ml-4">
{(task.status === 'failed' || task.status === 'completed') && (
<button
onClick={() => handleRetry(task.id)}
disabled={retryMutation.isPending}
className="px-3 py-1.5 text-sm bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors disabled:opacity-50"
>
🔄 Повторить
</button>
)}
{task.status !== 'in_progress' && (
<button
onClick={() => handleDelete(task.id)}
disabled={deleteMutation.isPending}
className="px-3 py-1.5 text-sm bg-red-100 text-red-700 rounded hover:bg-red-200 transition-colors disabled:opacity-50"
>
🗑 Удалить
</button>
)}
</div>
</div>
</div>
))}
</div>
{tasksData?.items.length === 0 && (
<div className="text-center py-12 bg-gray-50 rounded-lg">
<p className="text-gray-500">
{statusFilter ? `Нет задач со статусом "${statusFilter}"` : 'Нет задач в очереди'}
</p>
</div>
)}
{/* Modals */}
<Modal
isOpen={showModal}
onClose={() => setShowModal(false)}
title={modalMessage.includes('❌') ? 'Ошибка' : modalMessage.includes('✅') ? 'Успешно' : 'Уведомление'}
type={modalMessage.includes('❌') ? 'error' : modalMessage.includes('✅') ? 'success' : 'info'}
>
<p className="text-gray-700 whitespace-pre-line">{modalMessage}</p>
</Modal>
<ConfirmModal
isOpen={showConfirm}
onClose={() => setShowConfirm(false)}
onConfirm={() => {
if (confirmAction) confirmAction();
setShowConfirm(false);
}}
title="Подтверждение"
message={confirmMessage}
/>
</div>
);
}