Skip to content

Commit 68c669d

Browse files
fix[frontend](): added missing confirm modal and language keys (#2343)
1 parent 148cd64 commit 68c669d

12 files changed

Lines changed: 186 additions & 78 deletions

File tree

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { cn } from '@/shared/lib/utils'
2525
import { Button } from '@/shared/components/ui/button'
2626
import { Input } from '@/shared/components/ui/input'
2727
import { InfiniteScrollSentinel } from '@/shared/components/ui/infinite-scroll'
28+
import { ConfirmDialog } from '@/shared/components/ui/confirm-dialog'
2829
import { YamlCodeEditor } from '@/shared/components/YamlCodeEditor'
2930
import { useDateFormat } from '@/shared/lib/datetime'
3031
import {
@@ -79,6 +80,8 @@ export function AlertingRulesPage() {
7980
const [dataTypeOptions, setDataTypeOptions] = useState<DataTypeOption[]>([])
8081
const [open, setOpen] = useState<CorrelationRule | null>(null)
8182
const [creating, setCreating] = useState(false)
83+
const [pendingDelete, setPendingDelete] = useState<CorrelationRule | null>(null)
84+
const [deleting, setDeleting] = useState(false)
8285

8386
const fileInputRef = useRef<HTMLInputElement>(null)
8487
const [importBusy, setImportBusy] = useState(false)
@@ -162,15 +165,21 @@ export function AlertingRulesPage() {
162165
}
163166
}
164167

165-
const remove = async (r: CorrelationRule) => {
166-
if (!confirm(t('alertingRules.deleteConfirm', { name: r.name }))) return
168+
const remove = (r: CorrelationRule) => setPendingDelete(r)
169+
170+
const confirmDelete = async () => {
171+
if (!pendingDelete) return
172+
setDeleting(true)
167173
try {
168-
await svc.remove(r.relPath)
174+
await svc.remove(pendingDelete.relPath)
169175
toast.success(t('alertingRules.toast.deleted'))
170176
setOpen(null)
171177
refresh()
172178
} catch (e) {
173179
toast.error(e instanceof AlertingRulesHttpError ? e.message : t('alertingRules.toast.deleteError'))
180+
} finally {
181+
setPendingDelete(null)
182+
setDeleting(false)
174183
}
175184
}
176185

@@ -259,6 +268,16 @@ export function AlertingRulesPage() {
259268
{open && <RuleDrawer rule={open} dataTypeOptions={dataTypeOptions} onClose={() => setOpen(null)} onToggle={toggleActive} onDelete={remove} onSaved={() => { setOpen(null); refresh() }} t={t} />}
260269
{creating && <RuleDrawer create dataTypeOptions={dataTypeOptions} onClose={() => setCreating(false)} onSaved={() => { setCreating(false); refresh() }} t={t} />}
261270
{importResults && <ImportResultsDialog res={importResults} onClose={() => setImportResults(null)} t={t} />}
271+
<ConfirmDialog
272+
open={pendingDelete != null}
273+
title={t('alertingRules.editor.delete') ?? 'Delete'}
274+
body={pendingDelete ? t('alertingRules.deleteConfirm', { name: pendingDelete.name }) : ''}
275+
confirmLabel={t('alertingRules.editor.delete') ?? undefined}
276+
danger
277+
busy={deleting}
278+
onClose={() => !deleting && setPendingDelete(null)}
279+
onConfirm={confirmDelete}
280+
/>
262281
</div>
263282
)
264283
}

frontend/src/features/alerts/pages/AlertsPage.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useLocation, useNavigate, useParams } from 'react-router-dom'
44
import { useTranslation } from 'react-i18next'
55
import { Button } from '@/shared/components/ui/button'
66
import { InfiniteScrollSentinel } from '@/shared/components/ui/infinite-scroll'
7+
import { ConfirmDialog } from '@/shared/components/ui/confirm-dialog'
78
import { presetRange, type TimeRange } from '@/shared/components/ui/time-range-picker'
89
import { FILTER_OPS, TS } from '../lib/alert-meta'
910
import { alertToRuleConditions } from '../lib/tagging-rule-meta'
@@ -65,6 +66,8 @@ export function AlertsPage() {
6566
| { kind: 'create'; tags?: AlertTag[]; conditions?: FilterType[] }
6667
| null
6768
>(null)
69+
const [pendingDelete, setPendingDelete] = useState<TaggingRule | null>(null)
70+
const [deleting, setDeleting] = useState(false)
6871

6972
const toggleEchoes = (id: string) =>
7073
setExpandedEchoes((prev) => {
@@ -212,10 +215,18 @@ export function AlertsPage() {
212215
if (ok) setRuleDrawer(null)
213216
}
214217

215-
const removeRule = async (r: TaggingRule) => {
216-
if (!confirm(t('taggingRules.deleteConfirm', { name: r.name }))) return
217-
const ok = await deleteRule(r.id, r.name)
218-
if (ok) setRuleDrawer(null)
218+
const removeRule = async (r: TaggingRule) => setPendingDelete(r)
219+
220+
const confirmDelete = async () => {
221+
if (!pendingDelete) return
222+
setDeleting(true)
223+
try {
224+
const ok = await deleteRule(pendingDelete.id, pendingDelete.name)
225+
if (ok) setRuleDrawer(null)
226+
} finally {
227+
setDeleting(false)
228+
setPendingDelete(null)
229+
}
219230
}
220231

221232
const selectedAlerts = useMemo(() => alerts.filter((a) => selected.has(a.id)), [alerts, selected])
@@ -440,6 +451,17 @@ export function AlertsPage() {
440451
onCreateTag={createTag}
441452
/>
442453
)}
454+
455+
<ConfirmDialog
456+
open={pendingDelete != null}
457+
title={t('taggingRules.deleteTitle') ?? 'Delete rule'}
458+
body={pendingDelete ? t('taggingRules.deleteConfirm', { name: pendingDelete.name }) : ''}
459+
confirmLabel={t('common.actions.delete') ?? undefined}
460+
danger
461+
busy={deleting}
462+
onClose={() => !deleting && setPendingDelete(null)}
463+
onConfirm={confirmDelete}
464+
/>
443465
</div>
444466
)
445467
}

frontend/src/features/alerts/pages/TaggingRulesPage.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { cn } from '@/shared/lib/utils'
55
import { Button } from '@/shared/components/ui/button'
66
import { Input } from '@/shared/components/ui/input'
77
import { InfiniteScrollSentinel } from '@/shared/components/ui/infinite-scroll'
8+
import { ConfirmDialog } from '@/shared/components/ui/confirm-dialog'
89
import { useTaggingRulesList } from '../hooks/use-tagging-rules-list'
910
import { useTaggingRuleMutations } from '../hooks/use-tagging-rule-mutations'
1011
import { useAlertTagCatalog } from '../hooks/use-alert-tag-catalog'
@@ -24,6 +25,8 @@ export function TaggingRulesPage() {
2425

2526
const [open, setOpen] = useState<TaggingRule | null>(null)
2627
const [creating, setCreating] = useState(false)
28+
const [pendingDelete, setPendingDelete] = useState<TaggingRule | null>(null)
29+
const [deleting, setDeleting] = useState(false)
2730

2831
// Debounce the search box.
2932
useEffect(() => {
@@ -59,10 +62,18 @@ export function TaggingRulesPage() {
5962
}
6063
}
6164

62-
const remove = async (r: TaggingRule) => {
63-
if (!confirm(t('taggingRules.deleteConfirm', { name: r.name }))) return
64-
const ok = await deleteRule(r.id, r.name)
65-
if (ok) setOpen(null)
65+
const remove = async (r: TaggingRule) => setPendingDelete(r)
66+
67+
const confirmDelete = async () => {
68+
if (!pendingDelete) return
69+
setDeleting(true)
70+
try {
71+
const ok = await deleteRule(pendingDelete.id, pendingDelete.name)
72+
if (ok) setOpen(null)
73+
} finally {
74+
setDeleting(false)
75+
setPendingDelete(null)
76+
}
6677
}
6778

6879
return (
@@ -158,6 +169,17 @@ export function TaggingRulesPage() {
158169
onCreateTag={createTag}
159170
/>
160171
)}
172+
173+
<ConfirmDialog
174+
open={pendingDelete != null}
175+
title={t('taggingRules.deleteTitle') ?? 'Delete rule'}
176+
body={pendingDelete ? t('taggingRules.deleteConfirm', { name: pendingDelete.name }) : ''}
177+
confirmLabel={t('common.actions.delete') ?? undefined}
178+
danger
179+
busy={deleting}
180+
onClose={() => !deleting && setPendingDelete(null)}
181+
onConfirm={confirmDelete}
182+
/>
161183
</div>
162184
)
163185
}

frontend/src/features/incidents/components/incident-alerts-tab.tsx

Lines changed: 49 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,53 +2,66 @@ import { Trash2 } from 'lucide-react'
22
import { useTranslation } from 'react-i18next'
33
import { Link } from 'react-router-dom'
44
import { cn } from '@/shared/lib/utils'
5+
import { ConfirmDialog } from '@/shared/components/ui/confirm-dialog'
56
import { sevKey } from '../lib/incident-meta'
67
import { useIncidentAlertsTab } from '../hooks/use-incident-alerts-tab'
78
import { TabEmpty, TabError, TabLoader } from './ui-primitives'
89

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

1314
if (error) return <TabError onRetry={reload} />
1415
if (rows === null) return <TabLoader />
1516
if (rows.length === 0) return <TabEmpty>{t('incidents.alerts.empty')}</TabEmpty>
1617
return (
17-
<div className="overflow-hidden rounded-lg border border-border bg-card">
18-
{rows.map((a) => (
19-
<Link
20-
to="/threat-management/alerts"
21-
state={{
22-
socaiFilters: [
23-
{ field: 'id', operator: 'IS', value: a.alertId },
24-
{ field: '@timestamp', operator: 'IS_BETWEEN', value: ['now-30d', 'now'] },
25-
],
26-
socaiTime: 'now-30d',
27-
}}
28-
key={a.id}
29-
className="group flex items-center gap-3 border-b border-border/60 px-4 py-2.5 text-xs last:border-b-0"
30-
>
31-
<span
32-
className={cn(
33-
'h-4 w-1 shrink-0 rounded-full',
34-
a.alertSeverity === 3 ? 'bg-red-500' : a.alertSeverity === 2 ? 'bg-amber-500' : 'bg-sky-500'
35-
)}
36-
/>
37-
<span className="min-w-0 flex-1 truncate font-medium" title={a.alertName}>
38-
{a.alertName}
39-
</span>
40-
<span className="shrink-0 font-mono text-[10px] text-muted-foreground">
41-
{t(`incidents.sev.${sevKey(a.alertSeverity)}`)}
42-
</span>
43-
<button
44-
onClick={() => void remove(a)}
45-
title={t('incidents.alerts.remove')}
46-
className="shrink-0 rounded p-1 text-muted-foreground opacity-0 transition hover:bg-muted hover:text-red-500 group-hover:opacity-100"
18+
<>
19+
<div className="overflow-hidden rounded-lg border border-border bg-card">
20+
{rows.map((a) => (
21+
<Link
22+
to="/threat-management/alerts"
23+
state={{
24+
socaiFilters: [
25+
{ field: 'id', operator: 'IS', value: a.alertId },
26+
{ field: '@timestamp', operator: 'IS_BETWEEN', value: ['now-30d', 'now'] },
27+
],
28+
socaiTime: 'now-30d',
29+
}}
30+
key={a.id}
31+
className="group flex items-center gap-3 border-b border-border/60 px-4 py-2.5 text-xs last:border-b-0"
4732
>
48-
<Trash2 size={13} />
49-
</button>
50-
</Link>
51-
))}
52-
</div>
33+
<span
34+
className={cn(
35+
'h-4 w-1 shrink-0 rounded-full',
36+
a.alertSeverity === 3 ? 'bg-red-500' : a.alertSeverity === 2 ? 'bg-amber-500' : 'bg-sky-500'
37+
)}
38+
/>
39+
<span className="min-w-0 flex-1 truncate font-medium" title={a.alertName}>
40+
{a.alertName}
41+
</span>
42+
<span className="shrink-0 font-mono text-[10px] text-muted-foreground">
43+
{t(`incidents.sev.${sevKey(a.alertSeverity)}`)}
44+
</span>
45+
<button
46+
onClick={() => remove(a)}
47+
title={t('incidents.alerts.remove')}
48+
className="shrink-0 rounded p-1 text-muted-foreground opacity-0 transition hover:bg-muted hover:text-red-500 group-hover:opacity-100"
49+
>
50+
<Trash2 size={13} />
51+
</button>
52+
</Link>
53+
))}
54+
</div>
55+
<ConfirmDialog
56+
open={pendingDelete != null}
57+
title={t('incidents.alerts.remove') ?? 'Remove alert'}
58+
body={pendingDelete ? t('incidents.alerts.removeConfirm', { name: pendingDelete.alertName }) : ''}
59+
confirmLabel={t('common.actions.remove') ?? t('incidents.alerts.remove') ?? undefined}
60+
danger
61+
busy={deleting}
62+
onClose={() => !deleting && cancelDelete()}
63+
onConfirm={confirmDelete}
64+
/>
65+
</>
5366
)
5467
}

frontend/src/features/incidents/hooks/use-incident-alerts-tab.ts

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,20 @@ export interface UseIncidentAlertsTabResult {
88
rows: IncidentAlert[] | null
99
error: boolean
1010
reload: () => Promise<void>
11-
remove: (a: IncidentAlert) => Promise<void>
11+
remove: (a: IncidentAlert) => void
12+
pendingDelete: IncidentAlert | null
13+
deleting: boolean
14+
cancelDelete: () => void
15+
confirmDelete: () => Promise<void>
1216
}
1317

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

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

34-
const remove = useCallback(
35-
async (a: IncidentAlert) => {
36-
if (!confirm(t('incidents.alerts.removeConfirm', { name: a.alertName }))) return
37-
try {
38-
await svc.removeAlert(a.id)
39-
toast.success(t('incidents.alerts.removed'))
40-
await load()
41-
onChanged()
42-
} catch (e) {
43-
toast.error(e instanceof IncidentsHttpError ? e.message : t('incidents.alerts.removeError'))
44-
}
45-
},
46-
[load, onChanged, t]
47-
)
48-
49-
return { rows, error, reload: load, remove }
40+
const remove = useCallback((a: IncidentAlert) => setPendingDelete(a), [])
41+
42+
const cancelDelete = useCallback(() => setPendingDelete(null), [])
43+
44+
const confirmDelete = useCallback(async () => {
45+
if (!pendingDelete) return
46+
setDeleting(true)
47+
try {
48+
await svc.removeAlert(pendingDelete.id)
49+
toast.success(t('incidents.alerts.removed'))
50+
await load()
51+
onChanged()
52+
} catch (e) {
53+
toast.error(e instanceof IncidentsHttpError ? e.message : t('incidents.alerts.removeError'))
54+
} finally {
55+
setDeleting(false)
56+
setPendingDelete(null)
57+
}
58+
}, [pendingDelete, load, onChanged, t])
59+
60+
return { rows, error, reload: load, remove, pendingDelete, deleting, cancelDelete, confirmDelete }
5061
}

frontend/src/shared/i18n/locales/de.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
"save": "Speichern",
1919
"cancel": "Abbrechen",
2020
"retry": "Wiederholen",
21-
"refresh": "Aktualisieren"
21+
"refresh": "Aktualisieren",
22+
"delete": "Löschen",
23+
"remove": "Entfernen"
2224
},
2325
"confirm": {
2426
"delete": "Löschen",
@@ -5032,6 +5034,7 @@
50325034
"createError": "Regel konnte nicht erstellt werden.",
50335035
"updateError": "Regel konnte nicht aktualisiert werden.",
50345036
"deleteError": "Regel konnte nicht gelöscht werden."
5035-
}
5037+
},
5038+
"deleteTitle": "Tagging-Regel löschen"
50365039
}
50375040
}

frontend/src/shared/i18n/locales/en.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
"save": "Save",
1919
"cancel": "Cancel",
2020
"retry": "Retry",
21-
"refresh": "Refresh"
21+
"refresh": "Refresh",
22+
"delete": "Delete",
23+
"remove": "Remove"
2224
},
2325
"confirm": {
2426
"delete": "Delete",
@@ -4248,7 +4250,8 @@
42484250
"createError": "Failed to create rule.",
42494251
"updateError": "Failed to update rule.",
42504252
"deleteError": "Failed to delete rule."
4251-
}
4253+
},
4254+
"deleteTitle": "Delete tagging rule"
42524255
},
42534256
"incidents": {
42544257
"title": "Incidents",

0 commit comments

Comments
 (0)