/** * DataSourceSelector - Full-featured dynamic data source picker * Fetches ALL available sources from backend with search/filter */ import { useState, useEffect, useMemo } from 'react'; import { Check, Database, RefreshCw, Plus, Search, X, Wifi, Globe, Cpu, Activity, Zap, Server, Brain, HardDrive } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { cn } from '@/lib/utils'; import { API_URL } from '@/config/api'; export interface DataSourceConfig { id: string; name: string; description: string; endpoint: string; type: 'rest' | 'websocket' | 'mcp' | 'graphql'; category: 'system' | 'data' | 'ai' | 'external' | 'realtime' | 'custom'; refreshInterval?: number; tags: string[]; active: boolean; } interface DataSourcesResponse { total: number; categories: string[]; types: string[]; sources: DataSourceConfig[]; } // Category icons const categoryIcons: Record = { system: Server, data: HardDrive, ai: Brain, external: Globe, realtime: Zap, custom: Cpu }; // Type colors const typeColors: Record = { rest: 'bg-green-500/20 text-green-400 border-green-500/30', websocket: 'bg-blue-500/20 text-blue-400 border-blue-500/30', mcp: 'bg-purple-500/20 text-purple-400 border-purple-500/30', graphql: 'bg-pink-500/20 text-pink-400 border-pink-500/30' }; interface DataSourceSelectorProps { /** Currently selected data source */ selectedSource?: DataSourceConfig; /** Called when a new source is selected */ onSourceChange: (source: DataSourceConfig) => void; /** Show as dialog (default) or inline */ inline?: boolean; /** Dialog open state (controlled) */ open?: boolean; /** Dialog close callback */ onOpenChange?: (open: boolean) => void; className?: string; } export function DataSourceSelector({ selectedSource, onSourceChange, inline = false, open, onOpenChange, className }: DataSourceSelectorProps) { const [sources, setSources] = useState([]); const [categories, setCategories] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [selectedCategory, setSelectedCategory] = useState('all'); const [testingSource, setTestingSource] = useState(null); // Fetch sources from backend const fetchSources = async () => { setIsLoading(true); setError(null); try { const response = await fetch(`${API_URL}/api/datasources`); if (!response.ok) throw new Error('Failed to fetch data sources'); const data: DataSourcesResponse = await response.json(); setSources(data.sources); setCategories(['all', ...data.categories]); } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error'); // Fallback to empty setSources([]); } finally { setIsLoading(false); } }; useEffect(() => { fetchSources(); }, []); // Filter sources based on search and category const filteredSources = useMemo(() => { return sources.filter(source => { // Category filter if (selectedCategory !== 'all' && source.category !== selectedCategory) { return false; } // Search filter if (searchQuery) { const query = searchQuery.toLowerCase(); return ( source.name.toLowerCase().includes(query) || source.description.toLowerCase().includes(query) || source.endpoint.toLowerCase().includes(query) || source.tags.some(t => t.toLowerCase().includes(query)) ); } return true; }); }, [sources, searchQuery, selectedCategory]); // Group by category for display const groupedSources = useMemo(() => { const groups: Record = {}; filteredSources.forEach(source => { if (!groups[source.category]) { groups[source.category] = []; } groups[source.category].push(source); }); return groups; }, [filteredSources]); // Test connection const testConnection = async (sourceId: string) => { setTestingSource(sourceId); try { await fetch(`${API_URL}/api/datasources/${sourceId}/test`, { method: 'POST' }); } catch { // Silent fail } finally { setTestingSource(null); } }; const handleSelect = (source: DataSourceConfig) => { onSourceChange(source); onOpenChange?.(false); }; const content = (
{/* Search Bar */}
setSearchQuery(e.target.value)} className="pl-10 pr-10" /> {searchQuery && ( )}
{/* Category Tabs */} {categories.map(cat => ( {cat === 'all' ? 'Alle' : cat} {cat === 'all' ? sources.length : sources.filter(s => s.category === cat).length} ))} {/* Loading State */} {isLoading && (
Henter datakilder...
)} {/* Error State */} {error && (

{error}

)} {/* Sources List */} {!isLoading && !error && (
{Object.entries(groupedSources).map(([category, categorySources]) => { const Icon = categoryIcons[category] || Database; return (
{/* Category Header */}
{category} ({categorySources.length})
{/* Sources Grid */}
{categorySources.map(source => { const isSelected = selectedSource?.id === source.id; const isTesting = testingSource === source.id; return (
handleSelect(source)} className={cn( "p-3 rounded-lg border cursor-pointer transition-all", "hover:border-primary/50 hover:bg-primary/5", isSelected ? "bg-primary/10 border-primary/50 ring-1 ring-primary/30" : "bg-secondary/30 border-border/30" )} >
{source.name} {source.type.toUpperCase()} {isSelected && ( )}

{source.description}

{source.endpoint} {source.refreshInterval && ( {Math.round(source.refreshInterval / 1000)}s )}
{/* Tags */} {source.tags.length > 0 && (
{source.tags.slice(0, 4).map(tag => ( {tag} ))} {source.tags.length > 4 && ( +{source.tags.length - 4} )}
)}
); })}
); })} {/* Empty State */} {filteredSources.length === 0 && (

{searchQuery ? `Ingen datakilder matcher "${searchQuery}"` : 'Ingen datakilder fundet'}

)}
)} {/* Footer Stats */}
{filteredSources.length} af {sources.length} datakilder
); // Inline mode if (inline) { return content; } // Dialog mode return ( Vælg Datakilde Vælg en datakilde fra listen nedenfor. Datakilder opdateres automatisk. {content} ); } // Compact trigger button for CyberCard export function DataSourceTrigger({ selectedSource, onClick, className }: { selectedSource?: DataSourceConfig; onClick: () => void; className?: string; }) { return ( ); } export default DataSourceSelector;