Skip to content

Commit aeec33e

Browse files
Frandoclaude
andcommitted
fix: json tree toggle, timeline detail cleanup, and lab stop status
JsonTree: clicking any key in an expanded object now collapses it back. Collapsed summaries use correct singular/plural ("1 item" vs "3 items"). Timeline detail panel: remove the raw JSON dump below the KvPairs fields. KvPairs gains a `vertical` prop for multi-line rendering in the detail pane while staying inline elsewhere. Core: emit LabStopping in LabInner::drop so state.json ends with status "stopped" instead of staying stuck on "running". Add assertion in the devtools e2e Rust test and playwright test. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 49d2696 commit aeec33e

8 files changed

Lines changed: 38 additions & 18 deletions

File tree

patchbay/src/core.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@ pub(crate) struct LabInner {
567567

568568
impl Drop for LabInner {
569569
fn drop(&mut self) {
570+
self.emit(crate::event::LabEventKind::LabStopping);
570571
self.cancel.cancel();
571572
}
572573
}

patchbay/src/event.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ pub struct LabState {
300300
pub lab_prefix: String,
301301
/// Human-readable label.
302302
pub label: Option<String>,
303-
/// Lab status (`"running"`, `"stopping"`).
303+
/// Lab status (`"running"` or `"stopped"`).
304304
pub status: String,
305305
/// Lab creation timestamp.
306306
pub created_at: Option<DateTime<Utc>>,
@@ -497,7 +497,7 @@ impl LabState {
497497
}
498498
}
499499
LabEventKind::LabStopping => {
500-
self.status = "stopping".into();
500+
self.status = "stopped".into();
501501
}
502502
LabEventKind::IxCreated {
503503
bridge,

patchbay/src/tests/devtools.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ async fn simple_lab_for_e2e() -> Result<()> {
115115
state_content.contains("\"client\""),
116116
"state.json missing client device"
117117
);
118+
assert!(
119+
state_content.contains("\"stopped\""),
120+
"state.json status should be 'stopped' after lab drop"
121+
);
118122

119123
// Verify per-namespace tracing files (flat: {kind}.{name}.{ext}).
120124
// Files are created lazily on first write — only namespaces that actually log

ui/e2e/devtools.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ test('devtools ui shows all views', async ({ page }) => {
4444
await expect(selector).toBeVisible()
4545
await expect(selector.locator('option', { hasText: 'e2e-test' })).toBeAttached()
4646

47+
// Verify the run status shows "stopped" (not stuck on "running").
48+
await expect(page.getByText('stopped')).toBeVisible({ timeout: 5_000 })
49+
4750
// Step 4: Verify topology tab shows router and device nodes (default tab).
4851
await expect(page.getByText('dc')).toBeVisible({ timeout: 10_000 })
4952
await expect(page.getByText('home')).toBeVisible()

ui/src/components/JsonTree.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,19 @@ function JsonValue({ value, depth, defaultDepth }: { value: unknown; depth: numb
2222
function JsonArray({ items, depth, defaultDepth }: { items: unknown[]; depth: number; defaultDepth: number }) {
2323
const [open, setOpen] = useState(depth < defaultDepth)
2424
if (items.length === 0) return <span className="jt-brace">[]</span>
25+
const toggle = () => setOpen((v) => !v)
2526
if (!open) {
2627
return (
27-
<span className="jt-toggle" onClick={() => setOpen(true)}>
28+
<span className="jt-toggle" onClick={toggle}>
2829
<span className="jt-brace">[</span>
29-
<span className="jt-ellipsis">{items.length} items</span>
30+
<span className="jt-ellipsis">{items.length} {items.length === 1 ? 'item' : 'items'}</span>
3031
<span className="jt-brace">]</span>
3132
</span>
3233
)
3334
}
3435
return (
3536
<span>
36-
<span className="jt-toggle jt-brace" onClick={() => setOpen(false)}>[</span>
37+
<span className="jt-toggle jt-brace" onClick={toggle}>[</span>
3738
<div className="jt-indent">
3839
{items.map((item, i) => (
3940
<div key={i} className="jt-row">
@@ -51,22 +52,23 @@ function JsonObject({ obj, depth, defaultDepth }: { obj: Record<string, unknown>
5152
const [open, setOpen] = useState(depth < defaultDepth)
5253
const entries = Object.entries(obj)
5354
if (entries.length === 0) return <span className="jt-brace">{'{}'}</span>
55+
const toggle = () => setOpen((v) => !v)
5456
if (!open) {
5557
return (
56-
<span className="jt-toggle" onClick={() => setOpen(true)}>
58+
<span className="jt-toggle" onClick={toggle}>
5759
<span className="jt-brace">{'{'}</span>
58-
<span className="jt-ellipsis">{entries.length} fields</span>
60+
<span className="jt-ellipsis">{entries.length} {entries.length === 1 ? 'field' : 'fields'}</span>
5961
<span className="jt-brace">{'}'}</span>
6062
</span>
6163
)
6264
}
6365
return (
6466
<span>
65-
<span className="jt-toggle jt-brace" onClick={() => setOpen(false)}>{'{'}</span>
67+
<span className="jt-toggle jt-brace" onClick={toggle}>{'{'}</span>
6668
<div className="jt-indent">
6769
{entries.map(([key, val], i) => (
6870
<div key={key} className="jt-row">
69-
<span className="jt-key">"{key}"</span>
71+
<span className="jt-key jt-toggle" onClick={toggle}>"{key}"</span>
7072
<span className="jt-colon">: </span>
7173
<JsonValue value={val} depth={depth + 1} defaultDepth={defaultDepth} />
7274
{i < entries.length - 1 && <span className="jt-comma">,</span>}

ui/src/components/KvPairs.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
/** Renders key=value pairs with colored keys for easy scanning. */
2-
export default function KvPairs({ pairs }: { pairs: Array<{ key: string; value: string }> }) {
2+
export default function KvPairs({ pairs, vertical }: { pairs: Array<{ key: string; value: string }>; vertical?: boolean }) {
33
if (pairs.length === 0) return <span className="kv-empty">(no fields)</span>
4+
if (vertical) {
5+
return (
6+
<div className="kv-pairs-vertical">
7+
{pairs.map((p, i) => (
8+
<div key={i} className="kv-pair">
9+
<span className="kv-key">{p.key}</span>
10+
<span className="kv-eq">=</span>
11+
<span className="kv-value">{p.value}</span>
12+
</div>
13+
))}
14+
</div>
15+
)
16+
}
417
return (
518
<span className="kv-pairs">
619
{pairs.map((p, i) => (

ui/src/components/TimelineTab.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,7 @@ export default function TimelineTab({ base, logs, labEvents, onJumpToLog }: Prop
258258
)}
259259
</div>
260260
<div className="timeline-detail-body">
261-
<div className="timeline-detail-fields"><KvPairs pairs={selected.fieldPairs} /></div>
262-
<div className="timeline-detail-raw">{selected.details}</div>
261+
<div className="timeline-detail-fields"><KvPairs pairs={selected.fieldPairs} vertical /></div>
263262
</div>
264263
</>
265264
) : (

ui/src/index.css

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -518,14 +518,8 @@ tbody td {
518518
.timeline-detail-fields {
519519
color: #e6edf3;
520520
font-size: 13px;
521-
margin-bottom: 8px;
522521
line-height: 1.5;
523522
}
524-
.timeline-detail-raw {
525-
color: var(--text-muted);
526-
font-size: 11px;
527-
line-height: 1.4;
528-
}
529523

530524
/* ── Qlog tab ── */
531525
.qlog-layout {
@@ -640,6 +634,10 @@ tbody td {
640634
color: var(--text-muted);
641635
font-style: italic;
642636
}
637+
.kv-pairs-vertical .kv-pair {
638+
display: block;
639+
line-height: 1.6;
640+
}
643641

644642
/* ── Events / JSONL table ── */
645643
.events-time-cell {

0 commit comments

Comments
 (0)