feat: highlight search matches in log results
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m7s
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled

Recursive case-insensitive highlighting of the search query in
collapsed message, expanded full message, and stack trace. Uses the
project's amber accent color for the highlight mark.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-02 16:34:15 +02:00
parent 4d66d6ab23
commit 8c7c9911c4
3 changed files with 28 additions and 7 deletions

View File

@@ -175,6 +175,13 @@
color: var(--text-primary);
}
.highlight {
background: var(--amber);
color: var(--bg-deep);
border-radius: 2px;
padding: 0 1px;
}
.linkBtn {
background: none;
border: none;

View File

@@ -36,11 +36,25 @@ function truncate(text: string, max: number): string {
return text.length > max ? text.slice(0, max) + '\u2026' : text;
}
interface LogEntryProps {
entry: LogEntryResponse;
function highlightText(text: string, query: string | undefined): React.ReactNode {
if (!query || !text) return text;
const idx = text.toLowerCase().indexOf(query.toLowerCase());
if (idx === -1) return text;
return (
<>
{text.slice(0, idx)}
<mark className={styles.highlight}>{text.slice(idx, idx + query.length)}</mark>
{highlightText(text.slice(idx + query.length), query)}
</>
);
}
export function LogEntry({ entry }: LogEntryProps) {
interface LogEntryProps {
entry: LogEntryResponse;
query?: string;
}
export function LogEntry({ entry, query }: LogEntryProps) {
const [expanded, setExpanded] = useState(false);
const navigate = useNavigate();
@@ -68,7 +82,7 @@ export function LogEntry({ entry }: LogEntryProps) {
<span className={styles.logger} title={entry.loggerName ?? ''}>
{abbreviateLogger(entry.loggerName)}
</span>
<span className={styles.message}>{truncate(entry.message, 200)}</span>
<span className={styles.message}>{highlightText(truncate(entry.message, 200), query)}</span>
<span className={styles.chips}>
{hasStack && <span className={styles.chip}>Stack</span>}
{hasExchange && (
@@ -98,10 +112,10 @@ export function LogEntry({ entry }: LogEntryProps) {
)}
</div>
<div className={styles.fullMessage}>{entry.message}</div>
<div className={styles.fullMessage}>{highlightText(entry.message, query)}</div>
{hasStack && (
<pre className={styles.stackTrace}>{entry.stackTrace}</pre>
<pre className={styles.stackTrace}>{highlightText(entry.stackTrace!, query)}</pre>
)}
{entry.mdc && Object.keys(entry.mdc).length > 0 && (

View File

@@ -187,7 +187,7 @@ export function LogSearch({ defaultApplication, defaultRouteId }: LogSearchProps
) : (
<>
{entries.map((entry, i) => (
<LogEntry key={`${entry.timestamp}-${i}`} entry={entry} />
<LogEntry key={`${entry.timestamp}-${i}`} entry={entry} query={debouncedQuery || undefined} />
))}
{!liveTail && hasMore && (
<div className={styles.loadMore}>