Use attributeBadgeColor() (hash-based) instead of "auto" so the same application name gets the same badge color across all pages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
136 lines
5.0 KiB
TypeScript
136 lines
5.0 KiB
TypeScript
import { useState, useCallback } from 'react';
|
|
import { useNavigate } from 'react-router';
|
|
import { Badge } from '@cameleer/design-system';
|
|
import type { LogEntryResponse } from '../../api/queries/logs';
|
|
import { attributeBadgeColor } from '../../utils/attribute-color';
|
|
import styles from './LogEntry.module.css';
|
|
|
|
function levelColor(level: string): string {
|
|
switch (level?.toUpperCase()) {
|
|
case 'ERROR': return 'var(--error)';
|
|
case 'WARN': return 'var(--warning)';
|
|
case 'INFO': return 'var(--success)';
|
|
case 'DEBUG': return 'var(--running)';
|
|
case 'TRACE': return 'var(--text-muted)';
|
|
default: return 'var(--text-secondary)';
|
|
}
|
|
}
|
|
|
|
function formatTime(iso: string): string {
|
|
const d = new Date(iso);
|
|
const h = String(d.getHours()).padStart(2, '0');
|
|
const m = String(d.getMinutes()).padStart(2, '0');
|
|
const s = String(d.getSeconds()).padStart(2, '0');
|
|
const ms = String(d.getMilliseconds()).padStart(3, '0');
|
|
return `${h}:${m}:${s}.${ms}`;
|
|
}
|
|
|
|
function abbreviateLogger(name: string | null): string {
|
|
if (!name) return '';
|
|
const parts = name.split('.');
|
|
if (parts.length <= 2) return name;
|
|
return parts.slice(0, -1).map((p) => p[0]).join('.') + '.' + parts[parts.length - 1];
|
|
}
|
|
|
|
function truncate(text: string, max: number): string {
|
|
return text.length > max ? text.slice(0, max) + '\u2026' : text;
|
|
}
|
|
|
|
interface LogEntryProps {
|
|
entry: LogEntryResponse;
|
|
}
|
|
|
|
export function LogEntry({ entry }: LogEntryProps) {
|
|
const [expanded, setExpanded] = useState(false);
|
|
const navigate = useNavigate();
|
|
|
|
const hasStack = !!entry.stackTrace;
|
|
const hasExchange = !!entry.exchangeId;
|
|
|
|
const handleViewExchange = useCallback((e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
if (!entry.exchangeId || !entry.application) return;
|
|
const routeId = entry.mdc?.['camel.routeId'] || '_';
|
|
navigate(`/exchanges/${entry.application}/${routeId}/${entry.exchangeId}`);
|
|
}, [entry, navigate]);
|
|
|
|
const handleCopyMessage = useCallback(async (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
await navigator.clipboard.writeText(entry.message);
|
|
}, [entry.message]);
|
|
|
|
return (
|
|
<div className={`${styles.entry} ${expanded ? styles.expanded : ''}`} onClick={() => setExpanded(!expanded)}>
|
|
<div className={styles.row}>
|
|
<span className={styles.timestamp}>{formatTime(entry.timestamp)}</span>
|
|
<span className={styles.level} style={{ color: levelColor(entry.level) }}>{entry.level}</span>
|
|
{entry.application && <Badge label={entry.application} color={attributeBadgeColor(entry.application)} />}
|
|
<span className={styles.logger} title={entry.loggerName ?? ''}>
|
|
{abbreviateLogger(entry.loggerName)}
|
|
</span>
|
|
<span className={styles.message}>{truncate(entry.message, 200)}</span>
|
|
<span className={styles.chips}>
|
|
{hasStack && <span className={styles.chip}>Stack</span>}
|
|
{hasExchange && (
|
|
<span className={styles.chip} onClick={handleViewExchange}>Exchange</span>
|
|
)}
|
|
</span>
|
|
</div>
|
|
|
|
{expanded && (
|
|
<div className={styles.detail}>
|
|
<div className={styles.detailGrid}>
|
|
<span className={styles.detailLabel}>Logger</span>
|
|
<span className={styles.detailValue}>{entry.loggerName}</span>
|
|
<span className={styles.detailLabel}>Thread</span>
|
|
<span className={styles.detailValue}>{entry.threadName}</span>
|
|
<span className={styles.detailLabel}>Instance</span>
|
|
<span className={styles.detailValue}>{entry.instanceId}</span>
|
|
{hasExchange && (
|
|
<>
|
|
<span className={styles.detailLabel}>Exchange</span>
|
|
<span className={styles.detailValue}>
|
|
<button className={styles.linkBtn} onClick={handleViewExchange}>
|
|
{entry.exchangeId}
|
|
</button>
|
|
</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div className={styles.fullMessage}>{entry.message}</div>
|
|
|
|
{hasStack && (
|
|
<pre className={styles.stackTrace}>{entry.stackTrace}</pre>
|
|
)}
|
|
|
|
{entry.mdc && Object.keys(entry.mdc).length > 0 && (
|
|
<div className={styles.mdcSection}>
|
|
<span className={styles.detailLabel}>MDC</span>
|
|
<div className={styles.mdcGrid}>
|
|
{Object.entries(entry.mdc).map(([k, v]) => (
|
|
<div key={k} className={styles.mdcEntry}>
|
|
<span className={styles.mdcKey}>{k}</span>
|
|
<span className={styles.mdcValue}>{v}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className={styles.actions}>
|
|
{hasExchange && (
|
|
<button className={styles.actionBtn} onClick={handleViewExchange}>
|
|
View Exchange
|
|
</button>
|
|
)}
|
|
<button className={styles.actionBtn} onClick={handleCopyMessage}>
|
|
Copy Message
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|