Skip to content

Commit 6abb87e

Browse files
authored
fix(showcase): improve cell drilldown dialog UX (CopilotKit#4535)
## Summary - Widen dialog from `w-72` (288px) to `w-[480px]` so error details are not cramped - Extract key signal fields (`errorDesc`, `error`, `failureSummary`, `backendUrl`, `apiRequestCount`, `step`) as readable key-value pairs instead of raw JSON - Deduplicate `errorDesc` and `error` when both present (first match wins) - Make raw signal payload collapsible ("Raw Signal" toggle) for debugging - Remove duplicate tooltip text that repeated status/timing info already conveyed by badge color and failure metadata - Compact fail_count and first_failure_at into a single inline row ## Test plan - [x] All 11 cell-drilldown tests pass (4 new tests added for extracted fields, collapsible toggle, deduplication, and wider width) - [x] No new TS errors introduced - [x] Pre-existing test/TS failures in other files are unrelated
2 parents 763aeb7 + 7556b0d commit 6abb87e

2 files changed

Lines changed: 205 additions & 42 deletions

File tree

showcase/shell-dashboard/src/components/__tests__/cell-drilldown.test.tsx

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,16 @@ describe("CellDrilldown", () => {
118118
expect(healthBadge.textContent).toContain("Apr");
119119
});
120120

121-
it("shows signal payload for red badges", () => {
121+
it("extracts signal fields as readable text for red badges", () => {
122122
const live = mapOf([
123123
row("e2e:lgp/agentic-chat", "e2e", "red", {
124124
fail_count: 2,
125125
first_failure_at: "2026-04-18T12:00:00Z",
126-
signal: { error: "assertion failed", step: "login" },
126+
signal: {
127+
errorDesc: "Agent returned empty response",
128+
backendUrl: "https://lgp.example.com",
129+
apiRequestCount: 3,
130+
},
127131
}),
128132
]);
129133
const { getByTestId } = render(
@@ -136,6 +140,40 @@ describe("CellDrilldown", () => {
136140
onClose={() => {}}
137141
/>,
138142
);
143+
// Extracted fields should be visible without expanding raw signal
144+
expect(
145+
getByTestId("signal-field-error").textContent,
146+
).toBe("Agent returned empty response");
147+
expect(
148+
getByTestId("signal-field-backend-url").textContent,
149+
).toBe("https://lgp.example.com");
150+
expect(
151+
getByTestId("signal-field-api-requests").textContent,
152+
).toBe("3");
153+
});
154+
155+
it("shows raw signal payload behind collapsible toggle", () => {
156+
const live = mapOf([
157+
row("e2e:lgp/agentic-chat", "e2e", "red", {
158+
fail_count: 2,
159+
first_failure_at: "2026-04-18T12:00:00Z",
160+
signal: { error: "assertion failed", step: "login" },
161+
}),
162+
]);
163+
const { getByTestId, queryByTestId } = render(
164+
<CellDrilldown
165+
slug="lgp"
166+
featureId="agentic-chat"
167+
integrationName="LangGraph Python"
168+
featureName="Agentic Chat"
169+
liveStatus={live}
170+
onClose={() => {}}
171+
/>,
172+
);
173+
// Raw signal should be collapsed by default
174+
expect(queryByTestId("signal-payload")).toBeNull();
175+
// Click the toggle to expand
176+
fireEvent.click(getByTestId("signal-toggle"));
139177
const signalEl = getByTestId("signal-payload");
140178
expect(signalEl.textContent).toContain("assertion failed");
141179
expect(signalEl.textContent).toContain("login");
@@ -195,4 +233,48 @@ describe("CellDrilldown", () => {
195233
const strikethroughEl = healthBadge.querySelector(".line-through");
196234
expect(strikethroughEl).not.toBeNull();
197235
});
236+
237+
it("deduplicates errorDesc and error (only shows first match)", () => {
238+
const live = mapOf([
239+
row("e2e:lgp/agentic-chat", "e2e", "red", {
240+
fail_count: 1,
241+
signal: {
242+
errorDesc: "Agent timed out",
243+
error: "timeout",
244+
},
245+
}),
246+
]);
247+
const { getByTestId, queryAllByTestId } = render(
248+
<CellDrilldown
249+
slug="lgp"
250+
featureId="agentic-chat"
251+
integrationName="LangGraph Python"
252+
featureName="Agentic Chat"
253+
liveStatus={live}
254+
onClose={() => {}}
255+
/>,
256+
);
257+
// errorDesc wins — only one "Error:" line
258+
expect(
259+
getByTestId("signal-field-error").textContent,
260+
).toBe("Agent timed out");
261+
// Should not have a second error field
262+
expect(queryAllByTestId("signal-field-error").length).toBe(1);
263+
});
264+
265+
it("uses wider dialog (480px)", () => {
266+
const { getByTestId } = render(
267+
<CellDrilldown
268+
slug="lgp"
269+
featureId="agentic-chat"
270+
integrationName="LangGraph Python"
271+
featureName="Agentic Chat"
272+
liveStatus={new Map()}
273+
onClose={() => {}}
274+
/>,
275+
);
276+
const dialog = getByTestId("cell-drilldown");
277+
expect(dialog.className).toContain("w-[480px]");
278+
expect(dialog.className).not.toContain("w-72");
279+
});
198280
});

showcase/shell-dashboard/src/components/cell-drilldown.tsx

Lines changed: 121 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
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";
1011
import { resolveCell } from "@/lib/live-status";
1112
import 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+
4789
function 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+
63130
function 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

Comments
 (0)