invocations-sparkline.tsx67 lines · main
| 1 | interface Hour { |
| 2 | hour: string; |
| 3 | count: number; |
| 4 | errCount: number; |
| 5 | } |
| 6 | |
| 7 | /** |
| 8 | * Tiny inline SVG bar chart. No deps, no client JS — server-rendered. |
| 9 | * Renders one bar per hour; height scales to the max bar in the series. |
| 10 | * Error count overlays the bar in red. |
| 11 | */ |
| 12 | export function InvocationsSparkline({ hours }: { hours: Hour[] }) { |
| 13 | if (hours.length === 0) return null; |
| 14 | const total = hours.reduce((acc, h) => acc + h.count, 0); |
| 15 | if (total === 0) return null; |
| 16 | const max = Math.max(...hours.map((h) => h.count)); |
| 17 | const W = 280; |
| 18 | const H = 40; |
| 19 | const barW = W / hours.length; |
| 20 | const gap = 1; |
| 21 | |
| 22 | return ( |
| 23 | <div className="flex flex-col gap-1"> |
| 24 | <svg |
| 25 | viewBox={`0 0 ${W} ${H}`} |
| 26 | width="100%" |
| 27 | height={H} |
| 28 | preserveAspectRatio="none" |
| 29 | aria-label="invocations per hour, last 24 hours" |
| 30 | > |
| 31 | {hours.map((h, i) => { |
| 32 | const barH = max === 0 ? 0 : Math.max(1, (h.count / max) * H); |
| 33 | const errH = max === 0 ? 0 : (h.errCount / max) * H; |
| 34 | return ( |
| 35 | <g key={i}> |
| 36 | <rect |
| 37 | x={i * barW + gap / 2} |
| 38 | y={H - barH} |
| 39 | width={barW - gap} |
| 40 | height={barH} |
| 41 | fill="var(--color-primary)" |
| 42 | opacity={0.6} |
| 43 | > |
| 44 | <title>{`${new Date(h.hour).toISOString().slice(11, 16)} · ${h.count} invocations${h.errCount > 0 ? ` · ${h.errCount} err` : ''}`}</title> |
| 45 | </rect> |
| 46 | {errH > 0 ? ( |
| 47 | <rect |
| 48 | x={i * barW + gap / 2} |
| 49 | y={H - errH} |
| 50 | width={barW - gap} |
| 51 | height={errH} |
| 52 | fill="#f87171" |
| 53 | opacity={0.9} |
| 54 | /> |
| 55 | ) : null} |
| 56 | </g> |
| 57 | ); |
| 58 | })} |
| 59 | </svg> |
| 60 | <div className="flex justify-between font-mono text-[9px] text-[var(--color-text-subtle)]"> |
| 61 | <span>−24h</span> |
| 62 | <span>{total.toLocaleString()} invocations</span> |
| 63 | <span>now</span> |
| 64 | </div> |
| 65 | </div> |
| 66 | ); |
| 67 | } |