code-review-agent/frontend/src/pages/Organizations.tsx
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

381 lines
14 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.

/**
* Organizations page
*/
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
getOrganizations,
createOrganization,
updateOrganization,
deleteOrganization,
scanOrganization,
} from '../api/organizations';
import { Organization, OrganizationCreate, OrganizationPlatform } from '../types/organization';
import { Modal, ConfirmModal } from '../components/Modal';
export default function Organizations() {
const queryClient = useQueryClient();
const [isFormOpen, setIsFormOpen] = useState(false);
const [editingOrg, setEditingOrg] = useState<Organization | null>(null);
// 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, isLoading } = useQuery({
queryKey: ['organizations'],
queryFn: () => getOrganizations(),
});
const createMutation = useMutation({
mutationFn: createOrganization,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['organizations'] });
setIsFormOpen(false);
setModalMessage('✅ Организация успешно добавлена');
setShowModal(true);
},
onError: (error: Error) => {
setModalMessage(`❌ Ошибка: ${error.message}`);
setShowModal(true);
},
});
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: any }) => updateOrganization(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['organizations'] });
setEditingOrg(null);
setModalMessage('✅ Организация успешно обновлена');
setShowModal(true);
},
onError: (error: Error) => {
setModalMessage(`❌ Ошибка: ${error.message}`);
setShowModal(true);
},
});
const deleteMutation = useMutation({
mutationFn: deleteOrganization,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['organizations'] });
setModalMessage('✅ Организация успешно удалена');
setShowModal(true);
},
onError: (error: Error) => {
setModalMessage(`❌ Ошибка: ${error.message}`);
setShowModal(true);
},
});
const scanMutation = useMutation({
mutationFn: scanOrganization,
onSuccess: (result) => {
queryClient.invalidateQueries({ queryKey: ['organizations'] });
queryClient.invalidateQueries({ queryKey: ['tasks'] });
let message = `✅ Сканирование завершено!\n\n`;
message += `📦 Репозиториев найдено: ${result.repositories_found}\n`;
message += ` Репозиториев добавлено: ${result.repositories_added}\n`;
message += `🔀 PR найдено: ${result.pull_requests_found}\n`;
message += `📝 Задач создано: ${result.tasks_created}`;
if (result.errors.length > 0) {
message += `\n\n⚠ Ошибки:\n${result.errors.join('\n')}`;
}
setModalMessage(message);
setShowModal(true);
},
onError: (error: Error) => {
setModalMessage(`❌ Ошибка сканирования: ${error.message}`);
setShowModal(true);
},
});
const handleDelete = (org: Organization) => {
setConfirmMessage(`Вы уверены, что хотите удалить организацию "${org.name}"?`);
setConfirmAction(() => () => deleteMutation.mutate(org.id));
setShowConfirm(true);
};
const handleScan = (org: Organization) => {
setConfirmMessage(`Начать сканирование организации "${org.name}"?\n\удут найдены все репозитории и PR.`);
setConfirmAction(() => () => scanMutation.mutate(org.id));
setShowConfirm(true);
};
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 className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Организации</h1>
<p className="text-gray-600 mt-1">
Управление организациями и автоматическое сканирование репозиториев
</p>
</div>
<button
onClick={() => setIsFormOpen(true)}
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
>
Добавить организацию
</button>
</div>
{/* Organizations list */}
<div className="grid gap-4">
{data?.items.map((org) => (
<div
key={org.id}
className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3">
<h3 className="text-xl font-semibold text-gray-900">{org.name}</h3>
<span className={`px-2 py-1 rounded text-xs font-medium ${
org.is_active ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
}`}>
{org.is_active ? 'Активна' : 'Неактивна'}
</span>
<span className="px-2 py-1 rounded text-xs font-medium bg-blue-100 text-blue-800">
{org.platform.toUpperCase()}
</span>
</div>
<div className="mt-2 space-y-1 text-sm text-gray-600">
<div>🌐 {org.base_url}</div>
{org.last_scan_at && (
<div>
🔍 Последнее сканирование:{' '}
{new Date(org.last_scan_at).toLocaleString('ru-RU')}
</div>
)}
</div>
<div className="mt-3 text-xs text-gray-500">
Webhook: <code className="bg-gray-100 px-2 py-1 rounded">{org.webhook_url}</code>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => handleScan(org)}
disabled={scanMutation.isPending}
className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors disabled:opacity-50"
>
🔍 Сканировать
</button>
<button
onClick={() => setEditingOrg(org)}
className="px-3 py-1.5 text-sm bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors"
>
Изменить
</button>
<button
onClick={() => handleDelete(org)}
className="px-3 py-1.5 text-sm bg-red-100 text-red-700 rounded hover:bg-red-200 transition-colors"
>
🗑 Удалить
</button>
</div>
</div>
</div>
))}
</div>
{data?.items.length === 0 && (
<div className="text-center py-12 bg-gray-50 rounded-lg">
<p className="text-gray-500">Нет организаций</p>
<button
onClick={() => setIsFormOpen(true)}
className="mt-4 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
>
Добавить первую организацию
</button>
</div>
)}
{/* Create Form Modal */}
{isFormOpen && (
<OrganizationForm
onSubmit={(data) => createMutation.mutate(data)}
onCancel={() => setIsFormOpen(false)}
isSubmitting={createMutation.isPending}
/>
)}
{/* Edit Form Modal */}
{editingOrg && (
<OrganizationForm
organization={editingOrg}
onSubmit={(data) => updateMutation.mutate({ id: editingOrg.id, data })}
onCancel={() => setEditingOrg(null)}
isSubmitting={updateMutation.isPending}
/>
)}
{/* 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>
);
}
// Organization Form Component
function OrganizationForm({
organization,
onSubmit,
onCancel,
isSubmitting,
}: {
organization?: Organization;
onSubmit: (data: OrganizationCreate) => void;
onCancel: () => void;
isSubmitting: boolean;
}) {
const [formData, setFormData] = useState<OrganizationCreate>({
name: organization?.name || '',
platform: (organization?.platform as OrganizationPlatform) || 'gitea',
base_url: organization?.base_url || '',
api_token: '',
webhook_secret: '',
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit(formData);
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-6 max-w-md w-full mx-4">
<h2 className="text-2xl font-bold mb-4">
{organization ? 'Редактировать организацию' : 'Новая организация'}
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Название организации *
</label>
<input
type="text"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
placeholder="inno-js"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Платформа *
</label>
<select
value={formData.platform}
onChange={(e) => setFormData({ ...formData, platform: e.target.value as OrganizationPlatform })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
>
<option value="gitea">Gitea</option>
<option value="github">GitHub</option>
<option value="bitbucket">Bitbucket</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Base URL *
</label>
<input
type="url"
required
value={formData.base_url}
onChange={(e) => setFormData({ ...formData, base_url: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
placeholder="https://git.example.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
API токен
</label>
<input
type="password"
value={formData.api_token}
onChange={(e) => setFormData({ ...formData, api_token: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Опционально (используется master токен если не указан)"
/>
<p className="text-xs text-gray-500 mt-1">
💡 Если не указан, будет использован master токен из конфигурации сервера
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Webhook Secret
</label>
<input
type="text"
value={formData.webhook_secret}
onChange={(e) => setFormData({ ...formData, webhook_secret: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Опционально (генерируется автоматически)"
/>
</div>
<div className="flex gap-3 pt-4">
<button
type="submit"
disabled={isSubmitting}
className="flex-1 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50"
>
{isSubmitting ? 'Сохранение...' : organization ? 'Сохранить' : 'Создать'}
</button>
<button
type="button"
onClick={onCancel}
className="flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors"
>
Отмена
</button>
</div>
</form>
</div>
</div>
);
}