/** * Database Management Page * Multi-database comparison UI for DuckLake, PostgreSQL, TypeDB, Oxigraph, and Qdrant * * DuckLake is the primary analytics database (server-side DuckDB with lakehouse features) * Falls back to DuckDB-WASM for in-browser queries when server is unavailable * * Qdrant is the vector database for semantic search and embedding visualization */ import { useState } from 'react'; import { useLanguage } from '../contexts/LanguageContext'; import { DuckLakePanel } from '../components/database/DuckLakePanel'; import { PostgreSQLPanel } from '../components/database/PostgreSQLPanel'; import { TypeDBPanel } from '../components/database/TypeDBPanel'; import { OxigraphPanel } from '../components/database/OxigraphPanel'; import { QdrantPanel } from '../components/database/QdrantPanel'; import { SyncPanel } from '../components/database/SyncPanel'; import { useDuckDB } from '../hooks/useDuckDB'; import { useDuckLake } from '../hooks/useDuckLake'; import { usePostgreSQL } from '../hooks/usePostgreSQL'; import { useTypeDB } from '../hooks/useTypeDB'; import { useOxigraph } from '../hooks/useOxigraph'; import { useQdrant } from '../hooks/useQdrant'; import './Database.css'; // Database types - DuckLake is primary (with DuckDB-WASM fallback) type DatabaseType = 'duckdb' | 'postgres' | 'typedb' | 'oxigraph' | 'qdrant'; interface DatabaseInfo { id: DatabaseType; name: string; description: { nl: string; en: string }; icon: string; color: string; } // Stats interface for status cards interface DatabaseStats { isConnected: boolean; isLoading: boolean; primaryStat: number; primaryLabel: string; secondaryStat: number; secondaryLabel: string; tertiaryStat?: number; tertiaryLabel?: string; responseTimeMs?: number; } const DATABASES: DatabaseInfo[] = [ { id: 'duckdb', name: 'DuckLake', description: { nl: 'Lakehouse met tijdreizen, ACID-transacties en schema-evolutie', en: 'Lakehouse with time travel, ACID transactions, and schema evolution', }, icon: '🦆', color: '#FFC107', }, { id: 'postgres', name: 'PostgreSQL', description: { nl: 'Relationele database voor gestructureerde erfgoeddata', en: 'Relational database for structured heritage data', }, icon: '🐘', color: '#336791', }, { id: 'typedb', name: 'TypeDB', description: { nl: 'Kennisgraaf voor complexe erfgoedrelaties', en: 'Knowledge graph for complex heritage relationships', }, icon: '🔷', color: '#6B5CE7', }, { id: 'oxigraph', name: 'Oxigraph', description: { nl: 'SPARQL-triplestore voor Linked Data', en: 'SPARQL triplestore for Linked Data', }, icon: '🔗', color: '#00A86B', }, { id: 'qdrant', name: 'Qdrant', description: { nl: 'Vector database voor semantisch zoeken en embeddings', en: 'Vector database for semantic search and embeddings', }, icon: '⚡', color: '#DC2F5C', }, ]; // Bilingual text object for translations const TEXT = { // Page header pageTitle: { nl: 'Database Vergelijking', en: 'Database Comparison' }, pageSubtitle: { nl: 'Vergelijk en beheer meerdere database-backends voor erfgoeddata', en: 'Compare and manage multiple database backends for heritage data', }, // Database tabs allDatabases: { nl: 'Alle databases', en: 'All Databases' }, // Status indicators connected: { nl: 'Verbonden', en: 'Connected' }, disconnected: { nl: 'Niet verbonden', en: 'Disconnected' }, loading: { nl: 'Laden...', en: 'Loading...' }, // Quick comparison quickComparison: { nl: 'Gegevensbanken', en: 'Databases' }, database: { nl: 'Database', en: 'Database' }, status: { nl: 'Status', en: 'Status' }, dataCount: { nl: 'Data', en: 'Data' }, responseTime: { nl: 'Responstijd', en: 'Response Time' }, lastChecked: { nl: 'Laatst gecontroleerd', en: 'Last Checked' }, // Data upload uploadData: { nl: 'Data uploaden', en: 'Upload Data' }, uploadDescription: { nl: 'Upload verrijkte NDE-bestanden naar geselecteerde databases', en: 'Upload enriched NDE files to selected databases', }, // Info databaseTypes: { nl: 'Database types', en: 'Database Types' }, ducklakeDescription: { nl: 'Lakehouse - ACID transacties, tijdreizen, schema-evolutie. Server-side DuckDB met DuckLake extensie.', en: 'Lakehouse - ACID transactions, time travel, schema evolution. Server-side DuckDB with DuckLake extension.', }, relationalDescription: { nl: 'Relational - Gestructureerde data met ACID-garanties', en: 'Relational - Structured data with ACID guarantees', }, graphDescription: { nl: 'Knowledge Graph - Complexe relaties en type-inferentie', en: 'Knowledge Graph - Complex relationships and type inference', }, rdfDescription: { nl: 'RDF Triplestore - Linked Data en SPARQL-queries', en: 'RDF Triplestore - Linked Data and SPARQL queries', }, vectorDescription: { nl: 'Vector Database - Semantisch zoeken en embedding-visualisatie', en: 'Vector Database - Semantic search and embedding visualization', }, }; export function Database() { const { language } = useLanguage(); const t = (key: keyof typeof TEXT) => TEXT[key][language]; const [activeDatabase, setActiveDatabase] = useState('all'); return (

{t('pageTitle')}

{t('pageSubtitle')}

