new file: .claude/settings.local.json
new file: .gitignore new file: backend/.env.example new file: backend/.gitignore new file: backend/ecosystem.config.js new file: backend/nodemon.json new file: backend/package-lock.json new file: backend/package.json new file: backend/src/config/database.ts new file: backend/src/config/dynamicSwagger.ts new file: backend/src/config/environment.ts new file: backend/src/config/swagger.ts new file: backend/src/controllers/apiKeyController.ts new file: backend/src/controllers/authController.ts new file: backend/src/controllers/databaseController.ts new file: backend/src/controllers/databaseManagementController.ts new file: backend/src/controllers/dynamicApiController.ts new file: backend/src/controllers/endpointController.ts new file: backend/src/controllers/folderController.ts new file: backend/src/controllers/logsController.ts new file: backend/src/controllers/userController.ts new file: backend/src/middleware/apiKey.ts new file: backend/src/middleware/auth.ts new file: backend/src/middleware/logging.ts new file: backend/src/migrations/001_initial_schema.sql new file: backend/src/migrations/002_add_logging.sql new file: backend/src/migrations/003_add_scripting.sql new file: backend/src/migrations/004_add_superadmin.sql new file: backend/src/migrations/run.ts new file: backend/src/migrations/seed.ts new file: backend/src/routes/apiKeys.ts new file: backend/src/routes/auth.ts new file: backend/src/routes/databaseManagement.ts new file: backend/src/routes/databases.ts new file: backend/src/routes/dynamic.ts new file: backend/src/routes/endpoints.ts new file: backend/src/routes/folders.ts new file: backend/src/routes/logs.ts new file: backend/src/routes/users.ts new file: backend/src/server.ts new file: backend/src/services/DatabasePoolManager.ts new file: backend/src/services/ScriptExecutor.ts new file: backend/src/services/SqlExecutor.ts new file: backend/src/types/index.ts new file: backend/tsconfig.json new file: frontend/.gitignore new file: frontend/index.html new file: frontend/nginx.conf new file: frontend/package-lock.json new file: frontend/package.json new file: frontend/postcss.config.js new file: frontend/src/App.tsx new file: frontend/src/components/CodeEditor.tsx
This commit is contained in:
186
frontend/src/pages/Endpoints.tsx
Normal file
186
frontend/src/pages/Endpoints.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import { useState } 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 toast from 'react-hot-toast';
|
||||
import EndpointModal from '@/components/EndpointModal';
|
||||
import Dialog from '@/components/Dialog';
|
||||
|
||||
export default function Endpoints() {
|
||||
const queryClient = useQueryClient();
|
||||
const [search, setSearch] = useState('');
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingEndpoint, setEditingEndpoint] = useState<Endpoint | null>(null);
|
||||
const [dialog, setDialog] = useState<{
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
type: 'alert' | 'confirm';
|
||||
onConfirm?: () => void;
|
||||
}>({
|
||||
isOpen: false,
|
||||
title: '',
|
||||
message: '',
|
||||
type: 'alert',
|
||||
});
|
||||
|
||||
const { data: endpoints, isLoading } = useQuery({
|
||||
queryKey: ['endpoints', search],
|
||||
queryFn: () => endpointsApi.getAll(search).then(res => res.data),
|
||||
});
|
||||
|
||||
const { data: databases } = useQuery({
|
||||
queryKey: ['databases'],
|
||||
queryFn: () => databasesApi.getAll().then(res => res.data),
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => endpointsApi.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['endpoints'] });
|
||||
toast.success('Эндпоинт успешно удален');
|
||||
},
|
||||
onError: () => toast.error('Не удалось удалить эндпоинт'),
|
||||
});
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
setDialog({
|
||||
isOpen: true,
|
||||
title: 'Подтверждение',
|
||||
message: 'Вы уверены, что хотите удалить этот эндпоинт?',
|
||||
type: 'confirm',
|
||||
onConfirm: () => {
|
||||
deleteMutation.mutate(id);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleEdit = (endpoint: Endpoint) => {
|
||||
setEditingEndpoint(endpoint);
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditingEndpoint(null);
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<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>
|
||||
|
||||
<div className="card p-4 mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Search size={20} className="text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Поиск эндпоинтов по имени, пути или SQL запросу..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="flex-1 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Загрузка эндпоинтов...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{endpoints?.map((endpoint) => (
|
||||
<div key={endpoint.id} className="card p-6 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-xl font-semibold text-gray-900">{endpoint.name}</h3>
|
||||
<span className={`px-3 py-1 text-xs font-semibold rounded ${
|
||||
endpoint.method === 'GET' ? 'bg-green-100 text-green-700' :
|
||||
endpoint.method === 'POST' ? 'bg-blue-100 text-blue-700' :
|
||||
endpoint.method === 'PUT' ? 'bg-yellow-100 text-yellow-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
{endpoint.method}
|
||||
</span>
|
||||
{endpoint.is_public && (
|
||||
<span className="px-3 py-1 text-xs font-semibold rounded bg-purple-100 text-purple-700">
|
||||
Публичный
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-gray-600 mb-2">{endpoint.description}</p>
|
||||
<code className="text-sm bg-gray-100 px-3 py-1 rounded text-gray-800">
|
||||
{endpoint.path}
|
||||
</code>
|
||||
{endpoint.folder_name && (
|
||||
<span className="ml-2 text-sm text-gray-500">📁 {endpoint.folder_name}</span>
|
||||
)}
|
||||
{endpoint.parameters && endpoint.parameters.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<span className="text-xs text-gray-500">Параметры: </span>
|
||||
{endpoint.parameters.map((param: any, idx: number) => (
|
||||
<span key={idx} className="inline-block text-xs bg-gray-200 text-gray-700 px-2 py-1 rounded mr-1 mb-1">
|
||||
{param.name} ({param.type}){param.required && '*'}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleEdit(endpoint)}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
title="Редактировать"
|
||||
>
|
||||
<Edit2 size={18} className="text-gray-600" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(endpoint.id)}
|
||||
className="p-2 hover:bg-red-50 rounded-lg transition-colors"
|
||||
title="Удалить"
|
||||
>
|
||||
<Trash2 size={18} className="text-red-600" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{endpoints?.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500">Эндпоинты не найдены. Создайте первый эндпоинт!</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showModal && (
|
||||
<EndpointModal
|
||||
endpoint={editingEndpoint}
|
||||
databases={databases || []}
|
||||
onClose={() => setShowModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Dialog
|
||||
isOpen={dialog.isOpen}
|
||||
onClose={() => setDialog({ ...dialog, isOpen: false })}
|
||||
title={dialog.title}
|
||||
message={dialog.message}
|
||||
type={dialog.type}
|
||||
onConfirm={dialog.onConfirm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user