Skip to content

Commit d71555b

Browse files
fix[frontend](pipelines/soarflows/alerts): standarized column handling
1 parent d5e7392 commit d71555b

4 files changed

Lines changed: 276 additions & 217 deletions

File tree

Lines changed: 76 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ReactNode } from 'react'
12
import type { TFunction } from 'i18next'
23
import { Crosshair, Lock } from 'lucide-react'
34
import { cn } from '@/shared/lib/utils'
@@ -8,60 +9,86 @@ import { DataTypeChip } from './rule-form'
89
import { SelectAllCheckbox } from './select-all-checkbox'
910
import { 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'
1214
const 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
}

frontend/src/features/alerting-rules/pages/AlertingRulesPage.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -281,15 +281,23 @@ export function AlertingRulesPage() {
281281
) : rules.length === 0 ? (
282282
<Center>{t('alertingRules.empty')}</Center>
283283
) : (
284-
<div className="min-h-0 flex-1 overflow-y-auto">
285-
<Table rules={rules} selected={selected} onToggleSelected={toggleSelected} onSelectAll={(v) => setSelected(v ? new Set(rules.map((r) => r.relPath)) : new Set())} onOpen={setOpen} onToggle={toggleActive} t={t} />
286-
<InfiniteScrollSentinel
287-
onReach={() => setPage((p) => p + 1)}
288-
hasMore={rules.length < total}
289-
loading={loading}
290-
endLabel={t('common.allLoaded', { count: total })}
291-
/>
292-
</div>
284+
<Table
285+
rules={rules}
286+
selected={selected}
287+
onToggleSelected={toggleSelected}
288+
onSelectAll={(v) => setSelected(v ? new Set(rules.map((r) => r.relPath)) : new Set())}
289+
onOpen={setOpen}
290+
onToggle={toggleActive}
291+
t={t}
292+
footer={
293+
<InfiniteScrollSentinel
294+
onReach={() => setPage((p) => p + 1)}
295+
hasMore={rules.length < total}
296+
loading={loading}
297+
endLabel={t('common.allLoaded', { count: total })}
298+
/>
299+
}
300+
/>
293301
)}
294302

295303
{open && <RuleDrawer rule={open} dataTypeOptions={dataTypeOptions} onClose={() => setOpen(null)} onToggle={toggleActive} onDelete={remove} onSaved={() => { setOpen(null); refresh() }} t={t} />}

0 commit comments

Comments
 (0)