11import { Fragment } from "react" ;
2+ import { Link } from "react-router-dom" ;
23import clsx from "clsx" ;
3- import type { HealthCell } from "../api/types" ;
4+ import type { Filter , HealthCell } from "../api/types" ;
45import { formatTimestamp } from "../lib/utils" ;
56
67interface Props {
78 // Outer = days (chronological), inner = 96 cells per day (15-minute spans).
89 grid : HealthCell [ ] [ ] ;
10+ filter ?: Filter ;
911}
1012
1113function cellTone ( cell : HealthCell , maxTotal : number ) : string {
@@ -19,7 +21,7 @@ function cellTone(cell: HealthCell, maxTotal: number): string {
1921 return "bg-success/40" ;
2022}
2123
22- export default function HealthGrid ( { grid } : Props ) {
24+ export default function HealthGrid ( { grid, filter } : Props ) {
2325 if ( ! grid || grid . length === 0 ) {
2426 return (
2527 < div className = "bg-panel border border-border rounded-lg p-6 text-muted text-sm text-center" >
@@ -63,14 +65,27 @@ export default function HealthGrid({ grid }: Props) {
6365 </ div >
6466 { days . map ( ( day , di ) => {
6567 const cell = day . hours [ hour ] ;
68+ const title = `${ day . title } ${ hourLabel ( hour ) } — ${ cell . total } requests, ${ cell . failed } failed${
69+ cell . bucket ? ` (${ formatTimestamp ( cell . bucket ) } )` : ""
70+ } `;
6671 return (
6772 < div key = { `${ di } -${ hour } ` } className = "flex h-3 items-center justify-center" >
68- < div
69- className = { clsx ( "h-3 w-3 rounded-[2px]" , cellTone ( cell , maxTotal ) ) }
70- title = { `${ day . title } ${ hourLabel ( hour ) } — ${ cell . total } requests, ${ cell . failed } failed${
71- cell . bucket ? ` (${ formatTimestamp ( cell . bucket ) } )` : ""
72- } `}
73- />
73+ { cell . total > 0 && cell . bucket ? (
74+ < Link
75+ to = { { pathname : "/events" , search : eventSearch ( cell , filter ) } }
76+ className = { clsx (
77+ "block h-3 w-3 rounded-[2px] transition-shadow hover:ring-1 hover:ring-accent focus:outline-none focus:ring-1 focus:ring-accent" ,
78+ cellTone ( cell , maxTotal ) ,
79+ ) }
80+ title = { title }
81+ aria-label = { `Open events for ${ title } ` }
82+ />
83+ ) : (
84+ < div
85+ className = { clsx ( "h-3 w-3 rounded-[2px]" , cellTone ( cell , maxTotal ) ) }
86+ title = { title }
87+ />
88+ ) }
7489 </ div >
7590 ) ;
7691 } ) }
@@ -82,6 +97,32 @@ export default function HealthGrid({ grid }: Props) {
8297 ) ;
8398}
8499
100+ function eventSearch ( cell : HealthCell , filter ?: Filter ) : string {
101+ const start = new Date ( cell . bucket ) ;
102+ if ( Number . isNaN ( start . getTime ( ) ) ) return "" ;
103+ const end = new Date ( start . getTime ( ) + 60 * 60 * 1000 ) ;
104+ const sp = new URLSearchParams ( ) ;
105+ sp . set ( "range" , "custom" ) ;
106+ sp . set ( "start" , formatDateTimeParam ( start ) ) ;
107+ sp . set ( "end" , formatDateTimeParam ( end ) ) ;
108+ for ( const model of filter ?. models ?? [ ] ) sp . append ( "model" , model ) ;
109+ for ( const source of filter ?. sources ?? [ ] ) sp . append ( "source" , source ) ;
110+ for ( const key of filter ?. apiKey ?? [ ] ) sp . append ( "api_key" , key ) ;
111+ if ( filter ?. authIndex ) sp . set ( "auth_index" , filter . authIndex ) ;
112+ if ( filter ?. result ) sp . set ( "result" , filter . result ) ;
113+ return `?${ sp . toString ( ) } ` ;
114+ }
115+
116+ function formatDateTimeParam ( d : Date ) : string {
117+ const year = d . getFullYear ( ) ;
118+ const month = String ( d . getMonth ( ) + 1 ) . padStart ( 2 , "0" ) ;
119+ const day = String ( d . getDate ( ) ) . padStart ( 2 , "0" ) ;
120+ const hour = String ( d . getHours ( ) ) . padStart ( 2 , "0" ) ;
121+ const minute = String ( d . getMinutes ( ) ) . padStart ( 2 , "0" ) ;
122+ const second = String ( d . getSeconds ( ) ) . padStart ( 2 , "0" ) ;
123+ return `${ year } -${ month } -${ day } T${ hour } :${ minute } :${ second } ` ;
124+ }
125+
85126function hourlyCells ( day : HealthCell [ ] ) : HealthCell [ ] {
86127 return Array . from ( { length : 24 } , ( _ , hour ) => {
87128 const cells = day . slice ( hour * 4 , hour * 4 + 4 ) ;
0 commit comments