import { useState, useCallback, useEffect } from 'react' import { X, Plus, Trash2, Database, Globe, FileJson, Play, ChevronRight, CheckCircle2, XCircle, Loader2, Eye, EyeOff, RefreshCw, } from 'lucide-react' import type { DataSource, DataCacheEntry } from '../App' const EMPTY: DataSource = { id: '', name: '', type: 'rest', url: '', method: 'GET', headers: '{}', body: '', staticData: '[\n {\n "id": 1,\n "title": "Example item",\n "description": "A sample data entry"\n }\n]', path: '', } function getFieldsFromData(data: Record[]): string[] { if (!data.length) return [] return Object.keys(data[0]) } function resolvePath(obj: unknown, path: string): unknown { if (!path.trim()) return obj return path.split('.').reduce((acc: unknown, key) => { if (acc && typeof acc === 'object') return (acc as Record)[key] return undefined }, obj) } async function fetchSource(source: DataSource): Promise<{ data: Record[]; error: string | null }> { if (source.type === 'static') { try { const parsed = JSON.parse(source.staticData || '[]') const arr = Array.isArray(parsed) ? parsed : [parsed] return { data: arr as Record[], error: null } } catch { return { data: [], error: 'Invalid JSON' } } } try { const hdrs: Record = { 'Content-Type': 'application/json' } try { Object.assign(hdrs, JSON.parse(source.headers || '{}')) } catch {} const res = await fetch(source.url, { method: source.method, headers: hdrs, body: source.method === 'POST' && source.body ? source.body : undefined, }) if (!res.ok) return { data: [], error: `HTTP ${res.status} ${res.statusText}` } const json = await res.json() const resolved = source.path ? resolvePath(json, source.path) : json const arr = Array.isArray(resolved) ? resolved : (resolved ? [resolved] : []) return { data: arr as Record[], error: null } } catch (e) { return { data: [], error: String(e) } } } // ── Example presets ──────────────────────────────────────────── const PRESETS = [ { label: 'JSONPlaceholder Posts', source: { ...EMPTY, name: 'Blog Posts', type: 'rest' as const, url: 'https://jsonplaceholder.typicode.com/posts', method: 'GET' as const, path: '' }, }, { label: 'JSONPlaceholder Users', source: { ...EMPTY, name: 'Users', type: 'rest' as const, url: 'https://jsonplaceholder.typicode.com/users', method: 'GET' as const, path: '' }, }, { label: 'Open Library Search', source: { ...EMPTY, name: 'Books', type: 'rest' as const, url: 'https://openlibrary.org/search.json?q=javascript&limit=10', method: 'GET' as const, path: 'docs' }, }, { label: 'Static JSON', source: { ...EMPTY, name: 'My Data', type: 'static' as const }, }, ] export function DataSourceManager({ dataSources, dataCache, onSave, onDelete, onRefresh, onClose, }: { dataSources: DataSource[] dataCache: Record onSave: (source: DataSource) => void onDelete: (id: string) => void onRefresh: (id: string) => void onClose: () => void }) { const [editing, setEditing] = useState(null) const [testResult, setTestResult] = useState<{ data: Record[]; error: string | null } | null>(null) const [testing, setTesting] = useState(false) const [showPreview, setShowPreview] = useState(false) const [confirmDelete, setConfirmDelete] = useState(null) const startNew = (preset?: DataSource) => { const id = `ds-${Date.now()}` setEditing({ ...(preset ?? EMPTY), id }) setTestResult(null) setShowPreview(false) } const handleTest = useCallback(async () => { if (!editing) return setTesting(true) setTestResult(null) const result = await fetchSource(editing) setTestResult(result) setTesting(false) setShowPreview(true) }, [editing]) const handleSave = () => { if (!editing || !editing.name.trim()) return onSave(editing) setEditing(null) setTestResult(null) } const fields = testResult?.data ? getFieldsFromData(testResult.data) : [] return (
e.stopPropagation()} > {/* Source list */}

Data Sources

{dataSources.length} configured

{/* Presets */} {dataSources.length === 0 && !editing && (

Quick start

{PRESETS.map(p => ( ))}
)}
{dataSources.length === 0 && (

No data sources yet

)} {dataSources.map(source => { const cache = dataCache[source.id] const isEditing = editing?.id === source.id return (
{ setEditing({ ...source }); setTestResult(null); setShowPreview(false) }} style={{ padding: '0.625rem 0.75rem', borderRadius: '8px', cursor: 'pointer', border: isEditing ? '2px solid #6366f1' : '2px solid transparent', background: isEditing ? '#f5f3ff' : 'transparent', marginBottom: '2px', transition: 'all 0.1s', }} onMouseEnter={e => { if (!isEditing) (e.currentTarget as HTMLDivElement).style.background = '#f1f5f9' }} onMouseLeave={e => { if (!isEditing) (e.currentTarget as HTMLDivElement).style.background = 'transparent' }} >
{source.type === 'static' ? : } {source.name} {cache?.loading && } {!cache?.loading && cache?.error && } {!cache?.loading && !cache?.error && cache?.data?.length > 0 && }
{source.type === 'static' ? 'Static' : `${source.method} · ${source.url.split('/').slice(0, 4).join('/')}`} {cache?.data && {cache.data.length} items}
) })}
{/* Edit pane */}
{/* Header */}
{editing ? (editing.name || 'New data source') : 'Data Sources'}
{editing ? (
{/* Form */}
setEditing(s => s ? { ...s, name: e.target.value } : s)} placeholder="My API" />
{editing.type === 'rest' && (
)} {editing.type === 'rest' && (
setEditing(s => s ? { ...s, url: e.target.value } : s)} placeholder="https://api.example.com/items" />
)} {editing.type === 'rest' && (
setEditing(s => s ? { ...s, path: e.target.value } : s)} placeholder="data.results" />
)} {editing.type === 'rest' && (
setEditing(s => s ? { ...s, headers: e.target.value } : s)} placeholder='{"Authorization": "Bearer ..."}' />
)} {editing.type === 'rest' && editing.method === 'POST' && (