diff --git a/alerts.go b/alerts.go index 0b50f20be..a1020f05e 100644 --- a/alerts.go +++ b/alerts.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "math" "sort" @@ -44,18 +45,30 @@ func countersToLabelStats(counters map[string]map[string]int) models.LabelNameSt Name: name, Values: models.LabelValueStatsList{}, } + for value, hits := range valueMap { nameStats.Hits += hits valueStats := models.LabelValueStats{ Value: value, + Raw: fmt.Sprintf("%s=%s", name, value), Hits: hits, } nameStats.Values = append(nameStats.Values, valueStats) } + + // now that we have total hits we can calculate % 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) + + // now that we have all % and values are sorted we can calculate offsets + offset := 0 + for i, value := range nameStats.Values { + nameStats.Values[i].Offset = offset + offset += value.Percent + } data = append(data, nameStats) } diff --git a/internal/models/api.go b/internal/models/api.go index 57a835fab..eebaffd48 100644 --- a/internal/models/api.go +++ b/internal/models/api.go @@ -40,8 +40,10 @@ type LabelsCountMap map[string]map[string]int type LabelValueStats struct { Value string `json:"value"` + Raw string `json:"raw"` Hits int `json:"hits"` Percent int `json:"percent"` + Offset int `json:"offset"` } type LabelValueStatsList []LabelValueStats diff --git a/internal/models/api_test.go b/internal/models/api_test.go index 557ab9c9a..0474a4700 100644 --- a/internal/models/api_test.go +++ b/internal/models/api_test.go @@ -265,13 +265,17 @@ func TestNameStatsSort(t *testing.T) { Values: models.LabelValueStatsList{ models.LabelValueStats{ Value: "suppressed", + Raw: "@state=suppressed", Hits: 8, Percent: 33, + Offset: 67, }, models.LabelValueStats{ Value: "active", + Raw: "@state=actuve", Hits: 16, Percent: 67, + Offset: 0, }, }, }, @@ -281,18 +285,24 @@ func TestNameStatsSort(t *testing.T) { Values: models.LabelValueStatsList{ models.LabelValueStats{ Value: "dev", + Raw: "cluster=dev", Hits: 10, Percent: 42, + Offset: 0, }, models.LabelValueStats{ Value: "prod", + Raw: "cluster=prod", Hits: 6, Percent: 25, + Offset: 42, }, models.LabelValueStats{ Value: "staging", + Raw: "cluster=staging", Hits: 8, Percent: 33, + Offset: 67, }, }, }, @@ -302,23 +312,32 @@ func TestNameStatsSort(t *testing.T) { Values: models.LabelValueStatsList{ models.LabelValueStats{ Value: "HTTP_Probe_Failed", + Raw: "alertname=HTTP_Probe_Failed", Hits: 4, Percent: 17, + Offset: 0, }, models.LabelValueStats{ Value: "Host_Down", + Raw: "alertname=Host_Down", Hits: 16, Percent: 67, + Offset: 17, }, models.LabelValueStats{ - Value: "Free_Disk_Space_Too_Low", + Value: "Free_Disk_Space_Too_Low", + Raw: "alertname=Free_Disk_Space_Too_Low", + Hits: 2, Percent: 8, + Offset: 84, }, models.LabelValueStats{ Value: "Memory_Usage_Too_High", + Raw: "alertname=Memory_Usage_Too_High", Hits: 2, Percent: 8, + Offset: 92, }, }, }, @@ -328,53 +347,72 @@ func TestNameStatsSort(t *testing.T) { Values: models.LabelValueStatsList{ models.LabelValueStats{ Value: "server4", + Raw: "instance=server4", Hits: 2, Percent: 8, }, models.LabelValueStats{ Value: "server5", + Raw: "instance=server5", Hits: 4, Percent: 17, + Offset: 17, }, models.LabelValueStats{ Value: "server6", + Raw: "instance=server6", Hits: 2, Percent: 8, + Offset: 17, }, models.LabelValueStats{ Value: "server1", + Raw: "instance=server1", Hits: 2, Percent: 8, + Offset: 17, }, models.LabelValueStats{ Value: "server2", + Raw: "instance=server2", Hits: 4, Percent: 17, + Offset: 17, }, models.LabelValueStats{ Value: "server3", + Raw: "instance=server3", Hits: 2, Percent: 8, + Offset: 17, }, models.LabelValueStats{ Value: "server7", + Raw: "instance=server7", Hits: 2, Percent: 8, + Offset: 17, }, models.LabelValueStats{ Value: "server8", + Raw: "instance=server8", Hits: 2, Percent: 8, + Offset: 17, }, models.LabelValueStats{ Value: "web1", + Raw: "instance=web1", Hits: 2, Percent: 8, + Offset: 17, }, models.LabelValueStats{ Value: "web2", + Raw: "instance=web2", Hits: 2, Percent: 8, + Offset: 17, }, }, }, @@ -384,13 +422,17 @@ func TestNameStatsSort(t *testing.T) { Values: models.LabelValueStatsList{ models.LabelValueStats{ Value: "by-name", + Raw: "@receiver=by-name", Hits: 12, Percent: 50, + Offset: 0, }, models.LabelValueStats{ Value: "by-cluster-service", + Raw: "@receiver=by-cluster-service", Hits: 12, Percent: 50, + Offset: 50, }, }, }, @@ -400,13 +442,17 @@ func TestNameStatsSort(t *testing.T) { Values: models.LabelValueStatsList{ models.LabelValueStats{ Value: "node_exporter", + Raw: "job=node_exporter", Hits: 8, Percent: 50, + Offset: 0, }, models.LabelValueStats{ Value: "node_ping", + Raw: "job=node_ping", Hits: 8, Percent: 50, + Offset: 50, }, }, }, diff --git a/ui/src/Components/Labels/LabelWithPercent/__snapshots__/index.test.js.snap b/ui/src/Components/Labels/LabelWithPercent/__snapshots__/index.test.js.snap index 68faeead3..a6b248c68 100644 --- a/ui/src/Components/Labels/LabelWithPercent/__snapshots__/index.test.js.snap +++ b/ui/src/Components/Labels/LabelWithPercent/__snapshots__/index.test.js.snap @@ -1,5 +1,55 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[` matches snapshot with isActive=true 1`] = ` +" +
+ + + 25 + + + + foo: + + + bar + + + + + + + +
+
+
+
+
+" +`; + exports[` matches snapshot with offset=0 1`] = ` "
matches snapshot with offset=0 1`] = ` 25 - - foo: - - - bar + + + foo: + + + bar +
@@ -45,11 +97,13 @@ exports[` matches snapshot with offset=25 1`] = ` 25 - - foo: - - - bar + + + foo: + + + bar +
diff --git a/ui/src/Components/Labels/LabelWithPercent/index.js b/ui/src/Components/Labels/LabelWithPercent/index.js index 4965e77ca..896f2b248 100644 --- a/ui/src/Components/Labels/LabelWithPercent/index.js +++ b/ui/src/Components/Labels/LabelWithPercent/index.js @@ -3,7 +3,11 @@ import PropTypes from "prop-types"; import { inject, observer } from "mobx-react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faTimes } from "@fortawesome/free-solid-svg-icons/faTimes"; + import { AlertStore } from "Stores/AlertStore"; +import { QueryOperators, FormatQuery } from "Common/Query"; import { TooltipWrapper } from "Components/TooltipWrapper"; import { BaseLabel } from "Components/Labels/BaseLabel"; @@ -11,18 +15,26 @@ import "./index.scss"; const LabelWithPercent = inject("alertStore")( observer( - class FilteringLabel extends BaseLabel { + class LabelWithPercent 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, - offset: PropTypes.number.isRequired + offset: PropTypes.number.isRequired, + isActive: PropTypes.bool.isRequired + }; + + removeFromFilters = () => { + const { alertStore, name, value } = this.props; + alertStore.filters.removeFilter( + FormatQuery(name, QueryOperators.Equal, value) + ); }; render() { - const { name, value, hits, percent, offset } = this.props; + const { name, value, hits, percent, offset, isActive } = this.props; let cs = this.getClassAndStyle( name, @@ -39,16 +51,22 @@ const LabelWithPercent = inject("alertStore")( return ( - this.handleClick(e)} - > + {hits} - {name}:{" "} - {value} + this.handleClick(e)}> + {name}:{" "} + {value} + + {isActive ? ( + + ) : null}
{offset === 0 ? null : ( diff --git a/ui/src/Components/Labels/LabelWithPercent/index.test.js b/ui/src/Components/Labels/LabelWithPercent/index.test.js index e5e871c3d..6f2390fc6 100644 --- a/ui/src/Components/Labels/LabelWithPercent/index.test.js +++ b/ui/src/Components/Labels/LabelWithPercent/index.test.js @@ -14,7 +14,14 @@ beforeEach(() => { alertStore = new AlertStore([]); }); -const MountedLabelWithPercent = (name, value, hits, percent, offset) => { +const MountedLabelWithPercent = ( + name, + value, + hits, + percent, + offset, + isActive +) => { return mount( { hits={hits} percent={percent} offset={offset} + isActive={isActive} /> ); }; const RenderAndClick = (name, value, clickOptions) => { - const tree = MountedLabelWithPercent(name, value, 25, 50, 0); - tree.find(".components-label").simulate("click", clickOptions || {}); + const tree = MountedLabelWithPercent(name, value, 25, 50, 0, false); + tree + .find(".components-label") + .find("span") + .at(2) + .simulate("click", clickOptions || {}); }; describe("", () => { it("matches snapshot with offset=0", () => { - const tree = MountedLabelWithPercent("foo", "bar", 25, 50, 0); + const tree = MountedLabelWithPercent("foo", "bar", 25, 50, 0, false); expect(toDiffableHtml(tree.html())).toMatchSnapshot(); }); it("matches snapshot with offset=25", () => { - const tree = MountedLabelWithPercent("foo", "bar", 25, 50, 25); + const tree = MountedLabelWithPercent("foo", "bar", 25, 50, 25, false); expect(toDiffableHtml(tree.html())).toMatchSnapshot(); }); - it("calling onClick() adds a new filter 'foo=bar'", () => { + it("matches snapshot with isActive=true", () => { + const tree = MountedLabelWithPercent("foo", "bar", 25, 50, 0, true); + expect(toDiffableHtml(tree.html())).toMatchSnapshot(); + }); + + it("calling adds a new filter 'foo=bar'", () => { RenderAndClick("foo", "bar"); expect(alertStore.filters.values).toHaveLength(1); expect(alertStore.filters.values).toContainEqual( @@ -51,6 +68,18 @@ describe("", () => { ); }); + it("clicking the X buttom removes label from filters", () => { + const tree = MountedLabelWithPercent("foo", "bar", 25, 50, 0, true); + tree + .find(".components-label") + .find("svg") + .simulate("click"); + expect(alertStore.filters.values).toHaveLength(0); + expect(alertStore.filters.values).not.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); @@ -60,17 +89,17 @@ describe("", () => { }); it("uses bg-danger when percent is >66", () => { - const tree = MountedLabelWithPercent("foo", "bar", 25, 67, 0); + const tree = MountedLabelWithPercent("foo", "bar", 25, 67, 0, false); expect(tree.html()).toMatch(/progress-bar bg-danger/); }); it("uses bg-warning when percent is >33", () => { - const tree = MountedLabelWithPercent("foo", "bar", 25, 66, 0); + const tree = MountedLabelWithPercent("foo", "bar", 25, 66, 0, false); expect(tree.html()).toMatch(/progress-bar bg-warning/); }); it("uses bg-success when percent is <=33", () => { - const tree = MountedLabelWithPercent("foo", "bar", 25, 33, 0); + const tree = MountedLabelWithPercent("foo", "bar", 25, 33, 0, false); expect(tree.html()).toMatch(/progress-bar bg-success/); }); }); diff --git a/ui/src/Components/OverviewModal/OverviewModalContent.js b/ui/src/Components/OverviewModal/OverviewModalContent.js index cc18854db..dd6b2bc33 100644 --- a/ui/src/Components/OverviewModal/OverviewModalContent.js +++ b/ui/src/Components/OverviewModal/OverviewModalContent.js @@ -23,17 +23,19 @@ const LabelsTable = observer(({ alertStore }) => ( - {nameStats.values.slice(0, 9).map((valueStats, i, array) => ( + {nameStats.values.slice(0, 9).map((valueStats, i) => ( ns.percent) - .reduce((a, b) => a + b, 0)} + offset={valueStats.offset} + isActive={ + alertStore.filters.values.filter( + f => f.raw === valueStats.raw + ).length > 0 + } /> ))} diff --git a/ui/src/Components/OverviewModal/OverviewModalContent.test.js b/ui/src/Components/OverviewModal/OverviewModalContent.test.js index 3efa460b0..c337a6119 100644 --- a/ui/src/Components/OverviewModal/OverviewModalContent.test.js +++ b/ui/src/Components/OverviewModal/OverviewModalContent.test.js @@ -6,7 +6,7 @@ import { mount } from "enzyme"; import toDiffableHtml from "diffable-html"; -import { AlertStore } from "Stores/AlertStore"; +import { AlertStore, NewUnappliedFilter } from "Stores/AlertStore"; import { OverviewModalContent } from "./OverviewModalContent"; let alertStore; @@ -23,14 +23,31 @@ afterEach(() => { describe("", () => { it("matches snapshot with labels to show", () => { + alertStore.filters.values = [ + NewUnappliedFilter("abc=xyz"), + NewUnappliedFilter("foo=bar") + ]; alertStore.data.counters = [ { name: "foo", hits: 16, values: [ - { value: "bar1", hits: 8, percent: 50 }, - { value: "bar2", hits: 4, percent: 25 }, - { value: "bar3", hits: 4, percent: 25 } + { 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: "alertname", + hits: 5, + values: [ + { + value: "Host_Down", + raw: "alertname=Host_Down", + hits: 5, + percent: 100, + offset: 0 + } ] } ]; diff --git a/ui/src/Components/OverviewModal/__snapshots__/OverviewModalContent.test.js.snap b/ui/src/Components/OverviewModal/__snapshots__/OverviewModalContent.test.js.snap index 53950a672..9946996cb 100644 --- a/ui/src/Components/OverviewModal/__snapshots__/OverviewModalContent.test.js.snap +++ b/ui/src/Components/OverviewModal/__snapshots__/OverviewModalContent.test.js.snap @@ -44,11 +44,13 @@ exports[` matches snapshot with labels to show 1`] = ` 8 - - foo: - - - bar1 + + + foo: + + + bar1 +
@@ -72,11 +74,13 @@ exports[` matches snapshot with labels to show 1`] = ` 4 - - foo: - - - bar2 + + + foo: + + + bar2 +
@@ -108,11 +112,13 @@ exports[` matches snapshot with labels to show 1`] = ` 4 - - foo: - - - bar3 + + + foo: + + + bar3 +
@@ -136,6 +142,52 @@ exports[` matches snapshot with labels to show 1`] = `
+ + + + + 5 + + alertname + + + +
+ + + 5 + + + + alertname: + + + Host_Down + + + +
+
+
+
+
+ +