Skip to content

Commit 4be3cbb

Browse files
committed
Allow folding updates
1 parent 44b284f commit 4be3cbb

9 files changed

Lines changed: 212 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: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
11
import { useSignal, useSignalEffect } from "@preact/signals";
22
import { Button } from "./Button";
33
import { Settings } from "../types";
4+
import { settingsStore } from "../models/SettingsModel";
45

5-
interface SettingsPanelProps {
6-
isVisible: boolean;
7-
settings: Settings;
8-
onApply: (settings: Settings) => void;
9-
onCancel: () => void;
10-
}
6+
export function SettingsPanel() {
7+
const onCancel = settingsStore.hideSettings;
8+
const onApply = settingsStore.applySettings;
9+
const settings = settingsStore.settings;
10+
const isVisible = settingsStore.showSettings;
1111

12-
export function SettingsPanel({
13-
isVisible,
14-
settings,
15-
onApply,
16-
onCancel,
17-
}: SettingsPanelProps) {
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 & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,9 @@ function SignalsDevToolsPanel() {
1414

1515
return (
1616
<div id="app">
17-
<Header onToggleSettings={settingsStore.toggleSettings} />
17+
<Header />
1818

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

2621
<main className="main-content">
2722
<div className="tabs">
@@ -43,15 +38,8 @@ function SignalsDevToolsPanel() {
4338
<EmptyState onRefresh={connectionStore.refreshConnection} />
4439
) : (
4540
<>
46-
{activeTab.value === "updates" && (
47-
<UpdatesContainer
48-
updates={updatesStore.updates.value}
49-
signalCounts={updatesStore.signalCounts.value}
50-
/>
51-
)}
52-
{activeTab.value === "graph" && (
53-
<GraphVisualization updates={updatesStore.updates} />
54-
)}
41+
{activeTab.value === "updates" && <UpdatesContainer />}
42+
{activeTab.value === "graph" && <GraphVisualization />}
5543
</>
5644
)}
5745
</div>

0 commit comments

Comments
 (0)