Kraft102's picture
fix: sql.js Docker/Alpine compatibility layer for PatternMemory and FailureMemory
5a81b95
import { useEffect, useState } from 'react';
import { Maximize2 } from 'lucide-react';
import SourceLink, { DataSource } from './SourceLink';
import { DrillDownModal, DrillDownData } from './DrillDownModal';
interface DataPanelProps {
title: string;
value: string | number;
subtitle?: string;
trend?: 'up' | 'down' | 'stable';
animate?: boolean;
source?: DataSource;
/** Enable drill-down capability */
drillDown?: DrillDownData;
/** Backend endpoint for fetching detailed data */
drillDownEndpoint?: string;
}
const DataPanel = ({ title, value, subtitle, trend, animate = true, source, drillDown, drillDownEndpoint }: DataPanelProps) => {
const [displayValue, setDisplayValue] = useState(animate ? '---' : value);
const [showDrillDown, setShowDrillDown] = useState(false);
// Build drill-down data from props
const drillDownData: DrillDownData = drillDown || {
id: title.toLowerCase().replace(/\s+/g, '-'),
title: title,
description: subtitle,
endpoint: drillDownEndpoint,
metrics: [
{ label: 'Nuværende værdi', value: String(value), trend }
],
details: {
title,
value,
subtitle,
trend,
source: source?.name
}
};
const hasDrillDown = drillDown || drillDownEndpoint;
useEffect(() => {
if (!animate) return;
const chars = '0123456789ABCDEF';
let iterations = 0;
const maxIterations = 10;
const interval = setInterval(() => {
if (iterations >= maxIterations) {
setDisplayValue(value);
clearInterval(interval);
return;
}
setDisplayValue(
String(value)
.split('')
.map((char, index) => {
if (index < iterations) return char;
return chars[Math.floor(Math.random() * chars.length)];
})
.join('')
);
iterations++;
}, 50);
return () => clearInterval(interval);
}, [value, animate]);
return (
<>
<div
className={`group relative p-4 bg-secondary/30 border border-border/30 hover:border-primary/50 transition-all duration-300 ${hasDrillDown ? 'cursor-pointer' : ''}`}
onClick={hasDrillDown ? () => setShowDrillDown(true) : undefined}
>
<div className="flex items-start justify-between mb-2">
<span className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
{title}
</span>
<div className="flex items-center gap-2">
{hasDrillDown && (
<Maximize2 className="h-3 w-3 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
)}
{source && <SourceLink source={source} variant="icon" />}
{trend && (
<span className={`text-xs ${
trend === 'up' ? 'text-primary' :
trend === 'down' ? 'text-destructive' :
'text-muted-foreground'
}`}>
{trend === 'up' ? '▲' : trend === 'down' ? '▼' : '●'}
</span>
)}
</div>
</div>
<div className="font-display text-2xl sm:text-3xl font-bold text-foreground group-hover:text-primary transition-colors">
{displayValue}
</div>
{subtitle && (
<div className="mt-1 flex items-center justify-between gap-2">
<p className="font-mono text-xs text-muted-foreground">
{subtitle}
</p>
{source && <SourceLink source={source} variant="text" />}
</div>
)}
{/* Scan line on hover */}
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity">
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-primary to-transparent animate-scan" />
</div>
</div>
{/* Drill-Down Modal */}
{hasDrillDown && (
<DrillDownModal
isOpen={showDrillDown}
onClose={() => setShowDrillDown(false)}
data={drillDownData}
/>
)}
</>
);
};
export default DataPanel;