404 lines
14 KiB
TypeScript
404 lines
14 KiB
TypeScript
/**
|
||
* Database Management Page
|
||
* Multi-database comparison UI for DuckLake, PostgreSQL, TypeDB, and Oxigraph
|
||
*
|
||
* 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
|
||
*/
|
||
|
||
import { useState } from 'react';
|
||
import { useLanguage } from '../contexts/LanguageContext';
|
||
import { DuckDBPanel } from '../components/database/DuckDBPanel';
|
||
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 { 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 './Database.css';
|
||
|
||
// Database types - DuckLake is primary (with DuckDB-WASM fallback)
|
||
type DatabaseType = 'duckdb' | 'postgres' | 'typedb' | 'oxigraph';
|
||
|
||
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',
|
||
},
|
||
];
|
||
|
||
// 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: 'Snelle vergelijking', en: 'Quick Comparison' },
|
||
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',
|
||
},
|
||
};
|
||
|
||
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>
|
||
|
||
{/* Database Type Tabs */}
|
||
<nav className="database-nav">
|
||
<button
|
||
className={`db-nav-button ${activeDatabase === 'all' ? 'active' : ''}`}
|
||
onClick={() => setActiveDatabase('all')}
|
||
>
|
||
<span className="db-icon">📊</span>
|
||
<span className="db-name">{t('allDatabases')}</span>
|
||
</button>
|
||
{DATABASES.map((db) => (
|
||
<button
|
||
key={db.id}
|
||
className={`db-nav-button ${activeDatabase === db.id ? 'active' : ''}`}
|
||
onClick={() => setActiveDatabase(db.id)}
|
||
style={{ '--db-color': db.color } as React.CSSProperties}
|
||
>
|
||
<span className="db-icon">{db.icon}</span>
|
||
<span className="db-name">{db.name}</span>
|
||
</button>
|
||
))}
|
||
</nav>
|
||
|
||
{/* Content based on active tab */}
|
||
<div className="database-content">
|
||
{activeDatabase === 'all' && <AllDatabasesView language={language} />}
|
||
{activeDatabase === 'duckdb' && <DuckLakePanel />}
|
||
{activeDatabase === 'postgres' && <PostgreSQLPanel />}
|
||
{activeDatabase === 'typedb' && <TypeDBPanel />}
|
||
{activeDatabase === 'oxigraph' && <OxigraphPanel />}
|
||
</div>
|
||
|
||
{/* Info Section */}
|
||
<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>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* All Databases Overview View
|
||
*/
|
||
function AllDatabasesView({ language }: { language: 'nl' | 'en' }) {
|
||
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();
|
||
|
||
// 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,
|
||
},
|
||
};
|
||
|
||
return (
|
||
<div className="all-databases-view">
|
||
{/* Quick Comparison 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]}
|
||
/>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* Compact panels for each database */}
|
||
<div className="database-panels-grid">
|
||
<div className="panel-wrapper duckdb">
|
||
<DuckDBPanel compact />
|
||
</div>
|
||
<div className="panel-wrapper postgres">
|
||
<PostgreSQLPanel compact />
|
||
</div>
|
||
<div className="panel-wrapper typedb">
|
||
<TypeDBPanel compact />
|
||
</div>
|
||
<div className="panel-wrapper oxigraph">
|
||
<OxigraphPanel compact />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Database Status Card for comparison view
|
||
*/
|
||
function DatabaseStatusCard({
|
||
db,
|
||
language,
|
||
stats,
|
||
}: {
|
||
db: DatabaseInfo;
|
||
language: 'nl' | 'en';
|
||
stats: DatabaseStats;
|
||
}) {
|
||
// 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"
|
||
style={{ '--db-color': db.color } as React.CSSProperties}
|
||
>
|
||
<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>
|
||
);
|
||
}
|