atmosphere explorer
at main 130 lines 4.8 kB view raw
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};