code-review-agent/frontend/src/pages/Organizations.tsx

381 lines
15 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-dark-card rounded-lg shadow-sm border border-dark-border 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-dark-text-primary">{org.name}</h3>
<span className={`px-2 py-1 rounded text-xs font-medium border ${
org.is_active ? 'bg-green-900/30 text-green-400 border-green-700' : 'bg-dark-card text-dark-text-muted border-dark-border'
}`}>
{org.is_active ? 'Активна' : 'Неактивна'}
</span>
<span className="px-2 py-1 rounded text-xs font-medium bg-blue-900/30 text-blue-400 border border-blue-700">
{org.platform.toUpperCase()}
</span>
</div>
<div className="mt-2 space-y-1 text-sm text-dark-text-secondary">
<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-dark-text-muted">
Webhook: <code className="bg-dark-bg px-2 py-1 rounded border border-dark-border">{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-dark-hover text-dark-text-primary rounded hover:bg-dark-border transition-colors border border-dark-border"
>
Изменить
</button>
<button
onClick={() => handleDelete(org)}
className="px-3 py-1.5 text-sm bg-red-900/30 text-red-400 rounded hover:bg-red-900/50 transition-colors border border-red-700"
>
🗑 Удалить
</button>
</div>
</div>
</div>
))}
</div>
{data?.items.length === 0 && (
<div className="text-center py-12 bg-dark-card rounded-lg border border-dark-border">
<p className="text-dark-text-muted">Нет организаций</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-75 flex items-center justify-center z-50">
<div className="bg-dark-card rounded-lg shadow-xl p-6 max-w-md w-full mx-4 border border-dark-border">
<h2 className="text-2xl font-bold mb-4 text-dark-text-primary">
{organization ? 'Редактировать организацию' : 'Новая организация'}
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-dark-text-primary 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-dark-border bg-dark-bg text-dark-text-primary rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="inno-js"
/>
</div>
<div>
<label className="block text-sm font-medium text-dark-text-primary 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-dark-border bg-dark-bg text-dark-text-primary rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-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-dark-text-primary 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-dark-border bg-dark-bg text-dark-text-primary rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="https://git.example.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-dark-text-primary 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-dark-border bg-dark-bg text-dark-text-primary rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Опционально (используется master токен если не указан)"
/>
<p className="text-xs text-dark-text-muted mt-1">
💡 Если не указан, будет использован master токен из конфигурации сервера
</p>
</div>
<div>
<label className="block text-sm font-medium text-dark-text-primary 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-dark-border bg-dark-bg text-dark-text-primary rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Опционально (генерируется автоматически)"
/>
</div>
<div className="flex gap-3 pt-4">
<button
type="submit"
disabled={isSubmitting}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
>
{isSubmitting ? 'Сохранение...' : organization ? 'Сохранить' : 'Создать'}
</button>
<button
type="button"
onClick={onCancel}
className="flex-1 px-4 py-2 bg-dark-hover text-dark-text-primary border border-dark-border rounded-lg hover:bg-dark-border transition-colors"
>
Отмена
</button>
</div>
</form>
</div>
</div>
);
}