1+ import type { ReactNode } from 'react'
12import type { TFunction } from 'i18next'
23import { Crosshair , Lock } from 'lucide-react'
34import { cn } from '@/shared/lib/utils'
@@ -8,60 +9,86 @@ import { DataTypeChip } from './rule-form'
89import { SelectAllCheckbox } from './select-all-checkbox'
910import { Toggle } from './toggle'
1011
11- const COLS = '32px 1.4fr 1fr 110px 100px 80px 48px 50px'
12+ const TH = 'whitespace-nowrap px-3 py-2.5 text-left align-middle font-medium'
13+ const TD = 'whitespace-nowrap px-3 py-2.5 align-middle'
1214const IMPACT_TONE : Record < string , string > = { high : 'text-red-500' , medium : 'text-amber-500' , low : 'text-sky-500' , none : 'text-muted-foreground' }
1315
14- export function Table ( { rules, selected, onToggleSelected, onSelectAll, onOpen, onToggle, t } : { rules : CorrelationRule [ ] ; selected : Set < string > ; onToggleSelected : ( relPath : string ) => void ; onSelectAll : ( checked : boolean ) => void ; onOpen : ( r : CorrelationRule ) => void ; onToggle : ( r : CorrelationRule , next : boolean ) => void ; t : TFunction } ) {
16+ export function Table ( { rules, selected, onToggleSelected, onSelectAll, onOpen, onToggle, t, footer } : { rules : CorrelationRule [ ] ; selected : Set < string > ; onToggleSelected : ( relPath : string ) => void ; onSelectAll : ( checked : boolean ) => void ; onOpen : ( r : CorrelationRule ) => void ; onToggle : ( r : CorrelationRule , next : boolean ) => void ; t : TFunction ; footer ?: ReactNode } ) {
1517 const allChecked = rules . length > 0 && rules . every ( ( r ) => selected . has ( r . relPath ) )
1618 const someChecked = ! allChecked && rules . some ( ( r ) => selected . has ( r . relPath ) )
1719 return (
18- < div className = "mt-4 min-h-0 flex-1 overflow-y-auto rounded-xl border border-border" >
19- < div className = "grid items-center gap-3 border-b border-border bg-muted/30 px-4 py-2.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground" style = { { gridTemplateColumns : COLS } } >
20- < div className = "flex justify-center" >
21- < SelectAllCheckbox checked = { allChecked } indeterminate = { someChecked } onChange = { onSelectAll } label = { t ( 'alertingRules.table.selectAll' ) } />
22- </ div >
23- < div > { t ( 'alertingRules.table.name' ) } </ div >
24- < div > { t ( 'alertingRules.table.dataTypes' ) } </ div >
25- < div > { t ( 'alertingRules.table.category' ) } </ div >
26- < div > { t ( 'alertingRules.table.technique' ) } </ div >
27- < div > { t ( 'alertingRules.table.adversary' ) } </ div >
28- < div className = "text-center" > { t ( 'alertingRules.table.impact' ) } </ div >
29- < div className = "text-center" > { t ( 'alertingRules.table.active' ) } </ div >
30- </ div >
31- { rules . map ( ( r ) => {
32- const dts = ( r . dataTypes ?? [ ] ) . filter ( ( d ) => d . included ) . map ( ( d ) => d . dataType )
33- return (
34- < div key = { r . relPath } className = "grid items-center gap-3 border-b border-border/60 px-4 py-3 text-sm last:border-b-0 hover:bg-muted/30" style = { { gridTemplateColumns : COLS } } >
35- < div className = "flex justify-center" >
36- < input
37- type = "checkbox"
38- checked = { selected . has ( r . relPath ) }
39- onChange = { ( ) => onToggleSelected ( r . relPath ) }
40- onClick = { ( e ) => e . stopPropagation ( ) }
41- aria-label = { t ( 'alertingRules.table.selectRow' , { name : r . name } ) }
42- className = "h-4 w-4 cursor-pointer accent-primary"
43- />
44- </ div >
45- < button onClick = { ( ) => onOpen ( r ) } className = "min-w-0 text-left" >
46- < div className = "flex items-center gap-1.5" >
47- < span className = "truncate font-medium" > { r . name } </ span >
48- { r . systemOwner && < Lock size = { 11 } className = "shrink-0 text-muted-foreground/50" /> }
49- </ div >
50- { r . description && < div className = "truncate text-xs text-muted-foreground" > { r . description } </ div > }
51- </ button >
52- < button onClick = { ( ) => onOpen ( r ) } className = "flex min-w-0 flex-wrap items-center gap-1 text-left" >
53- { dts . slice ( 0 , 2 ) . map ( ( dt ) => < DataTypeChip key = { dt } dataType = { dt } /> ) }
54- { dts . length > 2 && < span className = "text-[10px] text-muted-foreground" > +{ dts . length - 2 } </ span > }
55- { dts . length === 0 && < span className = "text-xs text-muted-foreground/60" > —</ span > }
56- </ button >
57- < button onClick = { ( ) => onOpen ( r ) } className = "truncate text-left text-xs text-muted-foreground" > { r . category || '—' } </ button >
58- < button onClick = { ( ) => onOpen ( r ) } className = "truncate text-left font-mono text-xs text-muted-foreground" title = { r . technique } > { r . technique || '—' } </ button >
59- < div className = "flex items-center gap-1 text-xs text-muted-foreground" > < Crosshair size = { 11 } /> { r . adversary ? t ( `alertingRules.adversary.${ r . adversary } ` ) : '—' } </ div >
60- < div className = "text-center" > < span className = { cn ( 'font-mono text-xs font-semibold' , IMPACT_TONE [ impactKey ( maxImpact ( r ) ) ] ) } > { maxImpact ( r ) } </ span > </ div >
61- < div className = "flex justify-center" > < Toggle on = { r . ruleActive } onChange = { ( v ) => onToggle ( r , v ) } /> </ div >
62- </ div >
63- )
64- } ) }
20+ < div className = "mt-4 min-h-0 flex-1 overflow-auto rounded-xl border border-border" >
21+ < table className = "min-w-full border-collapse" >
22+ < thead className = "sticky top-0 z-10 bg-muted/90 text-[10px] uppercase tracking-wider text-muted-foreground" >
23+ < tr className = "border-b border-border" >
24+ < th className = { `${ TH } text-center` } >
25+ < div className = "flex justify-center" >
26+ < SelectAllCheckbox checked = { allChecked } indeterminate = { someChecked } onChange = { onSelectAll } label = { t ( 'alertingRules.table.selectAll' ) } />
27+ </ div >
28+ </ th >
29+ < th className = { TH } > { t ( 'alertingRules.table.name' ) } </ th >
30+ < th className = { TH } > { t ( 'alertingRules.table.dataTypes' ) } </ th >
31+ < th className = { TH } > { t ( 'alertingRules.table.category' ) } </ th >
32+ < th className = { TH } > { t ( 'alertingRules.table.technique' ) } </ th >
33+ < th className = { TH } > { t ( 'alertingRules.table.adversary' ) } </ th >
34+ < th className = { `${ TH } text-center` } > { t ( 'alertingRules.table.impact' ) } </ th >
35+ < th className = { `${ TH } text-center` } > { t ( 'alertingRules.table.active' ) } </ th >
36+ </ tr >
37+ </ thead >
38+ < tbody >
39+ { rules . map ( ( r ) => {
40+ const dts = ( r . dataTypes ?? [ ] ) . filter ( ( d ) => d . included ) . map ( ( d ) => d . dataType )
41+ return (
42+ < tr key = { r . relPath } className = "border-b border-border/60 text-sm last:border-b-0 hover:bg-muted/30" >
43+ < td className = { `${ TD } text-center` } >
44+ < div className = "flex justify-center" >
45+ < input
46+ type = "checkbox"
47+ checked = { selected . has ( r . relPath ) }
48+ onChange = { ( ) => onToggleSelected ( r . relPath ) }
49+ onClick = { ( e ) => e . stopPropagation ( ) }
50+ aria-label = { t ( 'alertingRules.table.selectRow' , { name : r . name } ) }
51+ className = "h-4 w-4 cursor-pointer accent-primary"
52+ />
53+ </ div >
54+ </ td >
55+ < td className = { `${ TD } max-w-[360px]` } >
56+ < button onClick = { ( ) => onOpen ( r ) } className = "min-w-0 text-left" >
57+ < div className = "flex items-center gap-1.5" >
58+ < span className = "truncate font-medium" > { r . name } </ span >
59+ { r . systemOwner && < Lock size = { 11 } className = "shrink-0 text-muted-foreground/50" /> }
60+ </ div >
61+ { r . description && < div className = "truncate text-xs text-muted-foreground" > { r . description } </ div > }
62+ </ button >
63+ </ td >
64+ < td className = { TD } >
65+ < button onClick = { ( ) => onOpen ( r ) } className = "flex min-w-0 flex-wrap items-center gap-1 text-left" >
66+ { dts . slice ( 0 , 2 ) . map ( ( dt ) => < DataTypeChip key = { dt } dataType = { dt } /> ) }
67+ { dts . length > 2 && < span className = "text-[10px] text-muted-foreground" > +{ dts . length - 2 } </ span > }
68+ { dts . length === 0 && < span className = "text-xs text-muted-foreground/60" > —</ span > }
69+ </ button >
70+ </ td >
71+ < td className = { `${ TD } max-w-[200px]` } >
72+ < button onClick = { ( ) => onOpen ( r ) } className = "block w-full truncate text-left text-xs text-muted-foreground" > { r . category || '—' } </ button >
73+ </ td >
74+ < td className = { `${ TD } max-w-[200px]` } >
75+ < button onClick = { ( ) => onOpen ( r ) } className = "block w-full truncate text-left font-mono text-xs text-muted-foreground" title = { r . technique } > { r . technique || '—' } </ button >
76+ </ td >
77+ < td className = { TD } >
78+ < div className = "flex items-center gap-1 text-xs text-muted-foreground" > < Crosshair size = { 11 } /> { r . adversary ? t ( `alertingRules.adversary.${ r . adversary } ` ) : '—' } </ div >
79+ </ td >
80+ < td className = { `${ TD } text-center` } >
81+ < span className = { cn ( 'font-mono text-xs font-semibold' , IMPACT_TONE [ impactKey ( maxImpact ( r ) ) ] ) } > { maxImpact ( r ) } </ span >
82+ </ td >
83+ < td className = { `${ TD } text-center` } >
84+ < div className = "flex justify-center" > < Toggle on = { r . ruleActive } onChange = { ( v ) => onToggle ( r , v ) } /> </ div >
85+ </ td >
86+ </ tr >
87+ )
88+ } ) }
89+ </ tbody >
90+ </ table >
91+ { footer }
6592 </ div >
6693 )
6794}
0 commit comments