- Add SyncPanel component with bilingual (NL/EN) support - Add relative URL handling for production (bronhouder.nl) - Integrate SyncPanel into Database page - Show sync status for all 4 databases (DuckLake, PostgreSQL, Oxigraph, Qdrant) - Support dry-run mode and file limit options
449 lines
16 KiB
TypeScript
449 lines
16 KiB
TypeScript
/**
|
|
* 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<DatabaseType | 'all'>('all');
|
|
|
|
return (
|
|
<div className="database-page">
|
|
<header className="database-header">
|
|
<h1>{t('pageTitle')}</h1>
|
|
<p>{t('pageSubtitle')}</p>
|
|
</header>
|
|
|
|
{/* Content based on active tab */}
|
|
<div className="database-content">
|
|
{activeDatabase === 'all' && <AllDatabasesView language={language} onSelectDatabase={setActiveDatabase} />}
|
|
{activeDatabase === 'duckdb' && (
|
|
<>
|
|
<button className="back-button" onClick={() => setActiveDatabase('all')}>
|
|
← {language === 'nl' ? 'Terug naar overzicht' : 'Back to overview'}
|
|
</button>
|
|
<DuckLakePanel />
|
|
</>
|
|
)}
|
|
{activeDatabase === 'postgres' && (
|
|
<>
|
|
<button className="back-button" onClick={() => setActiveDatabase('all')}>
|
|
← {language === 'nl' ? 'Terug naar overzicht' : 'Back to overview'}
|
|
</button>
|
|
<PostgreSQLPanel />
|
|
</>
|
|
)}
|
|
{activeDatabase === 'typedb' && (
|
|
<>
|
|
<button className="back-button" onClick={() => setActiveDatabase('all')}>
|
|
← {language === 'nl' ? 'Terug naar overzicht' : 'Back to overview'}
|
|
</button>
|
|
<TypeDBPanel />
|
|
</>
|
|
)}
|
|
{activeDatabase === 'oxigraph' && (
|
|
<>
|
|
<button className="back-button" onClick={() => setActiveDatabase('all')}>
|
|
← {language === 'nl' ? 'Terug naar overzicht' : 'Back to overview'}
|
|
</button>
|
|
<OxigraphPanel />
|
|
</>
|
|
)}
|
|
{activeDatabase === 'qdrant' && (
|
|
<>
|
|
<button className="back-button" onClick={() => setActiveDatabase('all')}>
|
|
← {language === 'nl' ? 'Terug naar overzicht' : 'Back to overview'}
|
|
</button>
|
|
<QdrantPanel />
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Info Section - only show on overview */}
|
|
{activeDatabase === 'all' && (
|
|
<section className="info-card">
|
|
<h2>{t('databaseTypes')}</h2>
|
|
<div className="db-types-grid">
|
|
<div className="db-type-card" style={{ borderLeftColor: '#FFC107' }}>
|
|
<span className="db-type-icon">🦆</span>
|
|
<div>
|
|
<strong>DuckLake</strong>
|
|
<p>{t('ducklakeDescription')}</p>
|
|
</div>
|
|
</div>
|
|
<div className="db-type-card" style={{ borderLeftColor: '#336791' }}>
|
|
<span className="db-type-icon">🐘</span>
|
|
<div>
|
|
<strong>PostgreSQL</strong>
|
|
<p>{t('relationalDescription')}</p>
|
|
</div>
|
|
</div>
|
|
<div className="db-type-card" style={{ borderLeftColor: '#6B5CE7' }}>
|
|
<span className="db-type-icon">🔷</span>
|
|
<div>
|
|
<strong>TypeDB</strong>
|
|
<p>{t('graphDescription')}</p>
|
|
</div>
|
|
</div>
|
|
<div className="db-type-card" style={{ borderLeftColor: '#00A86B' }}>
|
|
<span className="db-type-icon">🔗</span>
|
|
<div>
|
|
<strong>Oxigraph</strong>
|
|
<p>{t('rdfDescription')}</p>
|
|
</div>
|
|
</div>
|
|
<div className="db-type-card" style={{ borderLeftColor: '#DC2F5C' }}>
|
|
<span className="db-type-icon">⚡</span>
|
|
<div>
|
|
<strong>Qdrant</strong>
|
|
<p>{t('vectorDescription')}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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<DatabaseType, DatabaseStats> = {
|
|
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 (
|
|
<div className="all-databases-view">
|
|
{/* Sync Panel - Synchronize YAML files to all databases */}
|
|
<SyncPanel language={language} />
|
|
|
|
{/* Database Cards Grid */}
|
|
<section className="comparison-section">
|
|
<h2>{t('quickComparison')}</h2>
|
|
<div className="comparison-grid">
|
|
{DATABASES.map((db) => (
|
|
<DatabaseStatusCard
|
|
key={db.id}
|
|
db={db}
|
|
language={language}
|
|
stats={dbStats[db.id]}
|
|
onClick={() => onSelectDatabase(db.id)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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 (
|
|
<div
|
|
className="db-status-card clickable"
|
|
style={{ '--db-color': db.color } as React.CSSProperties}
|
|
onClick={onClick}
|
|
role="button"
|
|
tabIndex={0}
|
|
onKeyDown={(e) => e.key === 'Enter' && onClick()}
|
|
>
|
|
<div className="db-status-header">
|
|
<span className="db-icon-large">{db.icon}</span>
|
|
<div>
|
|
<h3>{db.name}</h3>
|
|
<p>{db.description[language]}</p>
|
|
</div>
|
|
</div>
|
|
<div className="db-status-body">
|
|
{stats.isLoading ? (
|
|
<div className="status-placeholder">
|
|
<span className="status-dot loading" />
|
|
<span>{language === 'nl' ? 'Laden...' : 'Loading...'}</span>
|
|
</div>
|
|
) : stats.isConnected ? (
|
|
<div className="db-stats-grid">
|
|
<div className="db-stat-item">
|
|
<span className="status-dot connected" />
|
|
<span className="stat-label">{language === 'nl' ? 'Verbonden' : 'Connected'}</span>
|
|
{stats.responseTimeMs && (
|
|
<span className="stat-response-time">{stats.responseTimeMs}ms</span>
|
|
)}
|
|
</div>
|
|
<div className="db-stat-numbers">
|
|
<div className="stat-number">
|
|
<span className="stat-value">{formatNumber(stats.primaryStat)}</span>
|
|
<span className="stat-unit">{stats.primaryLabel}</span>
|
|
</div>
|
|
<div className="stat-number">
|
|
<span className="stat-value">{formatNumber(stats.secondaryStat)}</span>
|
|
<span className="stat-unit">{stats.secondaryLabel}</span>
|
|
</div>
|
|
{stats.tertiaryStat !== undefined && stats.tertiaryLabel && (
|
|
<div className="stat-number">
|
|
<span className="stat-value">{formatNumber(stats.tertiaryStat)}</span>
|
|
<span className="stat-unit">{stats.tertiaryLabel}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="status-placeholder">
|
|
<span className="status-dot disconnected" />
|
|
<span>{language === 'nl' ? 'Niet verbonden' : 'Disconnected'}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|