Переработано окно эндпоинта, добавлены элементы дебага, добавлена возможность сохранять и загружать конфигурацию эндпоинта, добавлено отображение ошибок при загрузке конфигурации. Исправлены мелкие баги.

This commit is contained in:
2026-03-01 16:00:26 +03:00
parent 7e2e0103fe
commit 6766cd81a1
15 changed files with 1677 additions and 172 deletions

View File

@@ -1,10 +1,11 @@
import { useState } from 'react';
import { useState, useRef } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { endpointsApi, databasesApi } from '@/services/api';
import { Endpoint } from '@/types';
import { Plus, Search, Edit2, Trash2 } from 'lucide-react';
import { Endpoint, ImportPreviewResponse } from '@/types';
import { Plus, Search, Edit2, Trash2, Download, Upload } from 'lucide-react';
import toast from 'react-hot-toast';
import EndpointModal from '@/components/EndpointModal';
import ImportEndpointModal from '@/components/ImportEndpointModal';
import Dialog from '@/components/Dialog';
export default function Endpoints() {
@@ -12,6 +13,10 @@ export default function Endpoints() {
const [search, setSearch] = useState('');
const [showModal, setShowModal] = useState(false);
const [editingEndpoint, setEditingEndpoint] = useState<Endpoint | null>(null);
const [showImportModal, setShowImportModal] = useState(false);
const [importFile, setImportFile] = useState<File | null>(null);
const [importPreview, setImportPreview] = useState<ImportPreviewResponse | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const [dialog, setDialog] = useState<{
isOpen: boolean;
title: string;
@@ -66,6 +71,42 @@ export default function Endpoints() {
setShowModal(true);
};
const handleExport = async (endpointId: string, endpointName: string) => {
try {
const response = await endpointsApi.exportEndpoint(endpointId);
const blob = new Blob([response.data]);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${endpointName.replace(/[^a-zA-Z0-9_\-а-яА-ЯёЁ]/g, '_')}.kabe`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success('Эндпоинт экспортирован');
} catch {
toast.error('Ошибка экспорта эндпоинта');
}
};
const handleImportFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.name.endsWith('.kabe')) {
toast.error('Выберите файл с расширением .kabe');
return;
}
try {
const response = await endpointsApi.importPreview(file);
setImportFile(file);
setImportPreview(response.data);
setShowImportModal(true);
} catch (error: any) {
toast.error(error.response?.data?.error || 'Ошибка чтения файла');
}
e.target.value = '';
};
return (
<div>
<div className="flex items-center justify-between mb-6">
@@ -73,10 +114,26 @@ export default function Endpoints() {
<h1 className="text-3xl font-bold text-gray-900 mb-2">API Эндпоинты</h1>
<p className="text-gray-600">Управление динамическими API эндпоинтами</p>
</div>
<button onClick={handleCreate} className="btn btn-primary flex items-center gap-2">
<Plus size={20} />
Новый эндпоинт
</button>
<div className="flex gap-2">
<input
ref={fileInputRef}
type="file"
accept=".kabe"
className="hidden"
onChange={handleImportFileSelect}
/>
<button
onClick={() => fileInputRef.current?.click()}
className="btn btn-secondary flex items-center gap-2"
>
<Upload size={20} />
Импорт
</button>
<button onClick={handleCreate} className="btn btn-primary flex items-center gap-2">
<Plus size={20} />
Новый эндпоинт
</button>
</div>
</div>
<div className="card p-4 mb-6">
@@ -138,6 +195,13 @@ export default function Endpoints() {
)}
</div>
<div className="flex gap-2">
<button
onClick={() => handleExport(endpoint.id, endpoint.name)}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
title="Экспорт"
>
<Download size={18} className="text-gray-600" />
</button>
<button
onClick={() => handleEdit(endpoint)}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
@@ -173,6 +237,18 @@ export default function Endpoints() {
/>
)}
{showImportModal && importPreview && importFile && (
<ImportEndpointModal
preview={importPreview}
file={importFile}
onClose={() => {
setShowImportModal(false);
setImportFile(null);
setImportPreview(null);
}}
/>
)}
<Dialog
isOpen={dialog.isOpen}
onClose={() => setDialog({ ...dialog, isOpen: false })}