Skip to content

Commit 555066f

Browse files
committed
Improve layout and button appearance
1 parent c4cd04c commit 555066f

File tree

6 files changed

+146
-77
lines changed

6 files changed

+146
-77
lines changed

ui/src/components/Button.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { FC, ButtonHTMLAttributes } from "react";
2+
3+
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
4+
variant?: "primary" | "secondary" | "danger";
5+
size?: "sm" | "md";
6+
}
7+
8+
export const Button: FC<ButtonProps> = ({
9+
children,
10+
className = "",
11+
variant = "secondary",
12+
size = "md",
13+
disabled,
14+
...props
15+
}) => {
16+
const baseStyles = "rounded-md font-medium transition-all duration-150 active:scale-95 focus:outline-none focus:ring-2 focus:ring-offset-2";
17+
18+
const variantStyles = {
19+
primary: "bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white focus:ring-blue-500 disabled:bg-gray-300 disabled:text-gray-500",
20+
secondary: "bg-gray-600 hover:bg-gray-700 active:bg-gray-800 text-white focus:ring-gray-500 disabled:bg-gray-300 disabled:text-gray-500",
21+
danger: "bg-red-600 hover:bg-red-700 active:bg-red-800 text-white focus:ring-red-500 disabled:bg-gray-300 disabled:text-gray-500"
22+
};
23+
24+
const sizeStyles = {
25+
sm: "px-2 py-1 text-sm",
26+
md: "px-4 py-2 text-sm"
27+
};
28+
29+
const disabledStyles = disabled ? "cursor-not-allowed" : "cursor-pointer";
30+
31+
return (
32+
<button
33+
className={`${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]} ${disabledStyles} ${className}`}
34+
disabled={disabled}
35+
{...props}
36+
>
37+
{children}
38+
</button>
39+
);
40+
};

ui/src/components/Graph/modules/Canvas.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const Canvas: FC = () => {
2323
} = useSimContext();
2424
const isDragging = useRef(false);
2525
const dragStart = useRef({ x: 0, y: 0 });
26+
const hasMoved = useRef(false);
2627
const pointerCapture = useRef<number | undefined>(undefined);
2728

2829
useEffect(() => {
@@ -56,6 +57,11 @@ export const Canvas: FC = () => {
5657
return;
5758
}
5859

60+
// Only handle clicks if we weren't dragging
61+
if (hasMoved.current) {
62+
return;
63+
}
64+
5965
const rect = canvas.getBoundingClientRect();
6066
const x = ev.clientX - rect.left;
6167
const y = ev.clientY - rect.top;
@@ -74,6 +80,12 @@ export const Canvas: FC = () => {
7480
type: "SET_CURRENT_NODE",
7581
payload: currentNode === node ? undefined : node,
7682
});
83+
} else {
84+
// Click on background - unset current node
85+
dispatch({
86+
type: "SET_CURRENT_NODE",
87+
payload: undefined,
88+
});
7789
}
7890
},
7991
[canvasScale, currentNode, canvasOffsetX, canvasOffsetY],
@@ -116,6 +128,7 @@ export const Canvas: FC = () => {
116128
}
117129

