From f0fee594d955cc808c1754a1badae42e92d87a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Sat, 6 Jul 2019 10:57:05 +0100 Subject: [PATCH 1/5] feat(api): add labels stats to the api response These will be used by an overview modal, showing top labels. --- alerts.go | 42 ++++++++ api_test.go | 192 ++++++++++++++++++++++++++++++++++++ internal/models/api.go | 47 +++++++++ internal/models/api_test.go | 178 +++++++++++++++++++++++++++++++++ ui/src/Stores/AlertStore.js | 3 +- ui/src/__mocks__/Fetch.js | 1 + views.go | 6 ++ views_test.go | 3 + 8 files changed, 471 insertions(+), 1 deletion(-) diff --git a/alerts.go b/alerts.go index 5d06bd338..0b50f20be 100644 --- a/alerts.go +++ b/alerts.go @@ -1,6 +1,9 @@ package main import ( + "math" + "sort" + "github.com/prymitive/karma/internal/alertmanager" "github.com/prymitive/karma/internal/filters" "github.com/prymitive/karma/internal/models" @@ -22,6 +25,45 @@ func getFiltersFromQuery(filterStrings []string) ([]filters.FilterT, bool) { return matchFilters, validFilters } +func countLabel(countStore map[string]map[string]int, key string, val string) { + if _, found := countStore[key]; !found { + countStore[key] = make(map[string]int) + } + if _, found := countStore[key][val]; found { + countStore[key][val]++ + } else { + countStore[key][val] = 1 + } +} + +func countersToLabelStats(counters map[string]map[string]int) models.LabelNameStatsList { + data := models.LabelNameStatsList{} + + for name, valueMap := range counters { + nameStats := models.LabelNameStats{ + Name: name, + Values: models.LabelValueStatsList{}, + } + for value, hits := range valueMap { + nameStats.Hits += hits + valueStats := models.LabelValueStats{ + Value: value, + Hits: hits, + } + nameStats.Values = append(nameStats.Values, valueStats) + } + for i, value := range nameStats.Values { + nameStats.Values[i].Percent = int(math.Round((float64(value.Hits) / float64(nameStats.Hits)) * 100.0)) + } + sort.Sort(nameStats.Values) + data = append(data, nameStats) + } + + sort.Sort(data) + + return data +} + func getUpstreams() models.AlertmanagerAPISummary { summary := models.AlertmanagerAPISummary{} diff --git a/api_test.go b/api_test.go index a94bc076c..fc94dd7ad 100644 --- a/api_test.go +++ b/api_test.go @@ -647,6 +647,160 @@ 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: "instance", + Hits: 24, + Values: models.LabelValueStatsList{ + models.LabelValueStats{ + Value: "server1", + Hits: 2, + Percent: 8, + }, + models.LabelValueStats{ + Value: "server2", + Hits: 4, + Percent: 17, + }, + models.LabelValueStats{ + Value: "server3", + Hits: 2, + Percent: 8, + }, + 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: "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 { @@ -836,6 +990,44 @@ func TestVerifyAllGroups(t *testing.T) { t.Errorf("[%s] Silences mismatch, expected >0 but got %d", version, len(am)) } + 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/internal/models/api.go b/internal/models/api.go index 41999007d..9284d3349 100644 --- a/internal/models/api.go +++ b/internal/models/api.go @@ -35,6 +35,52 @@ type LabelColors struct { // LabelsColorMap is a map of "Label Key" -> "Label Value" -> karmaLabelColors type LabelsColorMap map[string]map[string]LabelColors +// LabelsCountMap is a map of "Label Key" -> "Label Value" -> number of occurence +type LabelsCountMap map[string]map[string]int + +type LabelValueStats struct { + Value string `json:"value"` + Hits int `json:"hits"` + Percent int `json:"percent"` +} + +type LabelValueStatsList []LabelValueStats + +func (lvsl LabelValueStatsList) Len() int { + return len(lvsl) +} +func (lvsl LabelValueStatsList) Swap(i, j int) { + lvsl[i], lvsl[j] = lvsl[j], lvsl[i] +} +func (lvsl LabelValueStatsList) Less(i, j int) bool { + if lvsl[i].Hits == lvsl[j].Hits { + return lvsl[i].Value > lvsl[j].Value + } + return lvsl[i].Hits > lvsl[j].Hits +} + +// LabelStats is used in the overview modal, it shows top labels across alerts +type LabelNameStats struct { + Name string `json:"name"` + Values LabelValueStatsList `json:"values"` + Hits int `json:"hits"` +} + +type LabelNameStatsList []LabelNameStats + +func (lnsl LabelNameStatsList) Len() int { + return len(lnsl) +} +func (lnsl LabelNameStatsList) Swap(i, j int) { + lnsl[i], lnsl[j] = lnsl[j], lnsl[i] +} +func (lnsl LabelNameStatsList) Less(i, j int) bool { + if lnsl[i].Hits == lnsl[j].Hits { + return lnsl[i].Name > lnsl[j].Name + } + return lnsl[i].Hits > lnsl[j].Hits +} + // APIAlertGroupSharedMaps defines shared part of APIAlertGroup type APIAlertGroupSharedMaps struct { Annotations Annotations `json:"annotations"` @@ -248,6 +294,7 @@ type AlertsResponse struct { TotalAlerts int `json:"totalAlerts"` Colors LabelsColorMap `json:"colors"` Filters []Filter `json:"filters"` + Counters LabelNameStatsList `json:"counters"` Settings Settings `json:"settings"` } diff --git a/internal/models/api_test.go b/internal/models/api_test.go index 8e1797798..557ab9c9a 100644 --- a/internal/models/api_test.go +++ b/internal/models/api_test.go @@ -2,6 +2,7 @@ package models_test import ( "encoding/json" + "sort" "testing" "github.com/pmezard/go-difflib/difflib" @@ -255,3 +256,180 @@ func TestDedupSharedMapsWithSingleAlert(t *testing.T) { } ag.DedupSharedMaps() } + +func TestNameStatsSort(t *testing.T) { + var nameStats = models.LabelNameStatsList{ + { + Name: "@state", + Hits: 24, + Values: models.LabelValueStatsList{ + models.LabelValueStats{ + Value: "suppressed", + Hits: 8, + Percent: 33, + }, + models.LabelValueStats{ + Value: "active", + Hits: 16, + Percent: 67, + }, + }, + }, + { + 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: "alertname", + Hits: 24, + Values: models.LabelValueStatsList{ + models.LabelValueStats{ + Value: "HTTP_Probe_Failed", + Hits: 4, + Percent: 17, + }, + models.LabelValueStats{ + Value: "Host_Down", + Hits: 16, + Percent: 67, + }, + models.LabelValueStats{ + Value: "Free_Disk_Space_Too_Low", + Hits: 2, + Percent: 8, + }, + models.LabelValueStats{ + Value: "Memory_Usage_Too_High", + Hits: 2, + Percent: 8, + }, + }, + }, + { + Name: "instance", + Hits: 24, + Values: models.LabelValueStatsList{ + 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: "server1", + Hits: 2, + Percent: 8, + }, + models.LabelValueStats{ + Value: "server2", + Hits: 4, + Percent: 17, + }, + models.LabelValueStats{ + Value: "server3", + 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: "@receiver", + Hits: 24, + Values: models.LabelValueStatsList{ + models.LabelValueStats{ + Value: "by-name", + Hits: 12, + Percent: 50, + }, + models.LabelValueStats{ + Value: "by-cluster-service", + Hits: 12, + Percent: 50, + }, + }, + }, + { + Name: "job", + Hits: 16, + Values: models.LabelValueStatsList{ + models.LabelValueStats{ + Value: "node_exporter", + Hits: 8, + Percent: 50, + }, + models.LabelValueStats{ + Value: "node_ping", + Hits: 8, + Percent: 50, + }, + }, + }, + } + + b, err := json.Marshal(nameStats) + if err != nil { + t.Error(err) + } + before := string(b) + + for _, n := range nameStats { + sort.Sort(n.Values) + } + sort.Sort(nameStats) + + a, err := json.Marshal(nameStats) + if err != nil { + t.Error(err) + } + after := string(a) + + if after == before { + t.Errorf("Sorting LabelNameStatsList produces the same output as unsorted instance") + } +} diff --git a/ui/src/Stores/AlertStore.js b/ui/src/Stores/AlertStore.js index 25059aa8c..99a5d38ba 100644 --- a/ui/src/Stores/AlertStore.js +++ b/ui/src/Stores/AlertStore.js @@ -137,6 +137,7 @@ class AlertStore { data = observable( { colors: {}, + counters: {}, groups: {}, silences: {}, upstreams: { instances: [], clusters: {} }, @@ -302,7 +303,7 @@ class AlertStore { let updates = {}; // update data dicts if they changed - for (const key of ["colors", "silences", "upstreams"]) { + for (const key of ["colors", "counters", "silences", "upstreams"]) { if (!equal(this.data[key], result[key])) { updates[key] = result[key]; } diff --git a/ui/src/__mocks__/Fetch.js b/ui/src/__mocks__/Fetch.js index f074457a9..9763a8c0b 100644 --- a/ui/src/__mocks__/Fetch.js +++ b/ui/src/__mocks__/Fetch.js @@ -22,6 +22,7 @@ const EmptyAPIResponse = () => ({ isValid: true } ], + counters: {}, settings: { sorting: { grid: { diff --git a/views.go b/views.go index 47c69625c..22ab2ba4e 100644 --- a/views.go +++ b/views.go @@ -122,6 +122,7 @@ func alerts(c *gin.Context) { // set pointers for data store objects, need a lock until end of view is reached alerts := map[string]models.APIAlertGroup{} colors := models.LabelsColorMap{} + counters := map[string]map[string]int{} dedupedAlerts := alertmanager.DedupAlerts() dedupedColors := alertmanager.DedupColors() @@ -170,6 +171,9 @@ func alerts(c *gin.Context) { alert.UpdateFingerprints() agCopy.Alerts = append(agCopy.Alerts, alert) + 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 { @@ -209,6 +213,7 @@ func alerts(c *gin.Context) { colors[key][value] = color } } + countLabel(counters, key, value) } } } @@ -248,6 +253,7 @@ func alerts(c *gin.Context) { resp.AlertGroups = alerts resp.Silences = silences resp.Colors = colors + resp.Counters = countersToLabelStats(counters) resp.Filters = populateAPIFilters(matchFilters) data, err := json.Marshal(resp) diff --git a/views_test.go b/views_test.go index f51bbf7b2..26a115fc5 100644 --- a/views_test.go +++ b/views_test.go @@ -136,6 +136,9 @@ 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.AlertGroups { for _, a := range ag.Alerts { linkCount := 0 From d9c04616bfd55b9d4a31cd94cdafcf9b26e86c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Sat, 29 Jun 2019 16:00:28 +0100 Subject: [PATCH 2/5] feat(ui): add indicators on open modal buttons --- ui/src/Components/MainModal/index.js | 6 +++++- ui/src/Components/SilenceModal/index.js | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/ui/src/Components/MainModal/index.js b/ui/src/Components/MainModal/index.js index bba66f8c2..42e72068e 100644 --- a/ui/src/Components/MainModal/index.js +++ b/ui/src/Components/MainModal/index.js @@ -45,7 +45,11 @@ const MainModal = observer( return ( -
  • +
  • -
  • +
  • Date: Tue, 9 Jul 2019 22:08:02 +0100 Subject: [PATCH 3/5] feat(demo): generate alerts with long label names --- demo/generator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demo/generator.py b/demo/generator.py index 370e7ca6d..2628cb1dd 100755 --- a/demo/generator.py +++ b/demo/generator.py @@ -321,7 +321,8 @@ class LongNameAlerts(AlertGenerator): return [newAlert( self._labels(instance="server{}".format(i), cluster=cluster, severity="info", job="textfile_exporter", - region="CN"), + region="CN", + thisIsAVeryLongLabelNameToTestLabelTruncationInAllThePlacesWeRenderItLoremIpsumDolorSitAmet="1"), self._annotations( verylong="Lorem ipsum dolor sit amet, consectetur " "adipiscing elit, sed do eiusmod tempor incididunt" From 70e69fda093478b69f3b74e87e9beff4ff3f0a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Tue, 9 Jul 2019 22:08:31 +0100 Subject: [PATCH 4/5] feat(ui): add a modal with labels overview Fixes #766 --- demo/karma.yaml | 2 +- .../__snapshots__/index.test.js.snap | 34 ++++ .../Labels/LabelWithPercent/index.js | 69 ++++++++ .../Labels/LabelWithPercent/index.scss | 4 + .../Labels/LabelWithPercent/index.test.js | 63 +++++++ ui/src/Components/NavBar/index.js | 11 +- ui/src/Components/NavBar/index.test.js | 2 +- .../OverviewModal/OverviewModalContent.js | 81 +++++++++ .../OverviewModalContent.test.js | 66 ++++++++ .../OverviewModalContent.test.js.snap | 154 ++++++++++++++++++ ui/src/Components/OverviewModal/index.js | 84 ++++++++++ ui/src/Components/OverviewModal/index.scss | 10 ++ ui/src/Components/OverviewModal/index.test.js | 96 +++++++++++ ui/src/Stores/AlertStore.js | 2 +- 14 files changed, 667 insertions(+), 11 deletions(-) create mode 100644 ui/src/Components/Labels/LabelWithPercent/__snapshots__/index.test.js.snap create mode 100644 ui/src/Components/Labels/LabelWithPercent/index.js create mode 100644 ui/src/Components/Labels/LabelWithPercent/index.scss create mode 100644 ui/src/Components/Labels/LabelWithPercent/index.test.js create mode 100644 ui/src/Components/OverviewModal/OverviewModalContent.js create mode 100644 ui/src/Components/OverviewModal/OverviewModalContent.test.js create mode 100644 ui/src/Components/OverviewModal/__snapshots__/OverviewModalContent.test.js.snap create mode 100644 ui/src/Components/OverviewModal/index.js create mode 100644 ui/src/Components/OverviewModal/index.scss create mode 100644 ui/src/Components/OverviewModal/index.test.js diff --git a/demo/karma.yaml b/demo/karma.yaml index 1f69348a7..2edca4575 100644 --- a/demo/karma.yaml +++ b/demo/karma.yaml @@ -53,7 +53,7 @@ labels: color: "#ff220c" log: config: false - level: warning + level: debug sentry: private: https://84a9ef37a6ed4fdb80e9ea2310d1ed26:8c6ee6f0ab02406482ff4b4e824e2c27@sentry.io/1279017 public: https://84a9ef37a6ed4fdb80e9ea2310d1ed26@sentry.io/1279017 diff --git a/ui/src/Components/Labels/LabelWithPercent/__snapshots__/index.test.js.snap b/ui/src/Components/Labels/LabelWithPercent/__snapshots__/index.test.js.snap new file mode 100644 index 000000000..d87242fe6 --- /dev/null +++ b/ui/src/Components/Labels/LabelWithPercent/__snapshots__/index.test.js.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` matches snapshot 1`] = ` +" +
    + + + 25 + + + foo: + + + bar + + +
    +
    +
    +
    +
    +" +`; diff --git a/ui/src/Components/Labels/LabelWithPercent/index.js b/ui/src/Components/Labels/LabelWithPercent/index.js new file mode 100644 index 000000000..c4f135e95 --- /dev/null +++ b/ui/src/Components/Labels/LabelWithPercent/index.js @@ -0,0 +1,69 @@ +import React from "react"; +import PropTypes from "prop-types"; + +import { inject, observer } from "mobx-react"; + +import { AlertStore } from "Stores/AlertStore"; +import { TooltipWrapper } from "Components/TooltipWrapper"; +import { BaseLabel } from "Components/Labels/BaseLabel"; + +import "./index.scss"; + +const LabelWithPercent = inject("alertStore")( + observer( + class FilteringLabel extends BaseLabel { + static propTypes = { + alertStore: PropTypes.instanceOf(AlertStore).isRequired, + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + hits: PropTypes.number.isRequired, + percent: PropTypes.number.isRequired + }; + + render() { + const { name, value, hits, percent } = this.props; + + let cs = this.getClassAndStyle( + name, + value, + "components-label-with-hover mb-0 pl-0 text-left" + ); + + const progressBarBg = + percent > 66 + ? "bg-danger" + : percent > 66 + ? "bg-warning" + : "bg-success"; + + return ( + + this.handleClick(e)} + > + + {hits} + + {name}:{" "} + {value} + +
    +
    +
    + + ); + } + } + ) +); + +export { LabelWithPercent }; diff --git a/ui/src/Components/Labels/LabelWithPercent/index.scss b/ui/src/Components/Labels/LabelWithPercent/index.scss new file mode 100644 index 000000000..c6589aa4d --- /dev/null +++ b/ui/src/Components/Labels/LabelWithPercent/index.scss @@ -0,0 +1,4 @@ +.components-labelWithPercent-percent { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} diff --git a/ui/src/Components/Labels/LabelWithPercent/index.test.js b/ui/src/Components/Labels/LabelWithPercent/index.test.js new file mode 100644 index 000000000..8d2a8e18f --- /dev/null +++ b/ui/src/Components/Labels/LabelWithPercent/index.test.js @@ -0,0 +1,63 @@ +import React from "react"; + +import { mount } from "enzyme"; + +import toDiffableHtml from "diffable-html"; + +import { AlertStore, NewUnappliedFilter } from "Stores/AlertStore"; + +import { LabelWithPercent } from "."; + +let alertStore; + +beforeEach(() => { + alertStore = new AlertStore([]); +}); + +const MountedLabelWithPercent = (name, value) => { + return mount( + + ).find(".components-label"); +}; + +const RenderAndClick = (name, value, clickOptions) => { + const tree = MountedLabelWithPercent(name, value); + tree.find(".components-label").simulate("click", clickOptions || {}); +}; + +describe("", () => { + it("matches snapshot", () => { + const tree = mount( + + ); + expect(toDiffableHtml(tree.html())).toMatchSnapshot(); + }); + + it("calling onClick() adds a new filter 'foo=bar'", () => { + RenderAndClick("foo", "bar"); + expect(alertStore.filters.values).toHaveLength(1); + expect(alertStore.filters.values).toContainEqual( + NewUnappliedFilter("foo=bar") + ); + }); + + it("calling onClick() while holding Alt key adds a new filter 'foo!=bar'", () => { + RenderAndClick("foo", "bar", { altKey: true }); + expect(alertStore.filters.values).toHaveLength(1); + expect(alertStore.filters.values).toContainEqual( + NewUnappliedFilter("foo!=bar") + ); + }); +}); diff --git a/ui/src/Components/NavBar/index.js b/ui/src/Components/NavBar/index.js index 5d7a05a20..d6d50daf8 100644 --- a/ui/src/Components/NavBar/index.js +++ b/ui/src/Components/NavBar/index.js @@ -8,13 +8,12 @@ import ReactResizeDetector from "react-resize-detector"; import IdleTimer from "react-idle-timer"; -import Flash from "react-reveal/Flash"; - import { AlertStore } from "Stores/AlertStore"; import { Settings } from "Stores/Settings"; import { SilenceFormStore } from "Stores/SilenceFormStore"; import { IsMobile } from "Common/Device"; import { NavBarSlide } from "Components/Animations/NavBarSlide"; +import { OverviewModal } from "Components/OverviewModal"; import { MainModal } from "Components/MainModal"; import { SilenceModal } from "Components/SilenceModal"; import { FetchIndicator } from "./FetchIndicator"; @@ -141,12 +140,8 @@ const NavBar = observer( >