@@ -31,6 +31,7 @@ import { RunTag } from "~/components/runs/v3/RunTag";
3131import { formatCurrencyAccurate } from "~/utils/numberFormatter" ;
3232import type { TaskRunStatus } from "@trigger.dev/database" ;
3333import { PacketDisplay } from "~/components/runs/v3/PacketDisplay" ;
34+ import type { ReactNode } from "react" ;
3435
3536// Types for the run context endpoint response
3637type RunContextData = {
@@ -64,6 +65,7 @@ type LogDetailViewProps = {
6465 // If we have the log entry from the list, we can display it immediately
6566 initialLog ?: LogEntry ;
6667 onClose : ( ) => void ;
68+ searchTerm ?: string ;
6769} ;
6870
6971type TabType = "details" | "run" ;
@@ -128,25 +130,61 @@ function getKindLabel(kind: string): string {
128130 }
129131}
130132
131- // Helper to unescape newlines in JSON strings for better readability
132- function unescapeNewlines ( obj : unknown ) : unknown {
133- if ( typeof obj === "string" ) {
134- return obj . replace ( / \\ n / g, "\n" ) ;
135- }
136- if ( Array . isArray ( obj ) ) {
137- return obj . map ( unescapeNewlines ) ;
133+ function formatStringJSON ( str : string ) : string {
134+ return str
135+ . replace ( / \\ n / g, "\n" ) // Converts literal "\n" to newline
136+ . replace ( / \\ t / g, "\t" ) ; // Converts literal "\t" to tab
137+ }
138+
139+ // Highlight search term in JSON string - returns React nodes with highlights
140+ function highlightJsonWithSearch ( json : string , searchTerm : string | undefined ) : ReactNode {
141+ if ( ! searchTerm || searchTerm . trim ( ) === "" ) {
142+ return json ;
138143 }
139- if ( obj !== null && typeof obj === "object" ) {
140- const result : Record < string , unknown > = { } ;
141- for ( const [ key , value ] of Object . entries ( obj ) ) {
142- result [ key ] = unescapeNewlines ( value ) ;
144+
145+ // Escape special regex characters in the search term
146+ const escapedSearch = searchTerm . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) ;
147+ const regex = new RegExp ( escapedSearch , "gi" ) ;
148+
149+ const parts : ReactNode [ ] = [ ] ;
150+ let lastIndex = 0 ;
151+ let match ;
152+ let matchCount = 0 ;
153+
154+ while ( ( match = regex . exec ( json ) ) !== null ) {
155+ // Add text before match
156+ if ( match . index > lastIndex ) {
157+ parts . push ( json . substring ( lastIndex , match . index ) ) ;
143158 }
144- return result ;
159+ // Add highlighted match with inline styles
160+ parts . push (
161+ < span
162+ key = { `match-${ matchCount } ` }
163+ style = { {
164+ backgroundColor : "#facc15" ,
165+ color : "#000000" ,
166+ fontWeight : "500" ,
167+ borderRadius : "0.25rem" ,
168+ padding : "0 0.125rem" ,
169+ } }
170+ >
171+ { match [ 0 ] }
172+ </ span >
173+ ) ;
174+ lastIndex = regex . lastIndex ;
175+ matchCount ++ ;
145176 }
146- return obj ;
177+
178+ // Add remaining text
179+ if ( lastIndex < json . length ) {
180+ parts . push ( json . substring ( lastIndex ) ) ;
181+ }
182+
183+ return parts . length > 0 ? parts : json ;
147184}
148185
149- export function LogDetailView ( { logId, initialLog, onClose } : LogDetailViewProps ) {
186+
187+ export function LogDetailView ( { logId, initialLog, onClose, searchTerm } : LogDetailViewProps ) {
150188 const organization = useOrganization ( ) ;
151189 const project = useProject ( ) ;
152190 const environment = useEnvironment ( ) ;
@@ -262,7 +300,7 @@ export function LogDetailView({ logId, initialLog, onClose }: LogDetailViewProps
262300 { /* Content */ }
263301 < div className = "flex-1 overflow-y-auto p-4" >
264302 { activeTab === "details" && (
265- < DetailsTab log = { log } runPath = { runPath } />
303+ < DetailsTab log = { log } runPath = { runPath } searchTerm = { searchTerm } />
266304 ) }
267305 { activeTab === "run" && (
268306 < RunTab log = { log } runPath = { runPath } />
@@ -272,89 +310,39 @@ export function LogDetailView({ logId, initialLog, onClose }: LogDetailViewProps
272310 ) ;
273311}
274312
275- function DetailsTab ( { log, runPath } : { log : LogEntry ; runPath : string } ) {
276- // Extract metadata and attributes - handle both parsed and raw string forms
313+ function DetailsTab ( { log, runPath, searchTerm } : { log : LogEntry ; runPath : string ; searchTerm ?: string } ) {
277314 const logWithExtras = log as LogEntry & {
278315 metadata ?: Record < string , unknown > ;
279- rawMetadata ?: string ;
280316 attributes ?: Record < string , unknown > ;
281- rawAttributes ?: string ;
282317 } ;
283318
284- const rawMetadata = logWithExtras . rawMetadata ;
285- const rawAttributes = logWithExtras . rawAttributes ;
286319
287320 let metadata : Record < string , unknown > | null = null ;
288321 let beautifiedMetadata : string | null = null ;
322+ let beautifiedAttributes : string | null = null ;
323+
289324 if ( logWithExtras . metadata ) {
290- metadata = logWithExtras . metadata ;
291- const unescaped = unescapeNewlines ( metadata ) ;
292- beautifiedMetadata = JSON . stringify ( unescaped , null , 2 ) ;
293- } else if ( rawMetadata ) {
294- try {
295- metadata = JSON . parse ( rawMetadata ) as Record < string , unknown > ;
296- const unescaped = unescapeNewlines ( metadata ) ;
297- beautifiedMetadata = JSON . stringify ( unescaped , null , 2 ) ;
298- } catch {
299- // Ignore parse errors
300- }
325+ beautifiedMetadata = JSON . stringify ( logWithExtras . metadata , null , 2 ) ;
326+ beautifiedMetadata = formatStringJSON ( beautifiedMetadata ) ;
301327 }
302328
303- let attributes : Record < string , unknown > | null = null ;
304- let beautifiedAttributes : string | null = null ;
305329 if ( logWithExtras . attributes ) {
306- attributes = logWithExtras . attributes ;
307- const unescaped = unescapeNewlines ( attributes ) ;
308- beautifiedAttributes = JSON . stringify ( unescaped , null , 2 ) ;
309- } else if ( rawAttributes ) {
310- try {
311- attributes = JSON . parse ( rawAttributes ) as Record < string , unknown > ;
312- const unescaped = unescapeNewlines ( attributes ) ;
313- beautifiedAttributes = JSON . stringify ( unescaped , null , 2 ) ;
314- } catch {
315- // Ignore parse errors
316- }
330+ beautifiedAttributes = JSON . stringify ( logWithExtras . attributes , null , 2 ) ;
331+ beautifiedAttributes = formatStringJSON ( beautifiedAttributes ) ;
317332 }
318333
319- const errorInfo = metadata ?. error as { message ?: string ; attributes ?: Record < string , unknown > } | undefined ;
320-
321- // Check if we should show metadata/attributes sections
322- const showMetadata = rawMetadata && rawMetadata !== "{}" ;
323- const showAttributes = rawAttributes && rawAttributes !== "{}" ;
334+ const showMetadata = beautifiedMetadata && beautifiedMetadata !== "{}" ;
335+ const showAttributes = beautifiedAttributes && beautifiedAttributes !== "{}" ;
324336
325337 return (
326338 < >
327- { /* Error Details - show prominently for error status */ }
328- { errorInfo && (
329- < div className = "mb-6" >
330- < Header3 className = "mb-2 text-error" > Error Details</ Header3 >
331- < div className = "rounded-md border border-error/30 bg-error/5 p-3" >
332- { errorInfo . message && (
333- < pre className = "mb-3 whitespace-pre-wrap break-words font-mono text-sm text-error" >
334- { errorInfo . message }
335- </ pre >
336- ) }
337- { errorInfo . attributes && Object . keys ( errorInfo . attributes ) . length > 0 && (
338- < div className = "border-t border-error/20 pt-3" >
339- < Paragraph variant = "extra-small" className = "mb-2 text-text-dimmed" >
340- Error Attributes
341- </ Paragraph >
342- < pre className = "whitespace-pre-wrap break-words font-mono text-xs text-text-bright" >
343- { JSON . stringify ( errorInfo . attributes , null , 2 ) }
344- </ pre >
345- </ div >
346- ) }
347- </ div >
348- </ div >
349- ) }
350-
351339 { /* Message */ }
352340 < div className = "mb-6" >
353341 < Header3 className = "mb-2" > Message</ Header3 >
354342 < div className = "rounded-md border border-grid-dimmed bg-charcoal-850 p-3" >
355- < pre className = "whitespace-pre-wrap break-words font-mono text-sm text-text-bright" >
356- { log . message }
357- </ pre >
343+ < div className = "whitespace-pre-wrap break-words font-mono text-sm text-text-bright" >
344+ { highlightJsonWithSearch ( log . message , searchTerm ) }
345+ </ div >
358346 </ div >
359347 </ div >
360348
@@ -363,11 +351,7 @@ function DetailsTab({ log, runPath }: { log: LogEntry; runPath: string }) {
363351 < Header3 className = "mb-2" > Run</ Header3 >
364352 < div className = "flex items-center gap-3" >
365353 < span className = "font-mono text-sm text-text-bright" > { log . runId } </ span >
366- < Link
367- to = { runPath }
368- target = "_blank"
369- rel = "noopener noreferrer"
370- >
354+ < Link to = { runPath } target = "_blank" rel = "noopener noreferrer" >
371355 < Button variant = "tertiary/small" LeadingIcon = { ArrowTopRightOnSquareIcon } >
372356 View in Run
373357 </ Button >
@@ -431,14 +415,24 @@ function DetailsTab({ log, runPath }: { log: LogEntry; runPath: string }) {
431415 { /* Metadata - only available in full log detail */ }
432416 { showMetadata && beautifiedMetadata && (
433417 < div className = "mb-6" >
434- < PacketDisplay data = { beautifiedMetadata } dataType = "application/json" title = "Metadata" />
418+ < PacketDisplay
419+ data = { beautifiedMetadata }
420+ dataType = "application/json"
421+ title = "Metadata"
422+ searchTerm = { searchTerm }
423+ />
435424 </ div >
436425 ) }
437426
438427 { /* Attributes - only available in full log detail */ }
439428 { showAttributes && beautifiedAttributes && (
440429 < div className = "mb-6" >
441- < PacketDisplay data = { beautifiedAttributes } dataType = "application/json" title = "Attributes" />
430+ < PacketDisplay
431+ data = { beautifiedAttributes }
432+ dataType = "application/json"
433+ title = "Attributes"
434+ searchTerm = { searchTerm }
435+ />
442436 </ div >
443437 ) }
444438 </ >
0 commit comments