Skip to content

Commit 35afec9

Browse files
authored
Allow folding updates (#742)
1 parent eb2cd11 commit 35afec9

9 files changed

Lines changed: 211 additions & 76 deletions

File tree

extension/src/components/Graph.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,10 @@ import {
88
ComponentGroup,
99
SignalUpdate,
1010
} from "../types";
11+
import { updatesStore } from "../models/UpdatesModel";
1112

12-
export function GraphVisualization({
13-
updates,
14-
}: {
15-
updates: Signal<(SignalUpdate | Divider)[]>;
16-
}) {
13+
export function GraphVisualization() {
14+
const updates = updatesStore.updates;
1715
const svgRef = useRef<SVGSVGElement>(null);
1816

1917
// Build graph data from updates signal using a computed

extension/src/components/Header.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@ import { StatusIndicator } from "./StatusIndicator";
22
import { Button } from "./Button";
33
import { connectionStore } from "../models/ConnectionModel";
44
import { updatesStore } from "../models/UpdatesModel";
5+
import { settingsStore } from "../models/SettingsModel";
56

6-
interface HeaderProps {
7-
onToggleSettings?: () => void;
8-
}
9-
10-
export function Header({ onToggleSettings }: HeaderProps) {
7+
export function Header() {
8+
const onToggleSettings = settingsStore.toggleSettings;
119
const onTogglePause = () => {
1210
updatesStore.isPaused.value = !updatesStore.isPaused.value;
1311
};

extension/src/components/SettingsPanel.tsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,12 @@ import { Button } from "./Button";
33
import { Settings } from "../types";
44
import { settingsStore } from "../models/SettingsModel";
55

6-
interface SettingsPanelProps {
7-
isVisible: boolean;
8-
onApply: (settings: Settings) => void;
9-
onCancel: () => void;
10-
}
11-
12-
export function SettingsPanel({
13-
isVisible,
14-
onApply,
15-
onCancel,
16-
}: SettingsPanelProps) {
6+
export function SettingsPanel() {
7+
const onCancel = settingsStore.hideSettings;
8+
const onApply = settingsStore.applySettings;
179
const settings = settingsStore.settings;
10+
const isVisible = settingsStore.showSettings;
11+
1812
const localSettings = useSignal<Settings>(settings);
1913

2014
useSignalEffect(() => {

extension/src/components/UpdateItem.tsx

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export function UpdateItem({ update }: UpdateItemProps) {
88
const time = new Date(
99
update.timestamp || update.receivedAt
1010
).toLocaleTimeString();
11-
const depth = " ".repeat(update.depth || 0);
1211

1312
const formatValue = (value: any): string => {
1413
if (value === null) return "null";
@@ -27,13 +26,10 @@ export function UpdateItem({ update }: UpdateItemProps) {
2726

2827
if (update.type === "effect") {
2928
return (
30-
<div
31-
style={{ marginLeft: `${(update.depth || 0) * 4}px` }}
32-
className={`update-item ${update.type}`}
33-
>
29+
<div className={`update-item ${update.type}`}>
3430
<div className="update-header">
3531
<span className="signal-name">
36-
{depth}↪️ {update.signalName}
32+
↪️ {update.signalName}
3733
{update.componentNames && update.componentNames.length > 0 && (
3834
<ul class="component-list">
3935
<span class="component-name-header">Rerendered</span>
@@ -56,13 +52,9 @@ export function UpdateItem({ update }: UpdateItemProps) {
5652
const newValue = formatValue(update.newValue);
5753

5854
return (
59-
<div
60-
style={{ marginLeft: `${(update.depth || 0) * 4}px` }}
61-
class={`update-item ${update.type}`}
62-
>
55+
<div class={`update-item ${update.type}`}>
6356
<div class="update-header">
6457
<span class="signal-name">
65-
{depth}
6658
{update.depth === 0 ? "🎯" : "↪️"} {update.signalName}
6759
</span>
6860
<span class="update-time">{time}</span>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { useSignal } from "@preact/signals";
2+
import { UpdateTreeNode } from "../models/UpdatesModel";
3+
import { UpdateItem } from "./UpdateItem";
4+
5+
interface UpdateTreeNodeProps {
6+
node: UpdateTreeNode;
7+
}
8+
9+
export function UpdateTreeNodeComponent({ node }: UpdateTreeNodeProps) {
10+
const isCollapsed = useSignal(false);
11+
12+
const toggleCollapse = () => {
13+
isCollapsed.value = !isCollapsed.value;
14+
};
15+
16+
const hasChildren = node.children.length > 0;
17+
18+
return (
19+
<div className="tree-node">
20+
<div className="tree-node-content">
21+
{hasChildren && (
22+
<button
23+
className="collapse-button"
24+
onClick={toggleCollapse}
25+
aria-label={isCollapsed.value ? "Expand" : "Collapse"}
26+
>
27+
{isCollapsed.value ? "▶" : "▼"}
28+
</button>
29+
)}
30+
{!hasChildren && <div className="collapse-spacer" />}
31+
<div className="update-content">
32+
<UpdateItem update={node.update} />
33+
</div>
34+
</div>
35+
36+
{hasChildren && !isCollapsed.value && (
37+
<div className="tree-children">
38+
{node.children.map(child => (
39+
<UpdateTreeNodeComponent key={child.id} node={child} />
40+
))}
41+
</div>
42+
)}
43+
</div>
44+
);
45+
}

extension/src/components/UpdatesContainer.tsx

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,38 @@
1-
import { useEffect, useRef } from "preact/hooks";
2-
import { Divider, SignalUpdate } from "../types";
3-
import { UpdateItem } from "./UpdateItem";
1+
import { useRef } from "preact/hooks";
2+
import { updatesStore } from "../models/UpdatesModel";
3+
import { UpdateTreeNodeComponent } from "./UpdateTreeNode";
4+
import { useSignalEffect } from "@preact/signals";
45

5-
export function UpdatesContainer({
6-
updates,
7-
signalCounts,
8-
}: {
9-
updates: (SignalUpdate | Divider)[];
10-
signalCounts: Map<string, number>;
11-
}) {
6+
export function UpdatesContainer() {
127
const updatesListRef = useRef<HTMLDivElement>(null);
13-
const recentUpdates = updates.slice(-50).reverse();
8+
const updateTree = updatesStore.updateTree.value;
149

15-
useEffect(() => {
10+
useSignalEffect(() => {
11+
// Register scroll restoration
12+
// When a new update is added we scroll to top
13+
const tree = updatesStore.updateTree.value;
1614
if (updatesListRef.current) {
1715
updatesListRef.current.scrollTop = 0;
1816
}
19-
}, [updates]);
17+
});
2018

2119
return (
2220
<div className="updates-container">
2321
<div className="updates-header">
2422
<div className="updates-stats">
2523
<span>
26-
Updates:{" "}
27-
<strong>{updates.filter(x => x.type !== "divider").length}</strong>
24+
Updates: <strong>{updatesStore.totalUpdates.value}</strong>
2825
</span>
2926
<span>
30-
Signals: <strong>{signalCounts.size}</strong>
27+
Signals: <strong>{updatesStore.signalCounts.value.size}</strong>
3128
</span>
3229
</div>
3330
</div>
3431

3532
<div className="updates-list" ref={updatesListRef}>
36-
{recentUpdates.map((update, index) =>
37-
update.type === "divider" ? (
38-
index === recentUpdates.length - 1 ? null : (
39-
<div key={`${update.type}-${index}`} className="divider" />
40-
)
41-
) : (
42-
<div key={`${update.receivedAt}-${index}`}>
43-
<UpdateItem update={update} />
44-
</div>
45-
)
46-
)}
33+
{updateTree.map(node => (
34+
<UpdateTreeNodeComponent key={node.id} node={node} />
35+
))}
4736
</div>
4837
</div>
4938
);

extension/src/models/UpdatesModel.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { signal, computed, effect } from "@preact/signals";
22
import { Divider, SignalUpdate } from "../types";
33

4+
export interface UpdateTreeNode {
5+
id: string;
6+
update: SignalUpdate;
7+
children: UpdateTreeNode[];
8+
depth: number;
9+
hasChildren: boolean;
10+
}
11+
412
const createUpdatesModel = () => {
513
const updates = signal<(SignalUpdate | Divider)[]>([]);
614
const lastUpdateId = signal<number>(0);
@@ -34,6 +42,62 @@ const createUpdatesModel = () => {
3442
return counts;
3543
});
3644

45+
const updateTree = computed(() => {
46+
const buildTree = (
47+
updates: (SignalUpdate | Divider)[]
48+
): UpdateTreeNode[] => {
49+
const tree: UpdateTreeNode[] = [];
50+
const stack: UpdateTreeNode[] = [];
51+
52+
// Process updates in reverse order to show newest first
53+
const recentUpdates = updates.slice(-100).reverse();
54+
55+
for (let i = 0; i < recentUpdates.length; i++) {
56+
const item = recentUpdates[i];
57+
58+
// Skip dividers for tree building
59+
if (item.type === "divider") {
60+
continue;
61+
}
62+
63+
const update = item as SignalUpdate;
64+
const depth = update.depth || 0;
65+
66+
// Create a unique ID for this node
67+
const nodeId = `${update.signalName}-${update.receivedAt}-${i}`;
68+
69+
const node: UpdateTreeNode = {
70+
id: nodeId,
71+
update,
72+
children: [],
73+
depth,
74+
hasChildren: false,
75+
};
76+
77+
// Find the correct parent based on depth
78+
while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
79+
stack.pop();
80+
}
81+
82+
if (stack.length === 0) {
83+
// This is a root node
84+
tree.push(node);
85+
} else {
86+
// This is a child node
87+
const parent = stack[stack.length - 1];
88+
parent.children.push(node);
89+
parent.hasChildren = true;
90+
}
91+
92+
stack.push(node);
93+
}
94+
95+
return tree;
96+
};
97+
98+
return buildTree(updates.value);
99+
});
100+
37101
const clearUpdates = () => {
38102
updates.value = [];
39103
lastUpdateId.value = 0;
@@ -72,6 +136,8 @@ const createUpdatesModel = () => {
72136

73137
return {
74138
updates,
139+
updateTree,
140+
totalUpdates: computed(() => Object.keys(updateTree.value).length),
75141
signalCounts,
76142
addUpdate,
77143
clearUpdates,

extension/src/panel.tsx

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,17 @@ import { EmptyState } from "./components/EmptyState";
44
import { Header } from "./components/Header";
55
import { SettingsPanel } from "./components/SettingsPanel";
66
import { GraphVisualization } from "./components/Graph";
7-
import { updatesStore } from "./models/UpdatesModel";
87
import { UpdatesContainer } from "./components/UpdatesContainer";
98
import { connectionStore } from "./models/ConnectionModel";
10-
import { settingsStore } from "./models/SettingsModel";
119

1210
function SignalsDevToolsPanel() {
1311
const activeTab = useSignal<"updates" | "graph">("updates");
1412

1513
return (
1614
<div id="app">
17-
<Header onToggleSettings={settingsStore.toggleSettings} />
15+
<Header />
1816

19-
<SettingsPanel
20-
isVisible={settingsStore.showSettings}
21-
onApply={settingsStore.applySettings}
22-
onCancel={settingsStore.hideSettings}
23-
/>
17+
<SettingsPanel />
2418

2519
<main className="main-content">
2620
<div className="tabs">
@@ -42,15 +36,8 @@ function SignalsDevToolsPanel() {
4236
<EmptyState onRefresh={connectionStore.refreshConnection} />
4337
) : (
4438
<>
45-
{activeTab.value === "updates" && (
46-
<UpdatesContainer
47-
updates={updatesStore.updates.value}
48-
signalCounts={updatesStore.signalCounts.value}
49-
/>
50-
)}
51-
{activeTab.value === "graph" && (
52-
<GraphVisualization updates={updatesStore.updates} />
53-
)}
39+
{activeTab.value === "updates" && <UpdatesContainer />}
40+
{activeTab.value === "graph" && <GraphVisualization />}
5441
</>
5542
)}
5643
</div>

0 commit comments

Comments
 (0)