feat(alerting): server-side state+severity filters, ButtonGroup filter UI
Backend: `GET /environments/{envSlug}/alerts` now accepts optional multi-value
`state=…` and `severity=…` query params. Filters are pushed down to
PostgresAlertInstanceRepository, which appends `AND state::text = ANY(?)` /
`AND severity::text = ANY(?)` to the inbox query (null/empty = no filter).
`AlertInstanceRepository.listForInbox` gained a 7-arg overload; the old 5-arg
form is preserved as a default delegate so existing callers (evaluator,
AlertingFullLifecycleIT, PostgresAlertInstanceRepositoryIT) compile unchanged.
`InAppInboxQuery.listInbox` also has a new filtered overload.
UI: InboxPage severity filter migrated from `SegmentedTabs` (single-select,
no color cues) to `ButtonGroup` (multi-select with severity-coloured dots),
matching the topnavbar status-filter pattern. `useAlerts` forwards the
filters as query params and cache-keys on the filter tuple so each combo
is independently cached.
Unit + hook tests updated to the new contract (5 UI tests + 8 Java unit
tests passing). OpenAPI types regenerated from the fresh local backend.
This commit is contained in:
@@ -8,6 +8,8 @@ import com.cameleer.server.app.web.EnvPath;
|
||||
import com.cameleer.server.core.alerting.AlertInstance;
|
||||
import com.cameleer.server.core.alerting.AlertInstanceRepository;
|
||||
import com.cameleer.server.core.alerting.AlertReadRepository;
|
||||
import com.cameleer.server.core.alerting.AlertSeverity;
|
||||
import com.cameleer.server.core.alerting.AlertState;
|
||||
import com.cameleer.server.core.runtime.Environment;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
@@ -54,10 +56,12 @@ public class AlertController {
|
||||
@GetMapping
|
||||
public List<AlertDto> list(
|
||||
@EnvPath Environment env,
|
||||
@RequestParam(defaultValue = "50") int limit) {
|
||||
@RequestParam(defaultValue = "50") int limit,
|
||||
@RequestParam(required = false) List<AlertState> state,
|
||||
@RequestParam(required = false) List<AlertSeverity> severity) {
|
||||
String userId = currentUserId();
|
||||
int effectiveLimit = Math.min(limit, 200);
|
||||
return inboxQuery.listInbox(env.id(), userId, effectiveLimit)
|
||||
return inboxQuery.listInbox(env.id(), userId, state, severity, effectiveLimit)
|
||||
.stream().map(AlertDto::from).toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.cameleer.server.app.alerting.dto.UnreadCountResponse;
|
||||
import com.cameleer.server.core.alerting.AlertInstance;
|
||||
import com.cameleer.server.core.alerting.AlertInstanceRepository;
|
||||
import com.cameleer.server.core.alerting.AlertSeverity;
|
||||
import com.cameleer.server.core.alerting.AlertState;
|
||||
import com.cameleer.server.core.rbac.RbacService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -54,9 +55,22 @@ public class InAppInboxQuery {
|
||||
* or target a role the user holds. Empty target lists mean "broadcast to all".
|
||||
*/
|
||||
public List<AlertInstance> listInbox(UUID envId, String userId, int limit) {
|
||||
return listInbox(envId, userId, null, null, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtered variant of {@link #listInbox(UUID, String, int)}: optional {@code states}
|
||||
* and {@code severities} narrow the result set. {@code null} or empty lists mean
|
||||
* "no filter on that dimension".
|
||||
*/
|
||||
public List<AlertInstance> listInbox(UUID envId,
|
||||
String userId,
|
||||
List<AlertState> states,
|
||||
List<AlertSeverity> severities,
|
||||
int limit) {
|
||||
List<String> groupIds = resolveGroupIds(userId);
|
||||
List<String> roleNames = resolveRoleNames(userId);
|
||||
return instanceRepo.listForInbox(envId, groupIds, userId, roleNames, limit);
|
||||
return instanceRepo.listForInbox(envId, groupIds, userId, roleNames, states, severities, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -98,12 +98,14 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository
|
||||
List<String> userGroupIdFilter,
|
||||
String userId,
|
||||
List<String> userRoleNames,
|
||||
List<AlertState> states,
|
||||
List<AlertSeverity> severities,
|
||||
int limit) {
|
||||
// Build arrays for group UUIDs and role names
|
||||
Array groupArray = toUuidArrayFromStrings(userGroupIdFilter);
|
||||
Array roleArray = toTextArray(userRoleNames);
|
||||
|
||||
String sql = """
|
||||
StringBuilder sql = new StringBuilder("""
|
||||
SELECT * FROM alert_instances
|
||||
WHERE environment_id = ?
|
||||
AND (
|
||||
@@ -111,10 +113,24 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository
|
||||
OR target_group_ids && ?
|
||||
OR target_role_names && ?
|
||||
)
|
||||
ORDER BY fired_at DESC
|
||||
LIMIT ?
|
||||
""";
|
||||
return jdbc.query(sql, rowMapper(), environmentId, userId, groupArray, roleArray, limit);
|
||||
""");
|
||||
List<Object> args = new ArrayList<>(List.of(environmentId, userId, groupArray, roleArray));
|
||||
|
||||
if (states != null && !states.isEmpty()) {
|
||||
Array stateArray = toTextArray(states.stream().map(Enum::name).toList());
|
||||
sql.append(" AND state::text = ANY(?)");
|
||||
args.add(stateArray);
|
||||
}
|
||||
if (severities != null && !severities.isEmpty()) {
|
||||
Array severityArray = toTextArray(severities.stream().map(Enum::name).toList());
|
||||
sql.append(" AND severity::text = ANY(?)");
|
||||
args.add(severityArray);
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY fired_at DESC LIMIT ?");
|
||||
args.add(limit);
|
||||
|
||||
return jdbc.query(sql.toString(), rowMapper(), args.toArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user