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
25 changes: 22 additions & 3 deletions frontend/src/features/alerting-rules/pages/AlertingRulesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { cn } from '@/shared/lib/utils'
import { Button } from '@/shared/components/ui/button'
import { Input } from '@/shared/components/ui/input'
import { InfiniteScrollSentinel } from '@/shared/components/ui/infinite-scroll'
import { ConfirmDialog } from '@/shared/components/ui/confirm-dialog'
import { YamlCodeEditor } from '@/shared/components/YamlCodeEditor'
import { useDateFormat } from '@/shared/lib/datetime'
import {
Expand Down Expand Up @@ -79,6 +80,8 @@ export function AlertingRulesPage() {
const [dataTypeOptions, setDataTypeOptions] = useState<DataTypeOption[]>([])
const [open, setOpen] = useState<CorrelationRule | null>(null)
const [creating, setCreating] = useState(false)
const [pendingDelete, setPendingDelete] = useState<CorrelationRule | null>(null)
const [deleting, setDeleting] = useState(false)

const fileInputRef = useRef<HTMLInputElement>(null)
const [importBusy, setImportBusy] = useState(false)
Expand Down Expand Up @@ -162,15 +165,21 @@ export function AlertingRulesPage() {
}
}

const remove = async (r: CorrelationRule) => {
if (!confirm(t('alertingRules.deleteConfirm', { name: r.name }))) return
const remove = (r: CorrelationRule) => setPendingDelete(r)

const confirmDelete = async () => {
if (!pendingDelete) return
setDeleting(true)
try {
await svc.remove(r.relPath)
await svc.remove(pendingDelete.relPath)
toast.success(t('alertingRules.toast.deleted'))
setOpen(null)
refresh()
} catch (e) {
toast.error(e instanceof AlertingRulesHttpError ? e.message : t('alertingRules.toast.deleteError'))
} finally {
setPendingDelete(null)
setDeleting(false)
}
}

Expand Down Expand Up @@ -259,6 +268,16 @@ export function AlertingRulesPage() {
{open && <RuleDrawer rule={open} dataTypeOptions={dataTypeOptions} onClose={() => setOpen(null)} onToggle={toggleActive} onDelete={remove} onSaved={() => { setOpen(null); refresh() }} t={t} />}
{creating && <RuleDrawer create dataTypeOptions={dataTypeOptions} onClose={() => setCreating(false)} onSaved={() => { setCreating(false); refresh() }} t={t} />}
{importResults && <ImportResultsDialog res={importResults} onClose={() => setImportResults(null)} t={t} />}
<ConfirmDialog
open={pendingDelete != null}
title={t('alertingRules.editor.delete') ?? 'Delete'}
body={pendingDelete ? t('alertingRules.deleteConfirm', { name: pendingDelete.name }) : ''}
confirmLabel={t('alertingRules.editor.delete') ?? undefined}
danger
busy={deleting}
onClose={() => !deleting && setPendingDelete(null)}
onConfirm={confirmDelete}
/>
</div>
)
}
Expand Down
30 changes: 26 additions & 4 deletions frontend/src/features/alerts/pages/AlertsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { Button } from '@/shared/components/ui/button'
import { InfiniteScrollSentinel } from '@/shared/components/ui/infinite-scroll'
import { ConfirmDialog } from '@/shared/components/ui/confirm-dialog'
import { presetRange, type TimeRange } from '@/shared/components/ui/time-range-picker'
import { FILTER_OPS, TS } from '../lib/alert-meta'
import { alertToRuleConditions } from '../lib/tagging-rule-meta'
Expand Down Expand Up @@ -65,6 +66,8 @@ export function AlertsPage() {
| { kind: 'create'; tags?: AlertTag[]; conditions?: FilterType[] }
| null
>(null)
const [pendingDelete, setPendingDelete] = useState<TaggingRule | null>(null)
const [deleting, setDeleting] = useState(false)

const toggleEchoes = (id: string) =>
setExpandedEchoes((prev) => {
Expand Down Expand Up @@ -212,10 +215,18 @@ export function AlertsPage() {
if (ok) setRuleDrawer(null)
}

const removeRule = async (r: TaggingRule) => {
if (!confirm(t('taggingRules.deleteConfirm', { name: r.name }))) return
const ok = await deleteRule(r.id, r.name)
if (ok) setRuleDrawer(null)
const removeRule = async (r: TaggingRule) => setPendingDelete(r)

const confirmDelete = async () => {
if (!pendingDelete) return
setDeleting(true)
try {
const ok = await deleteRule(pendingDelete.id, pendingDelete.name)
if (ok) setRuleDrawer(null)
} finally {
setDeleting(false)
setPendingDelete(null)
}
}

const selectedAlerts = useMemo(() => alerts.filter((a) => selected.has(a.id)), [alerts, selected])
Expand Down Expand Up @@ -440,6 +451,17 @@ export function AlertsPage() {
onCreateTag={createTag}
/>
)}

<ConfirmDialog
open={pendingDelete != null}
title={t('taggingRules.deleteTitle') ?? 'Delete rule'}
body={pendingDelete ? t('taggingRules.deleteConfirm', { name: pendingDelete.name }) : ''}
confirmLabel={t('common.actions.delete') ?? undefined}
danger
busy={deleting}
onClose={() => !deleting && setPendingDelete(null)}
onConfirm={confirmDelete}
/>
</div>
)
}
30 changes: 26 additions & 4 deletions frontend/src/features/alerts/pages/TaggingRulesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { cn } from '@/shared/lib/utils'
import { Button } from '@/shared/components/ui/button'
import { Input } from '@/shared/components/ui/input'
import { InfiniteScrollSentinel } from '@/shared/components/ui/infinite-scroll'
import { ConfirmDialog } from '@/shared/components/ui/confirm-dialog'
import { useTaggingRulesList } from '../hooks/use-tagging-rules-list'
import { useTaggingRuleMutations } from '../hooks/use-tagging-rule-mutations'
import { useAlertTagCatalog } from '../hooks/use-alert-tag-catalog'
Expand All @@ -24,6 +25,8 @@ export function TaggingRulesPage() {

const [open, setOpen] = useState<TaggingRule | null>(null)
const [creating, setCreating] = useState(false)
const [pendingDelete, setPendingDelete] = useState<TaggingRule | null>(null)
const [deleting, setDeleting] = useState(false)

// Debounce the search box.
useEffect(() => {
Expand Down Expand Up @@ -59,10 +62,18 @@ export function TaggingRulesPage() {
}
}

const remove = async (r: TaggingRule) => {
if (!confirm(t('taggingRules.deleteConfirm', { name: r.name }))) return
const ok = await deleteRule(r.id, r.name)
if (ok) setOpen(null)
const remove = async (r: TaggingRule) => setPendingDelete(r)

const confirmDelete = async () => {
if (!pendingDelete) return
setDeleting(true)
try {
const ok = await deleteRule(pendingDelete.id, pendingDelete.name)
if (ok) setOpen(null)
} finally {
setDeleting(false)
setPendingDelete(null)
}
}

return (
Expand Down Expand Up @@ -158,6 +169,17 @@ export function TaggingRulesPage() {
onCreateTag={createTag}
/>
)}

<ConfirmDialog
open={pendingDelete != null}
title={t('taggingRules.deleteTitle') ?? 'Delete rule'}
body={pendingDelete ? t('taggingRules.deleteConfirm', { name: pendingDelete.name }) : ''}
confirmLabel={t('common.actions.delete') ?? undefined}
danger
busy={deleting}
onClose={() => !deleting && setPendingDelete(null)}
onConfirm={confirmDelete}
/>
</div>
)
}
Expand Down
85 changes: 49 additions & 36 deletions frontend/src/features/incidents/components/incident-alerts-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,66 @@ import { Trash2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { cn } from '@/shared/lib/utils'
import { ConfirmDialog } from '@/shared/components/ui/confirm-dialog'
import { sevKey } from '../lib/incident-meta'
import { useIncidentAlertsTab } from '../hooks/use-incident-alerts-tab'
import { TabEmpty, TabError, TabLoader } from './ui-primitives'

export function IncidentAlertsTab({ incidentId, onChanged }: { incidentId: number; onChanged: () => void }) {
const { t } = useTranslation()
const { rows, error, reload, remove } = useIncidentAlertsTab(incidentId, onChanged)
const { rows, error, reload, remove, pendingDelete, deleting, cancelDelete, confirmDelete } = useIncidentAlertsTab(incidentId, onChanged)

if (error) return <TabError onRetry={reload} />
if (rows === null) return <TabLoader />
if (rows.length === 0) return <TabEmpty>{t('incidents.alerts.empty')}</TabEmpty>
return (
<div className="overflow-hidden rounded-lg border border-border bg-card">
{rows.map((a) => (
<Link
to="/threat-management/alerts"
state={{
socaiFilters: [
{ field: 'id', operator: 'IS', value: a.alertId },
{ field: '@timestamp', operator: 'IS_BETWEEN', value: ['now-30d', 'now'] },
],
socaiTime: 'now-30d',
}}
key={a.id}
className="group flex items-center gap-3 border-b border-border/60 px-4 py-2.5 text-xs last:border-b-0"
>
<span
className={cn(
'h-4 w-1 shrink-0 rounded-full',
a.alertSeverity === 3 ? 'bg-red-500' : a.alertSeverity === 2 ? 'bg-amber-500' : 'bg-sky-500'
)}
/>
<span className="min-w-0 flex-1 truncate font-medium" title={a.alertName}>
{a.alertName}
</span>
<span className="shrink-0 font-mono text-[10px] text-muted-foreground">
{t(`incidents.sev.${sevKey(a.alertSeverity)}`)}
</span>
<button
onClick={() => void remove(a)}
title={t('incidents.alerts.remove')}
className="shrink-0 rounded p-1 text-muted-foreground opacity-0 transition hover:bg-muted hover:text-red-500 group-hover:opacity-100"
<>
<div className="overflow-hidden rounded-lg border border-border bg-card">
{rows.map((a) => (
<Link
to="/threat-management/alerts"
state={{
socaiFilters: [
{ field: 'id', operator: 'IS', value: a.alertId },
{ field: '@timestamp', operator: 'IS_BETWEEN', value: ['now-30d', 'now'] },
],
socaiTime: 'now-30d',
}}
key={a.id}
className="group flex items-center gap-3 border-b border-border/60 px-4 py-2.5 text-xs last:border-b-0"
>
<Trash2 size={13} />
</button>
</Link>
))}
</div>
<span
className={cn(
'h-4 w-1 shrink-0 rounded-full',
a.alertSeverity === 3 ? 'bg-red-500' : a.alertSeverity === 2 ? 'bg-amber-500' : 'bg-sky-500'
)}
/>
<span className="min-w-0 flex-1 truncate font-medium" title={a.alertName}>
{a.alertName}
</span>
<span className="shrink-0 font-mono text-[10px] text-muted-foreground">
{t(`incidents.sev.${sevKey(a.alertSeverity)}`)}
</span>
<button
onClick={() => remove(a)}
title={t('incidents.alerts.remove')}
className="shrink-0 rounded p-1 text-muted-foreground opacity-0 transition hover:bg-muted hover:text-red-500 group-hover:opacity-100"
>
<Trash2 size={13} />
</button>
</Link>
))}
</div>
<ConfirmDialog
open={pendingDelete != null}
title={t('incidents.alerts.remove') ?? 'Remove alert'}
body={pendingDelete ? t('incidents.alerts.removeConfirm', { name: pendingDelete.alertName }) : ''}
confirmLabel={t('common.actions.remove') ?? t('incidents.alerts.remove') ?? undefined}
danger
busy={deleting}
onClose={() => !deleting && cancelDelete()}
onConfirm={confirmDelete}
/>
</>
)
}
45 changes: 28 additions & 17 deletions frontend/src/features/incidents/hooks/use-incident-alerts-tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,20 @@ export interface UseIncidentAlertsTabResult {
rows: IncidentAlert[] | null
error: boolean
reload: () => Promise<void>
remove: (a: IncidentAlert) => Promise<void>
remove: (a: IncidentAlert) => void
pendingDelete: IncidentAlert | null
deleting: boolean
cancelDelete: () => void
confirmDelete: () => Promise<void>
}

/** Data + remove-alert mutation for the incident drawer's "Alerts" tab. */
export function useIncidentAlertsTab(incidentId: number, onChanged: () => void): UseIncidentAlertsTabResult {
const { t } = useTranslation()
const [rows, setRows] = useState<IncidentAlert[] | null>(null)
const [error, setError] = useState(false)
const [pendingDelete, setPendingDelete] = useState<IncidentAlert | null>(null)
const [deleting, setDeleting] = useState(false)

const load = useCallback(async () => {
setError(false)
Expand All @@ -31,20 +37,25 @@ export function useIncidentAlertsTab(incidentId: number, onChanged: () => void):
void load()
}, [load])

const remove = useCallback(
async (a: IncidentAlert) => {
if (!confirm(t('incidents.alerts.removeConfirm', { name: a.alertName }))) return
try {
await svc.removeAlert(a.id)
toast.success(t('incidents.alerts.removed'))
await load()
onChanged()
} catch (e) {
toast.error(e instanceof IncidentsHttpError ? e.message : t('incidents.alerts.removeError'))
}
},
[load, onChanged, t]
)

return { rows, error, reload: load, remove }
const remove = useCallback((a: IncidentAlert) => setPendingDelete(a), [])

const cancelDelete = useCallback(() => setPendingDelete(null), [])

const confirmDelete = useCallback(async () => {
if (!pendingDelete) return
setDeleting(true)
try {
await svc.removeAlert(pendingDelete.id)
toast.success(t('incidents.alerts.removed'))
await load()
onChanged()
} catch (e) {
toast.error(e instanceof IncidentsHttpError ? e.message : t('incidents.alerts.removeError'))
} finally {
setDeleting(false)
setPendingDelete(null)
}
}, [pendingDelete, load, onChanged, t])

return { rows, error, reload: load, remove, pendingDelete, deleting, cancelDelete, confirmDelete }
}
7 changes: 5 additions & 2 deletions frontend/src/shared/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"save": "Speichern",
"cancel": "Abbrechen",
"retry": "Wiederholen",
"refresh": "Aktualisieren"
"refresh": "Aktualisieren",
"delete": "Löschen",
"remove": "Entfernen"
},
"confirm": {
"delete": "Löschen",
Expand Down Expand Up @@ -5032,6 +5034,7 @@
"createError": "Regel konnte nicht erstellt werden.",
"updateError": "Regel konnte nicht aktualisiert werden.",
"deleteError": "Regel konnte nicht gelöscht werden."
}
},
"deleteTitle": "Tagging-Regel löschen"
}
}
7 changes: 5 additions & 2 deletions frontend/src/shared/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"save": "Save",
"cancel": "Cancel",
"retry": "Retry",
"refresh": "Refresh"
"refresh": "Refresh",
"delete": "Delete",
"remove": "Remove"
},
"confirm": {
"delete": "Delete",
Expand Down Expand Up @@ -4248,7 +4250,8 @@
"createError": "Failed to create rule.",
"updateError": "Failed to update rule.",
"deleteError": "Failed to delete rule."
}
},
"deleteTitle": "Delete tagging rule"
},
"incidents": {
"title": "Incidents",
Expand Down
Loading
Loading