[{event.context}]{' '}
{event.message}
{ts}
{event.metadata && Object.keys(event.metadata).length > 0 && (
{JSON.stringify(event.metadata, null, 2)}
)}
);
}
// ===========================================================================
// Collapsible — a sub-section with title, count badge, chevron toggle
// ===========================================================================
interface CollapsibleProps {
title: string;
count?: number;
defaultOpen: boolean;
children: React.ReactNode;
headerRight?: React.ReactNode;
}
function Collapsible({ title, count, defaultOpen, children, headerRight }: CollapsibleProps) {
const [open, setOpen] = useState(defaultOpen);
return (
('all');
const filteredEvents = useMemo(() => {
if (level === 'all') return log.events;
return log.events.filter((e) => e.level === level);
}, [log.events, level]);
const fullEventLog = useMemo(
() => log.events.map(formatEventLine).join('\n'),
[log.events]
);
const resultText = useMemo(
() => (log.result ? JSON.stringify(log.result, null, 2) : ''),
[log.result]
);
const hasResult = !!(log.result && Object.keys(log.result).length > 0);
return (
{log.bullJobId && (
Bull Job ID:
{log.bullJobId}
)}
{log.events.length > 0 && (
}
>
{filteredEvents.length === 0 ? (
No events at level "{level}".
) : (
{filteredEvents.map((event) => (
))}
)}
)}
{hasResult && (
}
>
{resultText}
)}
{log.errorMessage && (
}
>
{log.errorMessage}
)}
);
}
// ===========================================================================
// LevelFilterPills — small group toggle
// ===========================================================================
function LevelFilterPills({
value,
onChange,
}: {
value: Level;
onChange: (next: Level) => void;
}) {
const options: { key: Level; label: string }[] = [
{ key: 'all', label: 'All' },
{ key: 'info', label: 'Info' },
{ key: 'warn', label: 'Warn' },
{ key: 'error', label: 'Error' },
];
return (
{options.map((opt) => (
))}
);
}