Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 76 additions & 49 deletions frontend/src/features/alerting-rules/components/table.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ReactNode } from 'react'
import type { TFunction } from 'i18next'
import { Crosshair, Lock } from 'lucide-react'
import { cn } from '@/shared/lib/utils'
Expand All @@ -8,60 +9,86 @@ import { DataTypeChip } from './rule-form'
import { SelectAllCheckbox } from './select-all-checkbox'
import { Toggle } from './toggle'

const COLS = '32px 1.4fr 1fr 110px 100px 80px 48px 50px'
const TH = 'whitespace-nowrap px-3 py-2.5 text-left align-middle font-medium'
const TD = 'whitespace-nowrap px-3 py-2.5 align-middle'
const IMPACT_TONE: Record<string, string> = { high: 'text-red-500', medium: 'text-amber-500', low: 'text-sky-500', none: 'text-muted-foreground' }

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 }) {
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 }) {
const allChecked = rules.length > 0 && rules.every((r) => selected.has(r.relPath))
const someChecked = !allChecked && rules.some((r) => selected.has(r.relPath))
return (
<div className="mt-4 min-h-0 flex-1 overflow-y-auto rounded-xl border border-border">
<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 }}>
<div className="flex justify-center">
<SelectAllCheckbox checked={allChecked} indeterminate={someChecked} onChange={onSelectAll} label={t('alertingRules.table.selectAll')} />
</div>
<div>{t('alertingRules.table.name')}</div>
<div>{t('alertingRules.table.dataTypes')}</div>
<div>{t('alertingRules.table.category')}</div>
<div>{t('alertingRules.table.technique')}</div>
<div>{t('alertingRules.table.adversary')}</div>
<div className="text-center">{t('alertingRules.table.impact')}</div>
<div className="text-center">{t('alertingRules.table.active')}</div>
</div>
{rules.map((r) => {
const dts = (r.dataTypes ?? []).filter((d) => d.included).map((d) => d.dataType)
return (
<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 }}>
<div className="flex justify-center">
<input
type="checkbox"
checked={selected.has(r.relPath)}
onChange={() => onToggleSelected(r.relPath)}
onClick={(e) => e.stopPropagation()}
aria-label={t('alertingRules.table.selectRow', { name: r.name })}
className="h-4 w-4 cursor-pointer accent-primary"
/>
</div>
<button onClick={() => onOpen(r)} className="min-w-0 text-left">
<div className="flex items-center gap-1.5">
<span className="truncate font-medium">{r.name}</span>
{r.systemOwner && <Lock size={11} className="shrink-0 text-muted-foreground/50" />}
</div>
{r.description && <div className="truncate text-xs text-muted-foreground">{r.description}</div>}
</button>
<button onClick={() => onOpen(r)} className="flex min-w-0 flex-wrap items-center gap-1 text-left">
{dts.slice(0, 2).map((dt) => <DataTypeChip key={dt} dataType={dt} />)}
{dts.length > 2 && <span className="text-[10px] text-muted-foreground">+{dts.length - 2}</span>}
{dts.length === 0 && <span className="text-xs text-muted-foreground/60">—</span>}
</button>
<button onClick={() => onOpen(r)} className="truncate text-left text-xs text-muted-foreground">{r.category || '—'}</button>
<button onClick={() => onOpen(r)} className="truncate text-left font-mono text-xs text-muted-foreground" title={r.technique}>{r.technique || '—'}</button>
<div className="flex items-center gap-1 text-xs text-muted-foreground"><Crosshair size={11} /> {r.adversary ? t(`alertingRules.adversary.${r.adversary}`) : '—'}</div>
<div className="text-center"><span className={cn('font-mono text-xs font-semibold', IMPACT_TONE[impactKey(maxImpact(r))])}>{maxImpact(r)}</span></div>
<div className="flex justify-center"><Toggle on={r.ruleActive} onChange={(v) => onToggle(r, v)} /></div>
</div>
)
})}
<div className="mt-4 min-h-0 flex-1 overflow-auto rounded-xl border border-border">
<table className="min-w-full border-collapse">
<thead className="sticky top-0 z-10 bg-muted/90 text-[10px] uppercase tracking-wider text-muted-foreground">
<tr className="border-b border-border">
<th className={`${TH} text-center`}>
<div className="flex justify-center">
<SelectAllCheckbox checked={allChecked} indeterminate={someChecked} onChange={onSelectAll} label={t('alertingRules.table.selectAll')} />
</div>
</th>
<th className={TH}>{t('alertingRules.table.name')}</th>
<th className={TH}>{t('alertingRules.table.dataTypes')}</th>
<th className={TH}>{t('alertingRules.table.category')}</th>
<th className={TH}>{t('alertingRules.table.technique')}</th>
<th className={TH}>{t('alertingRules.table.adversary')}</th>
<th className={`${TH} text-center`}>{t('alertingRules.table.impact')}</th>
<th className={`${TH} text-center`}>{t('alertingRules.table.active')}</th>
</tr>
</thead>
<tbody>
{rules.map((r) => {
const dts = (r.dataTypes ?? []).filter((d) => d.included).map((d) => d.dataType)
return (
<tr key={r.relPath} className="border-b border-border/60 text-sm last:border-b-0 hover:bg-muted/30">
<td className={`${TD} text-center`}>
<div className="flex justify-center">
<input
type="checkbox"
checked={selected.has(r.relPath)}
onChange={() => onToggleSelected(r.relPath)}
onClick={(e) => e.stopPropagation()}
aria-label={t('alertingRules.table.selectRow', { name: r.name })}
className="h-4 w-4 cursor-pointer accent-primary"
/>
</div>
</td>
<td className={`${TD} max-w-[360px]`}>
<button onClick={() => onOpen(r)} className="min-w-0 text-left">
<div className="flex items-center gap-1.5">
<span className="truncate font-medium">{r.name}</span>
{r.systemOwner && <Lock size={11} className="shrink-0 text-muted-foreground/50" />}
</div>
{r.description && <div className="truncate text-xs text-muted-foreground">{r.description}</div>}
</button>
</td>
<td className={TD}>
<button onClick={() => onOpen(r)} className="flex min-w-0 flex-wrap items-center gap-1 text-left">
{dts.slice(0, 2).map((dt) => <DataTypeChip key={dt} dataType={dt} />)}
{dts.length > 2 && <span className="text-[10px] text-muted-foreground">+{dts.length - 2}</span>}
{dts.length === 0 && <span className="text-xs text-muted-foreground/60">—</span>}
</button>
</td>
<td className={`${TD} max-w-[200px]`}>
<button onClick={() => onOpen(r)} className="block w-full truncate text-left text-xs text-muted-foreground">{r.category || '—'}</button>
</td>
<td className={`${TD} max-w-[200px]`}>
<button onClick={() => onOpen(r)} className="block w-full truncate text-left font-mono text-xs text-muted-foreground" title={r.technique}>{r.technique || '—'}</button>
</td>
<td className={TD}>
<div className="flex items-center gap-1 text-xs text-muted-foreground"><Crosshair size={11} /> {r.adversary ? t(`alertingRules.adversary.${r.adversary}`) : '—'}</div>
</td>
<td className={`${TD} text-center`}>
<span className={cn('font-mono text-xs font-semibold', IMPACT_TONE[impactKey(maxImpact(r))])}>{maxImpact(r)}</span>
</td>
<td className={`${TD} text-center`}>
<div className="flex justify-center"><Toggle on={r.ruleActive} onChange={(v) => onToggle(r, v)} /></div>
</td>
</tr>
)
})}
</tbody>
</table>
{footer}
</div>
)
}
26 changes: 17 additions & 9 deletions frontend/src/features/alerting-rules/pages/AlertingRulesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -281,15 +281,23 @@ export function AlertingRulesPage() {
) : rules.length === 0 ? (
<Center>{t('alertingRules.empty')}</Center>
) : (
<div className="min-h-0 flex-1 overflow-y-auto">
<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} />
<InfiniteScrollSentinel
onReach={() => setPage((p) => p + 1)}
hasMore={rules.length < total}
loading={loading}
endLabel={t('common.allLoaded', { count: total })}
/>
</div>
<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}
footer={
<InfiniteScrollSentinel
onReach={() => setPage((p) => p + 1)}
hasMore={rules.length < total}
loading={loading}
endLabel={t('common.allLoaded', { count: total })}
/>
}
/>
)}

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