gadget/gadget-code/frontend/src/components/LogPanel.tsx
Rob Colbert 24975b58c4 Define missing socket event types and enforce typed events in frontend build
Adds type definitions + forwarding for status, reconnect_attempt, reconnect_failed, reconnect events.
Frontend build now runs tsc --noEmit before vite build so undefined socket events cause failures.
Fixes pre-existing type errors exposed by strict mode in the frontend.
2026-05-12 10:42:31 -04:00

126 lines
4.1 KiB
TypeScript

import { useRef, useEffect, useState } from 'react';
interface LogEntry {
id: string;
timestamp: Date;
level: string;
component: string;
message: string;
metadata?: unknown;
}
interface LogPanelProps {
logs: LogEntry[];
expanded: boolean;
onToggleExpand: () => void;
}
const levelColors: Record<string, string> = {
debug: 'text-gray-500',
info: 'text-green-400',
warn: 'text-yellow-400',
alert: 'text-red-400',
error: 'bg-red-800 text-white',
crit: 'bg-red-800 text-yellow-300',
fatal: 'bg-red-800 text-gray-400',
};
function formatTimestamp(date: Date): string {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
const hh = String(date.getHours()).padStart(2, '0');
const mm = String(date.getMinutes()).padStart(2, '0');
const ss = String(date.getSeconds()).padStart(2, '0');
const ms = String(date.getMilliseconds()).padStart(3, '0');
return `${y}-${m}-${d} ${hh}:${mm}:${ss}.${ms}`;
}
export default function LogPanel({ logs, expanded, onToggleExpand }: LogPanelProps) {
const scrollRef = useRef<HTMLDivElement>(null);
const [expandedMetadata, setExpandedMetadata] = useState<Set<string>>(new Set());
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [logs]);
const toggleMetadata = (id: string) => {
setExpandedMetadata(prev => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
return (
<div
className={`border-t border-border-subtle bg-bg-secondary flex flex-col ${
expanded ? 'flex-1' : 'h-48'
}`}
>
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary shrink-0">
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider">
Log
</h3>
<button
onClick={onToggleExpand}
className="w-6 h-6 flex items-center justify-center text-text-muted hover:text-text-secondary transition-colors"
title={expanded ? 'Collapse' : 'Expand'}
>
{expanded ? '▾' : '▴'}
</button>
</div>
<div
ref={scrollRef}
className="flex-1 overflow-y-auto p-2 font-mono text-xs leading-relaxed"
>
{logs.length === 0 ? (
<div className="text-text-muted">No log entries</div>
) : (
logs.map((entry) => {
const levelClass = levelColors[entry.level] || 'text-text-secondary';
return (
<div key={entry.id} className="flex gap-2 py-0.5 hover:bg-bg-elevated/50">
<span className="text-gray-600 shrink-0 select-none">
{formatTimestamp(entry.timestamp)}
</span>
<span className={`shrink-0 font-bold ${levelClass}`}>
{entry.level.toUpperCase().padEnd(5)}
</span>
<span className="text-cyan-400 shrink-0">
{entry.component}
</span>
<span className="text-text-secondary break-all min-w-0">
{entry.message}
</span>
{entry.metadata != null && (
<button
onClick={() => toggleMetadata(entry.id)}
className="shrink-0 text-text-muted hover:text-text-secondary transition-colors"
title={expandedMetadata.has(entry.id) ? 'Hide metadata' : 'Show metadata'}
>
{expandedMetadata.has(entry.id) ? '⊟' : '⊞'}
</button>
)}
{entry.metadata != null && expandedMetadata.has(entry.id) && (
<div className="w-full text-text-muted pl-[1em] border-l border-border-subtle mt-0.5 mb-1">
<pre className="whitespace-pre-wrap break-all">
{JSON.stringify(entry.metadata, null, 2)}
</pre>
</div>
)}
</div>
);
})
)}
</div>
</div>
);
}