From d71555b0752f19803133bb614bd210a882d3ff62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Thu, 13 Aug 2026 11:20:31 -0600 Subject: [PATCH] fix[frontend](pipelines/soarflows/alerts): standarized column handling --- .../alerting-rules/components/table.tsx | 125 ++++++---- .../pages/AlertingRulesPage.tsx | 26 +- .../pages/ParsingFiltersPage.tsx | 223 +++++++++--------- .../src/features/soar/pages/FlowsPage.tsx | 119 +++++----- 4 files changed, 276 insertions(+), 217 deletions(-) diff --git a/frontend/src/features/alerting-rules/components/table.tsx b/frontend/src/features/alerting-rules/components/table.tsx index 4fc236c10..f0ddf9783 100644 --- a/frontend/src/features/alerting-rules/components/table.tsx +++ b/frontend/src/features/alerting-rules/components/table.tsx @@ -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' @@ -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 = { 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; 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; 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 ( -
-
-
- -
-
{t('alertingRules.table.name')}
-
{t('alertingRules.table.dataTypes')}
-
{t('alertingRules.table.category')}
-
{t('alertingRules.table.technique')}
-
{t('alertingRules.table.adversary')}
-
{t('alertingRules.table.impact')}
-
{t('alertingRules.table.active')}
-
- {rules.map((r) => { - const dts = (r.dataTypes ?? []).filter((d) => d.included).map((d) => d.dataType) - return ( -
-
- 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" - /> -
- - - - -
{r.adversary ? t(`alertingRules.adversary.${r.adversary}`) : '—'}
-
{maxImpact(r)}
-
onToggle(r, v)} />
-
- ) - })} +
+ + + + + + + + + + + + + + + {rules.map((r) => { + const dts = (r.dataTypes ?? []).filter((d) => d.included).map((d) => d.dataType) + return ( + + + + + + + + + + + ) + })} + +
+
+ +
+
{t('alertingRules.table.name')}{t('alertingRules.table.dataTypes')}{t('alertingRules.table.category')}{t('alertingRules.table.technique')}{t('alertingRules.table.adversary')}{t('alertingRules.table.impact')}{t('alertingRules.table.active')}
+
+ 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" + /> +
+
+ + + + + + + + +
{r.adversary ? t(`alertingRules.adversary.${r.adversary}`) : '—'}
+
+ {maxImpact(r)} + +
onToggle(r, v)} />
+
+ {footer}
) } diff --git a/frontend/src/features/alerting-rules/pages/AlertingRulesPage.tsx b/frontend/src/features/alerting-rules/pages/AlertingRulesPage.tsx index df1f6d34e..3f8f9f4ac 100644 --- a/frontend/src/features/alerting-rules/pages/AlertingRulesPage.tsx +++ b/frontend/src/features/alerting-rules/pages/AlertingRulesPage.tsx @@ -281,15 +281,23 @@ export function AlertingRulesPage() { ) : rules.length === 0 ? (
{t('alertingRules.empty')}
) : ( -
- setSelected(v ? new Set(rules.map((r) => r.relPath)) : new Set())} onOpen={setOpen} onToggle={toggleActive} t={t} /> - setPage((p) => p + 1)} - hasMore={rules.length < total} - loading={loading} - endLabel={t('common.allLoaded', { count: total })} - /> - +
setSelected(v ? new Set(rules.map((r) => r.relPath)) : new Set())} + onOpen={setOpen} + onToggle={toggleActive} + t={t} + footer={ + setPage((p) => p + 1)} + hasMore={rules.length < total} + loading={loading} + endLabel={t('common.allLoaded', { count: total })} + /> + } + /> )} {open && setOpen(null)} onToggle={toggleActive} onDelete={remove} onSaved={() => { setOpen(null); refresh() }} t={t} />} diff --git a/frontend/src/features/parsing-filters/pages/ParsingFiltersPage.tsx b/frontend/src/features/parsing-filters/pages/ParsingFiltersPage.tsx index b559746e1..0559db204 100644 --- a/frontend/src/features/parsing-filters/pages/ParsingFiltersPage.tsx +++ b/frontend/src/features/parsing-filters/pages/ParsingFiltersPage.tsx @@ -17,7 +17,8 @@ import { displayName } from '../lib/filter-model' type Tab = 'all' | 'active' | 'inactive' | 'system' | 'user' const TABS: Tab[] = ['all', 'active', 'inactive', 'system', 'user'] -const COLS = 'minmax(180px,1fr) minmax(160px,1.3fr) 100px 80px 60px 64px' +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' // The name the engine matches on: the file's base name without its extension. function pipelineIdentity(relPath: string): string { @@ -235,55 +236,58 @@ export function ParsingFiltersPage() {
-
-
{t('parsingFilters.cols.filter')}
-
{t('parsingFilters.cols.dataTypes')}
-
{t('parsingFilters.cols.type')}
-
{t('parsingFilters.cols.active')}
-
-
-
-
- {loading && items.length === 0 ? ( -
- {t('parsingFilters.loading')} -
- ) : error ? ( -
- {t('parsingFilters.loadError')} - -
- ) : items.length === 0 ? ( -
{t('parsingFilters.empty')}
- ) : ( - <> - {items.map((f, i) => ( - setEditing({ filter: f, creating: false })} - onToggle={() => toggleActive(f)} - onMoveUp={() => moveOrder(i, -1)} - onMoveDown={() => moveOrder(i, 1)} - canMoveUp={i > 0} - canMoveDown={i < items.length - 1} - reordering={reordering} - onBroadcastDelete={(selector) => onBroadcastDelete(f, selector)} - onBroadcastActivate={(active, selector) => onBroadcastActivate(f, active, selector)} - /> - ))} - setPage((p) => p + 1)} - hasMore={items.length < total} - loading={loading} - endLabel={t('common.allLoaded', { count: total })} - /> - +
+
+ + + + + + + + + + {loading && items.length === 0 ? ( + + ) : error ? ( + + ) : items.length === 0 ? ( + + ) : ( + items.map((f, i) => ( + setEditing({ filter: f, creating: false })} + onToggle={() => toggleActive(f)} + onMoveUp={() => moveOrder(i, -1)} + onMoveDown={() => moveOrder(i, 1)} + canMoveUp={i > 0} + canMoveDown={i < items.length - 1} + reordering={reordering} + onBroadcastDelete={(selector) => onBroadcastDelete(f, selector)} + onBroadcastActivate={(active, selector) => onBroadcastActivate(f, active, selector)} + /> + )) + )} + +
{t('parsingFilters.cols.filter')}{t('parsingFilters.cols.dataTypes')}{t('parsingFilters.cols.type')}{t('parsingFilters.cols.active')} + +
+ {t('parsingFilters.loading')} +
+ {t('parsingFilters.loadError')} + +
{t('parsingFilters.empty')}
+ {items.length > 0 && ( + setPage((p) => p + 1)} + hasMore={items.length < total} + loading={loading} + endLabel={t('common.allLoaded', { count: total })} + /> )}
@@ -349,23 +353,26 @@ function Row({ }) { const { t } = useTranslation() return ( -
-
- - {f.order} - - - {displayName(f.relPath)} -
- -
+ +
+ + {f.order} + + + {displayName(f.relPath)} +
+ + + + + } {t(f.system ? 'parsingFilters.system' : 'parsingFilters.user')} -
-
e.stopPropagation()}> - - onBroadcastActivate(f.active, selector)} - variant="ghost" - size="sm" - /> -
-
e.stopPropagation()}> - {t('parsingFilters.view')} - {!f.system && ( + + e.stopPropagation()}> +
+ onBroadcastActivate(f.active, selector)} variant="ghost" size="sm" /> - )} -
-
e.stopPropagation()}> - - -
-
+
+ + e.stopPropagation()}> +
+ {t('parsingFilters.view')} + {!f.system && ( + + )} +
+ + e.stopPropagation()}> +
+ + +
+ + ) } diff --git a/frontend/src/features/soar/pages/FlowsPage.tsx b/frontend/src/features/soar/pages/FlowsPage.tsx index 69325f0c9..9856f86f3 100644 --- a/frontend/src/features/soar/pages/FlowsPage.tsx +++ b/frontend/src/features/soar/pages/FlowsPage.tsx @@ -16,7 +16,8 @@ import type { Flow } from '../types/soar.types' type ListTab = 'all' | 'active' | 'inactive' | 'system' | 'user' const LIST_TABS: ListTab[] = ['all', 'active', 'inactive', 'system', 'user'] -const COLS = 'minmax(200px,1fr) 110px 78px 84px 130px 72px 52px' +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' function flowName(relPath: string): string { return (relPath.split('/').pop() ?? relPath).replace(/\.ya?ml$/i, '') @@ -189,39 +190,43 @@ export function FlowsPage() {
-
-
-
{t('soar.cols.flow')}
-
{t('soar.cols.platform')}
-
{t('soar.cols.conditions')}
-
{t('soar.cols.commands')}
-
{t('soar.cols.lastRun')}
-
{t('soar.cols.active')}
-
-
- {loading && items.length === 0 ? ( -
{t('soar.loading')}
- ) : error ? ( -
- {t('soar.loadError')} - -
- ) : items.length === 0 ? ( -
{t('soar.empty')}
- ) : ( - <> - {items.map((f) => ( + + + + + + + + + + + + + {loading && items.length === 0 ? ( + + ) : error ? ( + + ) : items.length === 0 ? ( + + ) : ( + items.map((f) => ( setEditing({ flow: f, creating: false })} onToggle={() => toggleActive(f)} t={t} /> - ))} - setPage((p) => p + 1)} - hasMore={items.length < total} - loading={loading} - endLabel={t('common.allLoaded', { count: total })} - /> - - )} - + )) + )} + +
{t('soar.cols.flow')}{t('soar.cols.platform')}{t('soar.cols.conditions')}{t('soar.cols.commands')}{t('soar.cols.lastRun')}{t('soar.cols.active')} +
{t('soar.loading')}
+ {t('soar.loadError')} + +
{t('soar.empty')}
+ {items.length > 0 && ( + setPage((p) => p + 1)} + hasMore={items.length < total} + loading={loading} + endLabel={t('common.allLoaded', { count: total })} + /> + )}
@@ -302,24 +307,26 @@ function FlowKpis({ refreshKey }: { refreshKey: number }) { function FlowRow({ f, stat, onOpen, onToggle, t }: { f: Flow; stat?: FlowStat; onOpen: () => void; onToggle: () => void; t: ReturnType['t'] }) { return ( -
-
- -
-
{f.name || flowName(f.relPath)}
- {f.description &&
{f.description}
} + + +
+ +
+
{f.name || flowName(f.relPath)}
+ {f.description &&
{f.description}
} +
-
-
+ + {f.agentPlatform ? ( {f.agentPlatform} ) : ( )} -
-
{f.conditions?.length ?? 0}
-
{f.commands?.length ?? 0}
-
+ + {f.conditions?.length ?? 0} + {f.commands?.length ?? 0} + {stat?.last ? (
{relativeTime(stat.last)} @@ -332,15 +339,19 @@ function FlowRow({ f, stat, onOpen, onToggle, t }: { f: Flow; stat?: FlowStat; o ) : ( {t('soar.neverRun')} )} -
-
e.stopPropagation()}> - -
-
- {f.systemOwner && } - {t('soar.view')} -
-
+ + e.stopPropagation()}> +
+ +
+ + +
+ {f.systemOwner && } + {t('soar.view')} +
+ + ) }