-
-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathConnectionIndicator.tsx
More file actions
122 lines (114 loc) · 3.56 KB
/
ConnectionIndicator.tsx
File metadata and controls
122 lines (114 loc) · 3.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import { memo } from "react";
import { RefreshCwIcon } from "lucide-react";
import { useConnectionHealth } from "../hooks/useConnectionHealth";
import { Tooltip, TooltipTrigger, TooltipPopup } from "./ui/tooltip";
function formatLatency(ms: number | null): string {
if (ms === null) return "--";
if (ms < 1) return "<1ms";
return `${Math.round(ms)}ms`;
}
function formatUptime(ms: number): string {
const seconds = Math.floor(ms / 1_000);
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return `${hours}h ${remainingMinutes}m`;
}
/**
* A compact dot indicator that shows the live WebSocket connection state.
*
* - Green pulsing dot: connected
* - Amber spinning dot: reconnecting
* - Red dot: disconnected
* - Blue pulsing dot: initial connect
*
* Hover reveals a tooltip with latency, uptime, and reconnect count.
*/
export const ConnectionIndicator = memo(function ConnectionIndicator() {
const { state, metrics } = useConnectionHealth();
// Non-null: `connecting` is always defined in STATE_DOT_CONFIG.
const config = (STATE_DOT_CONFIG[state] ?? STATE_DOT_CONFIG.connecting)!;
return (
<Tooltip>
<TooltipTrigger
className="relative flex items-center gap-1.5 rounded-md px-1.5 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
aria-label={`Connection: ${config.label}`}
>
<span className="relative flex h-2.5 w-2.5 items-center justify-center">
{config.pulse && (
<span
className={`absolute inline-flex h-full w-full animate-ping rounded-full opacity-60 ${config.pingColor}`}
/>
)}
{config.spin ? (
<RefreshCwIcon className={`h-2.5 w-2.5 animate-spin ${config.iconColor}`} />
) : (
<span className={`relative inline-flex h-2 w-2 rounded-full ${config.dotColor}`} />
)}
</span>
</TooltipTrigger>
<TooltipPopup side="bottom" align="end">
<div className="flex flex-col gap-1 py-0.5">
<span className="font-medium">{config.label}</span>
<div className="flex flex-col gap-0.5 text-[10px] text-muted-foreground">
<span>Latency: {formatLatency(metrics.latencyMs)}</span>
<span>Uptime: {formatUptime(metrics.uptimeMs)}</span>
{metrics.reconnectCount > 0 && <span>Reconnects: {metrics.reconnectCount}</span>}
</div>
</div>
</TooltipPopup>
</Tooltip>
);
});
interface DotConfig {
label: string;
dotColor: string;
pingColor: string;
iconColor: string;
pulse: boolean;
spin: boolean;
}
const STATE_DOT_CONFIG: Record<string, DotConfig> = {
open: {
label: "Connected",
dotColor: "bg-emerald-500",
pingColor: "bg-emerald-400",
iconColor: "",
pulse: true,
spin: false,
},
reconnecting: {
label: "Reconnecting...",
dotColor: "bg-amber-500",
pingColor: "bg-amber-400",
iconColor: "text-amber-500",
pulse: false,
spin: true,
},
closed: {
label: "Disconnected",
dotColor: "bg-red-500",
pingColor: "bg-red-400",
iconColor: "",
pulse: false,
spin: false,
},
connecting: {
label: "Connecting...",
dotColor: "bg-blue-500",
pingColor: "bg-blue-400",
iconColor: "",
pulse: true,
spin: false,
},
disposed: {
label: "Closed",
dotColor: "bg-zinc-400",
pingColor: "",
iconColor: "",
pulse: false,
spin: false,
},
};