Filter the queue by cluster

A team with a cluster per alert source can now narrow the queue to one
cluster. A dropdown under the status chips lists the clusters, and appears
once there are two or more to choose between, the rule the team selector
follows. The choice is kept per browser, like the selected team, and dropped
if the server no longer knows the cluster rather than leaving an empty list
with no explanation. The status chip counts follow it, and a row leaves out
its cluster chip once the queue is narrowed to one.

The filter is on the server. The list is capped at 50 rows, so filtering what
is on screen would quietly miss older incidents in the Resolved and Archived
lists. GET /api/incidents takes ?cluster=<value>, matched against the
incident's `cluster` group label, and GET /api/incidents/clusters lists the
distinct values from the last 90 days (optionally for one team), so the
dropdown is not limited to what the current page happens to show. Both are
additive: no existing parameter or JSON shape changed, so terdut-tui keeps
working unchanged and has nothing it must mirror.

An incident carries the label only when `cluster` is in Alertmanager's
group_by, so the filter only sees those; the README says so.
This commit is contained in:
Niklas Ye
2026-10-08 18:23:28 +02:00
parent db474ca909
commit a955356821
9 changed files with 218 additions and 12 deletions
+35
View File
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
"strings"
"testing"
"time"
@@ -972,3 +973,37 @@ func TestServiceAccount_AssignArchiveUnarchiveRecordActor(t *testing.T) {
}
}
}
// The queue can be narrowed to one cluster, and the distinct clusters are
// offered so the filter has something to list.
func TestIncidents_FilterByCluster(t *testing.T) {
s := newTS(t)
for _, c := range []string{"prod-eu", "prod-us"} {
fireGroupedAs(t, s, "fp-"+c,
map[string]string{"alertname": "PodRestarting", "cluster": c, "namespace": "n"},
`{}:{alertname="PodRestarting",cluster="`+c+`",namespace="n"}`)
}
// A cluster-less incident exists too, and must never match a cluster filter.
fireGroupedAs(t, s, "fp-none", map[string]string{"alertname": "DiskFull"}, `{}:{alertname="DiskFull"}`)
if got := len(listIncidents(t, s, "")); got != 3 {
t.Fatalf("expected 3 open incidents, got %d", got)
}
eu := listIncidents(t, s, "?cluster=prod-eu")
if len(eu) != 1 {
t.Fatalf("expected 1 incident for prod-eu, got %d", len(eu))
}
if labels, _ := eu[0]["group_labels"].(map[string]any); labels["cluster"] != "prod-eu" {
t.Errorf("filtered to the wrong cluster: %v", eu[0]["group_labels"])
}
if got := len(listIncidents(t, s, "?cluster=nowhere")); got != 0 {
t.Errorf("an unknown cluster should match nothing, got %d", got)
}
var clusters []string
decode(t, s.req(t, http.MethodGet, "/api/incidents/clusters", nil), &clusters)
if want := "prod-eu,prod-us"; strings.Join(clusters, ",") != want {
t.Errorf("clusters = %v, want %s", clusters, want)
}
}