signalforge
Version:
Fine-grained reactive state management with automatic dependency tracking - Ultra-optimized, zero dependencies
252 lines (251 loc) • 12.7 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import React, { useState, useEffect, useRef } from 'react';
export function LogViewer({ plugin, refreshInterval = 500, maxDisplayLogs = 200, showStats = true, autoScroll = true, style, }) {
const [logs, setLogs] = useState([]);
const [filteredLogs, setFilteredLogs] = useState([]);
const [searchQuery, setSearchQuery] = useState('');
const [filterType, setFilterType] = useState('all');
const [filterLevel, setFilterLevel] = useState('all');
const [filterSignal, setFilterSignal] = useState('all');
const [selectedLog, setSelectedLog] = useState(null);
const logsEndRef = useRef(null);
useEffect(() => {
const updateLogs = () => {
const allLogs = plugin.getLogs();
setLogs(allLogs);
};
updateLogs();
const interval = setInterval(updateLogs, refreshInterval);
return () => clearInterval(interval);
}, [plugin, refreshInterval]);
useEffect(() => {
let filtered = [...logs];
if (filterType !== 'all') {
filtered = filtered.filter(log => log.type === filterType);
}
if (filterLevel !== 'all') {
filtered = filtered.filter(log => log.level === filterLevel);
}
if (filterSignal !== 'all') {
filtered = filtered.filter(log => (log.signalLabel || log.signalId) === filterSignal);
}
if (searchQuery) {
filtered = plugin.search(searchQuery);
if (filterType !== 'all') {
filtered = filtered.filter(log => log.type === filterType);
}
if (filterLevel !== 'all') {
filtered = filtered.filter(log => log.level === filterLevel);
}
if (filterSignal !== 'all') {
filtered = filtered.filter(log => (log.signalLabel || log.signalId) === filterSignal);
}
}
if (filtered.length > maxDisplayLogs) {
filtered = filtered.slice(-maxDisplayLogs);
}
setFilteredLogs(filtered);
}, [logs, searchQuery, filterType, filterLevel, filterSignal, maxDisplayLogs, plugin]);
useEffect(() => {
if (autoScroll) {
logsEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}
}, [filteredLogs, autoScroll]);
const signalLabels = React.useMemo(() => {
const labels = new Set();
logs.forEach(log => {
const label = log.signalLabel || log.signalId;
labels.add(label);
});
return Array.from(labels).sort();
}, [logs]);
const stats = React.useMemo(() => plugin.getStats(), [logs, plugin]);
const handleClear = () => {
plugin.clear();
setLogs([]);
setFilteredLogs([]);
setSelectedLog(null);
};
const handleExport = () => {
const json = plugin.exportLogs();
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `signalforge-logs-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
};
const handleLogClick = (log) => {
setSelectedLog(log);
};
const handleCloseDetails = () => {
setSelectedLog(null);
};
return (_jsxs("div", { style: { ...containerStyle, ...style }, children: [_jsxs("div", { style: headerStyle, children: [_jsx("h3", { style: { margin: 0 }, children: "\uD83D\uDCDD Signal Logger" }), _jsxs("div", { style: { display: 'flex', gap: '10px' }, children: [_jsx("button", { onClick: handleClear, style: buttonStyle, children: "\uD83D\uDDD1\uFE0F Clear" }), _jsx("button", { onClick: handleExport, style: buttonStyle, children: "\uD83D\uDCBE Export" })] })] }), showStats && (_jsxs("div", { style: statsStyle, children: [_jsxs("div", { style: statItemStyle, children: [_jsx("strong", { children: "Total:" }), " ", stats.total] }), _jsxs("div", { style: statItemStyle, children: [_jsx("strong", { children: "Creates:" }), " ", stats.byType.create] }), _jsxs("div", { style: statItemStyle, children: [_jsx("strong", { children: "Updates:" }), " ", stats.byType.update] }), _jsxs("div", { style: statItemStyle, children: [_jsx("strong", { children: "Destroys:" }), " ", stats.byType.destroy] }), _jsxs("div", { style: statItemStyle, children: [_jsx("strong", { children: "Signals:" }), " ", Object.keys(stats.bySignal).length] })] })), _jsxs("div", { style: filtersStyle, children: [_jsx("input", { type: "text", placeholder: "\uD83D\uDD0D Search logs...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), style: searchInputStyle }), _jsxs("select", { value: filterType, onChange: (e) => setFilterType(e.target.value), style: selectStyle, children: [_jsx("option", { value: "all", children: "All Types" }), _jsx("option", { value: "create", children: "\uD83D\uDFE2 Create" }), _jsx("option", { value: "update", children: "\uD83D\uDD35 Update" }), _jsx("option", { value: "destroy", children: "\uD83D\uDD34 Destroy" })] }), _jsxs("select", { value: filterLevel, onChange: (e) => setFilterLevel(e.target.value), style: selectStyle, children: [_jsx("option", { value: "all", children: "All Levels" }), _jsx("option", { value: "debug", children: "Debug" }), _jsx("option", { value: "info", children: "Info" }), _jsx("option", { value: "warn", children: "Warning" }), _jsx("option", { value: "error", children: "Error" })] }), _jsxs("select", { value: filterSignal, onChange: (e) => setFilterSignal(e.target.value), style: selectStyle, children: [_jsx("option", { value: "all", children: "All Signals" }), signalLabels.map(label => (_jsx("option", { value: label, children: label }, label)))] })] }), _jsx("div", { style: logsContainerStyle, children: filteredLogs.length === 0 ? (_jsx("div", { style: emptyStateStyle, children: logs.length === 0 ? (_jsx(_Fragment, { children: "\uD83D\uDCED No logs yet. Signals will be logged here." })) : (_jsx(_Fragment, { children: "\uD83D\uDD0D No logs match your filters." })) })) : (_jsxs(_Fragment, { children: [filteredLogs.map(log => (_jsx(LogEntryRow, { log: log, onClick: () => handleLogClick(log), isSelected: selectedLog?.id === log.id }, log.id))), _jsx("div", { ref: logsEndRef })] })) }), selectedLog && (_jsx(LogDetailsPanel, { log: selectedLog, onClose: handleCloseDetails }))] }));
}
function LogEntryRow({ log, onClick, isSelected }) {
const icon = getLogIcon(log.type);
const timestamp = new Date(log.timestamp).toLocaleTimeString();
const label = log.signalLabel || log.signalId;
const rowStyle = {
...logRowStyle,
backgroundColor: isSelected ? '#e3f2fd' :
log.level === 'error' ? '#ffebee' :
log.level === 'warn' ? '#fff3e0' :
'#fff',
borderLeft: isSelected ? '3px solid #2196F3' : 'none',
};
return (_jsxs("div", { style: rowStyle, onClick: onClick, children: [_jsx("span", { style: { fontSize: '14px' }, children: icon }), _jsx("span", { style: { color: '#666', fontSize: '12px', minWidth: '80px' }, children: timestamp }), _jsx("span", { style: { fontWeight: 500, flex: 1 }, children: label }), log.type === 'update' && (_jsxs("span", { style: { fontSize: '12px', color: '#666' }, children: [formatValue(log.oldValue), " \u2192 ", formatValue(log.newValue)] })), log.type === 'create' && (_jsxs("span", { style: { fontSize: '12px', color: '#666' }, children: ["= ", formatValue(log.newValue)] }))] }));
}
function LogDetailsPanel({ log, onClose }) {
return (_jsx("div", { style: detailsOverlayStyle, children: _jsxs("div", { style: detailsPanelStyle, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', marginBottom: '15px' }, children: [_jsx("h4", { style: { margin: 0 }, children: "Log Details" }), _jsx("button", { onClick: onClose, style: closeButtonStyle, children: "\u2715" })] }), _jsxs("div", { style: detailsContentStyle, children: [_jsx(DetailRow, { label: "ID", value: log.id }), _jsx(DetailRow, { label: "Type", value: `${getLogIcon(log.type)} ${log.type}` }), _jsx(DetailRow, { label: "Level", value: log.level }), _jsx(DetailRow, { label: "Timestamp", value: new Date(log.timestamp).toISOString() }), _jsx(DetailRow, { label: "Signal ID", value: log.signalId }), log.signalLabel && (_jsx(DetailRow, { label: "Signal Label", value: log.signalLabel })), log.metadata && (_jsx(DetailRow, { label: "Metadata", value: JSON.stringify(log.metadata, null, 2), isCode: true })), log.oldValue !== undefined && (_jsx(DetailRow, { label: "Old Value", value: JSON.stringify(log.oldValue, null, 2), isCode: true })), log.newValue !== undefined && (_jsx(DetailRow, { label: "New Value", value: JSON.stringify(log.newValue, null, 2), isCode: true })), log.message && (_jsx(DetailRow, { label: "Message", value: log.message })), log.stack && (_jsx(DetailRow, { label: "Stack Trace", value: log.stack, isCode: true }))] })] }) }));
}
function DetailRow({ label, value, isCode }) {
return (_jsxs("div", { style: { marginBottom: '10px' }, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: '5px', fontSize: '12px', color: '#666' }, children: label }), isCode ? (_jsx("pre", { style: codeStyle, children: value })) : (_jsx("div", { children: value }))] }));
}
function getLogIcon(type) {
switch (type) {
case 'create': return '🟢';
case 'update': return '🔵';
case 'destroy': return '🔴';
default: return '⚪';
}
}
function formatValue(value) {
if (value === undefined)
return 'undefined';
if (value === null)
return 'null';
if (typeof value === 'object') {
try {
const json = JSON.stringify(value);
return json.length > 30 ? json.substring(0, 27) + '...' : json;
}
catch {
return '[Object]';
}
}
return String(value);
}
const containerStyle = {
display: 'flex',
flexDirection: 'column',
height: '100%',
backgroundColor: '#f5f5f5',
fontFamily: 'system-ui, -apple-system, sans-serif',
};
const headerStyle = {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '15px',
backgroundColor: '#fff',
borderBottom: '1px solid #e0e0e0',
};
const statsStyle = {
display: 'flex',
gap: '15px',
padding: '10px 15px',
backgroundColor: '#fff',
borderBottom: '1px solid #e0e0e0',
fontSize: '13px',
};
const statItemStyle = {
display: 'flex',
gap: '5px',
};
const filtersStyle = {
display: 'grid',
gridTemplateColumns: '2fr 1fr 1fr 1fr',
gap: '10px',
padding: '10px 15px',
backgroundColor: '#fff',
borderBottom: '1px solid #e0e0e0',
};
const searchInputStyle = {
padding: '8px 12px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '14px',
};
const selectStyle = {
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '14px',
backgroundColor: '#fff',
};
const logsContainerStyle = {
flex: 1,
overflowY: 'auto',
padding: '10px',
};
const logRowStyle = {
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '8px 12px',
marginBottom: '2px',
borderRadius: '4px',
cursor: 'pointer',
transition: 'background-color 0.15s',
fontSize: '14px',
};
const emptyStateStyle = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '200px',
color: '#999',
fontSize: '14px',
};
const buttonStyle = {
padding: '6px 12px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#fff',
cursor: 'pointer',
fontSize: '14px',
};
const detailsOverlayStyle = {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
};
const detailsPanelStyle = {
backgroundColor: '#fff',
borderRadius: '8px',
padding: '20px',
maxWidth: '600px',
maxHeight: '80vh',
overflow: 'auto',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.2)',
};
const detailsContentStyle = {
fontSize: '14px',
};
const closeButtonStyle = {
padding: '4px 8px',
border: 'none',
backgroundColor: 'transparent',
cursor: 'pointer',
fontSize: '18px',
color: '#666',
};
const codeStyle = {
backgroundColor: '#f5f5f5',
padding: '10px',
borderRadius: '4px',
fontSize: '12px',
fontFamily: 'monospace',
overflow: 'auto',
maxHeight: '200px',
};