From 907ac420d337ac0640d187fc68db76f2a719f38d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 9 Jul 2021 10:21:20 +0100 Subject: [PATCH] feat(api): use a dedicated API endpoint for overview modal --- CHANGELOG.md | 3 +- Makefile | 2 +- cmd/karma/__snapshots__/karma.snapshot | 801 ++++++++++++++++++ cmd/karma/api_test.go | 259 ------ cmd/karma/main.go | 1 + cmd/karma/script_test.go | 13 +- cmd/karma/views.go | 73 +- cmd/karma/views_test.go | 102 ++- go.mod | 1 + go.sum | 3 + internal/models/api.go | 6 +- .../OverviewModalContent.test.tsx | 161 ++-- .../OverviewModal/OverviewModalContent.tsx | 57 +- .../OverviewModal/index.stories.tsx | 95 ++- .../Components/OverviewModal/index.test.tsx | 72 ++ ui/src/Models/APITypes.ts | 6 +- ui/src/Stores/AlertStore.ts | 17 +- ui/src/__fixtures__/Fetch.ts | 7 +- ui/src/__fixtures__/Stories.ts | 70 -- ui/src/__fixtures__/useFetchGet.ts | 80 +- 20 files changed, 1395 insertions(+), 434 deletions(-) create mode 100644 cmd/karma/__snapshots__/karma.snapshot diff --git a/CHANGELOG.md b/CHANGELOG.md index 59c08cc65..1f9d006d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ ### Added -- Added dedicated API endpoint for silence previews. +- Added a dedicated API endpoint for silence previews. +- Added a dedicated API endpoint for overview modal. ## v0.87 diff --git a/Makefile b/Makefile index 64dab06bd..8cece47bb 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ include make/docker.mk include make/lint-versions.mk .PHONY: lint -lint: lint-bootstrap-version +lint: lint-go lint-bootstrap-version make -C ui lint-js .PHONY: test diff --git a/cmd/karma/__snapshots__/karma.snapshot b/cmd/karma/__snapshots__/karma.snapshot new file mode 100644 index 000000000..7533b2f91 --- /dev/null +++ b/cmd/karma/__snapshots__/karma.snapshot @@ -0,0 +1,801 @@ +/* snapshot: TestCounters/#00 */ +HTTP/1.1 200 OK +Connection: close +Cache-Control: no-cache, no-store, must-revalidate +Content-Type: application/json +Vary: Origin + +{ + "counters": [ + { + "hits": 48, + "name": "@cluster", + "values": [ + { + "hits": 24, + "offset": 0, + "percent": 50, + "raw": "@cluster=cluster", + "value": "cluster" + }, + { + "hits": 24, + "offset": 50, + "percent": 50, + "raw": "@cluster=single", + "value": "single" + } + ] + }, + { + "hits": 24, + "name": "@receiver", + "values": [ + { + "hits": 12, + "offset": 0, + "percent": 50, + "raw": "@receiver=by-cluster-service", + "value": "by-cluster-service" + }, + { + "hits": 12, + "offset": 50, + "percent": 50, + "raw": "@receiver=by-name", + "value": "by-name" + } + ] + }, + { + "hits": 24, + "name": "@state", + "values": [ + { + "hits": 16, + "offset": 0, + "percent": 67, + "raw": "@state=active", + "value": "active" + }, + { + "hits": 8, + "offset": 67, + "percent": 33, + "raw": "@state=suppressed", + "value": "suppressed" + } + ] + }, + { + "hits": 24, + "name": "alertname", + "values": [ + { + "hits": 16, + "offset": 0, + "percent": 67, + "raw": "alertname=Host_Down", + "value": "Host_Down" + }, + { + "hits": 4, + "offset": 67, + "percent": 17, + "raw": "alertname=HTTP_Probe_Failed", + "value": "HTTP_Probe_Failed" + }, + { + "hits": 2, + "offset": 84, + "percent": 8, + "raw": "alertname=Free_Disk_Space_Too_Low", + "value": "Free_Disk_Space_Too_Low" + }, + { + "hits": 2, + "offset": 92, + "percent": 8, + "raw": "alertname=Memory_Usage_Too_High", + "value": "Memory_Usage_Too_High" + } + ] + }, + { + "hits": 24, + "name": "cluster", + "values": [ + { + "hits": 10, + "offset": 0, + "percent": 42, + "raw": "cluster=dev", + "value": "dev" + }, + { + "hits": 8, + "offset": 42, + "percent": 33, + "raw": "cluster=staging", + "value": "staging" + }, + { + "hits": 6, + "offset": 75, + "percent": 25, + "raw": "cluster=prod", + "value": "prod" + } + ] + }, + { + "hits": 24, + "name": "instance", + "values": [ + { + "hits": 4, + "offset": 0, + "percent": 17, + "raw": "instance=server2", + "value": "server2" + }, + { + "hits": 4, + "offset": 17, + "percent": 17, + "raw": "instance=server5", + "value": "server5" + }, + { + "hits": 2, + "offset": 34, + "percent": 9, + "raw": "instance=server1", + "value": "server1" + }, + { + "hits": 2, + "offset": 43, + "percent": 9, + "raw": "instance=server3", + "value": "server3" + }, + { + "hits": 2, + "offset": 52, + "percent": 8, + "raw": "instance=server4", + "value": "server4" + }, + { + "hits": 2, + "offset": 60, + "percent": 8, + "raw": "instance=server6", + "value": "server6" + }, + { + "hits": 2, + "offset": 68, + "percent": 8, + "raw": "instance=server7", + "value": "server7" + }, + { + "hits": 2, + "offset": 76, + "percent": 8, + "raw": "instance=server8", + "value": "server8" + }, + { + "hits": 2, + "offset": 84, + "percent": 8, + "raw": "instance=web1", + "value": "web1" + }, + { + "hits": 2, + "offset": 92, + "percent": 8, + "raw": "instance=web2", + "value": "web2" + } + ] + }, + { + "hits": 24, + "name": "job", + "values": [ + { + "hits": 16, + "offset": 0, + "percent": 67, + "raw": "job=node_ping", + "value": "node_ping" + }, + { + "hits": 8, + "offset": 67, + "percent": 33, + "raw": "job=node_exporter", + "value": "node_exporter" + } + ] + }, + { + "hits": 16, + "name": "ip", + "values": [ + { + "hits": 2, + "offset": 0, + "percent": 13, + "raw": "ip=127.0.0.1", + "value": "127.0.0.1" + }, + { + "hits": 2, + "offset": 13, + "percent": 13, + "raw": "ip=127.0.0.2", + "value": "127.0.0.2" + }, + { + "hits": 2, + "offset": 26, + "percent": 13, + "raw": "ip=127.0.0.3", + "value": "127.0.0.3" + }, + { + "hits": 2, + "offset": 39, + "percent": 13, + "raw": "ip=127.0.0.4", + "value": "127.0.0.4" + }, + { + "hits": 2, + "offset": 52, + "percent": 12, + "raw": "ip=127.0.0.5", + "value": "127.0.0.5" + }, + { + "hits": 2, + "offset": 64, + "percent": 12, + "raw": "ip=127.0.0.6", + "value": "127.0.0.6" + }, + { + "hits": 2, + "offset": 76, + "percent": 12, + "raw": "ip=127.0.0.7", + "value": "127.0.0.7" + }, + { + "hits": 2, + "offset": 88, + "percent": 12, + "raw": "ip=127.0.0.8", + "value": "127.0.0.8" + } + ] + }, + { + "hits": 2, + "name": "disk", + "values": [ + { + "hits": 2, + "offset": 0, + "percent": 100, + "raw": "disk=sda", + "value": "sda" + } + ] + } + ], + "total": 24 +} + +/* snapshot: TestCounters/q=@cluster=single */ +HTTP/1.1 200 OK +Connection: close +Cache-Control: no-cache, no-store, must-revalidate +Content-Type: application/json +Vary: Origin + +{ + "counters": [ + { + "hits": 24, + "name": "@cluster", + "values": [ + { + "hits": 24, + "offset": 0, + "percent": 100, + "raw": "@cluster=single", + "value": "single" + } + ] + }, + { + "hits": 24, + "name": "@receiver", + "values": [ + { + "hits": 12, + "offset": 0, + "percent": 50, + "raw": "@receiver=by-cluster-service", + "value": "by-cluster-service" + }, + { + "hits": 12, + "offset": 50, + "percent": 50, + "raw": "@receiver=by-name", + "value": "by-name" + } + ] + }, + { + "hits": 24, + "name": "@state", + "values": [ + { + "hits": 16, + "offset": 0, + "percent": 67, + "raw": "@state=active", + "value": "active" + }, + { + "hits": 8, + "offset": 67, + "percent": 33, + "raw": "@state=suppressed", + "value": "suppressed" + } + ] + }, + { + "hits": 24, + "name": "alertname", + "values": [ + { + "hits": 16, + "offset": 0, + "percent": 67, + "raw": "alertname=Host_Down", + "value": "Host_Down" + }, + { + "hits": 4, + "offset": 67, + "percent": 17, + "raw": "alertname=HTTP_Probe_Failed", + "value": "HTTP_Probe_Failed" + }, + { + "hits": 2, + "offset": 84, + "percent": 8, + "raw": "alertname=Free_Disk_Space_Too_Low", + "value": "Free_Disk_Space_Too_Low" + }, + { + "hits": 2, + "offset": 92, + "percent": 8, + "raw": "alertname=Memory_Usage_Too_High", + "value": "Memory_Usage_Too_High" + } + ] + }, + { + "hits": 24, + "name": "cluster", + "values": [ + { + "hits": 10, + "offset": 0, + "percent": 42, + "raw": "cluster=dev", + "value": "dev" + }, + { + "hits": 8, + "offset": 42, + "percent": 33, + "raw": "cluster=staging", + "value": "staging" + }, + { + "hits": 6, + "offset": 75, + "percent": 25, + "raw": "cluster=prod", + "value": "prod" + } + ] + }, + { + "hits": 24, + "name": "instance", + "values": [ + { + "hits": 4, + "offset": 0, + "percent": 17, + "raw": "instance=server2", + "value": "server2" + }, + { + "hits": 4, + "offset": 17, + "percent": 17, + "raw": "instance=server5", + "value": "server5" + }, + { + "hits": 2, + "offset": 34, + "percent": 9, + "raw": "instance=server1", + "value": "server1" + }, + { + "hits": 2, + "offset": 43, + "percent": 9, + "raw": "instance=server3", + "value": "server3" + }, + { + "hits": 2, + "offset": 52, + "percent": 8, + "raw": "instance=server4", + "value": "server4" + }, + { + "hits": 2, + "offset": 60, + "percent": 8, + "raw": "instance=server6", + "value": "server6" + }, + { + "hits": 2, + "offset": 68, + "percent": 8, + "raw": "instance=server7", + "value": "server7" + }, + { + "hits": 2, + "offset": 76, + "percent": 8, + "raw": "instance=server8", + "value": "server8" + }, + { + "hits": 2, + "offset": 84, + "percent": 8, + "raw": "instance=web1", + "value": "web1" + }, + { + "hits": 2, + "offset": 92, + "percent": 8, + "raw": "instance=web2", + "value": "web2" + } + ] + }, + { + "hits": 24, + "name": "job", + "values": [ + { + "hits": 16, + "offset": 0, + "percent": 67, + "raw": "job=node_ping", + "value": "node_ping" + }, + { + "hits": 8, + "offset": 67, + "percent": 33, + "raw": "job=node_exporter", + "value": "node_exporter" + } + ] + }, + { + "hits": 16, + "name": "ip", + "values": [ + { + "hits": 2, + "offset": 0, + "percent": 13, + "raw": "ip=127.0.0.1", + "value": "127.0.0.1" + }, + { + "hits": 2, + "offset": 13, + "percent": 13, + "raw": "ip=127.0.0.2", + "value": "127.0.0.2" + }, + { + "hits": 2, + "offset": 26, + "percent": 13, + "raw": "ip=127.0.0.3", + "value": "127.0.0.3" + }, + { + "hits": 2, + "offset": 39, + "percent": 13, + "raw": "ip=127.0.0.4", + "value": "127.0.0.4" + }, + { + "hits": 2, + "offset": 52, + "percent": 12, + "raw": "ip=127.0.0.5", + "value": "127.0.0.5" + }, + { + "hits": 2, + "offset": 64, + "percent": 12, + "raw": "ip=127.0.0.6", + "value": "127.0.0.6" + }, + { + "hits": 2, + "offset": 76, + "percent": 12, + "raw": "ip=127.0.0.7", + "value": "127.0.0.7" + }, + { + "hits": 2, + "offset": 88, + "percent": 12, + "raw": "ip=127.0.0.8", + "value": "127.0.0.8" + } + ] + }, + { + "hits": 2, + "name": "disk", + "values": [ + { + "hits": 2, + "offset": 0, + "percent": 100, + "raw": "disk=sda", + "value": "sda" + } + ] + } + ], + "total": 24 +} + +/* snapshot: TestCounters/q=@receiver=by-cluster-service&q=alertname=Host_Down&q=@state=active */ +HTTP/1.1 200 OK +Connection: close +Cache-Control: no-cache, no-store, must-revalidate +Content-Type: application/json +Vary: Origin + +{ + "counters": [ + { + "hits": 10, + "name": "@cluster", + "values": [ + { + "hits": 5, + "offset": 0, + "percent": 50, + "raw": "@cluster=cluster", + "value": "cluster" + }, + { + "hits": 5, + "offset": 50, + "percent": 50, + "raw": "@cluster=single", + "value": "single" + } + ] + }, + { + "hits": 5, + "name": "@receiver", + "values": [ + { + "hits": 5, + "offset": 0, + "percent": 100, + "raw": "@receiver=by-cluster-service", + "value": "by-cluster-service" + } + ] + }, + { + "hits": 5, + "name": "@state", + "values": [ + { + "hits": 5, + "offset": 0, + "percent": 100, + "raw": "@state=active", + "value": "active" + } + ] + }, + { + "hits": 5, + "name": "alertname", + "values": [ + { + "hits": 5, + "offset": 0, + "percent": 100, + "raw": "alertname=Host_Down", + "value": "Host_Down" + } + ] + }, + { + "hits": 5, + "name": "cluster", + "values": [ + { + "hits": 3, + "offset": 0, + "percent": 60, + "raw": "cluster=staging", + "value": "staging" + }, + { + "hits": 2, + "offset": 60, + "percent": 40, + "raw": "cluster=prod", + "value": "prod" + } + ] + }, + { + "hits": 5, + "name": "instance", + "values": [ + { + "hits": 1, + "offset": 0, + "percent": 20, + "raw": "instance=server1", + "value": "server1" + }, + { + "hits": 1, + "offset": 20, + "percent": 20, + "raw": "instance=server2", + "value": "server2" + }, + { + "hits": 1, + "offset": 40, + "percent": 20, + "raw": "instance=server3", + "value": "server3" + }, + { + "hits": 1, + "offset": 60, + "percent": 20, + "raw": "instance=server4", + "value": "server4" + }, + { + "hits": 1, + "offset": 80, + "percent": 20, + "raw": "instance=server5", + "value": "server5" + } + ] + }, + { + "hits": 5, + "name": "ip", + "values": [ + { + "hits": 1, + "offset": 0, + "percent": 20, + "raw": "ip=127.0.0.1", + "value": "127.0.0.1" + }, + { + "hits": 1, + "offset": 20, + "percent": 20, + "raw": "ip=127.0.0.2", + "value": "127.0.0.2" + }, + { + "hits": 1, + "offset": 40, + "percent": 20, + "raw": "ip=127.0.0.3", + "value": "127.0.0.3" + }, + { + "hits": 1, + "offset": 60, + "percent": 20, + "raw": "ip=127.0.0.4", + "value": "127.0.0.4" + }, + { + "hits": 1, + "offset": 80, + "percent": 20, + "raw": "ip=127.0.0.5", + "value": "127.0.0.5" + } + ] + }, + { + "hits": 5, + "name": "job", + "values": [ + { + "hits": 5, + "offset": 0, + "percent": 100, + "raw": "job=node_ping", + "value": "node_ping" + } + ] + } + ], + "total": 5 +} + +/* snapshot: TestCounters/q=foo=bar */ +HTTP/1.1 200 OK +Connection: close +Cache-Control: no-cache, no-store, must-revalidate +Content-Type: application/json +Vary: Origin + +{ + "counters": [], + "total": 0 +} \ No newline at end of file diff --git a/cmd/karma/api_test.go b/cmd/karma/api_test.go index 89bbe91ad..c2473e9aa 100644 --- a/cmd/karma/api_test.go +++ b/cmd/karma/api_test.go @@ -632,217 +632,6 @@ var groupTests = []groupTest{ }, } -var countsMap = models.LabelNameStatsList{ - { - Name: "@receiver", - Hits: 24, - Values: models.LabelValueStatsList{ - models.LabelValueStats{ - Value: "by-cluster-service", - Hits: 12, - Percent: 50, - }, - models.LabelValueStats{ - Value: "by-name", - Hits: 12, - Percent: 50, - }, - }, - }, - { - Name: "@state", - Hits: 24, - Values: models.LabelValueStatsList{ - models.LabelValueStats{ - Value: "active", - Hits: 16, - Percent: 67, - }, - models.LabelValueStats{ - Value: "suppressed", - Hits: 8, - Percent: 33, - }, - }, - }, - { - Name: "alertname", - Hits: 24, - Values: models.LabelValueStatsList{ - models.LabelValueStats{ - Value: "Free_Disk_Space_Too_Low", - Hits: 2, - Percent: 8, - }, - models.LabelValueStats{ - Value: "HTTP_Probe_Failed", - Hits: 4, - Percent: 17, - }, - models.LabelValueStats{ - Value: "Host_Down", - Hits: 16, - Percent: 67, - }, - models.LabelValueStats{ - Value: "Memory_Usage_Too_High", - Hits: 2, - Percent: 8, - }, - }, - }, - { - Name: "cluster", - Hits: 24, - Values: models.LabelValueStatsList{ - models.LabelValueStats{ - Value: "dev", - Hits: 10, - Percent: 42, - }, - models.LabelValueStats{ - Value: "prod", - Hits: 6, - Percent: 25, - }, - models.LabelValueStats{ - Value: "staging", - Hits: 8, - Percent: 33, - }, - }, - }, - { - Name: "disk", - Hits: 2, - Values: models.LabelValueStatsList{ - models.LabelValueStats{ - Value: "sda", - Hits: 2, - Percent: 100, - }, - }, - }, - { - Name: "instance", - Hits: 24, - Values: models.LabelValueStatsList{ - models.LabelValueStats{ - Value: "server1", - Hits: 2, - Percent: 9, - }, - models.LabelValueStats{ - Value: "server2", - Hits: 4, - Percent: 17, - }, - models.LabelValueStats{ - Value: "server3", - Hits: 2, - Percent: 9, - }, - models.LabelValueStats{ - Value: "server4", - Hits: 2, - Percent: 8, - }, - models.LabelValueStats{ - Value: "server5", - Hits: 4, - Percent: 17, - }, - models.LabelValueStats{ - Value: "server6", - Hits: 2, - Percent: 8, - }, - models.LabelValueStats{ - Value: "server7", - Hits: 2, - Percent: 8, - }, - models.LabelValueStats{ - Value: "server8", - Hits: 2, - Percent: 8, - }, - models.LabelValueStats{ - Value: "web1", - Hits: 2, - Percent: 8, - }, - models.LabelValueStats{ - Value: "web2", - Hits: 2, - Percent: 8, - }, - }, - }, - { - Name: "ip", - Hits: 16, - Values: models.LabelValueStatsList{ - models.LabelValueStats{ - Value: "127.0.0.1", - Hits: 2, - Percent: 13, - }, - models.LabelValueStats{ - Value: "127.0.0.2", - Hits: 2, - Percent: 13, - }, - models.LabelValueStats{ - Value: "127.0.0.3", - Hits: 2, - Percent: 13, - }, - models.LabelValueStats{ - Value: "127.0.0.4", - Hits: 2, - Percent: 13, - }, - models.LabelValueStats{ - Value: "127.0.0.5", - Hits: 2, - Percent: 12, - }, - models.LabelValueStats{ - Value: "127.0.0.6", - Hits: 2, - Percent: 12, - }, - models.LabelValueStats{ - Value: "127.0.0.7", - Hits: 2, - Percent: 12, - }, - models.LabelValueStats{ - Value: "127.0.0.8", - Hits: 2, - Percent: 12, - }, - }, - }, - { - Name: "job", - Hits: 24, - Values: models.LabelValueStatsList{ - models.LabelValueStats{ - Value: "node_exporter", - Hits: 8, - Percent: 33, - }, - models.LabelValueStats{ - Value: "node_ping", - Hits: 16, - Percent: 67, - }, - }, - }, -} - var filtersExpected = []models.Filter{} func compareAlertGroups(testCase groupTest, group models.APIAlertGroup) bool { @@ -1032,54 +821,6 @@ func TestVerifyAllGroups(t *testing.T) { t.Errorf("[%s] Silences mismatch, expected >0 but got %d", version, len(am)) } - for _, nameStats := range ur.Counters { - var totalPercent int - for _, valueStats := range nameStats.Values { - totalPercent += valueStats.Percent - } - if totalPercent != 100 { - t.Errorf("[%s] Counters %s sum is != 100: %d", version, nameStats.Name, totalPercent) - } - } - - for _, expectedNameStats := range countsMap { - var foundName bool - for _, nameStats := range ur.Counters { - if nameStats.Name == expectedNameStats.Name { - if nameStats.Hits != expectedNameStats.Hits { - t.Errorf("[%s] Counters mismatch for '%s', expected %v hits but got %v", - version, nameStats.Name, expectedNameStats.Hits, nameStats.Hits) - } - for _, expectedValueStats := range expectedNameStats.Values { - var foundValue bool - for _, valueStats := range nameStats.Values { - if valueStats.Value == expectedValueStats.Value { - if valueStats.Hits != expectedValueStats.Hits { - t.Errorf("[%s] Counters mismatch for '%s: %s', expected %v hits but got %v", - version, nameStats.Name, valueStats.Value, expectedValueStats.Hits, valueStats.Hits) - } - if valueStats.Percent != expectedValueStats.Percent { - t.Errorf("[%s] Percent mismatch for '%s: %s', expected %v%% but got %v%%", - version, nameStats.Name, valueStats.Value, expectedValueStats.Percent, valueStats.Percent) - } - foundValue = true - break - } - } - if !foundValue { - if !foundName { - t.Errorf("[%s] Counters missing for label '%s: %s'", version, expectedNameStats.Name, expectedValueStats.Value) - } - } - } - foundName = true - break - } - } - if !foundName { - t.Errorf("[%s] Counters missing for label '%s'", version, expectedNameStats.Name) - } - } if !reflect.DeepEqual(ur.Filters, filtersExpected) { t.Errorf("[%s] Filters mismatch, expected %v but got %v", version, filtersExpected, ur.Filters) } diff --git a/cmd/karma/main.go b/cmd/karma/main.go index 849dfab1c..79cbf98b1 100644 --- a/cmd/karma/main.go +++ b/cmd/karma/main.go @@ -143,6 +143,7 @@ func setupRouter(router *chi.Mux, historyPoller *historyPoller) { router.Get(getViewURL("/labelNames.json"), knownLabelNames) router.Get(getViewURL("/labelValues.json"), knownLabelValues) router.Get(getViewURL("/silences.json"), silences) + router.Get(getViewURL("/counters.json"), counters) router.Post(getViewURL("/history.json"), func(w http.ResponseWriter, r *http.Request) { alertHistory(historyPoller, w, r) }) diff --git a/cmd/karma/script_test.go b/cmd/karma/script_test.go index 6cda9d85f..e31fdb8d8 100644 --- a/cmd/karma/script_test.go +++ b/cmd/karma/script_test.go @@ -1,9 +1,11 @@ package main import ( + "fmt" "os" "testing" + "github.com/beme/abide" "github.com/rogpeppe/go-internal/testscript" "github.com/rs/zerolog/log" "github.com/spf13/pflag" @@ -31,10 +33,17 @@ func mainShouldWork() int { } func TestMain(m *testing.M) { - os.Exit(testscript.RunMain(m, map[string]func() int{ + ecode := testscript.RunMain(m, map[string]func() int{ "karma.bin-should-fail": mainShoulFail, "karma.bin-should-work": mainShouldWork, - })) + }) + err := abide.Cleanup() + if err != nil { + fmt.Printf("abide.Cleanup() error: %v\n", err) + ecode = 1 + } + os.Exit(ecode) + } func TestScripts(t *testing.T) { diff --git a/cmd/karma/views.go b/cmd/karma/views.go index 2e8704640..2a387c0d6 100644 --- a/cmd/karma/views.go +++ b/cmd/karma/views.go @@ -222,7 +222,6 @@ func alerts(w http.ResponseWriter, r *http.Request) { grids := map[string]models.APIGrid{} colors := models.LabelsColorMap{} - counters := map[string]map[string]int{} dedupedAlerts := alertmanager.DedupAlerts() dedupedColors := alertmanager.DedupColors() @@ -342,19 +341,6 @@ func alerts(w http.ResponseWriter, r *http.Request) { agCopy.Alerts = append(agCopy.Alerts, alert) - if len(upstreams.Clusters) > 1 { - clusters := map[string]bool{} - for _, am := range alert.Alertmanager { - clusters[am.Cluster] = true - } - for cluster := range clusters { - countLabel(counters, "@cluster", cluster) - } - } - - countLabel(counters, "@state", alert.State) - - countLabel(counters, "@receiver", alert.Receiver) if ck, foundKey := dedupedColors["@receiver"]; foundKey { if cv, foundVal := ck[alert.Receiver]; foundVal { if _, found := colors["@receiver"]; !found { @@ -405,7 +391,6 @@ func alerts(w http.ResponseWriter, r *http.Request) { colors[key][value] = color } } - countLabel(counters, key, value) } } } @@ -493,7 +478,6 @@ func alerts(w http.ResponseWriter, r *http.Request) { resp.Grids = sortedGrids resp.Silences = silences resp.Colors = colors - resp.Counters = countersToLabelStats(counters) resp.Filters = populateAPIFilters(matchFilters) resp.Receivers = receivers @@ -761,3 +745,60 @@ func sortSliceOfLabels(labels []map[string]string, sortKeys []string, fallback s return sortorder.NaturalLess(labels[i][fallback], labels[j][fallback]) }) } + +func counters(w http.ResponseWriter, r *http.Request) { + noCache(w) + + // use full URI (including query args) as cache key + cacheKey := r.RequestURI + + d, found := apiCache.Get(cacheKey) + if found { + r := bytes.NewReader(d.([]byte)) + rawData, _ := decompressCachedResponse(r) + mimeJSON(w) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(rawData) + return + } + + q, _ := lookupQueryStringSlice(r, "q") + matchFilters := getFiltersFromQuery(q) + dedupedAlerts := alertmanager.DedupAlerts() + filtered := filterAlerts(dedupedAlerts, matchFilters) + upstreams := getUpstreams() + counters := map[string]map[string]int{} + + var total int + for _, ag := range filtered { + total += len(ag.Alerts) + for _, alert := range ag.Alerts { + if len(upstreams.Clusters) > 1 { + clusters := map[string]struct{}{} + for _, am := range alert.Alertmanager { + clusters[am.Cluster] = struct{}{} + } + for cluster := range clusters { + countLabel(counters, "@cluster", cluster) + } + } + countLabel(counters, "@state", alert.State) + countLabel(counters, "@receiver", alert.Receiver) + for key, value := range alert.Labels { + countLabel(counters, key, value) + } + } + } + + resp := models.Counters{ + Total: total, + Counters: countersToLabelStats(counters), + } + + mimeJSON(w) + w.WriteHeader(http.StatusOK) + data, _ := json.Marshal(resp) + compressedData, _ := compressResponse(data, nil) + _ = apiCache.Add(cacheKey, compressedData) + _, _ = w.Write(data) +} diff --git a/cmd/karma/views_test.go b/cmd/karma/views_test.go index 5fcb27df2..a3c542b1a 100644 --- a/cmd/karma/views_test.go +++ b/cmd/karma/views_test.go @@ -15,6 +15,7 @@ import ( "testing" "time" + "github.com/beme/abide" lru "github.com/hashicorp/golang-lru" "github.com/prymitive/karma/internal/alertmanager" "github.com/prymitive/karma/internal/config" @@ -275,9 +276,6 @@ func TestAlerts(t *testing.T) { if ur.Status != "success" { t.Errorf("[%s] Invalid status in response: %s", version, ur.Status) } - if len(ur.Counters) != 6 { - t.Errorf("[%s] Invalid number of counters in response (%d): %v", version, len(ur.Counters), ur.Counters) - } for _, ag := range ur.Grids[0].AlertGroups { for _, a := range ag.Alerts { linkCount := 0 @@ -3299,3 +3297,101 @@ func TestSortSliceOfLabels(t *testing.T) { }) } } + +func TestCounters(t *testing.T) { + type testCaseT struct { + args string + } + + testCases := []testCaseT{ + { + args: "q=foo=bar", + }, + { + args: "", + }, + { + args: "q=@receiver=by-cluster-service&q=alertname=Host_Down&q=@state=active", + }, + { + args: "q=@cluster=single", + }, + } + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + mockCache() + version := mock.ListAllMocks()[0] + + am1, err := alertmanager.NewAlertmanager( + "cluster", + "am1", + "http://localhost/1", + ) + if err != nil { + t.Error(err) + } + mock.RegisterURL("http://localhost/1/metrics", version, "metrics") + mock.RegisterURL("http://localhost/1/api/v2/status", version, "api/v2/status") + mock.RegisterURL("http://localhost/1/api/v2/silences", version, "api/v2/silences") + mock.RegisterURL("http://localhost/1/api/v2/alerts/groups", version, "api/v2/alerts/groups") + err = alertmanager.RegisterAlertmanager(am1) + if err != nil { + t.Error(err) + } + + am2, err := alertmanager.NewAlertmanager( + "cluster", + "am2", + "http://localhost/2", + ) + if err != nil { + t.Error(err) + } + mock.RegisterURL("http://localhost/2/metrics", version, "metrics") + mock.RegisterURL("http://localhost/2/api/v2/status", version, "api/v2/status") + mock.RegisterURL("http://localhost/2/api/v2/silences", version, "api/v2/silences") + mock.RegisterURL("http://localhost/2/api/v2/alerts/groups", version, "api/v2/alerts/groups") + err = alertmanager.RegisterAlertmanager(am2) + if err != nil { + t.Error(err) + } + + am3, err := alertmanager.NewAlertmanager( + "single", + "single", + "http://localhost/3", + ) + if err != nil { + t.Error(err) + } + mock.RegisterURL("http://localhost/3/metrics", version, "metrics") + mock.RegisterURL("http://localhost/3/api/v2/status", version, "api/v2/status") + mock.RegisterURL("http://localhost/3/api/v2/silences", version, "api/v2/silences") + mock.RegisterURL("http://localhost/3/api/v2/alerts/groups", version, "api/v2/alerts/groups") + err = alertmanager.RegisterAlertmanager(am3) + if err != nil { + t.Error(err) + } + + pullFromAlertmanager() + + for _, tc := range testCases { + t.Run(tc.args, func(t *testing.T) { + r := testRouter() + setupRouter(r, nil) + // re-run a few times to test the cache + for i := 1; i <= 3; i++ { + req := httptest.NewRequest("GET", "/counters.json?"+tc.args, nil) + resp := httptest.NewRecorder() + r.ServeHTTP(resp, req) + if resp.Code != http.StatusOK { + t.Errorf("GET /counters.json returned status %d", resp.Code) + } + abide.AssertHTTPResponse(t, t.Name(), resp.Result()) + } + }) + } +} diff --git a/go.mod b/go.mod index 38c829bba..0f51d6a44 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/Masterminds/semver/v3 v3.1.1 github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect + github.com/beme/abide v0.0.0-20190723115211-635a09831760 github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 github.com/fvbommel/sortorder v1.0.2 github.com/getsentry/sentry-go v0.11.0 diff --git a/go.sum b/go.sum index c8425d883..0a06c3c77 100644 --- a/go.sum +++ b/go.sum @@ -65,6 +65,8 @@ github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= +github.com/beme/abide v0.0.0-20190723115211-635a09831760 h1:FvTM5NSN5HYvfKpgL+8x73U5v063vHsd7AX05eV1DnM= +github.com/beme/abide v0.0.0-20190723115211-635a09831760/go.mod h1:6+8gCKsZnxzhGTmKRh4BSkLos9CbWRJNcrp55We4SqQ= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -525,6 +527,7 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= diff --git a/internal/models/api.go b/internal/models/api.go index 30f9c1e52..27ef2fbc5 100644 --- a/internal/models/api.go +++ b/internal/models/api.go @@ -381,7 +381,6 @@ type AlertsResponse struct { LabelNames []string `json:"labelNames"` Colors LabelsColorMap `json:"colors"` Filters []Filter `json:"filters"` - Counters LabelNameStatsList `json:"counters"` Settings Settings `json:"settings"` Authentication AuthenticationInfo `json:"authentication"` Receivers []string `json:"receivers"` @@ -393,3 +392,8 @@ type Autocomplete struct { Value string `json:"value"` Tokens []string `json:"tokens"` } + +type Counters struct { + Total int `json:"total"` + Counters LabelNameStatsList `json:"counters"` +} diff --git a/ui/src/Components/OverviewModal/OverviewModalContent.test.tsx b/ui/src/Components/OverviewModal/OverviewModalContent.test.tsx index 4b6a45a4d..8a0d35f91 100644 --- a/ui/src/Components/OverviewModal/OverviewModalContent.test.tsx +++ b/ui/src/Components/OverviewModal/OverviewModalContent.test.tsx @@ -2,6 +2,7 @@ import { mount } from "enzyme"; import toDiffableHtml from "diffable-html"; +import { useFetchGetMock } from "__fixtures__/useFetchGet"; import { AlertStore, NewUnappliedFilter } from "Stores/AlertStore"; import { OverviewModalContent } from "./OverviewModalContent"; @@ -29,44 +30,73 @@ const MountedOverviewModalContent = () => describe("", () => { it("matches snapshot with labels to show", () => { - alertStore.filters.setFilterValues([ - NewUnappliedFilter("abc=xyz"), - NewUnappliedFilter("foo=bar"), - ]); - alertStore.data.setCounters([ - { - name: "foo", - hits: 16, - values: [ - { value: "bar1", raw: "foo=bar1", hits: 8, percent: 50, offset: 0 }, - { value: "bar2", raw: "foo=bar2", hits: 4, percent: 25, offset: 50 }, - { value: "bar3", raw: "foo=bar3", hits: 4, percent: 25, offset: 75 }, - ], - }, - { - name: "bar", - hits: 20, - values: Array.from(Array(20).keys()).map((i) => ({ - value: `baz${i + 1}`, - raw: `bar=baz${i + 1}`, - hits: 1, - percent: 5, - offset: i * 5, - })), - }, - { - name: "alertname", - hits: 5, - values: [ + useFetchGetMock.fetch.setMockedData({ + response: { + total: 1, + counters: [ { - value: "Host_Down", - raw: "alertname=Host_Down", + name: "foo", + hits: 16, + values: [ + { + value: "bar1", + raw: "foo=bar1", + hits: 8, + percent: 50, + offset: 0, + }, + { + value: "bar2", + raw: "foo=bar2", + hits: 4, + percent: 25, + offset: 50, + }, + { + value: "bar3", + raw: "foo=bar3", + hits: 4, + percent: 25, + offset: 75, + }, + ], + }, + { + name: "bar", + hits: 20, + values: Array.from(Array(20).keys()).map((i) => ({ + value: `baz${i + 1}`, + raw: `bar=baz${i + 1}`, + hits: 1, + percent: 5, + offset: i * 5, + })), + }, + { + name: "alertname", hits: 5, - percent: 100, - offset: 0, + values: [ + { + value: "Host_Down", + raw: "alertname=Host_Down", + hits: 5, + percent: 100, + offset: 0, + }, + ], }, ], }, + error: null, + isLoading: false, + isRetrying: false, + retryCount: 0, + get: jest.fn(), + cancelGet: jest.fn(), + }); + alertStore.filters.setFilterValues([ + NewUnappliedFilter("abc=xyz"), + NewUnappliedFilter("foo=bar"), ]); const tree = MountedOverviewModalContent(); @@ -74,35 +104,62 @@ describe("", () => { }); it("matches snapshot with no labels to show", () => { - alertStore.data.setCounters([]); + useFetchGetMock.fetch.setMockedData({ + response: { + total: 20, + counters: [], + }, + error: null, + isLoading: false, + isRetrying: false, + retryCount: 0, + get: jest.fn(), + cancelGet: jest.fn(), + }); const tree = MountedOverviewModalContent(); expect(toDiffableHtml(tree.html())).toMatchSnapshot(); }); it("renders all labels after expand button click", () => { - alertStore.info.setTotalAlerts(5); - alertStore.data.setCounters([ - { - name: "foo", - hits: 5, - values: [ - { value: "bar", raw: "foo=bar", hits: 5, percent: 100, offset: 0 }, - ], - }, - { - name: "bar", - hits: 3, - values: [ + useFetchGetMock.fetch.setMockedData({ + response: { + total: 5, + counters: [ { - value: "foo", - raw: "bar=foo", + name: "foo", + hits: 5, + values: [ + { + value: "bar", + raw: "foo=bar", + hits: 5, + percent: 100, + offset: 0, + }, + ], + }, + { + name: "bar", hits: 3, - percent: 100, - offset: 0, + values: [ + { + value: "foo", + raw: "bar=foo", + hits: 3, + percent: 100, + offset: 0, + }, + ], }, ], }, - ]); + error: null, + isLoading: false, + isRetrying: false, + retryCount: 0, + get: jest.fn(), + cancelGet: jest.fn(), + }); const tree = MountedOverviewModalContent(); expect(tree.find("span.components-label")).toHaveLength(2 + 1); // +1 for toggle icon diff --git a/ui/src/Components/OverviewModal/OverviewModalContent.tsx b/ui/src/Components/OverviewModal/OverviewModalContent.tsx index 15c8eb488..db5aab280 100644 --- a/ui/src/Components/OverviewModal/OverviewModalContent.tsx +++ b/ui/src/Components/OverviewModal/OverviewModalContent.tsx @@ -2,8 +2,16 @@ import { FC, useState } from "react"; import { observer } from "mobx-react-lite"; -import type { APILabelCounterT } from "Models/APITypes"; -import type { AlertStore } from "Stores/AlertStore"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faSpinner } from "@fortawesome/free-solid-svg-icons/faSpinner"; + +import type { CountersResponseT, APILabelCounterT } from "Models/APITypes"; +import { + AlertStore, + FormatBackendURI, + FormatAPIFilterQuery, +} from "Stores/AlertStore"; +import { useFetchGet } from "Hooks/useFetchGet"; import { TooltipWrapper } from "Components/TooltipWrapper"; import LabelWithPercent from "Components/Labels/LabelWithPercent"; import { ToggleIcon } from "Components/ToggleIcon"; @@ -53,9 +61,10 @@ const TableRows: FC<{ const LabelsTable: FC<{ alertStore: AlertStore; + counters: CountersResponseT; showAllLabels: boolean; toggleAllLabels: () => void; -}> = observer(({ alertStore, showAllLabels, toggleAllLabels }) => ( +}> = observer(({ alertStore, counters, showAllLabels, toggleAllLabels }) => ( <> nameStats.hits >= alertStore.info.totalAlerts + nameStats={counters.counters.filter( + (nameStats) => nameStats.hits >= counters.total )} > - {alertStore.data.counters.filter( - (nameStats) => nameStats.hits < alertStore.info.totalAlerts + {counters.counters.filter( + (nameStats) => nameStats.hits < counters.total ).length > 0 ? (
@@ -87,8 +96,8 @@ const LabelsTable: FC<{ {showAllLabels ? ( nameStats.hits < alertStore.info.totalAlerts + nameStats={counters.counters.filter( + (nameStats) => nameStats.hits < counters.total )} > ) : null} @@ -105,11 +114,34 @@ const NothingToShow: FC = () => ( ); +const LoadingMessage: FC = () => ( +
+

+ +

+
+); + +const ErrorMessage: FC<{ error: string }> = ({ error }) => ( +
+

{error}

+
+); + const OverviewModalContent: FC<{ alertStore: AlertStore; onHide: () => void; }> = observer(({ alertStore, onHide }) => { const [showAllLabels, setShowAllLabels] = useState(false); + const { response, error } = useFetchGet( + FormatBackendURI( + `counters.json?${FormatAPIFilterQuery( + alertStore.filters.values.map((f) => f.raw) + )}` + ), + { deps: [alertStore.info.timestamp] } + ); + return ( <>
@@ -117,11 +149,16 @@ const OverviewModalContent: FC<{
- {alertStore.data.counters.length === 0 ? ( + {error !== null ? ( + + ) : response === null ? ( + + ) : response.counters.length === 0 ? ( ) : ( setShowAllLabels(!showAllLabels)} /> diff --git a/ui/src/Components/OverviewModal/index.stories.tsx b/ui/src/Components/OverviewModal/index.stories.tsx index a03ca0d21..d3daa7595 100644 --- a/ui/src/Components/OverviewModal/index.stories.tsx +++ b/ui/src/Components/OverviewModal/index.stories.tsx @@ -2,6 +2,8 @@ import type { FC } from "react"; import { storiesOf } from "@storybook/react"; +import fetchMock from "fetch-mock"; + import { MockGrid } from "../../__fixtures__/Stories"; import { AlertStore } from "../../Stores/AlertStore"; import { OverviewModalContent } from "./OverviewModalContent"; @@ -17,10 +19,99 @@ storiesOf("OverviewModal", module).add("OverviewModal", () => {
); - const alertStore = new AlertStore([]); + fetchMock.mock( + "begin:/counters.json?q=full", + { + total: 0, + counters: [ + { + name: "@receiver", + hits: 2, + values: [ + { + value: "by-cluster-service", + raw: "@receiver=by-cluster-service", + hits: 2, + percent: 100, + offset: 0, + }, + ], + }, + { + name: "alertname", + hits: 90, + values: [ + { + value: "Fake Alert", + raw: "alertname=Fake Alert", + hits: 45, + percent: 50, + offset: 0, + }, + { + value: "Second Fake Alert", + raw: "alertname=Second Fake Alert", + hits: 45, + percent: 50, + offset: 50, + }, + ], + }, + { + name: "group", + hits: 100, + values: [ + { + value: "group1", + raw: "group=group1", + hits: 25, + percent: 25, + offset: 0, + }, + { + value: "group2", + raw: "group=group2", + hits: 70, + percent: 70, + offset: 25, + }, + { + value: "group3", + raw: "group=group3", + hits: 4, + percent: 4, + offset: 95, + }, + { + value: "group4", + raw: "group=group4", + hits: 1, + percent: 1, + offset: 99, + }, + ], + }, + ], + }, + { + overwriteRoutes: true, + } + ); + fetchMock.mock( + "begin:/counters.json?q=empty", + { + total: 0, + counters: [], + }, + { + overwriteRoutes: true, + } + ); + + const alertStore = new AlertStore(["full"]); MockGrid(alertStore); - const emptyAlertStore = new AlertStore([]); + const emptyAlertStore = new AlertStore(["empty"]); return (
diff --git a/ui/src/Components/OverviewModal/index.test.tsx b/ui/src/Components/OverviewModal/index.test.tsx index d30c7b6cf..80bbd6f8e 100644 --- a/ui/src/Components/OverviewModal/index.test.tsx +++ b/ui/src/Components/OverviewModal/index.test.tsx @@ -2,6 +2,7 @@ import { act } from "react-dom/test-utils"; import { mount } from "enzyme"; +import { useFetchGetMock } from "__fixtures__/useFetchGet"; import { AlertStore } from "Stores/AlertStore"; import { OverviewModal } from "."; @@ -35,7 +36,53 @@ describe("", () => { expect(tree.find(".modal-content").find("svg.fa-spinner")).toHaveLength(1); }); + it("renders a spinner placeholder while fetch is in progress", () => { + useFetchGetMock.fetch.setMockedData({ + response: null, + error: null, + isLoading: true, + isRetrying: false, + retryCount: 0, + get: jest.fn(), + cancelGet: jest.fn(), + }); + const tree = MountedOverviewModal(); + const toggle = tree.find("div.navbar-brand"); + toggle.simulate("click"); + expect(tree.find("LoadingMessage")).toHaveLength(1); + expect(tree.find(".modal-content").find("svg.fa-spinner")).toHaveLength(1); + }); + + it("renders an error message on fetch error", () => { + useFetchGetMock.fetch.setMockedData({ + response: null, + error: "mock error", + isLoading: false, + isRetrying: false, + retryCount: 0, + get: jest.fn(), + cancelGet: jest.fn(), + }); + const tree = MountedOverviewModal(); + const toggle = tree.find("div.navbar-brand"); + toggle.simulate("click"); + expect(tree.find("ErrorMessage")).toHaveLength(1); + expect(tree.find("h1.text-danger").text()).toBe("mock error"); + }); + it("renders modal content if fallback is not used", () => { + useFetchGetMock.fetch.setMockedData({ + response: { + total: 20, + counters: [], + }, + error: null, + isLoading: false, + isRetrying: false, + retryCount: 0, + get: jest.fn(), + cancelGet: jest.fn(), + }); const tree = MountedOverviewModal(); const toggle = tree.find("div.navbar-brand"); toggle.simulate("click"); @@ -43,6 +90,31 @@ describe("", () => { expect(tree.find(".modal-content").find("svg.fa-spinner")).toHaveLength(0); }); + it("re-fetches counters after timestamp change", () => { + alertStore.info.setTimestamp("old"); + useFetchGetMock.fetch.setMockedData({ + response: { + total: 20, + counters: [], + }, + error: null, + isLoading: false, + isRetrying: false, + retryCount: 0, + get: jest.fn(), + cancelGet: jest.fn(), + }); + const tree = MountedOverviewModal(); + const toggle = tree.find("div.navbar-brand"); + toggle.simulate("click"); + expect(useFetchGetMock.fetch.calls).toHaveLength(1); + + act(() => { + alertStore.info.setTimestamp("new"); + }); + expect(useFetchGetMock.fetch.calls).toHaveLength(2); + }); + it("hides the modal when toggle() is called twice", () => { const tree = MountedOverviewModal(); const toggle = tree.find("div.navbar-brand"); diff --git a/ui/src/Models/APITypes.ts b/ui/src/Models/APITypes.ts index ae852252b..6577ea4b3 100644 --- a/ui/src/Models/APITypes.ts +++ b/ui/src/Models/APITypes.ts @@ -206,7 +206,6 @@ export interface APIAlertsResponseT { totalAlerts: number; colors: APIAlertsResponseColorsT; filters: APIFilterT[]; - counters: APILabelCounterT[]; settings: APISettingsT; authentication: APIAlertsResponseAuthenticationT; receivers: string[]; @@ -225,3 +224,8 @@ export interface HistoryResponseT { export interface AlertListResponseT { alerts: LabelsT[]; } + +export interface CountersResponseT { + total: number; + counters: APILabelCounterT[]; +} diff --git a/ui/src/Stores/AlertStore.ts b/ui/src/Stores/AlertStore.ts index 4f59799e6..b1ef4bece 100644 --- a/ui/src/Stores/AlertStore.ts +++ b/ui/src/Stores/AlertStore.ts @@ -12,7 +12,6 @@ import type { APILabelColorT, APIAlertsResponseT, APIAlertsResponseColorsT, - APILabelCounterT, APIGridT, APIAlertsResponseSilenceMapT, APIAlertsResponseUpstreamsT, @@ -137,7 +136,6 @@ interface AlertStoreFiltersT { interface AlertStoreDataT { colors: APIAlertsResponseColorsT; - counters: APILabelCounterT[]; grids: APIGridT[]; labelNames: string[]; setLabelNames: (v: string[]) => void; @@ -157,7 +155,6 @@ interface AlertStoreDataT { setUpstreams: (u: APIAlertsResponseUpstreamsT) => void; setClusters: (c: APIAlertsResponseUpstreamsClusterMapT) => void; setSilences: (s: APIAlertsResponseSilenceMapT) => void; - setCounters: (c: APILabelCounterT[]) => void; setReceivers: (r: string[]) => void; setColors: (c: APIAlertsResponseColorsT) => void; readonly upstreamsWithErrors: APIAlertmanagerUpstreamT[]; @@ -169,6 +166,7 @@ interface AlertStoreInfoT { username: string; }; totalAlerts: number; + timestamp: string; version: string; upgradeReady: boolean; upgradeNeeded: boolean; @@ -182,6 +180,7 @@ interface AlertStoreInfoT { setTotalAlerts: (n: number) => void; setAuthentication: (enabled: boolean, username: string) => void; setVersion: (v: string) => void; + setTimestamp: (v: string) => void; } interface AlertStoreSettingsT { @@ -291,7 +290,6 @@ class AlertStore { this.data = observable( { colors: {} as APIAlertsResponseColorsT, - counters: [] as APILabelCounterT[], grids: [] as APIGridT[], labelNames: [] as string[], setLabelNames(v: string[]) { @@ -373,9 +371,6 @@ class AlertStore { setSilences(s: APIAlertsResponseSilenceMapT) { this.silences = s; }, - setCounters(c: APILabelCounterT[]) { - this.counters = c; - }, setReceivers(r: string[]) { this.receivers = r; }, @@ -397,7 +392,6 @@ class AlertStore { setUpstreams: action.bound, setClusters: action.bound, setSilences: action.bound, - setCounters: action.bound, setReceivers: action.bound, setColors: action.bound, setLabelNames: action.bound, @@ -413,6 +407,7 @@ class AlertStore { }, totalAlerts: 0, version: "unknown", + timestamp: "", upgradeReady: false as boolean, upgradeNeeded: false as boolean, isRetrying: false as boolean, @@ -442,6 +437,9 @@ class AlertStore { setVersion(v: string) { this.version = v; }, + setTimestamp(v: string) { + this.timestamp = v; + }, }, { setIsRetrying: action.bound, @@ -451,6 +449,7 @@ class AlertStore { setTotalAlerts: action.bound, setAuthentication: action.bound, setVersion: action.bound, + setTimestamp: action.bound, }, { name: "API response info" } ); @@ -668,7 +667,6 @@ class AlertStore { const updates: Partial = {}; updates.colors = result.colors; - updates.counters = result.counters; updates.grids = result.grids; updates.labelNames = result.labelNames; updates.silences = result.silences; @@ -687,6 +685,7 @@ class AlertStore { // update extra root level keys that are stored under 'info' this.info.totalAlerts = result.totalAlerts; this.info.version = result.version; + this.info.timestamp = result.timestamp; this.info.authentication = result.authentication; // settings exported via API diff --git a/ui/src/__fixtures__/Fetch.ts b/ui/src/__fixtures__/Fetch.ts index 50b1acc32..f3ab5c58e 100644 --- a/ui/src/__fixtures__/Fetch.ts +++ b/ui/src/__fixtures__/Fetch.ts @@ -1,9 +1,5 @@ import { MockAlert, MockAlertGroup, MockSilence } from "__fixtures__/Alerts"; -import type { - APIAlertsResponseT, - APILabelCounterT, - APIManagedSilenceT, -} from "Models/APITypes"; +import type { APIAlertsResponseT, APIManagedSilenceT } from "Models/APITypes"; const EmptyAPIResponse = (): APIAlertsResponseT => ({ status: "success", @@ -44,7 +40,6 @@ const EmptyAPIResponse = (): APIAlertsResponseT => ({ isValid: true, }, ], - counters: [] as APILabelCounterT[], settings: { sorting: { grid: { diff --git a/ui/src/__fixtures__/Stories.ts b/ui/src/__fixtures__/Stories.ts index 0323da30f..4b34db235 100644 --- a/ui/src/__fixtures__/Stories.ts +++ b/ui/src/__fixtures__/Stories.ts @@ -132,76 +132,6 @@ const MockGrid = (alertStore: AlertStore): void => { }, }); - alertStore.data.setCounters([ - { - name: "@receiver", - hits: 2, - values: [ - { - value: "by-cluster-service", - raw: "@receiver=by-cluster-service", - hits: 2, - percent: 100, - offset: 0, - }, - ], - }, - { - name: "alertname", - hits: 90, - values: [ - { - value: "Fake Alert", - raw: "alertname=Fake Alert", - hits: 45, - percent: 50, - offset: 0, - }, - { - value: "Second Fake Alert", - raw: "alertname=Second Fake Alert", - hits: 45, - percent: 50, - offset: 50, - }, - ], - }, - { - name: "group", - hits: 100, - values: [ - { - value: "group1", - raw: "group=group1", - hits: 25, - percent: 25, - offset: 0, - }, - { - value: "group2", - raw: "group=group2", - hits: 70, - percent: 70, - offset: 25, - }, - { - value: "group3", - raw: "group=group3", - hits: 4, - percent: 4, - offset: 95, - }, - { - value: "group4", - raw: "group=group4", - hits: 1, - percent: 1, - offset: 99, - }, - ], - }, - ]); - const groups = []; for (let i = 1; i <= 10; i++) { const active = Math.max(1, Math.ceil(i / 3)); diff --git a/ui/src/__fixtures__/useFetchGet.ts b/ui/src/__fixtures__/useFetchGet.ts index e77446e99..b77db0c36 100644 --- a/ui/src/__fixtures__/useFetchGet.ts +++ b/ui/src/__fixtures__/useFetchGet.ts @@ -6,6 +6,7 @@ import type { APIAlertsResponseT, APIManagedSilenceT, AlertListResponseT, + CountersResponseT, } from "Models/APITypes"; type responseT = @@ -13,7 +14,8 @@ type responseT = | string[] | APIAlertsResponseT | APIManagedSilenceT[] - | AlertListResponseT; + | AlertListResponseT + | CountersResponseT; interface mockedDataT { response: undefined | responseT; @@ -126,6 +128,82 @@ const useFetchGetMock = ( re: /^.\/autocomplete\.json\?term=/, response: ["foo=bar", "foo=~bar"], }, + // counters + { + uri: "./counters.json", + response: { + total: 90, + counters: [ + { + name: "@receiver", + hits: 2, + values: [ + { + value: "by-cluster-service", + raw: "@receiver=by-cluster-service", + hits: 2, + percent: 100, + offset: 0, + }, + ], + }, + { + name: "alertname", + hits: 90, + values: [ + { + value: "Fake Alert", + raw: "alertname=Fake Alert", + hits: 45, + percent: 50, + offset: 0, + }, + { + value: "Second Fake Alert", + raw: "alertname=Second Fake Alert", + hits: 45, + percent: 50, + offset: 50, + }, + ], + }, + { + name: "group", + hits: 100, + values: [ + { + value: "group1", + raw: "group=group1", + hits: 25, + percent: 25, + offset: 0, + }, + { + value: "group2", + raw: "group=group2", + hits: 70, + percent: 70, + offset: 25, + }, + { + value: "group3", + raw: "group=group3", + hits: 4, + percent: 4, + offset: 95, + }, + { + value: "group4", + raw: "group=group4", + hits: 1, + percent: 1, + offset: 99, + }, + ], + }, + ], + }, + }, ]; for (const m of mockResponses) {