Files
prod-end-2026/frontend/src/components/shared/SwaggerImportModal.tsx
T
2026-03-17 18:32:44 +03:00

222 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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.
import React, { useState, useRef, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { FileCode, Upload, Loader2, FileJson, CheckCircle2 } from 'lucide-react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { toast } from 'sonner';
import { apiRequest } from '@/lib/api';
import { ENDPOINTS } from '@/constants/api';
interface SwaggerImportModalProps {
isOpen: boolean;
onClose: () => void;
onImport: (data: any, filename?: string) => void;
}
export const SwaggerImportModal: React.FC<SwaggerImportModalProps> = ({
isOpen,
onClose,
onImport,
}) => {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [spec, setSpec] = useState('');
const [isImporting, setIsImporting] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// Reset state when modal is closed
useEffect(() => {
if (!isOpen) {
setSelectedFile(null);
setSpec('');
setIsImporting(false);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
}, [isOpen]);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
if (file.type !== 'application/json' && !file.name.endsWith('.json') && !file.name.endsWith('.yaml') && !file.name.endsWith('.yml')) {
toast.error('Пожалуйста, выберите JSON или YAML файл');
return;
}
setSelectedFile(file);
}
};
const handleImportFile = async () => {
if (!selectedFile) {
toast.error('Пожалуйста, выберите файл');
return;
}
setIsImporting(true);
try {
const formData = new FormData();
// Отправляем файл напрямую как multipart/form-data
formData.append('file', selectedFile);
const result = await apiRequest<any>(ENDPOINTS.ACTIONS.INGEST, {
method: 'POST',
body: formData,
});
toast.success(`Файл ${selectedFile.name} успешно импортирован на сервер`);
onImport(result, selectedFile.name);
onClose();
} catch (error: any) {
toast.error(error.message || 'Ошибка при отправке файла на сервер');
console.error(error);
} finally {
setIsImporting(false);
}
};
const handleImportByContent = async () => {
if (!spec) {
toast.error('Пожалуйста, вставьте содержимое спецификации');
return;
}
setIsImporting(true);
try {
const formData = new FormData();
// Создаем блоб из текста и отправляем как файл
const specBlob = new Blob([spec], { type: 'application/json' });
formData.append('file', specBlob, 'manual_import.json');
const result = await apiRequest<any>(ENDPOINTS.ACTIONS.INGEST, {
method: 'POST',
body: formData,
});
toast.success('Методы успешно импортированы на сервер');
onImport(result, 'manual_import.json');
onClose();
} catch (error: any) {
toast.error(error.message || 'Ошибка при отправке спецификации на сервер');
console.error(error);
} finally {
setIsImporting(false);
}
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[600px] bg-card border-border">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FileCode className="h-5 w-5 text-primary" />
Import Swagger / OpenAPI
</DialogTitle>
<DialogDescription>
Выберите способ импорта вашей API спецификации для создания новых Actions.
</DialogDescription>
</DialogHeader>
<Tabs defaultValue="file" className="w-full mt-4">
<TabsList className="grid w-full grid-cols-2 bg-muted/50">
<TabsTrigger value="file" className="gap-2">
<FileJson className="h-4 w-4" />
Upload JSON
</TabsTrigger>
<TabsTrigger value="content" className="gap-2">
<Upload className="h-4 w-4" />
Paste JSON/YAML
</TabsTrigger>
</TabsList>
<TabsContent value="file" className="space-y-4 pt-4">
<div className="space-y-4">
<div
className={`border-2 border-dashed rounded-xl p-8 flex flex-col items-center justify-center transition-colors cursor-pointer ${selectedFile ? 'border-primary/50 bg-primary/5' : 'border-border hover:border-primary/30 hover:bg-muted/50'
}`}
onClick={() => fileInputRef.current?.click()}
>
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept=".json,.yaml,.yml"
className="hidden"
/>
{selectedFile ? (
<>
<div className="bg-primary/10 p-3 rounded-full mb-3">
<CheckCircle2 className="h-8 w-8 text-primary" />
</div>
<p className="text-sm font-medium text-foreground">{selectedFile.name}</p>
<p className="text-xs text-muted-foreground mt-1">
{(selectedFile.size / 1024).toFixed(2)} KB Нажмите, чтобы изменить
</p>
</>
) : (
<>
<div className="bg-muted p-3 rounded-full mb-3">
<Upload className="h-8 w-8 text-muted-foreground" />
</div>
<p className="text-sm font-medium text-foreground">Выберите JSON файл</p>
<p className="text-xs text-muted-foreground mt-1">
Перетащите файл сюда или нажмите для поиска
</p>
</>
)}
</div>
<div className="text-xs text-muted-foreground bg-muted/30 p-3 rounded-lg flex items-start gap-2">
<FileCode className="h-4 w-4 mt-0.5 shrink-0" />
<span>Поддерживаются форматы JSON и YAML (OpenAPI 3.0/ Swagger 2.0). Файл будет обработан локально в вашем браузере.</span>
</div>
</div>
<Button
className="w-full gap-2"
onClick={handleImportFile}
disabled={isImporting || !selectedFile}
>
{isImporting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Upload className="h-4 w-4" />}
{isImporting ? 'Обработка...' : 'Импортировать из файла'}
</Button>
</TabsContent>
<TabsContent value="content" className="space-y-4 pt-4">
<div className="space-y-2">
<Label htmlFor="swagger-content">Content</Label>
<Textarea
id="swagger-content"
placeholder='{"openapi": "3.0.0", ...}'
className="min-h-[250px] font-mono text-xs bg-background border-border"
value={spec}
onChange={(e) => setSpec(e.target.value)}
/>
</div>
<Button
className="w-full gap-2"
onClick={handleImportByContent}
disabled={isImporting}
>
{isImporting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Upload className="h-4 w-4" />}
{isImporting ? 'Импорт...' : 'Импортировать методы'}
</Button>
</TabsContent>
</Tabs>
<DialogFooter className="sm:justify-start">
<p className="text-[10px] text-muted-foreground uppercase tracking-wider font-semibold">
SECURE LOCAL PROCESSING VALIDATED BY AI
</p>
</DialogFooter>
</DialogContent>
</Dialog>
);
};