118130
isDragging.current = true;
131+
hasMoved.current = false;
119132
canvas.setPointerCapture(ev.pointerId);
120133
pointerCapture.current = ev.pointerId;
121134
dragStart.current = { x: ev.clientX, y: ev.clientY };
@@ -130,6 +143,11 @@ export const Canvas: FC = () => {
130143
const deltaX = ev.clientX - dragStart.current.x;
131144
const deltaY = ev.clientY - dragStart.current.y;
132145

146+
// Mark that we've moved if there's significant movement
147+
if (Math.abs(deltaX) > 2 || Math.abs(deltaY) > 2) {
148+
hasMoved.current = true;
149+
}
150+
133151
dispatch({
134152
type: "SET_CANVAS_PROPS",
135153
payload: {

ui/src/components/Sim/SimWrapper.tsx

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,9 @@ import { Playback } from "./modules/Playback";
1111
import { Stats } from "./modules/Stats";
1212
import { ITransformedNode } from "./types";
1313

14-
1514
export const SimWrapper: FC = () => {
1615
const {
17-
state: {
18-
topologyPath,
19-
topologyLoaded,
20-
},
16+
state: { topologyPath, topologyLoaded },
2117
dispatch,
2218
} = useSimContext();
2319

@@ -31,7 +27,10 @@ export const SimWrapper: FC = () => {
3127
const topographyRes = await fetch(topologyPath);
3228
const topography = parse(await topographyRes.text());
3329
const nodes = new Map<string, ITransformedNode>();
34-
const links = new Map<string, { source: string; target: string; latencyMs?: number }>();
30+
const links = new Map<
31+
string,
32+
{ source: string; target: string; latencyMs?: number }
33+
>();
3534
for (const [id, node] of Object.entries<Node<Coord2D>>(
3635
topography.nodes,
3736
)) {
@@ -48,12 +47,15 @@ export const SimWrapper: FC = () => {
4847
for (const [peerId, peerData] of Object.entries(node.producers)) {
4948
const linkIds = [id, peerId].sort();
5049
const linkKey = `${linkIds[0]}|${linkIds[1]}`;
51-
50+
5251
// Store latency from this node to the peer
53-
const latencyMs = (peerData as any)?.['latency-ms'];
54-
52+
const latencyMs = (peerData as any)?.["latency-ms"];
53+
5554
// Only set latency if we haven't seen this link before, or if this latency is valid
56-
if (!links.has(linkKey) || (latencyMs !== undefined && latencyMs !== null)) {
55+
if (
56+
!links.has(linkKey) ||
57+
(latencyMs !== undefined && latencyMs !== null)
58+
) {
5759
links.set(linkKey, {
5860
source: linkIds[0],
5961
target: linkIds[1],
@@ -73,22 +75,16 @@ export const SimWrapper: FC = () => {
7375
}, [topologyPath]);
7476

7577
return (
76-
<>
77-
<div className="flex flex-col items-center justify-between gap-4 z-10 absolute left-10 top-10">
78+
<div className="relative h-screen w-screen">
79+
<div className="flex flex-col items-start gap-4 z-10 absolute left-10 top-10">
80+
<Scenario />
7881
<Stats />
7982
</div>
80-
<div className="flex items-center justify-center gap-4 relative h-screen w-screen">
81-
{topologyLoaded ? <GraphWrapper /> : null}
82-
<div className="absolute bottom-12 flex w-3/4 gap-4 justify-center">
83-
<div className="flex flex-shrink-0 border-2 rounded-md p-4 border-gray-200 gap-4 my-4 mx-auto bg-white/80 backdrop-blur-xs">
84-
<Scenario />
85-
</div>
86-
<div className="flex border-2 rounded-md p-4 border-gray-200 gap-4 my-4 mx-auto w-full bg-white/80 backdrop-blur-xs">
87-
<Playback />
88-
<TimelineSlider />
89-
</div>
90-
</div>
83+
{topologyLoaded ? <GraphWrapper /> : null}
84+
<div className="absolute bottom-10 left-10 right-10 z-10 border-2 rounded-md p-4 border-gray-200 bg-white/80 backdrop-blur-xs flex gap-4">
85+
<Playback />
86+
<TimelineSlider />
9187
</div>
92-
</>
88+
</div>
9389
);
9490
};

ui/src/components/Sim/modules/Playback.tsx

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useSimContext } from "@/contexts/SimContext/context";
22
import { FC, useCallback, useEffect, useRef } from "react";
3+
import { Button } from "@/components/Button";
34

45
export const Playback: FC = () => {
56
const {
@@ -55,9 +56,12 @@ export const Playback: FC = () => {
5556
((now - lastUpdateRef.current) / 1000) * speedMultiplier;
5657
lastUpdateRef.current = now;
5758

58-
const newTime = Math.min(currentTimeRef.current + deltaTime, maxEventTime);
59+
const newTime = Math.min(
60+
currentTimeRef.current + deltaTime,
61+
maxEventTime,
62+
);
5963
currentTimeRef.current = newTime;
60-
64+
6165
dispatch({
6266
type: "SET_TIMELINE_TIME",
6367
payload: newTime,
@@ -97,50 +101,59 @@ export const Playback: FC = () => {
97101
return (
98102
<div className="flex items-center gap-2">
99103
{/* Play/Pause button */}
100-
<button
104+
<Button
101105
onClick={handlePlayPause}
102106
disabled={disabled}
103-
className="bg-blue-500 text-white px-3 py-2 rounded disabled:bg-gray-300 disabled:cursor-not-allowed w-16 text-sm"
107+
variant="primary"
108+
className="w-20"
104109
>
105110
{isPlaying ? "Pause" : "Play"}
106-
</button>
111+
</Button>
107112

108113
{/* Step controls: << < > >> */}
109-
<button
114+
<Button
110115
onClick={() => handleStep(-0.01)}
111116
disabled={disabled}
112-
className="bg-gray-500 text-white px-2 py-2 rounded disabled:bg-gray-300 disabled:cursor-not-allowed text-sm"
117+
variant="secondary"
118+
size="sm"
119+
className="px-2"
113120
title="Step backward 10ms"
114121
>
115122
&lt;&lt;
116-
</button>
123+
</Button>
117124

118-
<button
125+
<Button
119126
onClick={() => handleStep(-0.001)}
120127
disabled={disabled}
121-
className="bg-gray-500 text-white px-2 py-2 rounded disabled:bg-gray-300 disabled:cursor-not-allowed text-sm"
128+
variant="secondary"
129+
size="sm"
130+
className="px-2"
122131
title="Step backward 1ms"
123132
>
124133
&lt;
125-
</button>
134+
</Button>
126135

127-
<button
136+
<Button
128137
onClick={() => handleStep(0.001)}
129138
disabled={disabled}
130-
className="bg-gray-500 text-white px-2 py-2 rounded disabled:bg-gray-300 disabled:cursor-not-allowed text-sm"
139+
variant="secondary"
140+
size="sm"
141+
className="px-2"
131142
title="Step forward 1ms"
132143
>
133144
&gt;
134-
</button>
145+
</Button>
135146

136-
<button
147+
<Button
137148
onClick={() => handleStep(0.01)}
138149
disabled={disabled}
139-
className="bg-gray-500 text-white px-2 py-2 rounded disabled:bg-gray-300 disabled:cursor-not-allowed text-sm"
150+
variant="secondary"
151+
size="sm"
152+
className="px-2"
140153
title="Step forward 10ms"
141154
>
142155
&gt;&gt;
143-
</button>
156+
</Button>
144157

145158
{/* Speed control */}
146159
<div className="min-w-16">

ui/src/components/Sim/modules/Scenario.tsx

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
import { IScenario } from "@/contexts/SimContext/types";
66
import { ChangeEvent, FC, useCallback, useEffect, useState } from "react";
77
import { useStreamMessagesHandler } from "../hooks/useStreamMessagesHandler";
8+
import { Button } from "@/components/Button";
89

910
export const Scenario: FC = () => {
1011
const {
@@ -55,7 +56,7 @@ export const Scenario: FC = () => {
5556
const isLoaded = events.length > 0 || streaming;
5657

5758
return (
58-
<div className="flex items-center justify-start gap-4">
59+
<div className="flex items-center justify-start gap-4 border-2 rounded-md p-4 border-gray-200 bg-white/80 backdrop-blur-xs">
5960
<div className="min-w-32">
6061
<label htmlFor="scenario" className="block text-xs text-gray-600">
6162
Scenario
@@ -72,24 +73,6 @@ export const Scenario: FC = () => {
7273
</select>
7374
</div>
7475

75-
<div className="flex gap-2">
76-
<button
77-
className="bg-[blue] text-white rounded-md px-4 py-2"
78-
onClick={handleStartStream}
79-
disabled={streaming || isLoaded}
80-
>
81-
{streaming ? "Loading..." : isLoaded ? "Loaded" : "Load Scenario"}
82-
</button>
83-
{isLoaded && (
84-
<button
85-
className="bg-gray-400 text-white w-[80px] rounded-md px-4 py-2"
86-
onClick={handleUnloadScenario}
87-
>
88-
{streaming ? "Cancel" : "Unload"}
89-
</button>
90-
)}
91-
</div>
92-
9376
<div className="flex flex-col gap-1">
9477
<label className="flex items-center gap-2 text-sm">
9578
<input
@@ -101,6 +84,25 @@ export const Scenario: FC = () => {
10184
Include Transactions
10285
</label>
10386
</div>
87+
88+
<div className="flex gap-2">
89+
<Button
90+
variant="primary"
91+
onClick={handleStartStream}
92+
disabled={streaming || isLoaded}
93+
>
94+
{streaming ? "Loading..." : isLoaded ? "Loaded" : "Load Scenario"}
95+
</Button>
96+
{isLoaded && (
97+
<Button
98+
variant="secondary"
99+
onClick={handleUnloadScenario}
100+
className="w-[80px]"
101+
>
102+
{streaming ? "Cancel" : "Unload"}
103+
</Button>
104+
)}
105+
</div>
104106
</div>
105107
);
106108
};

ui/src/components/Sim/modules/Stats.tsx

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,24 @@ export const Stats: FC = () => {
1010

1111
return (
1212
<div
13-
className={`flex flex-col gap-4 backdrop-blur-xs bg-white/80 text-xl min-w-[300px]`}
13+
className={`flex flex-col gap-4 backdrop-blur-xs bg-white/80 min-w-[300px]`}
1414
>
1515
<div className="border-2 border-gray-200 rounded-sm p-4">
16-
<h2 className="font-bold uppercase mb-2">Global Stats</h2>
16+
<h4 className="font-bold uppercase mb-2">Global Stats</h4>
1717

18-
<div className="text-base">
19-
<h4 className="flex items-center justify-between gap-4">
20-
Loaded Events: <span>{events.length}</span>
21-
</h4>
22-
<h4 className="flex items-center justify-between gap-4">
23-
Current Time: <span>{currentTime.toFixed(2)}s</span>
24-
</h4>
25-
<h4 className="flex items-center justify-between gap-4">
26-
Events at Time: <span>{aggregatedData.eventCounts.total}</span>
27-
</h4>
28-
<br />
29-
<h4 className="font-semibold">Event Types</h4>
30-
{aggregatedData.eventCounts.total > 0 && (
18+
<h4 className="flex items-center justify-between gap-4">
19+
Loaded Events: <span>{events.length}</span>
20+
</h4>
21+
<h4 className="flex items-center justify-between gap-4">
22+
Current Time: <span>{currentTime.toFixed(2)}s</span>
23+
</h4>
24+
<h4 className="flex items-center justify-between gap-4">
25+
Events at Time: <span>{aggregatedData.eventCounts.total}</span>
26+
</h4>
27+
<br />
28+
{aggregatedData.eventCounts.total > 0 && (
29+
<>
30+
<h4 className="font-semibold">Event Types</h4>
3131
<div className="text-sm mt-2">
3232
{Object.values(EServerMessageType).map((eventType) => {
3333
const count = aggregatedData.eventCounts.byType[eventType];
@@ -38,8 +38,8 @@ export const Stats: FC = () => {
3838
);
3939
})}
4040
</div>
41-
)}
42-
</div>
41+
</>
42+
)}
4343
</div>
4444
</div>
4545
);

0 commit comments

Comments
 (0)