'use client' import { useState, useMemo, useRef } from 'react' import { ConfigType, Environment } from '@/types' import { cn, typeColor } from '@/lib/utils' import { FileText, Upload, Sparkles, AlertCircle, Check, X, Trash2, ArrowRight, Loader2, Key, Database, RefreshCw } from 'lucide-react' interface BulkImportModalProps { environmentId: string environment: Environment appId: string existingKeys: string[] onClose: () => void onSaved: () => void } interface ParsedEntry { id: string key: string value: string type: ConfigType description?: string isConflict: boolean } // Smart type detector function detectType(key: string, value: string): ConfigType { const upperKey = key.toUpperCase() const lowerVal = value.toLowerCase().trim() if ( upperKey.includes('SECRET') || upperKey.includes('KEY') || upperKey.includes('TOKEN') || upperKey.includes('PASSWORD') || upperKey.includes('AUTH') || upperKey.includes('PRIVATE') || upperKey.includes('CERT') || upperKey.includes('SIGNING') ) { return 'secret' } if (lowerVal.startsWith('http://') || lowerVal.startsWith('https://')) { return 'url' } if (lowerVal === 'true' || lowerVal === 'false') { return 'boolean' } if (/^-?\d+(\.\d+)?$/.test(lowerVal) && !lowerVal.startsWith('0') && lowerVal.length < 15) { return 'number' } if ( (lowerVal.startsWith('{') && lowerVal.endsWith('}')) || (lowerVal.startsWith('[') && lowerVal.endsWith(']')) ) { try { JSON.parse(value) return 'json' } catch { // not valid JSON } } return 'text' } export function BulkImportModal({ environmentId, environment, appId, existingKeys, onClose, onSaved, }: BulkImportModalProps) { const [rawText, setRawText] = useState('') const [overwrite, setOverwrite] = useState(true) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [customParsed, setCustomParsed] = useState(null) const fileInputRef = useRef(null) const existingKeysSet = useMemo(() => new Set(existingKeys.map(k => k.toUpperCase())), [existingKeys]) // Parse raw text into structured entries const parsedEntries = useMemo(() => { if (customParsed !== null) return customParsed if (!rawText.trim()) return [] const text = rawText.trim() // 1. Try JSON parsing if (text.startsWith('{') && text.endsWith('}')) { try { const obj = JSON.parse(text) if (typeof obj === 'object' && obj !== null) { const list: ParsedEntry[] = [] Object.entries(obj).forEach(([k, v], idx) => { const strVal = typeof v === 'object' ? JSON.stringify(v) : String(v) const cleanKey = k.trim().toUpperCase().replace(/\s+/g, '_') list.push({ id: `json-${idx}-${cleanKey}`, key: cleanKey, value: strVal, type: detectType(cleanKey, strVal), isConflict: existingKeysSet.has(cleanKey), }) }) return list } } catch { // Fallback to line by line .env parsing } } // 2. Line by line .env format parsing const lines = text.split(/\r?\n/) const list: ParsedEntry[] = [] let pendingComment = '' lines.forEach((line, idx) => { const trimmed = line.trim() if (!trimmed) { pendingComment = '' return } // Comment line if (trimmed.startsWith('#')) { pendingComment = trimmed.replace(/^#+\s*/, '') return } // Check if export PREFIX exists (e.g. export KEY=VALUE) let lineToParse = trimmed if (lineToParse.startsWith('export ')) { lineToParse = lineToParse.slice(7).trim() } // Split at first `=` const eqIdx = lineToParse.indexOf('=') if (eqIdx !== -1) { const k = lineToParse.slice(0, eqIdx).trim() let v = lineToParse.slice(eqIdx + 1).trim() // Strip surrounding quotes if matching: "value" or 'value' if ( (v.startsWith('"') && v.endsWith('"') && v.length >= 2) || (v.startsWith("'") && v.endsWith("'") && v.length >= 2) ) { v = v.slice(1, -1) } const cleanKey = k.toUpperCase().replace(/\s+/g, '_') if (cleanKey) { list.push({ id: `line-${idx}-${cleanKey}`, key: cleanKey, value: v, type: detectType(cleanKey, v), description: pendingComment || undefined, isConflict: existingKeysSet.has(cleanKey), }) pendingComment = '' } } }) return list }, [rawText, customParsed, existingKeysSet]) function handleTypeChange(id: string, newType: ConfigType) { const updated = parsedEntries.map(e => e.id === id ? { ...e, type: newType } : e) setCustomParsed(updated) } function handleKeyChange(id: string, newKey: string) { const cleanKey = newKey.toUpperCase().replace(/\s+/g, '_') const updated = parsedEntries.map(e => e.id === id ? { ...e, key: cleanKey, isConflict: existingKeysSet.has(cleanKey) } : e) setCustomParsed(updated) } function handleValueChange(id: string, newVal: string) { const updated = parsedEntries.map(e => e.id === id ? { ...e, value: newVal } : e) setCustomParsed(updated) } function handleRemove(id: string) { const updated = parsedEntries.filter(e => e.id !== id) setCustomParsed(updated) } function handleFileUpload(e: React.ChangeEvent) { const file = e.target.files?.[0] if (!file) return const reader = new FileReader() reader.onload = () => { if (typeof reader.result === 'string') { setCustomParsed(null) setRawText(reader.result) } } reader.readAsText(file) } async function handleImport() { if (parsedEntries.length === 0) return setLoading(true) setError('') try { const res = await fetch(`/api/apps/${appId}/config/bulk`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ environment_id: environmentId, environment, overwrite, entries: parsedEntries.map(e => ({ key: e.key, value: e.value, type: e.type, description: e.description, })), }), }) const data = await res.json() setLoading(false) if (!res.ok) { setError(data.error || 'İçe aktarma sırasında bir hata oluştu.') return } onSaved() } catch { setLoading(false) setError('Ağ hatası oluştu. Lütfen tekrar deneyin.') } } const conflictsCount = parsedEntries.filter(e => e.isConflict).length return (
{/* Top glow accent */}
{/* Modal Header */}

Toplu Config & Secret Yapıştır / İçe Aktar

Hedef Ortam: {environment} • .env veya JSON formatında doğrudan yapıştırabilirsiniz.

{/* Modal Body */}
{/* Paste Input Area */}
{rawText && ( )}