44 * single (integration, feature) cell.
55 *
66 * Renders all badge dimensions (d2/API, d5/CV, e2e/RT, health, smoke) with
7- * tone, label, tooltip, and — for red/amber badges — failure metadata:
8- * fail_count, first_failure_at, and the signal payload .
7+ * tone, label, and — for red/amber badges — failure metadata presented as
8+ * readable key-value pairs with the full signal collapsible for debugging .
99 */
10+ import { useState } from "react" ;
1011import { resolveCell } from "@/lib/live-status" ;
1112import type {
1213 CellState ,
@@ -44,6 +45,47 @@ function formatTimestamp(ts: string | null): string {
4445 return formatTs ( ts ) ;
4546}
4647
48+ /**
49+ * Keys we extract from the signal object and display as readable
50+ * key-value pairs rather than raw JSON. Ordered by display priority.
51+ */
52+ const SIGNAL_DISPLAY_KEYS : ReadonlyArray < {
53+ key : string ;
54+ label : string ;
55+ } > = [
56+ { key : "errorDesc" , label : "Error" } ,
57+ { key : "error" , label : "Error" } ,
58+ { key : "failureSummary" , label : "Failure" } ,
59+ { key : "backendUrl" , label : "Backend URL" } ,
60+ { key : "apiRequestCount" , label : "API Requests" } ,
61+ { key : "step" , label : "Step" } ,
62+ ] ;
63+
64+ /**
65+ * Extract human-readable fields from a signal object. Returns an array
66+ * of { label, value } pairs for display. Deduplicates the "Error" label
67+ * so that `errorDesc` and `error` don't both render when present.
68+ */
69+ function extractSignalFields (
70+ signal : unknown ,
71+ ) : Array < { label : string ; value : string } > {
72+ if ( signal == null || typeof signal !== "object" || Array . isArray ( signal ) )
73+ return [ ] ;
74+ const obj = signal as Record < string , unknown > ;
75+ const fields : Array < { label : string ; value : string } > = [ ] ;
76+ const usedLabels = new Set < string > ( ) ;
77+ for ( const { key, label } of SIGNAL_DISPLAY_KEYS ) {
78+ if ( usedLabels . has ( label ) ) continue ;
79+ const val = obj [ key ] ;
80+ if ( val == null ) continue ;
81+ const str = typeof val === "string" ? val : String ( val ) ;
82+ if ( str . length === 0 ) continue ;
83+ fields . push ( { label, value : str } ) ;
84+ usedLabels . add ( label ) ;
85+ }
86+ return fields ;
87+ }
88+
4789function formatSignal ( signal : unknown ) : string | null {
4890 if ( signal == null ) return null ;
4991 if ( typeof signal === "string" ) return signal || null ;
@@ -60,9 +102,37 @@ function formatSignal(signal: unknown): string | null {
60102 return String ( signal ) || null ;
61103}
62104
105+ function CollapsibleSignal ( { text } : { text : string } ) {
106+ const [ open , setOpen ] = useState ( false ) ;
107+ return (
108+ < div className = "mt-1" >
109+ < button
110+ type = "button"
111+ data-testid = "signal-toggle"
112+ onClick = { ( ) => setOpen ( ! open ) }
113+ className = "text-[10px] text-[var(--text-muted)] hover:text-[var(--text)] cursor-pointer flex items-center gap-1"
114+ >
115+ < span className = "text-[9px]" > { open ? "▼" : "▶" } </ span >
116+ Raw Signal
117+ </ button >
118+ { open && (
119+ < pre
120+ data-testid = "signal-payload"
121+ className = "mt-1 p-2 rounded bg-[var(--bg-muted)] text-[10px] text-[var(--text)] overflow-x-auto max-h-40 whitespace-pre-wrap break-all"
122+ >
123+ { text }
124+ </ pre >
125+ ) }
126+ </ div >
127+ ) ;
128+ }
129+
63130function BadgeRow ( { badge, label } : { badge : BadgeRender ; label : string } ) {
64131 const isFailure = badge . tone === "red" || badge . tone === "amber" ;
65132 const signalText = badge . row ? formatSignal ( badge . row . signal ) : null ;
133+ const signalFields = badge . row
134+ ? extractSignalFields ( badge . row . signal )
135+ : [ ] ;
66136
67137 return (
68138 < div
@@ -90,41 +160,52 @@ function BadgeRow({ badge, label }: { badge: BadgeRender; label: string }) {
90160 </ span >
91161 ) }
92162 </ div >
93- < p className = "mt-0.5 text-[10px] text-[var(--text-muted)] leading-tight" >
94- { badge . tooltip }
95- </ p >
96163 { isFailure && badge . row && (
97- < div className = "mt-1.5 pl-4 space-y-0.5" >
98- { badge . row . fail_count > 0 && (
99- < div className = "text-[10px]" >
100- < span className = "text-[var(--text-muted)]" > Failures:</ span > { " " }
101- < span
102- data-testid = "fail-count"
103- className = "text-[var(--danger)] font-semibold tabular-nums"
104- >
105- { badge . row . fail_count }
106- </ span >
164+ < div className = "mt-1.5 pl-4 space-y-1" >
165+ { /* Extracted signal fields — readable key-value pairs */ }
166+ { signalFields . length > 0 && (
167+ < div className = "space-y-0.5" >
168+ { signalFields . map ( ( { label : fieldLabel , value } ) => (
169+ < div key = { fieldLabel } className = "text-xs" >
170+ < span className = "text-[var(--text-muted)]" >
171+ { fieldLabel } :
172+ </ span > { " " }
173+ < span
174+ data-testid = { `signal-field-${ fieldLabel . toLowerCase ( ) . replace ( / \s + / g, "-" ) } ` }
175+ className = { `font-medium ${ badge . tone === "red" ? "text-[var(--danger)]" : "text-[var(--amber)]" } ` }
176+ >
177+ { value }
178+ </ span >
179+ </ div >
180+ ) ) }
107181 </ div >
108182 ) }
109- { badge . row . first_failure_at && (
110- < div className = "text-[10px]" >
111- < span className = "text-[var(--text-muted)]" > First failure:</ span > { " " }
112- < span data-testid = "first-failure" className = "text-[var(--text)]" >
113- { formatTimestamp ( badge . row . first_failure_at ) }
183+ < div className = "flex items-center gap-3 text-[10px] text-[var(--text-muted)]" >
184+ { badge . row . fail_count > 0 && (
185+ < span >
186+ Failures:{ " " }
187+ < span
188+ data-testid = "fail-count"
189+ className = "text-[var(--danger)] font-semibold tabular-nums"
190+ >
191+ { badge . row . fail_count }
192+ </ span >
114193 </ span >
115- </ div >
116- ) }
117- { signalText && (
118- < div className = "text-[10px]" >
119- < span className = "text-[var(--text-muted)]" > Signal:</ span >
120- < pre
121- data-testid = "signal-payload"
122- className = "mt-0.5 p-1.5 rounded bg-[var(--bg-muted)] text-[9px] text-[var(--text)] overflow-x-auto max-h-24 whitespace-pre-wrap break-all"
123- >
124- { signalText }
125- </ pre >
126- </ div >
127- ) }
194+ ) }
195+ { badge . row . first_failure_at && (
196+ < span >
197+ Since{ " " }
198+ < span
199+ data-testid = "first-failure"
200+ className = "text-[var(--text)]"
201+ >
202+ { formatTimestamp ( badge . row . first_failure_at ) }
203+ </ span >
204+ </ span >
205+ ) }
206+ </ div >
207+ { /* Raw signal — collapsible for debugging */ }
208+ { signalText && < CollapsibleSignal text = { signalText } /> }
128209 </ div >
129210 ) }
130211 </ div >
@@ -145,32 +226,32 @@ export function CellDrilldown({
145226 return (
146227 < div
147228 data-testid = "cell-drilldown"
148- className = "absolute z-50 mt-1 w-72 rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg"
229+ className = "absolute z-50 mt-1 w-[480px] rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg"
149230 role = "dialog"
150231 aria-label = { `${ integrationName } / ${ featureName } detail` }
151232 >
152233 { /* Header */ }
153- < div className = "flex items-center justify-between px-3 py-2 border-b border-[var(--border)] bg-[var(--bg-muted)] rounded-t-lg" >
234+ < div className = "flex items-center justify-between px-4 py-2.5 border-b border-[var(--border)] bg-[var(--bg-muted)] rounded-t-lg" >
154235 < div className = "min-w-0" >
155- < div className = "text-xs font-semibold text-[var(--text)] truncate" >
236+ < div className = "text-sm font-semibold text-[var(--text)] truncate" >
156237 { integrationName }
157238 </ div >
158- < div className = "text-[10px] text-[var(--text-muted)] truncate" >
239+ < div className = "text-xs text-[var(--text-muted)] truncate" >
159240 { featureName }
160241 </ div >
161242 </ div >
162243 < button
163244 type = "button"
164245 data-testid = "drilldown-close"
165246 onClick = { onClose }
166- className = "ml-2 p-0.5 rounded hover:bg-[var(--bg-hover)] text-[var(--text-muted)] text-sm leading-none cursor-pointer"
247+ className = "ml-2 p-1 rounded hover:bg-[var(--bg-hover)] text-[var(--text-muted)] text-sm leading-none cursor-pointer"
167248 aria-label = "Close"
168249 >
169250 x
170251 </ button >
171252 </ div >
172253 { /* Rollup */ }
173- < div className = "px-3 py-1.5 flex items-center gap-2 border-b border-[var(--border)]" >
254+ < div className = "px-4 py-2 flex items-center gap-2 border-b border-[var(--border)]" >
174255 < span className = "text-[10px] text-[var(--text-muted)] uppercase tracking-wider" >
175256 Rollup
176257 </ span >
@@ -182,7 +263,7 @@ export function CellDrilldown({
182263 </ span >
183264 </ div >
184265 { /* Badge rows */ }
185- < div className = "px-3 py-1" >
266+ < div className = "px-4 py-1" >
186267 { DIMENSIONS . map ( ( dim ) => (
187268 < BadgeRow key = { dim . key } badge = { cell [ dim . key ] } label = { dim . label } />
188269 ) ) }
0 commit comments