feat(alerts): per-group acknowledge button for related alert groups

Adds an 'Ack all (N)' button next to the '+N related' toggle on grouped
alerts. Acknowledges the primary and all related alerts in one action
using the existing bulkAcknowledge API endpoint, without touching
unrelated alerts.
This commit is contained in:
rcourtman 2026-06-27 14:58:00 +01:00
parent 48cf663237
commit e7de730c71
3 changed files with 63 additions and 9 deletions

View file

@ -128,15 +128,31 @@ export function AlertOverviewActiveAlertsSection(props: AlertOverviewActiveAlert
/>
<Show when={group.related.length > 0}>
<div class="ml-4 border-l-2 border-border pl-3 mt-1">
<button
type="button"
class="text-xs text-muted hover:text-base-content transition-colors py-1"
onClick={() => toggleGroup(group.key)}
>
{isGroupExpanded(group.key)
? 'Hide'
: `+${group.related.length} related`}
</button>
<div class="flex items-center gap-3 py-1">
<button
type="button"
class="text-xs text-muted hover:text-base-content transition-colors"
onClick={() => toggleGroup(group.key)}
>
{isGroupExpanded(group.key)
? 'Hide'
: `+${group.related.length} related`}
</button>
<Show when={[group.primary, ...group.related].some((a) => !a.acknowledged)}>
<button
type="button"
class="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors"
onClick={() => {
void props.state.handleGroupAcknowledge([
group.primary,
...group.related,
]);
}}
>
Ack all ({group.related.length + 1})
</button>
</Show>
</div>
<Show when={isGroupExpanded(group.key)}>
<div class="space-y-2 mt-1">
<For each={group.related}>

View file

@ -184,6 +184,41 @@ export function useAlertAcknowledgementState(props: UseAlertAcknowledgementState
}
};
const handleGroupAcknowledge = async (alerts: Alert[]) => {
const pending = alerts.filter((alert) => {
const id = getCanonicalAlertId(alert);
return !effectiveAlerts().find((e) => getCanonicalAlertId(e) === id)?.acknowledged;
});
if (pending.length === 0) return;
const identifiers = pending.map((alert) => getCanonicalAlertId(alert));
const groupProcessing = new Set(identifiers);
setProcessingAlerts((previous) => new Set([...previous, ...groupProcessing]));
try {
const result = await AlertsAPI.bulkAcknowledge(identifiers);
const acknowledgedAt = new Date().toISOString();
const successes = result.results.filter((entry) => entry.success);
successes.forEach((entry) => {
applyAlertUpdate(entry.alertIdentifier, {
acknowledged: true,
ackTime: acknowledgedAt,
ackUser: undefined,
});
});
if (successes.length > 0) {
notificationStore.success(getAlertOverviewBulkAcknowledgedNotification(successes.length));
}
} catch (error) {
logger.error('Group acknowledge failed', error);
notificationStore.error(getAlertOverviewBulkAcknowledgeGenericFailureNotification());
} finally {
groupProcessing.forEach((id) => releaseAlertProcessing(id));
}
};
return {
effectiveAlerts,
unacknowledgedAlerts,
@ -191,5 +226,6 @@ export function useAlertAcknowledgementState(props: UseAlertAcknowledgementState
bulkAckProcessing,
handleAlertAcknowledgement,
handleBulkAcknowledge,
handleGroupAcknowledge,
};
}

View file

@ -38,6 +38,7 @@ export function useAlertOverviewState(props: UseAlertOverviewStateProps) {
bulkAckProcessing,
handleAlertAcknowledgement,
handleBulkAcknowledge,
handleGroupAcknowledge,
} = useAlertAcknowledgementState({
alerts: activeAlerts,
updateAlert: props.updateAlert,
@ -109,6 +110,7 @@ export function useAlertOverviewState(props: UseAlertOverviewStateProps) {
bulkAckProcessing,
handleAlertAcknowledgement,
handleBulkAcknowledge,
handleGroupAcknowledge,
};
}