glam/frontend/src/pages/Database.tsx
2025-12-07 00:26:01 +01:00

404 lines
14 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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>
);
}