{/* Content based on active tab */}
{activeDatabase === 'all' && } {activeDatabase === 'duckdb' && ( <> )} {activeDatabase === 'postgres' && ( <> )} {activeDatabase === 'typedb' && ( <> )} {activeDatabase === 'oxigraph' && ( <> )} {activeDatabase === 'qdrant' && ( <> )}
{/* Info Section - only show on overview */} {activeDatabase === 'all' && (

{t('databaseTypes')}

🦆
DuckLake

{t('ducklakeDescription')}

🐘
PostgreSQL

{t('relationalDescription')}

🔷
TypeDB

{t('graphDescription')}

🔗
Oxigraph

{t('rdfDescription')}

Qdrant

{t('vectorDescription')}

)}
); } /** * All Databases Overview View */ function AllDatabasesView({ language, onSelectDatabase }: { language: 'nl' | 'en'; onSelectDatabase: (db: DatabaseType) => void }) { const t = (key: keyof typeof TEXT) => TEXT[key][language]; // Initialize all database hooks const duckdb = useDuckDB(); const ducklake = useDuckLake(); // Server-side DuckDB with lakehouse features const postgres = usePostgreSQL(); const typedb = useTypeDB(); const oxigraph = useOxigraph(); const qdrant = useQdrant(); // Unified DuckDB stats: prefer DuckLake (server) if connected, fallback to WASM const duckdbConnected = ducklake.status.isConnected || duckdb.status.isConnected; const duckdbLoading = ducklake.isLoading || duckdb.isLoading; // Use DuckLake stats if available (production data), otherwise use WASM stats const duckdbTables = ducklake.status.isConnected ? (ducklake.stats?.totalTables ?? 0) : (duckdb.stats?.totalTables ?? 0); const duckdbRows = ducklake.status.isConnected ? (ducklake.stats?.totalRows ?? 0) : (duckdb.stats?.totalRows ?? 0); const duckdbSnapshots = ducklake.stats?.totalSnapshots ?? 0; const duckdbResponseTime = ducklake.status.isConnected ? ducklake.status.responseTimeMs : duckdb.status.responseTimeMs; // Create a map of database stats const dbStats: Record = { duckdb: { isConnected: duckdbConnected, isLoading: duckdbLoading, primaryStat: duckdbTables, primaryLabel: language === 'nl' ? 'tabellen' : 'tables', secondaryStat: duckdbRows, secondaryLabel: language === 'nl' ? 'rijen' : 'rows', // Show snapshots if DuckLake is connected (lakehouse feature) tertiaryStat: ducklake.status.isConnected ? duckdbSnapshots : undefined, tertiaryLabel: ducklake.status.isConnected ? 'snapshots' : undefined, responseTimeMs: duckdbResponseTime, }, postgres: { isConnected: postgres.status.isConnected, isLoading: postgres.isLoading, primaryStat: postgres.stats?.totalTables ?? 0, primaryLabel: language === 'nl' ? 'tabellen' : 'tables', secondaryStat: postgres.stats?.totalRows ?? 0, secondaryLabel: language === 'nl' ? 'rijen' : 'rows', responseTimeMs: postgres.status.responseTimeMs, }, typedb: { isConnected: typedb.status.isConnected, isLoading: typedb.isLoading, primaryStat: typedb.stats?.totalEntities ?? 0, primaryLabel: language === 'nl' ? 'entiteiten' : 'entities', secondaryStat: typedb.stats?.totalRelations ?? 0, secondaryLabel: language === 'nl' ? 'relaties' : 'relations', responseTimeMs: typedb.status.responseTimeMs, }, oxigraph: { isConnected: oxigraph.status.isConnected, isLoading: oxigraph.isLoading, primaryStat: oxigraph.stats?.totalTriples ?? 0, primaryLabel: language === 'nl' ? 'triples' : 'triples', secondaryStat: oxigraph.stats?.totalGraphs ?? 0, secondaryLabel: language === 'nl' ? 'grafen' : 'graphs', responseTimeMs: oxigraph.status.responseTimeMs, }, qdrant: { isConnected: qdrant.status.isConnected, isLoading: qdrant.isLoading, primaryStat: qdrant.stats?.totalCollections ?? 0, primaryLabel: language === 'nl' ? 'collecties' : 'collections', secondaryStat: qdrant.stats?.totalVectors ?? 0, secondaryLabel: language === 'nl' ? 'vectoren' : 'vectors', responseTimeMs: qdrant.status.responseTimeMs, }, }; return (
{/* Sync Panel - Synchronize YAML files to all databases */} {/* Database Cards Grid */}

{t('quickComparison')}

{DATABASES.map((db) => ( onSelectDatabase(db.id)} /> ))}
); } /** * Database Status Card for comparison view */ function DatabaseStatusCard({ db, language, stats, onClick, }: { db: DatabaseInfo; language: 'nl' | 'en'; stats: DatabaseStats; onClick: () => void; }) { // Format large numbers const formatNumber = (n: number) => { if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`; if (n >= 1000) return `${(n / 1000).toFixed(1)}K`; return n.toLocaleString(); }; return (
e.key === 'Enter' && onClick()} >
{db.icon}

{db.name}

{db.description[language]}

{stats.isLoading ? (
{language === 'nl' ? 'Laden...' : 'Loading...'}
) : stats.isConnected ? (
{language === 'nl' ? 'Verbonden' : 'Connected'} {stats.responseTimeMs && ( {stats.responseTimeMs}ms )}
{formatNumber(stats.primaryStat)} {stats.primaryLabel}
{formatNumber(stats.secondaryStat)} {stats.secondaryLabel}
{stats.tertiaryStat !== undefined && stats.tertiaryLabel && (
{formatNumber(stats.tertiaryStat)} {stats.tertiaryLabel}
)}
) : (
{language === 'nl' ? 'Niet verbonden' : 'Disconnected'}
)}
); }