From a62ff5b06421c6e25a556373406bac25cb82c822 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 14 Apr 2026 23:30:29 +0200 Subject: [PATCH] feat: add source badge to LogViewer entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LogEntry now accepts an optional `source` field. When present, a small badge (container/app/agent) renders between the level and message columns. Backward compatible — entries without source render as before. Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 +- .../composites/LogViewer/LogViewer.module.css | 32 +++++++++++++++++++ .../composites/LogViewer/LogViewer.test.tsx | 23 +++++++++++-- .../composites/LogViewer/LogViewer.tsx | 12 +++++++ 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a3c1c45..87f2e33 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cameleer/design-system", - "version": "0.1.48", + "version": "0.1.49", "type": "module", "main": "./dist/index.es.js", "module": "./dist/index.es.js", diff --git a/src/design-system/composites/LogViewer/LogViewer.module.css b/src/design-system/composites/LogViewer/LogViewer.module.css index c1eb349..ae1d8ef 100644 --- a/src/design-system/composites/LogViewer/LogViewer.module.css +++ b/src/design-system/composites/LogViewer/LogViewer.module.css @@ -61,6 +61,38 @@ background: color-mix(in srgb, var(--text-faint) 8%, transparent); } +.sourceBadge { + flex-shrink: 0; + font-size: 9px; + font-family: var(--font-mono); + padding: 1px 6px; + border-radius: 3px; + line-height: 1.5; + white-space: nowrap; + min-width: 48px; + text-align: center; +} + +.sourceContainer { + color: var(--text-muted); + background: color-mix(in srgb, var(--text-muted) 10%, transparent); +} + +.sourceApp { + color: var(--running); + background: color-mix(in srgb, var(--running) 10%, transparent); +} + +.sourceAgent { + color: var(--warning); + background: color-mix(in srgb, var(--warning) 10%, transparent); +} + +.sourceDefault { + color: var(--text-muted); + background: color-mix(in srgb, var(--text-muted) 8%, transparent); +} + .message { font-size: 12px; font-family: var(--font-mono); diff --git a/src/design-system/composites/LogViewer/LogViewer.test.tsx b/src/design-system/composites/LogViewer/LogViewer.test.tsx index 4aa25c5..5ed058e 100644 --- a/src/design-system/composites/LogViewer/LogViewer.test.tsx +++ b/src/design-system/composites/LogViewer/LogViewer.test.tsx @@ -3,9 +3,9 @@ import { render, screen } from '@testing-library/react' import { LogViewer, type LogEntry } from './LogViewer' const entries: LogEntry[] = [ - { timestamp: '2024-01-15T10:30:00Z', level: 'info', message: 'Server started' }, - { timestamp: '2024-01-15T10:30:05Z', level: 'warn', message: 'High memory usage' }, - { timestamp: '2024-01-15T10:30:10Z', level: 'error', message: 'Connection failed' }, + { timestamp: '2024-01-15T10:30:00Z', level: 'info', message: 'Server started', source: 'app' }, + { timestamp: '2024-01-15T10:30:05Z', level: 'warn', message: 'High memory usage', source: 'container' }, + { timestamp: '2024-01-15T10:30:10Z', level: 'error', message: 'Connection failed', source: 'agent' }, { timestamp: '2024-01-15T10:30:15Z', level: 'debug', message: 'Query executed in 3ms' }, { timestamp: '2024-01-15T10:30:20Z', level: 'trace', message: 'Entering handleRequest()' }, ] @@ -52,6 +52,23 @@ describe('LogViewer', () => { expect(el.classList.contains('custom-class')).toBe(true) }) + it('renders source badges when source is provided', () => { + render() + expect(screen.getByText('app')).toBeInTheDocument() + expect(screen.getByText('container')).toBeInTheDocument() + expect(screen.getByText('agent')).toBeInTheDocument() + }) + + it('omits source badge when source is not provided', () => { + const noSourceEntries: LogEntry[] = [ + { timestamp: '2024-01-15T10:30:00Z', level: 'info', message: 'No source here' }, + ] + render() + expect(screen.getByText('No source here')).toBeInTheDocument() + expect(screen.queryByText('app')).not.toBeInTheDocument() + expect(screen.queryByText('container')).not.toBeInTheDocument() + }) + it('has role="log" for accessibility', () => { render() expect(screen.getByRole('log')).toBeInTheDocument() diff --git a/src/design-system/composites/LogViewer/LogViewer.tsx b/src/design-system/composites/LogViewer/LogViewer.tsx index 6400fff..0059036 100644 --- a/src/design-system/composites/LogViewer/LogViewer.tsx +++ b/src/design-system/composites/LogViewer/LogViewer.tsx @@ -5,6 +5,7 @@ export interface LogEntry { timestamp: string level: 'info' | 'warn' | 'error' | 'debug' | 'trace' message: string + source?: string } export interface LogViewerProps { @@ -21,6 +22,12 @@ const LEVEL_CLASS: Record = { trace: styles.levelTrace, } +const SOURCE_CLASS: Record = { + container: styles.sourceContainer, + app: styles.sourceApp, + agent: styles.sourceAgent, +} + function formatTime(iso: string): string { try { return new Date(iso).toLocaleTimeString('en-GB', { @@ -67,6 +74,11 @@ export function LogViewer({ entries, maxHeight = 400, className }: LogViewerProp {entry.level.toUpperCase()} + {entry.source && ( + + {entry.source} + + )} {entry.message} ))}