From 7adf1bbfd57277631f6dd88bfd88c0ff480389b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Mon, 8 Oct 2018 17:02:31 +0100 Subject: [PATCH 1/6] refactor(ui): move common modal code to a dedicated component --- .../Components/MainModal/MainModalContent.js | 85 ++---- .../MainModal/MainModalContent.test.js | 14 +- .../MainModalContent.test.js.snap | 236 +++++++-------- ui/src/Components/MainModal/index.js | 14 +- ui/src/Components/Modal/index.js | 66 ++++ ui/src/Components/Modal/index.test.js | 31 ++ .../NavBar/__snapshots__/index.test.js.snap | 283 ------------------ ui/src/Components/NavBar/index.test.js | 11 - .../SilenceModal/SilenceModalContent.js | 101 +++---- ui/src/Components/SilenceModal/index.js | 19 +- 10 files changed, 298 insertions(+), 562 deletions(-) create mode 100644 ui/src/Components/Modal/index.js create mode 100644 ui/src/Components/Modal/index.test.js delete mode 100644 ui/src/Components/NavBar/__snapshots__/index.test.js.snap diff --git a/ui/src/Components/MainModal/MainModalContent.js b/ui/src/Components/MainModal/MainModalContent.js index 67b0d88ab..ee10a468d 100644 --- a/ui/src/Components/MainModal/MainModalContent.js +++ b/ui/src/Components/MainModal/MainModalContent.js @@ -1,16 +1,12 @@ import React, { Component } from "react"; -import ReactDOM from "react-dom"; import PropTypes from "prop-types"; import { observer } from "mobx-react"; import { observable, action } from "mobx"; -import { disableBodyScroll, enableBodyScroll } from "body-scroll-lock"; - import { AlertStore } from "Stores/AlertStore"; import { Settings } from "Stores/Settings"; import { Configuration } from "./Configuration"; -import { MountModalBackdrop } from "Components/Animations/MountModal"; import { Help } from "./Help"; const Tab = ({ title, active, onClick }) => ( @@ -39,8 +35,7 @@ const MainModalContent = observer( static propTypes = { alertStore: PropTypes.instanceOf(AlertStore).isRequired, settingsStore: PropTypes.instanceOf(Settings).isRequired, - onHide: PropTypes.func.isRequired, - isVisible: PropTypes.bool.isRequired + onHide: PropTypes.func.isRequired }; tab = observable( @@ -53,58 +48,40 @@ const MainModalContent = observer( { setTab: action.bound } ); - componentDidMount() { - disableBodyScroll(document.querySelector(".modal")); - } - - componentWillUnmount() { - enableBodyScroll(document.querySelector(".modal")); - } - render() { - const { alertStore, settingsStore, onHide, isVisible } = this.props; + const { alertStore, settingsStore, onHide } = this.props; - return ReactDOM.createPortal( + return ( -
-
-
-
- -
-
- {this.tab.current === TabNames.Help ? : null} - {this.tab.current === TabNames.Configuration ? ( - - ) : null} -
-
- - Version: {alertStore.info.version} - -
-
-
+
+
- -
- - , - document.body +
+ {this.tab.current === TabNames.Help ? : null} + {this.tab.current === TabNames.Configuration ? ( + + ) : null} +
+
+ + Version: {alertStore.info.version} + +
+ ); } } diff --git a/ui/src/Components/MainModal/MainModalContent.test.js b/ui/src/Components/MainModal/MainModalContent.test.js index f9b033f4a..495f8a683 100644 --- a/ui/src/Components/MainModal/MainModalContent.test.js +++ b/ui/src/Components/MainModal/MainModalContent.test.js @@ -28,7 +28,6 @@ const FakeModal = () => { alertStore={alertStore} settingsStore={settingsStore} onHide={onHide} - isVisible={true} /> ); }; @@ -46,7 +45,18 @@ const ValidateSetTab = (title, callArg) => { describe("", () => { it("matches snapshot", () => { - const tree = FakeModal(); + // we have multiple fragments and enzyme only renders the first one + // in html() and text(), debug() would work but it's noisy + // https://github.com/airbnb/enzyme/issues/1213 + const tree = mount( + + + + ); expect(toDiffableHtml(tree.html())).toMatchSnapshot(); }); diff --git a/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap b/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap index eff9a7e04..2a6a1d42c 100644 --- a/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap +++ b/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap @@ -2,129 +2,121 @@ exports[` matches snapshot 1`] = ` " -
-
-
-
- -
-
-
-
- -
- - - 10s - - -
-
-
- - - - 30s - - -
-
-
-
- - - 120s - - -
-
-
-
-
- -
- - - 1 - - -
-
-
- - - - 5 - - -
-
-
-
- - - 10 - - -
-
-
-
-
- - Version: unknown + +
+
-
+ +
-
+
+
+
+ +
+ + + 10s + + +
+
+
+ + + + 30s + + +
+
+
+
+ + + 120s + + +
+
+
+
+
+ +
+ + + 1 + + +
+
+
+ + + + 5 + + +
+
+
+
+ + + 10 + + +
+
+
+
+
+ + Version: unknown + +
+ " `; diff --git a/ui/src/Components/MainModal/index.js b/ui/src/Components/MainModal/index.js index 5f8387743..66948d4ed 100644 --- a/ui/src/Components/MainModal/index.js +++ b/ui/src/Components/MainModal/index.js @@ -9,8 +9,8 @@ import { faCog } from "@fortawesome/free-solid-svg-icons/faCog"; import { AlertStore } from "Stores/AlertStore"; import { Settings } from "Stores/Settings"; -import { MountModal } from "Components/Animations/MountModal"; import { TooltipWrapper } from "Components/TooltipWrapper"; +import { Modal } from "Components/Modal"; import { MainModalContent } from "./MainModalContent"; const MainModal = observer( @@ -33,14 +33,6 @@ const MainModal = observer( { toggle: action.bound, hide: action.bound } ); - componentDidUpdate() { - document.body.classList.toggle("modal-open", this.toggle.show); - } - - componentWillUnmount() { - document.body.classList.remove("modal-open"); - } - render() { const { alertStore, settingsStore } = this.props; @@ -56,14 +48,14 @@ const MainModal = observer( - + - + ); } diff --git a/ui/src/Components/Modal/index.js b/ui/src/Components/Modal/index.js new file mode 100644 index 000000000..7fe8274c2 --- /dev/null +++ b/ui/src/Components/Modal/index.js @@ -0,0 +1,66 @@ +import React, { Component } from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; + +import { observer } from "mobx-react"; + +import { disableBodyScroll, enableBodyScroll } from "body-scroll-lock"; + +import { + MountModal, + MountModalBackdrop +} from "Components/Animations/MountModal"; + +const Modal = observer( + class Modal extends Component { + static propTypes = { + isOpen: PropTypes.bool.isRequired, + children: PropTypes.node.isRequired + }; + + toggleBodyClass = isOpen => { + document.body.classList.toggle("modal-open", isOpen); + if (isOpen) { + disableBodyScroll(document.querySelector(".modal")); + } else { + enableBodyScroll(document.querySelector(".modal")); + } + }; + + componentDidMount() { + const { isOpen } = this.props; + this.toggleBodyClass(isOpen); + } + + componentDidUpdate() { + const { isOpen } = this.props; + this.toggleBodyClass(isOpen); + } + + componentWillUnmount() { + this.toggleBodyClass(false); + } + + render() { + const { isOpen, children } = this.props; + + return ReactDOM.createPortal( + + +
+
+
{children}
+
+
+
+ +
+ + , + document.body + ); + } + } +); + +export { Modal }; diff --git a/ui/src/Components/Modal/index.test.js b/ui/src/Components/Modal/index.test.js new file mode 100644 index 000000000..758373ee0 --- /dev/null +++ b/ui/src/Components/Modal/index.test.js @@ -0,0 +1,31 @@ +import React from "react"; + +import { mount } from "enzyme"; + +import { Modal } from "."; + +const MountedModal = isOpen => { + return mount( + +
+ + ); +}; + +describe("", () => { + it("'modal-open' class is appended to body node when modal is visible", () => { + MountedModal(true); + expect(document.body.className.split(" ")).toContain("modal-open"); + }); + + it("'modal-open' class is removed from body node after modal is hidden", () => { + MountedModal(false); + expect(document.body.className.split(" ")).not.toContain("modal-open"); + }); + + it("'modal-open' class is removed from body node after modal is unmounted", () => { + const tree = MountedModal(true); + tree.unmount(); + expect(document.body.className.split(" ")).not.toContain("modal-open"); + }); +}); diff --git a/ui/src/Components/NavBar/__snapshots__/index.test.js.snap b/ui/src/Components/NavBar/__snapshots__/index.test.js.snap deleted file mode 100644 index 918b93382..000000000 --- a/ui/src/Components/NavBar/__snapshots__/index.test.js.snap +++ /dev/null @@ -1,283 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` matches snapshot with 0 alerts 1`] = ` -" -
- -
-" -`; - -exports[` matches snapshot with 5 alerts 1`] = ` -" -
- -
-" -`; diff --git a/ui/src/Components/NavBar/index.test.js b/ui/src/Components/NavBar/index.test.js index c416621cf..1beb2bd81 100644 --- a/ui/src/Components/NavBar/index.test.js +++ b/ui/src/Components/NavBar/index.test.js @@ -54,17 +54,6 @@ const ValidateNavClass = (totalFilters, expectedClass) => { }; describe("", () => { - it("matches snapshot with 0 alerts", () => { - const tree = RenderNavbar(); - expect(toDiffableHtml(tree.html())).toMatchSnapshot(); - }); - - it("matches snapshot with 5 alerts", () => { - alertStore.info.totalAlerts = 5; - const tree = RenderNavbar(); - expect(toDiffableHtml(tree.html())).toMatchSnapshot(); - }); - it("navbar-brand shows 15 alerts with totalAlerts=15", () => { alertStore.info.totalAlerts = 15; const tree = RenderNavbar(); diff --git a/ui/src/Components/SilenceModal/SilenceModalContent.js b/ui/src/Components/SilenceModal/SilenceModalContent.js index 33243f048..7703e2c2f 100644 --- a/ui/src/Components/SilenceModal/SilenceModalContent.js +++ b/ui/src/Components/SilenceModal/SilenceModalContent.js @@ -1,15 +1,11 @@ import React, { Component } from "react"; -import ReactDOM from "react-dom"; import PropTypes from "prop-types"; import { observer } from "mobx-react"; -import { disableBodyScroll, enableBodyScroll } from "body-scroll-lock"; - import { AlertStore } from "Stores/AlertStore"; import { SilenceFormStore, SilenceFormStage } from "Stores/SilenceFormStore"; import { Settings } from "Stores/Settings"; -import { MountModalBackdrop } from "Components/Animations/MountModal"; import { SilenceForm } from "./SilenceForm"; import { SilencePreview } from "./SilencePreview"; import { SilenceSubmitController } from "./SilenceSubmit/SilenceSubmitController"; @@ -23,14 +19,6 @@ const SilenceModalContent = observer( onHide: PropTypes.func.isRequired }; - componentDidMount() { - disableBodyScroll(document.querySelector(".modal")); - } - - componentWillUnmount() { - enableBodyScroll(document.querySelector(".modal")); - } - render() { const { alertStore, @@ -39,59 +27,46 @@ const SilenceModalContent = observer( onHide } = this.props; - return ReactDOM.createPortal( + return ( -
-
-
-
-
- {silenceFormStore.data.silenceID === null - ? silenceFormStore.data.currentStage === - SilenceFormStage.UserInput - ? "Add new silence" - : silenceFormStore.data.currentStage === - SilenceFormStage.Preview - ? "Preview silenced alerts" - : "Silence submitted" - : `Editing silence ${silenceFormStore.data.silenceID}`} -
- -
-
- {silenceFormStore.data.currentStage === - SilenceFormStage.UserInput ? ( - - ) : silenceFormStore.data.currentStage === - SilenceFormStage.Preview ? ( - - ) : ( - - )} -
-
-
+
+
+ {silenceFormStore.data.silenceID === null + ? silenceFormStore.data.currentStage === + SilenceFormStage.UserInput + ? "Add new silence" + : silenceFormStore.data.currentStage === + SilenceFormStage.Preview + ? "Preview silenced alerts" + : "Silence submitted" + : `Editing silence ${silenceFormStore.data.silenceID}`} +
+
- -
- - , - document.body +
+ {silenceFormStore.data.currentStage === + SilenceFormStage.UserInput ? ( + + ) : silenceFormStore.data.currentStage === + SilenceFormStage.Preview ? ( + + ) : ( + + )} +
+ ); } } diff --git a/ui/src/Components/SilenceModal/index.js b/ui/src/Components/SilenceModal/index.js index 9d58e8a2e..b70f13809 100644 --- a/ui/src/Components/SilenceModal/index.js +++ b/ui/src/Components/SilenceModal/index.js @@ -9,7 +9,7 @@ import { faBellSlash } from "@fortawesome/free-solid-svg-icons/faBellSlash"; import { AlertStore } from "Stores/AlertStore"; import { SilenceFormStore } from "Stores/SilenceFormStore"; import { Settings } from "Stores/Settings"; -import { MountModal } from "Components/Animations/MountModal"; +import { Modal } from "Components/Modal"; import { TooltipWrapper } from "Components/TooltipWrapper"; import { SilenceModalContent } from "./SilenceModalContent"; @@ -35,19 +35,6 @@ const SilenceModal = observer( } }; - componentDidUpdate() { - const { silenceFormStore } = this.props; - - document.body.classList.toggle( - "modal-open", - silenceFormStore.toggle.visible - ); - } - - componentWillUnmount() { - document.body.classList.remove("modal-open"); - } - render() { const { alertStore, silenceFormStore, settingsStore } = this.props; @@ -63,14 +50,14 @@ const SilenceModal = observer( - + - + ); } From f03887b56cd1c735bd4186616f4ef7df6b94d729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Mon, 8 Oct 2018 19:04:57 +0100 Subject: [PATCH 2/6] feat(api): add @silence_id filter This allows to filter alerts by silence ID, only matches silenced alerts --- internal/alertmanager/dedup_test.go | 4 +- internal/filters/filter_silence_author.go | 2 +- internal/filters/filter_silence_id.go | 63 +++++++++++++++++++ internal/filters/filter_silence_jira.go | 2 +- internal/filters/filter_test.go | 61 ++++++++++++++++++ internal/filters/registry.go | 11 +++- ui/src/Components/MainModal/Help.js | 20 +++++- .../MainModal/__snapshots__/Help.test.js.snap | 47 ++++++++++++++ views_test.go | 40 +++++++----- 9 files changed, 229 insertions(+), 21 deletions(-) create mode 100644 internal/filters/filter_silence_id.go diff --git a/internal/alertmanager/dedup_test.go b/internal/alertmanager/dedup_test.go index 2310fc0c9..3a7d8589a 100644 --- a/internal/alertmanager/dedup_test.go +++ b/internal/alertmanager/dedup_test.go @@ -85,10 +85,10 @@ func TestDedupAutocomplete(t *testing.T) { ac := alertmanager.DedupAutocomplete() // since we have alertmanager instance per mock adding new mocks will increase // the number of hints, so we need to calculate the expected value here - // there should be 56 hints excluding @alertmanager ones, use that as our base + // there should be 156 hints excluding @alertmanager ones, use that as our base // and add 2 hints per alertmanager instance (= and != hints) mockCount := len(mock.ListAllMockURIs()) - expected := 56 + mockCount*2 + expected := 156 + mockCount*2 if len(ac) != expected { t.Errorf("Expected %d autocomplete hints, got %d", expected, len(ac)) } diff --git a/internal/filters/filter_silence_author.go b/internal/filters/filter_silence_author.go index d33a65d3f..0ae7f6dd3 100644 --- a/internal/filters/filter_silence_author.go +++ b/internal/filters/filter_silence_author.go @@ -43,7 +43,7 @@ func newSilenceAuthorFilter() FilterT { return &f } -func sinceAuthorAutocomplete(name string, operators []string, alerts []models.Alert) []models.Autocomplete { +func silenceAuthorAutocomplete(name string, operators []string, alerts []models.Alert) []models.Autocomplete { tokens := map[string]models.Autocomplete{} for _, alert := range alerts { if alert.IsSilenced() { diff --git a/internal/filters/filter_silence_id.go b/internal/filters/filter_silence_id.go new file mode 100644 index 000000000..2c8b037b3 --- /dev/null +++ b/internal/filters/filter_silence_id.go @@ -0,0 +1,63 @@ +package filters + +import ( + "fmt" + "strings" + + "github.com/prymitive/karma/internal/models" +) + +type silenceIDFilter struct { + alertFilter +} + +func (filter *silenceIDFilter) Match(alert *models.Alert, matches int) bool { + if filter.IsValid { + var isMatch bool + if alert.IsSilenced() { + for _, silenceID := range alert.SilencedBy { + m := filter.Matcher.Compare(silenceID, filter.Value) + if m { + isMatch = m + } + } + } else { + isMatch = filter.Matcher.Compare("", filter.Value) + } + if isMatch { + filter.Hits++ + } + return isMatch + } + e := fmt.Sprintf("Match() called on invalid filter %#v", filter) + panic(e) +} + +func newsilenceIDFilter() FilterT { + f := silenceIDFilter{} + return &f +} + +func silenceIDAutocomplete(name string, operators []string, alerts []models.Alert) []models.Autocomplete { + tokens := map[string]models.Autocomplete{} + for _, alert := range alerts { + if alert.IsSilenced() { + for _, silenceID := range alert.SilencedBy { + for _, operator := range operators { + token := fmt.Sprintf("%s%s%s", name, operator, silenceID) + tokens[token] = makeAC(token, []string{ + name, + strings.TrimPrefix(name, "@"), + fmt.Sprintf("%s%s", name, operator), + silenceID, + }) + } + } + } + } + acData := []models.Autocomplete{} + for _, token := range tokens { + acData = append(acData, token) + } + return acData +} diff --git a/internal/filters/filter_silence_jira.go b/internal/filters/filter_silence_jira.go index b71f71a54..6c584b421 100644 --- a/internal/filters/filter_silence_jira.go +++ b/internal/filters/filter_silence_jira.go @@ -43,7 +43,7 @@ func newSilenceJiraFilter() FilterT { return &f } -func sinceJiraIDAutocomplete(name string, operators []string, alerts []models.Alert) []models.Autocomplete { +func silenceJiraIDAutocomplete(name string, operators []string, alerts []models.Alert) []models.Autocomplete { tokens := map[string]models.Autocomplete{} for _, alert := range alerts { if alert.IsSilenced() { diff --git a/internal/filters/filter_test.go b/internal/filters/filter_test.go index 559865c73..9a1f78eb6 100644 --- a/internal/filters/filter_test.go +++ b/internal/filters/filter_test.go @@ -84,6 +84,67 @@ var tests = []filterTest{ IsMatch: false, }, + filterTest{ + Expression: "@silence_id=abcdef", + IsValid: true, + Alert: models.Alert{State: "suppressed", SilencedBy: []string{"1"}}, + IsMatch: false, + }, + filterTest{ + Expression: "@silence_id=abcdef", + IsValid: true, + Alert: models.Alert{State: "active"}, + IsMatch: false, + }, + filterTest{ + Expression: "@silence_id=abcdef", + IsValid: true, + Alert: models.Alert{State: "active", SilencedBy: []string{"abcdef"}}, + IsMatch: false, + }, + filterTest{ + Expression: "@silence_id=abcdef", + IsValid: true, + Alert: models.Alert{State: "unprocessed"}, + IsMatch: false, + }, + filterTest{ + Expression: "@silence_id=abcdef", + IsValid: true, + Alert: models.Alert{State: "unprocessed", SilencedBy: []string{"abcdef"}}, + IsMatch: false, + }, + filterTest{ + Expression: "@silence_id=abcdef", + IsValid: true, + Alert: models.Alert{State: "suppressed", SilencedBy: []string{"abcdef"}}, + IsMatch: true, + }, + filterTest{ + Expression: "@silence_id!=abcdef", + IsValid: true, + Alert: models.Alert{State: "suppressed", SilencedBy: []string{"abcdef"}}, + IsMatch: false, + }, + filterTest{ + Expression: "@silence_id!=abcdef", + IsValid: true, + Alert: models.Alert{State: "suppressed", SilencedBy: []string{"1"}}, + IsMatch: true, + }, + filterTest{ + Expression: "@silence_id=~cde", + IsValid: false, + Alert: models.Alert{State: "suppressed", SilencedBy: []string{"abcdef"}}, + IsMatch: false, + }, + filterTest{ + Expression: "@silence_id!~abc", + IsValid: false, + Alert: models.Alert{State: "suppressed", SilencedBy: []string{"zwd"}}, + IsMatch: false, + }, + filterTest{ Expression: "@silence_jira=1", IsValid: true, diff --git a/internal/filters/registry.go b/internal/filters/registry.go index a30fca9a9..94a1c7653 100644 --- a/internal/filters/registry.go +++ b/internal/filters/registry.go @@ -68,19 +68,26 @@ var AllFilters = []filterConfig{ Factory: newAgeFilter, Autocomplete: ageAutocomplete, }, + filterConfig{ + Label: "@silence_id", + LabelRe: regexp.MustCompile("^@silence_id$"), + SupportedOperators: []string{equalOperator, notEqualOperator}, + Factory: newsilenceIDFilter, + Autocomplete: silenceIDAutocomplete, + }, filterConfig{ Label: "@silence_jira", LabelRe: regexp.MustCompile("^@silence_jira$"), SupportedOperators: []string{regexpOperator, negativeRegexOperator, equalOperator, notEqualOperator}, Factory: newSilenceJiraFilter, - Autocomplete: sinceJiraIDAutocomplete, + Autocomplete: silenceJiraIDAutocomplete, }, filterConfig{ Label: "@silence_author", LabelRe: regexp.MustCompile("^@silence_author$"), SupportedOperators: []string{regexpOperator, negativeRegexOperator, equalOperator, notEqualOperator}, Factory: newSilenceAuthorFilter, - Autocomplete: sinceAuthorAutocomplete, + Autocomplete: silenceAuthorAutocomplete, }, filterConfig{ Label: "@limit", diff --git a/ui/src/Components/MainModal/Help.js b/ui/src/Components/MainModal/Help.js index 55a5150be..db7aa6b95 100644 --- a/ui/src/Components/MainModal/Help.js +++ b/ui/src/Components/MainModal/Help.js @@ -8,7 +8,12 @@ const FilterOperatorHelp = ({ operator, description, children }) => (
- Example: key{operator}value + Example:{" "} + + key + {operator} + value +
{children}
@@ -179,6 +184,19 @@ const Help = () => ( + + Match alerts suppressed by silence abc123456789. + + + Match alerts suppressed by any silence except{" "} + abc123456789. + + + matches snapshot 1`] = ` +
+ Match suppressed alerts based on the silence ID +
+
+
+ Supported operators: + + = + + + != + +
+
+ Examples: +
+
    +
  • +
    + + @silence_id=abc123456789 + +
    +
    + Match alerts suppressed by silence + + abc123456789 + + . +
    +
  • +
  • +
    + + @silence_id!=abc123456789 + +
    +
    + Match alerts suppressed by any silence except + + abc123456789 + + . +
    +
  • +
+
Match alerts based on the author of silence
diff --git a/views_test.go b/views_test.go index 60c8366a6..e36d45ebf 100644 --- a/views_test.go +++ b/views_test.go @@ -192,7 +192,7 @@ type acTestCase struct { var acTests = []acTestCase{ acTestCase{ - Term: "a", + Term: "ale", Results: []string{ "alertname=Memory_Usage_Too_High", "alertname=Host_Down", @@ -204,10 +204,6 @@ var acTests = []acTestCase{ "alertname!=Free_Disk_Space_Too_Low", "@alertmanager=default", "@alertmanager!=default", - "@age>1h", - "@age>10m", - "@age<1h", - "@age<10m", }, }, acTestCase{ @@ -266,30 +262,46 @@ var acTests = []acTestCase{ }, }, acTestCase{ - Term: "@", + Term: "@st", Results: []string{ "@state=suppressed", "@state=active", "@state!=suppressed", "@state!=active", - "@silence_author=~john@example.com", - "@silence_author=john@example.com", - "@silence_author!~john@example.com", - "@silence_author!=john@example.com", + }, + }, + acTestCase{ + Term: "@r", + Results: []string{ "@receiver=by-name", "@receiver=by-cluster-service", "@receiver!=by-name", "@receiver!=by-cluster-service", - "@limit=50", - "@limit=10", - "@alertmanager=default", - "@alertmanager!=default", + }, + }, + acTestCase{ + Term: "@age", + Results: []string{ "@age>1h", "@age>10m", "@age<1h", "@age<10m", }, }, + acTestCase{ + Term: "@limit", + Results: []string{ + "@limit=50", + "@limit=10", + }, + }, + acTestCase{ + Term: "@alertmanager", + Results: []string{ + "@alertmanager=default", + "@alertmanager!=default", + }, + }, acTestCase{ Term: "nod", Results: []string{ From 01c108fd416fdabcbb9cb492292d5a7c08f81e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Mon, 8 Oct 2018 19:06:10 +0100 Subject: [PATCH 3/6] refactor(ui): move label list preview to a dedicated component This will be reused in delete confirmation modal --- ui/src/Components/LabelSetList/index.js | 53 +++++++++++++++++++ .../SilenceModal/SilencePreview/index.js | 45 +++------------- .../SilenceModal/SilencePreview/index.test.js | 6 +-- 3 files changed, 62 insertions(+), 42 deletions(-) create mode 100644 ui/src/Components/LabelSetList/index.js diff --git a/ui/src/Components/LabelSetList/index.js b/ui/src/Components/LabelSetList/index.js new file mode 100644 index 000000000..758ece060 --- /dev/null +++ b/ui/src/Components/LabelSetList/index.js @@ -0,0 +1,53 @@ +import React from "react"; +import PropTypes from "prop-types"; + +import hash from "object-hash"; + +import { AlertStore } from "Stores/AlertStore"; +import { StaticLabel } from "Components/Labels/StaticLabel"; + +// take a list of groups and outputs a list of label sets, this ignores +// the receiver, so we'll end up with only unique alerts +const GroupListToUniqueLabelsList = groups => { + const alerts = {}; + for (const group of groups) { + for (const alert of group.alerts) { + const alertLabels = Object.assign( + {}, + group.labels, + group.shared.labels, + alert.labels + ); + const alertHash = hash(alertLabels); + alerts[alertHash] = alertLabels; + } + } + return Object.values(alerts); +}; + +// used in new silence form preview stage and when deleting silences +const LabelSetList = ({ alertStore, labelsList }) => + labelsList.length > 0 ? ( +
    + {labelsList.map(labels => ( +
  • + {Object.entries(labels).map(([name, value]) => ( + + ))} +
  • + ))} +
+ ) : ( +

No alerts matched

+ ); +LabelSetList.propTypes = { + alertStore: PropTypes.instanceOf(AlertStore).isRequired, + labelsList: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export { LabelSetList, GroupListToUniqueLabelsList }; diff --git a/ui/src/Components/SilenceModal/SilencePreview/index.js b/ui/src/Components/SilenceModal/SilencePreview/index.js index 6f3144736..46fff071d 100644 --- a/ui/src/Components/SilenceModal/SilencePreview/index.js +++ b/ui/src/Components/SilenceModal/SilencePreview/index.js @@ -4,8 +4,6 @@ import PropTypes from "prop-types"; import { observable, action } from "mobx"; import { observer } from "mobx-react"; -import hash from "object-hash"; - import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faArrowLeft } from "@fortawesome/free-solid-svg-icons/faArrowLeft"; import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle"; @@ -13,7 +11,10 @@ import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclama import { AlertStore, FormatBackendURI, FormatAlertsQ } from "Stores/AlertStore"; import { SilenceFormStore } from "Stores/SilenceFormStore"; -import { StaticLabel } from "Components/Labels/StaticLabel"; +import { + LabelSetList, + GroupListToUniqueLabelsList +} from "Components/LabelSetList"; import { MatcherToFilter, AlertManagersToFilter } from "../Matchers"; const FetchError = ({ message }) => ( @@ -28,27 +29,6 @@ FetchError.propTypes = { message: PropTypes.node.isRequired }; -const Preview = ({ alertStore, labelsList }) => ( -
    - {labelsList.map(labels => ( -
  • - {Object.entries(labels).map(([name, value]) => ( - - ))} -
  • - ))} -
-); -Preview.propTypes = { - alertStore: PropTypes.instanceOf(AlertStore).isRequired, - labelsList: PropTypes.arrayOf(PropTypes.object).isRequired -}; - const SilencePreview = observer( class SilencePreview extends Component { static propTypes = { @@ -64,20 +44,7 @@ const SilencePreview = observer( // take a list of groups and outputs a list of label sets, this ignores // the receiver, so we'll end up with only unique alerts groupsToUniqueLabels(groups) { - const alerts = {}; - for (const group of groups) { - for (const alert of group.alerts) { - const alertLabels = Object.assign( - {}, - group.labels, - group.shared.labels, - alert.labels - ); - const alertHash = hash(alertLabels); - alerts[alertHash] = alertLabels; - } - } - this.alertLabels = Object.values(alerts); + this.alertLabels = GroupListToUniqueLabelsList(groups); }, setError(value) { this.error = value; @@ -135,7 +102,7 @@ const SilencePreview = observer( silence.

- diff --git a/ui/src/Components/SilenceModal/SilencePreview/index.test.js b/ui/src/Components/SilenceModal/SilencePreview/index.test.js index 91b10d70a..9288082bc 100644 --- a/ui/src/Components/SilenceModal/SilencePreview/index.test.js +++ b/ui/src/Components/SilenceModal/SilencePreview/index.test.js @@ -95,10 +95,10 @@ describe("", () => { tree.update(); expect(tree.find("FetchError")).toHaveLength(1); expect(consoleSpy).toHaveBeenCalled(); - expect(tree.find("Preview")).toHaveLength(0); + expect(tree.find("LabelSetList")).toHaveLength(0); }); - it("renders Preview on successful fetch", async () => { + it("renders LabelSetList on successful fetch", async () => { fetch.mockResponse(JSON.stringify(MockAPIResponse())); const tree = MountedSilencePreview(); @@ -106,7 +106,7 @@ describe("", () => { tree.update(); expect(tree.find("FetchError")).toHaveLength(0); - expect(tree.find("Preview")).toHaveLength(1); + expect(tree.find("LabelSetList")).toHaveLength(1); }); it("clicking on the submit button moves form to the 'Submit' stage", () => { From 66d9bba68096f92eb4fbb8f32200cbcb705c3afc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Mon, 8 Oct 2018 19:45:27 +0100 Subject: [PATCH 4/6] feat(ui): allow deleting silences (with preview) --- ui/src/Common/Query.js | 3 +- .../AlertGroup/Silence/DeleteSilence.js | 259 ++++++++++++++++++ .../AlertGroup/Silence/DeleteSilence.test.js | 178 ++++++++++++ .../Silence/__snapshots__/index.test.js.snap | 18 +- .../AlertGrid/AlertGroup/Silence/index.js | 18 +- .../AlertGroup/Silence/index.test.js | 1 + 6 files changed, 472 insertions(+), 5 deletions(-) create mode 100644 ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.js create mode 100644 ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.test.js diff --git a/ui/src/Common/Query.js b/ui/src/Common/Query.js index a57c299cc..9bbba6ae2 100644 --- a/ui/src/Common/Query.js +++ b/ui/src/Common/Query.js @@ -7,7 +7,8 @@ const StaticLabels = Object.freeze({ AlertName: "alertname", AlertManager: "@alertmanager", Receiver: "@receiver", - State: "@state" + State: "@state", + SilenceID: "@silence_id" }); function FormatQuery(name, operator, value) { diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.js new file mode 100644 index 000000000..673f588d4 --- /dev/null +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.js @@ -0,0 +1,259 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; + +import { observable, action } from "mobx"; +import { observer } from "mobx-react"; + +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash"; +import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle"; +import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle"; + +import { APIAlertmanagerUpstream } from "Models/API"; +import { AlertStore, FormatBackendURI, FormatAlertsQ } from "Stores/AlertStore"; +import { FormatQuery, QueryOperators, StaticLabels } from "Common/Query"; +import { Modal } from "Components/Modal"; +import { + LabelSetList, + GroupListToUniqueLabelsList +} from "Components/LabelSetList"; + +const ErrorMessage = ({ message }) => ( +
+ +

{message}

+
+); +ErrorMessage.propTypes = { + message: PropTypes.node.isRequired +}; + +const SuccessMessage = () => ( +
+ +

+ Silence deleted, it might take a few minutes for affected alerts to change + state +

+
+); + +const DeleteSilenceModalContent = observer( + class DeleteSilenceModalContent extends Component { + static propTypes = { + alertStore: PropTypes.instanceOf(AlertStore).isRequired, + alertmanager: APIAlertmanagerUpstream.isRequired, + silenceID: PropTypes.string.isRequired, + onHide: PropTypes.func.isRequired + }; + + previewState = observable( + { + fetch: null, + error: null, + alertLabels: [], + setError(err) { + this.error = err; + }, + groupsToUniqueLabels(groups) { + this.alertLabels = GroupListToUniqueLabelsList(groups); + } + }, + { + setError: action.bound, + groupsToUniqueLabels: action.bound + } + ); + + deleteState = observable( + { + fetch: null, + done: false, + error: null, + setDone() { + this.done = true; + }, + setError(err) { + this.error = err; + } + }, + { + setDone: action.bound, + setError: action.bound + } + ); + + parseAlertmanagerResponse = response => { + /* + {"status": "success"} + or + { + "status": "error", + "errorType": "bad_data", + "error": "silence 706959fd-4590-4e21-b983-859ba6ec0e1a already expired" + } + */ + if (response.status === "success") { + this.deleteState.setError(null); + } else if (response.status === "error" && response.error) { + this.deleteState.setError(response.error); + } else { + this.deleteState.setError(JSON.stringify(response)); + } + this.deleteState.setDone(); + }; + + onFetchPreview = () => { + const { silenceID } = this.props; + + const alertsURI = + FormatBackendURI("alerts.json?") + + FormatAlertsQ([ + FormatQuery(StaticLabels.SilenceID, QueryOperators.Equal, silenceID) + ]); + + this.previewState.fetch = fetch(alertsURI, { credentials: "include" }) + .then(result => { + return result.json(); + }) + .then(result => { + this.previewState.groupsToUniqueLabels(Object.values(result.groups)); + this.previewState.setError(null); + }) + .catch(err => { + console.trace(err); + return this.previewState.setError( + `Request fetching affected alerts failed with: ${err.message}` + ); + }); + }; + + onDelete = () => { + const { alertmanager, silenceID } = this.props; + + // if it's already deleted then do nothing + if (this.deleteState.done && this.deleteState.error === null) return; + + const uri = `${alertmanager.publicURI}/api/v1/silence/${silenceID}`; + this.deleteState.fetch = fetch(uri, { method: "DELETE" }) + .then(result => result.json()) + .then(result => this.parseAlertmanagerResponse(result)) + .catch(err => { + console.trace(err); + this.deleteState.setDone(); + this.deleteState.setError( + `Delete request failed with: ${err.message}` + ); + }); + }; + + componentDidMount() { + this.onFetchPreview(); + } + + render() { + const { alertStore, onHide } = this.props; + + const isDone = this.deleteState.done && this.deleteState.error === null; + + return ( + +
+
Delete silence
+ +
+
+ {this.deleteState.done ? ( + this.deleteState.error !== null ? ( + + ) : ( + + ) + ) : this.previewState.error === null ? ( +
+

+ Alerts affected by this silence +

+ +
+ ) : ( + + )} + {isDone ? null : ( +
+ +
+ )} +
+
+ ); + } + } +); + +const DeleteSilence = observer( + class DeleteSilence extends Component { + static propTypes = { + alertStore: PropTypes.instanceOf(AlertStore).isRequired, + alertmanager: APIAlertmanagerUpstream.isRequired, + silenceID: PropTypes.string.isRequired + }; + + toggle = observable( + { + visible: false, + toggle() { + this.visible = !this.visible; + } + }, + { toggle: action.bound } + ); + + render() { + const { alertStore, alertmanager, silenceID } = this.props; + + return ( + + + + Delete + + + + + + ); + } + } +); + +export { DeleteSilence, DeleteSilenceModalContent }; diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.test.js new file mode 100644 index 000000000..68a22b0df --- /dev/null +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.test.js @@ -0,0 +1,178 @@ +import React from "react"; + +import { mount } from "enzyme"; + +import { EmptyAPIResponse } from "__mocks__/Fetch"; +import { MockAlertGroup, MockAlert, MockAlertmanager } from "__mocks__/Alerts"; +import { AlertStore } from "Stores/AlertStore"; +import { DeleteSilence, DeleteSilenceModalContent } from "./DeleteSilence"; + +let alertmanager; +let alertStore; + +beforeEach(() => { + alertmanager = MockAlertmanager(); + alertStore = new AlertStore([]); + fetch.mockResponseOnce(JSON.stringify(MockAPIResponse())); + + jest.restoreAllMocks(); +}); + +afterEach(() => { + jest.restoreAllMocks(); + fetch.resetMocks(); +}); + +const MockOnHide = jest.fn(); + +const MockAPIResponse = () => { + const response = EmptyAPIResponse(); + response.groups = { + "1": MockAlertGroup( + { alertname: "foo" }, + [MockAlert([], { instance: "foo" }, "suppressed")], + [], + { job: "foo" } + ) + }; + return response; +}; + +const MountedDeleteSilence = () => { + return mount( + + ); +}; + +const MountedDeleteSilenceModalContent = () => { + return mount( + + ); +}; + +const VerifyResponse = async response => { + const tree = MountedDeleteSilenceModalContent(); + await expect(tree.instance().previewState.fetch).resolves.toBeUndefined(); + + fetch.mockResponseOnce(JSON.stringify(response)); + tree.find(".btn-outline-danger").simulate("click"); + await expect(tree.instance().deleteState.fetch).resolves.toBeUndefined(); + + return tree; +}; + +describe("", async () => { + it("label is 'Delete' by default", () => { + const tree = MountedDeleteSilence(); + expect(tree.text()).toBe("Delete"); + }); + + it("opens modal on click", async () => { + const tree = MountedDeleteSilence(); + tree.simulate("click"); + expect(tree.find(".modal-body")).toHaveLength(1); + }); +}); + +describe("", () => { + it("renders LabelSetList on mount", async () => { + const tree = MountedDeleteSilenceModalContent(); + expect(tree.find("LabelSetList")).toHaveLength(1); + }); + + it("fetches affected alerts on mount", async () => { + MountedDeleteSilenceModalContent(); + expect(fetch).toHaveBeenCalled(); + }); + + it("renders ErrorMessage on failed fetch", async () => { + jest.spyOn(console, "trace").mockImplementation(() => {}); + fetch.resetMocks(); + fetch.mockReject("Fetch error"); + + const tree = MountedDeleteSilenceModalContent(); + await expect(tree.instance().previewState.fetch).resolves.toBeUndefined(); + tree.update(); + expect(tree.find("ErrorMessage")).toHaveLength(1); + }); + + it("renders ErrorMessage on fetch with non-JSON response", async () => { + fetch.mockResponseOnce("not json"); + jest.spyOn(console, "trace").mockImplementation(() => {}); + fetch.resetMocks(); + fetch.mockReject("Fetch error"); + + const tree = MountedDeleteSilenceModalContent(); + await expect(tree.instance().previewState.fetch).resolves.toBeUndefined(); + tree.update(); + expect(tree.find("ErrorMessage")).toHaveLength(1); + }); + + it("sends a DELETE request after clicking 'Confirm' button", async () => { + await VerifyResponse({ status: "success" }); + expect(fetch.mock.calls[1][0]).toBe( + "http://am.example.com/api/v1/silence/123456789" + ); + expect(fetch.mock.calls[1][1]).toMatchObject({ method: "DELETE" }); + }); + + it("'Confirm' button is no-op after successful DELETE", async () => { + const tree = await VerifyResponse({ status: "success" }); + expect(fetch.mock.calls[1][0]).toBe( + "http://am.example.com/api/v1/silence/123456789" + ); + expect(fetch.mock.calls[1][1]).toMatchObject({ method: "DELETE" }); + + expect(fetch.mock.calls).toHaveLength(2); + tree.find(".btn-outline-danger").simulate("click"); + expect(fetch.mock.calls).toHaveLength(2); + }); + + it("renders SuccessMessage on 'success' response status", async () => { + const tree = await VerifyResponse({ status: "success" }); + tree.update(); + expect(tree.find("SuccessMessage")).toHaveLength(1); + }); + + it("renders ErrorMessage on 'error' response status", async () => { + const tree = await VerifyResponse({ status: "error", error: "fake error" }); + tree.update(); + expect(tree.find("ErrorMessage")).toHaveLength(1); + }); + + it("renders ErrorMessage on unhandled response status", async () => { + const tree = await VerifyResponse({ status: "foo bar" }); + tree.update(); + expect(tree.find("ErrorMessage")).toHaveLength(1); + }); + + it("renders ErrorMessage on unhandled response body", async () => { + const tree = await VerifyResponse({ foo: "bar" }); + tree.update(); + expect(tree.find("ErrorMessage")).toHaveLength(1); + }); + + it("renders ErrorMessage on failed fetch request", async () => { + const tree = MountedDeleteSilenceModalContent(); + await expect(tree.instance().previewState.fetch).resolves.toBeUndefined(); + + jest.spyOn(console, "trace").mockImplementation(() => {}); + fetch.resetMocks(); + fetch.mockReject("Fetch error"); + + tree.find(".btn-outline-danger").simulate("click"); + await expect(tree.instance().deleteState.fetch).resolves.toBeUndefined(); + + tree.update(); + expect(tree.find("ErrorMessage")).toHaveLength(1); + }); +}); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/__snapshots__/index.test.js.snap b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/__snapshots__/index.test.js.snap index a50c3fed5..33a2ff890 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/__snapshots__/index.test.js.snap +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/__snapshots__/index.test.js.snap @@ -172,7 +172,7 @@ exports[` matches snapshot with expaned details 1`] = ` in 5 hours - + matches snapshot with expaned details 1`] = ` Edit + + + + + + Delete +
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.js index 38d5f34af..4e26a0e13 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.js @@ -29,6 +29,7 @@ import { StaticLabels, QueryOperators } from "Common/Query"; import { FilteringLabel } from "Components/Labels/FilteringLabel"; import { TooltipWrapper } from "Components/TooltipWrapper"; import { RenderLinkAnnotation } from "../Annotation"; +import { DeleteSilence } from "./DeleteSilence"; import "./index.css"; @@ -87,7 +88,12 @@ SilenceExpiryBadgeWithProgress.propTypes = { progress: PropTypes.number.isRequired }; -const SilenceDetails = ({ alertmanager, silence, onEditSilence }) => { +const SilenceDetails = ({ + alertStore, + alertmanager, + silence, + onEditSilence +}) => { let expiresClass = ""; let expiresLabel = "Expires"; if (moment(silence.endsAt) < moment()) { @@ -119,12 +125,17 @@ const SilenceDetails = ({ alertmanager, silence, onEditSilence }) => { {expiresLabel} {silence.endsAt} Edit +
@@ -277,7 +288,7 @@ const Silence = inject("alertStore")( } render() { - const { alertmanagerState, silenceID } = this.props; + const { alertStore, alertmanagerState, silenceID } = this.props; const silence = this.getSilence(); if (!silence) @@ -320,6 +331,7 @@ const Silence = inject("alertStore")(
{this.collapse.value ? null : ( { return mount( Date: Mon, 8 Oct 2018 22:44:45 +0100 Subject: [PATCH 5/6] refactor(api): move autocomplete code to filters --- internal/alertmanager/models.go | 3 ++- internal/filters/autocomplete.go | 18 ++++++++++++++ .../autocomplete_test.go | 6 ++--- internal/transform/autocomplete.go | 24 ------------------- 4 files changed, 23 insertions(+), 28 deletions(-) rename internal/{transform => filters}/autocomplete_test.go (91%) delete mode 100644 internal/transform/autocomplete.go diff --git a/internal/alertmanager/models.go b/internal/alertmanager/models.go index ae1509251..21f5d8a1d 100644 --- a/internal/alertmanager/models.go +++ b/internal/alertmanager/models.go @@ -11,6 +11,7 @@ import ( "time" "github.com/prymitive/karma/internal/config" + "github.com/prymitive/karma/internal/filters" "github.com/prymitive/karma/internal/mapper" "github.com/prymitive/karma/internal/models" "github.com/prymitive/karma/internal/transform" @@ -270,7 +271,7 @@ func (am *Alertmanager) pullAlerts(version string) error { alerts = append(alerts, alert) } - for _, hint := range transform.BuildAutocomplete(alerts) { + for _, hint := range filters.BuildAutocomplete(alerts) { autocompleteMap[hint.Value] = hint } diff --git a/internal/filters/autocomplete.go b/internal/filters/autocomplete.go index 2018d0a79..c4c2097aa 100644 --- a/internal/filters/autocomplete.go +++ b/internal/filters/autocomplete.go @@ -14,3 +14,21 @@ func makeAC(value string, tokens []string) models.Autocomplete { acHint.Tokens = append(acHint.Tokens, value) return acHint } + +// BuildAutocomplete takes an alert object and generates list of autocomplete +// strings for it +func BuildAutocomplete(alerts []models.Alert) []models.Autocomplete { + acHints := map[string]models.Autocomplete{} + for _, filterConfig := range AllFilters { + if filterConfig.Autocomplete != nil { + for _, hint := range filterConfig.Autocomplete(filterConfig.Label, filterConfig.SupportedOperators, alerts) { + acHints[hint.Value] = hint + } + } + } + acHintsSlice := []models.Autocomplete{} + for _, hint := range acHints { + acHintsSlice = append(acHintsSlice, hint) + } + return acHintsSlice +} diff --git a/internal/transform/autocomplete_test.go b/internal/filters/autocomplete_test.go similarity index 91% rename from internal/transform/autocomplete_test.go rename to internal/filters/autocomplete_test.go index fa151a486..1bad262be 100644 --- a/internal/transform/autocomplete_test.go +++ b/internal/filters/autocomplete_test.go @@ -1,12 +1,12 @@ -package transform_test +package filters_test import ( "encoding/json" "sort" "testing" + "github.com/prymitive/karma/internal/filters" "github.com/prymitive/karma/internal/models" - "github.com/prymitive/karma/internal/transform" "github.com/pmezard/go-difflib/difflib" ) @@ -54,7 +54,7 @@ var acTests = []acTest{ func TestBuildAutocomplete(t *testing.T) { for _, acTest := range acTests { result := []string{} - for _, hint := range transform.BuildAutocomplete(acTest.Alerts) { + for _, hint := range filters.BuildAutocomplete(acTest.Alerts) { result = append(result, hint.Value) } diff --git a/internal/transform/autocomplete.go b/internal/transform/autocomplete.go deleted file mode 100644 index 472d24806..000000000 --- a/internal/transform/autocomplete.go +++ /dev/null @@ -1,24 +0,0 @@ -package transform - -import ( - "github.com/prymitive/karma/internal/filters" - "github.com/prymitive/karma/internal/models" -) - -// BuildAutocomplete takes an alert object and generates list of autocomplete -// strings for it -func BuildAutocomplete(alerts []models.Alert) []models.Autocomplete { - acHints := map[string]models.Autocomplete{} - for _, filterConfig := range filters.AllFilters { - if filterConfig.Autocomplete != nil { - for _, hint := range filterConfig.Autocomplete(filterConfig.Label, filterConfig.SupportedOperators, alerts) { - acHints[hint.Value] = hint - } - } - } - acHintsSlice := []models.Autocomplete{} - for _, hint := range acHints { - acHintsSlice = append(acHintsSlice, hint) - } - return acHintsSlice -} From 7a9168a606a1180d298f330ebe0bd4c8339bdd17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Mon, 8 Oct 2018 23:32:50 +0100 Subject: [PATCH 6/6] fix(tests): drop broken test Throwing errors in CI, not very useful, drop it for now --- ui/src/Components/SilenceModal/index.test.js | 21 +------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/ui/src/Components/SilenceModal/index.test.js b/ui/src/Components/SilenceModal/index.test.js index f31aaf94a..c2aaef799 100644 --- a/ui/src/Components/SilenceModal/index.test.js +++ b/ui/src/Components/SilenceModal/index.test.js @@ -4,12 +4,7 @@ import { mount } from "enzyme"; import { AlertStore } from "Stores/AlertStore"; import { Settings } from "Stores/Settings"; -import { - SilenceFormStore, - SilenceFormStage, - NewEmptyMatcher, - MatcherValueToObject -} from "Stores/SilenceFormStore"; +import { SilenceFormStore } from "Stores/SilenceFormStore"; import { SilenceModal } from "."; let alertStore; @@ -104,18 +99,4 @@ describe("", () => { tree.unmount(); expect(document.body.className.split(" ")).not.toContain("modal-open"); }); - - it("currentStage is set to 'UserInput' after modal is hidden", () => { - const matcher = NewEmptyMatcher(); - matcher.name = "foo"; - matcher.values = [MatcherValueToObject("bar")]; - silenceFormStore.data.matchers = [matcher]; - - silenceFormStore.toggle.visible = true; - const tree = MountedSilenceModal(); - silenceFormStore.data.currentStage = SilenceFormStage.Preview; - const toggle = tree.find("button.close"); - toggle.simulate("click"); - expect(silenceFormStore.data.currentStage).toBe(SilenceFormStage.UserInput); - }); });