forked from
pds.ls/pdsls
atmosphere explorer
1import { For, Show } from "solid-js";
2import { STREAM_CONFIGS, StreamType } from "./config";
3
4export type StreamStats = {
5 connectedAt?: number;
6 totalEvents: number;
7 eventsPerSecond: number;
8 eventTypes: Record<string, number>;
9 collections: Record<string, number>;
10};
11
12const formatUptime = (ms: number) => {
13 const seconds = Math.floor(ms / 1000);
14 const minutes = Math.floor(seconds / 60);
15 const hours = Math.floor(minutes / 60);
16
17 if (hours > 0) {
18 return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
19 } else if (minutes > 0) {
20 return `${minutes}m ${seconds % 60}s`;
21 } else {
22 return `${seconds}s`;
23 }
24};
25
26export const StreamStatsPanel = (props: {
27 stats: StreamStats;
28 currentTime: number;
29 streamType: StreamType;
30 showAllEvents?: boolean;
31}) => {
32 const config = () => STREAM_CONFIGS[props.streamType];
33 const uptime = () => (props.stats.connectedAt ? props.currentTime - props.stats.connectedAt : 0);
34
35 const shouldShowEventTypes = () => {
36 if (!config().showEventTypes) return false;
37 if (props.streamType === "jetstream") return props.showAllEvents === true;
38 return true;
39 };
40
41 const topCollections = () =>
42 Object.entries(props.stats.collections)
43 .sort(([, a], [, b]) => b - a)
44 .slice(0, 5);
45
46 const topEventTypes = () =>
47 Object.entries(props.stats.eventTypes)
48 .sort(([, a], [, b]) => b - a)
49 .slice(0, 5);
50
51 return (
52 <Show when={props.stats.connectedAt !== undefined}>
53 <div class="w-full text-sm">
54 <div class="mb-1 font-semibold">Statistics</div>
55 <div class="flex flex-wrap justify-between gap-x-4 gap-y-2">
56 <div>
57 <div class="text-xs text-neutral-500 dark:text-neutral-400">Uptime</div>
58 <div class="font-mono">{formatUptime(uptime())}</div>
59 </div>
60 <div>
61 <div class="text-xs text-neutral-500 dark:text-neutral-400">Total Events</div>
62 <div class="font-mono">{props.stats.totalEvents.toLocaleString()}</div>
63 </div>
64 <div>
65 <div class="text-xs text-neutral-500 dark:text-neutral-400">Events/sec</div>
66 <div class="font-mono">{props.stats.eventsPerSecond.toFixed(1)}</div>
67 </div>
68 <div>
69 <div class="text-xs text-neutral-500 dark:text-neutral-400">Avg/sec</div>
70 <div class="font-mono">
71 {uptime() > 0 ? ((props.stats.totalEvents / uptime()) * 1000).toFixed(1) : "0.0"}
72 </div>
73 </div>
74 </div>
75
76 <Show when={topEventTypes().length > 0 && shouldShowEventTypes()}>
77 <div class="mt-2">
78 <div class="mb-1 text-xs text-neutral-500 dark:text-neutral-400">Event Types</div>
79 <div class="grid grid-cols-[1fr_5rem_3rem] gap-x-1 gap-y-0.5 font-mono text-xs sm:gap-x-4">
80 <For each={topEventTypes()}>
81 {([type, count]) => {
82 const percentage = ((count / props.stats.totalEvents) * 100).toFixed(1);
83 return (
84 <>
85 <span class="text-neutral-700 dark:text-neutral-300">{type}</span>
86 <span class="text-right text-neutral-600 tabular-nums dark:text-neutral-400">
87 {count.toLocaleString()}
88 </span>
89 <span class="text-right text-neutral-400 tabular-nums dark:text-neutral-500">
90 {percentage}%
91 </span>
92 </>
93 );
94 }}
95 </For>
96 </div>
97 </div>
98 </Show>
99
100 <Show when={topCollections().length > 0}>
101 <div class="mt-2">
102 <div class="mb-1 text-xs text-neutral-500 dark:text-neutral-400">
103 {config().collectionsLabel}
104 </div>
105 <div class="grid grid-cols-[1fr_5rem_3rem] gap-x-1 gap-y-0.5 font-mono text-xs sm:gap-x-4">
106 <For each={topCollections()}>
107 {([collection, count]) => {
108 const percentage = ((count / props.stats.totalEvents) * 100).toFixed(1);
109 return (
110 <>
111 <span class="min-w-0 truncate text-neutral-700 dark:text-neutral-300">
112 {collection}
113 </span>
114 <span class="text-right text-neutral-600 tabular-nums dark:text-neutral-400">
115 {count.toLocaleString()}
116 </span>
117 <span class="text-right text-neutral-400 tabular-nums dark:text-neutral-500">
118 {percentage}%
119 </span>
120 </>
121 );
122 }}
123 </For>
124 </div>
125 </div>
126 </Show>
127 </div>
128 </Show>
129 );
130};