1
1
import { useMemo } from "react" ;
2
2
import { useAuth } from "@workos-inc/authkit-react" ;
3
3
import { useConvexAuth , useQuery } from "convex/react" ;
4
- import { formatTime , aggregateSuite } from "./helpers" ;
4
+ import { aggregateSuite } from "./helpers" ;
5
5
import type { EvalSuite , EvalCase , EvalIteration } from "./types" ;
6
6
7
7
interface SuiteRowProps {
8
8
suite : EvalSuite ;
9
9
onSelectSuite : ( id : string ) => void ;
10
10
}
11
11
12
- interface SuiteStatusBadgesProps {
13
- passed : number ;
14
- failed : number ;
15
- cancelled : number ;
16
- pending : number ;
17
- }
12
+ function formatCompactStatus (
13
+ passed : number ,
14
+ failed : number ,
15
+ cancelled : number ,
16
+ pending : number ,
17
+ ) : string {
18
+ const parts : string [ ] = [ ] ;
18
19
19
- function SuiteStatusBadges ( {
20
- passed,
21
- failed,
22
- cancelled,
23
- pending,
24
- } : SuiteStatusBadgesProps ) {
25
- return (
26
- < div className = "flex items-center gap-2 text-xs" >
27
- { passed > 0 && (
28
- < span className = "inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-green-700" >
29
- { passed } passed
30
- </ span >
31
- ) }
32
- { failed > 0 && (
33
- < span className = "inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-red-700" >
34
- { failed } failed
35
- </ span >
36
- ) }
37
- { cancelled > 0 && (
38
- < span className = "inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-gray-700" >
39
- { cancelled } cancelled
40
- </ span >
41
- ) }
42
- { pending > 0 && (
43
- < span className = "inline-flex items-center rounded-full bg-yellow-100 px-2 py-0.5 text-yellow-700" >
44
- { pending } pending
45
- </ span >
46
- ) }
47
- </ div >
48
- ) ;
20
+ if ( passed > 0 ) parts . push ( `${ passed } passed` ) ;
21
+ if ( failed > 0 ) parts . push ( `${ failed } failed` ) ;
22
+ if ( cancelled > 0 ) parts . push ( `${ cancelled } cancelled` ) ;
23
+ if ( pending > 0 ) parts . push ( `${ pending } pending` ) ;
24
+
25
+ return parts . join ( " · " ) || "No results" ;
49
26
}
50
27
51
28
export function SuiteRow ( { suite, onSelectSuite } : SuiteRowProps ) {
52
29
const { isAuthenticated } = useConvexAuth ( ) ;
53
30
const { user } = useAuth ( ) ;
31
+ const servers = suite . config ?. environment . servers ;
54
32
55
33
const enableQuery = isAuthenticated && ! ! user ;
56
34
const suiteDetails = useQuery (
@@ -73,40 +51,78 @@ export function SuiteRow({ suite, onSelectSuite }: SuiteRowProps) {
73
51
? suite . config . tests . length
74
52
: 0 ;
75
53
54
+ const serverTags = useMemo ( ( ) => {
55
+ if ( ! Array . isArray ( servers ) ) return [ ] as string [ ] ;
56
+
57
+ const sanitized = servers
58
+ . filter ( ( server ) : server is string => typeof server === "string" )
59
+ . map ( ( server ) => server . trim ( ) )
60
+ . filter ( Boolean ) ;
61
+
62
+ if ( sanitized . length <= 2 ) {
63
+ return sanitized ;
64
+ }
65
+
66
+ const remaining = sanitized . length - 2 ;
67
+ return [ ...sanitized . slice ( 0 , 2 ) , `+${ remaining } more` ] ;
68
+ } , [ servers ] ) ;
69
+
70
+ const totalIterations = aggregate ?. filteredIterations . length ?? 0 ;
71
+
72
+ const getBorderColor = ( ) => {
73
+ if ( ! aggregate ) return "bg-zinc-300/50" ;
74
+
75
+ const { passed, failed, cancelled, pending } = aggregate . totals ;
76
+ const total = passed + failed + cancelled + pending ;
77
+
78
+ if ( total === 0 ) return "bg-zinc-300/50" ;
79
+
80
+ const completedTotal = passed + failed ;
81
+ if ( completedTotal === 0 ) return "bg-zinc-300/50" ;
82
+
83
+ const failureRate = ( failed / completedTotal ) * 100 ;
84
+
85
+ if ( failureRate === 0 ) return "bg-emerald-500/50" ;
86
+ if ( failureRate <= 30 ) return "bg-amber-500/50" ;
87
+ return "bg-red-500/50" ;
88
+ } ;
89
+
76
90
return (
77
91
< button
78
92
onClick = { ( ) => onSelectSuite ( suite . _id ) }
79
- className = "grid w-full grid-cols-[minmax(0,1fr)_minmax(0,1.5fr)_minmax(0,0.8fr)] items-center gap-3 px -4 py-3 text-left transition-colors hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/60"
93
+ className = "group relative flex w-full items-center gap-4 py-3 pl -4 pr-4 text-left transition-colors hover:bg-muted/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/60 cursor-pointer "
80
94
>
81
- < div >
82
- < div className = "font-medium" >
83
- { new Date ( suite . _creationTime || 0 ) . toLocaleDateString ( "en-US" , {
84
- month : "short" ,
85
- day : "numeric" ,
86
- year : "numeric" ,
87
- hour : "numeric" ,
88
- minute : "2-digit" ,
89
- hour12 : true ,
90
- } ) }
95
+ < div className = { `absolute left-0 top-0 h-full w-1 ${ getBorderColor ( ) } ` } />
96
+ < div className = "grid min-w-0 flex-1 grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)] items-center gap-4" >
97
+ < div className = "min-w-0" >
98
+ < div className = "text-sm font-medium text-foreground" >
99
+ { new Date ( suite . _creationTime || 0 ) . toLocaleDateString ( "en-US" , {
100
+ month : "short" ,
101
+ day : "numeric" ,
102
+ year : "numeric" ,
103
+ hour : "numeric" ,
104
+ minute : "2-digit" ,
105
+ hour12 : true ,
106
+ } ) }
107
+ </ div >
108
+ < div className = "text-xs text-muted-foreground" >
109
+ { serverTags . length > 0 ? serverTags . join ( ", " ) : "No servers" }
110
+ </ div >
91
111
</ div >
92
- < div className = "text-xs text-muted-foreground" >
93
- { testCount } test{ testCount !== 1 ? "s" : "" }
112
+ < div className = "text-sm text-muted-foreground" >
113
+ { testCount } test{ testCount !== 1 ? "s" : "" } · { totalIterations } { " " }
114
+ iteration{ totalIterations !== 1 ? "s" : "" }
115
+ </ div >
116
+ < div className = "text-sm text-muted-foreground" >
117
+ { aggregate
118
+ ? formatCompactStatus (
119
+ aggregate . totals . passed ,
120
+ aggregate . totals . failed ,
121
+ aggregate . totals . cancelled ,
122
+ aggregate . totals . pending ,
123
+ )
124
+ : "Loading..." }
94
125
</ div >
95
- </ div >
96
- < div >
97
- { aggregate ? (
98
- < SuiteStatusBadges
99
- passed = { aggregate . totals . passed }
100
- failed = { aggregate . totals . failed }
101
- cancelled = { aggregate . totals . cancelled }
102
- pending = { aggregate . totals . pending }
103
- />
104
- ) : (
105
- < span className = "text-xs text-muted-foreground" > Loading...</ span >
106
- ) }
107
- </ div >
108
- < div className = "text-sm text-muted-foreground" >
109
- { formatTime ( suite . _creationTime ) }
110
126
</ div >
111
127
</ button >
112
128
) ;
0 commit comments