From e11295ad0b25aaf24fbe45fbe01aabc195f32ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Tue, 30 Jun 2020 20:56:41 +0100 Subject: [PATCH 1/4] feat(api): expose alert fingerprint in the API response --- internal/alertmanager/models.go | 1 + internal/filters/filter_fingerprint.go | 39 +++++++++++++ internal/filters/filter_test.go | 79 +++++++++++++++++++++++++- internal/filters/registry.go | 6 ++ internal/mapper/v017/api.go | 1 + internal/models/alert.go | 1 + internal/models/alertmanager.go | 5 +- internal/models/api_test.go | 24 +++++--- 8 files changed, 145 insertions(+), 11 deletions(-) create mode 100644 internal/filters/filter_fingerprint.go diff --git a/internal/alertmanager/models.go b/internal/alertmanager/models.go index 9fdc6c160..47d42f398 100644 --- a/internal/alertmanager/models.go +++ b/internal/alertmanager/models.go @@ -244,6 +244,7 @@ func (am *Alertmanager) pullAlerts(version string) error { alert.Alertmanager = []models.AlertmanagerInstance{ { + Fingerprint: alert.Fingerprint, Name: am.Name, Cluster: am.ClusterName(), State: alert.State, diff --git a/internal/filters/filter_fingerprint.go b/internal/filters/filter_fingerprint.go new file mode 100644 index 000000000..7c78b7ed6 --- /dev/null +++ b/internal/filters/filter_fingerprint.go @@ -0,0 +1,39 @@ +package filters + +import ( + "fmt" + + "github.com/prymitive/karma/internal/models" +) + +type fingerprintFilter struct { + alertFilter +} + +func (filter *fingerprintFilter) Match(alert *models.Alert, matches int) bool { + if filter.IsValid { + var isMatch bool + for _, am := range alert.Alertmanager { + m := filter.Matcher.Compare(am.Fingerprint, filter.Value) + if m { + isMatch = m + } + } + if isMatch { + filter.Hits++ + } + return isMatch + } + e := fmt.Sprintf("Match() called on invalid filter %#v", filter) + panic(e) +} + +func (filter *fingerprintFilter) MatchAlertmanager(am *models.AlertmanagerInstance) bool { + return filter.Matcher.Compare(am.Fingerprint, filter.Value) +} + +func newFingerprintFilter() FilterT { + f := fingerprintFilter{} + f.IsAlertmanagerFilter = true + return &f +} diff --git a/internal/filters/filter_test.go b/internal/filters/filter_test.go index 319c3d305..f8c72256f 100644 --- a/internal/filters/filter_test.go +++ b/internal/filters/filter_test.go @@ -114,6 +114,83 @@ var tests = []filterTest{ IsValid: false, }, + { + Expression: "@fingerprint=", + IsValid: false, + }, + { + Expression: "@fingerprint==", + IsValid: false, + }, + { + Expression: "@fingerprint<=active", + IsValid: false, + }, + { + Expression: "@fingerprint=123", + IsValid: true, + Alert: models.Alert{}, + IsMatch: false, + IsAlertmanagerMatch: true, + }, + { + Expression: "@fingerprint!=123", + IsValid: true, + Alert: models.Alert{}, + IsMatch: true, + IsAlertmanagerMatch: true, + }, + { + Expression: "@fingerprint=1234", + IsValid: true, + Alert: models.Alert{}, + Alertmanagers: []models.AlertmanagerInstance{ + {Fingerprint: "1234"}, + }, + IsMatch: true, + IsAlertmanagerMatch: true, + }, + { + Expression: "@fingerprint=~123", + IsValid: true, + Alert: models.Alert{}, + Alertmanagers: []models.AlertmanagerInstance{ + {Fingerprint: "01234"}, + }, + IsMatch: true, + IsAlertmanagerMatch: true, + }, + { + Expression: "@fingerprint=abc", + IsValid: true, + Alert: models.Alert{}, + Alertmanagers: []models.AlertmanagerInstance{ + {Fingerprint: "12345"}, + }, + IsMatch: false, + IsAlertmanagerMatch: false, + }, + { + Expression: "@fingerprint!=1a1", + IsValid: true, + Alert: models.Alert{}, + Alertmanagers: []models.AlertmanagerInstance{ + {Fingerprint: "1a1"}, + }, + IsMatch: false, + IsAlertmanagerMatch: false, + }, + { + Expression: "@fingerprint!=cde", + IsValid: true, + Alert: models.Alert{}, + Alertmanagers: []models.AlertmanagerInstance{ + {Fingerprint: "abc"}, + }, + IsMatch: true, + IsAlertmanagerMatch: true, + }, + { Expression: "@silence_id=abcdef", IsValid: true, @@ -699,7 +776,7 @@ func TestFilters(t *testing.T) { } if f.GetIsValid() { isAlertmanagerFilter := slices.StringInSlice( - []string{"@age", "@alertmanager", "@cluster", "@state", "@silence_id", "@silence_ticket", "@silence_author"}, + []string{"@age", "@alertmanager", "@cluster", "@state", "@silence_id", "@silence_ticket", "@silence_author", "@fingerprint"}, f.GetName()) if isAlertmanagerFilter != f.GetIsAlertmanagerFilter() { t.Errorf("[%s] GetIsAlertmanagerFilter() returned %#v while %#v was expected", ft.Expression, f.GetIsAlertmanagerFilter(), isAlertmanagerFilter) diff --git a/internal/filters/registry.go b/internal/filters/registry.go index 95418136b..8b5b90f1c 100644 --- a/internal/filters/registry.go +++ b/internal/filters/registry.go @@ -61,6 +61,12 @@ var AllFilters = []filterConfig{ Factory: newStateFilter, Autocomplete: stateAutocomplete, }, + { + Label: "@fingerprint", + LabelRe: regexp.MustCompile("^@fingerprint$"), + SupportedOperators: []string{regexpOperator, equalOperator, notEqualOperator}, + Factory: newFingerprintFilter, + }, { Label: "@receiver", LabelRe: regexp.MustCompile("^@receiver$"), diff --git a/internal/mapper/v017/api.go b/internal/mapper/v017/api.go index 8faa7a041..2ae073d83 100644 --- a/internal/mapper/v017/api.go +++ b/internal/mapper/v017/api.go @@ -56,6 +56,7 @@ func groups(c *client.Alertmanager, timeout time.Duration) ([]models.AlertGroup, } for _, alert := range group.Alerts { a := models.Alert{ + Fingerprint: *alert.Fingerprint, Receiver: *group.Receiver.Name, Annotations: models.AnnotationsFromMap(alert.Annotations), Labels: alert.Labels, diff --git a/internal/models/alert.go b/internal/models/alert.go index 194d7080c..9d18092b3 100644 --- a/internal/models/alert.go +++ b/internal/models/alert.go @@ -37,6 +37,7 @@ type Alert struct { // those are not exposed in JSON, Alertmanager specific value will be in kept // in the Alertmanager slice // skip those when generating alert fingerprint too + Fingerprint string `json:"-" hash:"-"` GeneratorURL string `json:"-" hash:"-"` SilencedBy []string `json:"-" hash:"-"` InhibitedBy []string `json:"-" hash:"-"` diff --git a/internal/models/alertmanager.go b/internal/models/alertmanager.go index 900b112c7..0b7318034 100644 --- a/internal/models/alertmanager.go +++ b/internal/models/alertmanager.go @@ -5,8 +5,9 @@ import "time" // AlertmanagerInstance describes the Alertmanager instance alert was collected // from type AlertmanagerInstance struct { - Name string `json:"name"` - Cluster string `json:"cluster"` + Fingerprint string `json:"fingerprint"` + Name string `json:"name"` + Cluster string `json:"cluster"` // per instance alert state State string `json:"state"` // timestamp collected from this instance, those on the alert itself diff --git a/internal/models/api_test.go b/internal/models/api_test.go index 2270f17dc..829e3661b 100644 --- a/internal/models/api_test.go +++ b/internal/models/api_test.go @@ -12,8 +12,10 @@ import ( func TestDedupSharedMaps(t *testing.T) { am := models.AlertmanagerInstance{ - Cluster: "fakeCluster", - SilencedBy: []string{"fakeSilence1", "fakeSilence2"}, + Fingerprint: "1", + Name: "am", + Cluster: "fakeCluster", + SilencedBy: []string{"fakeSilence1", "fakeSilence2"}, } ag := models.APIAlertGroup{ AlertGroup: models.AlertGroup{ @@ -98,7 +100,8 @@ func TestDedupSharedMaps(t *testing.T) { "state": "suppressed", "alertmanager": [ { - "name": "", + "fingerprint": "1", + "name": "am", "cluster": "fakeCluster", "state": "", "startsAt": "0001-01-01T00:00:00Z", @@ -110,7 +113,8 @@ func TestDedupSharedMaps(t *testing.T) { "inhibitedBy": null }, { - "name": "", + "fingerprint": "1", + "name": "am", "cluster": "fakeCluster", "state": "", "startsAt": "0001-01-01T00:00:00Z", @@ -135,7 +139,8 @@ func TestDedupSharedMaps(t *testing.T) { "state": "suppressed", "alertmanager": [ { - "name": "", + "fingerprint": "1", + "name": "am", "cluster": "fakeCluster", "state": "", "startsAt": "0001-01-01T00:00:00Z", @@ -147,7 +152,8 @@ func TestDedupSharedMaps(t *testing.T) { "inhibitedBy": null }, { - "name": "", + "fingerprint": "1", + "name": "am", "cluster": "fakeCluster", "state": "", "startsAt": "0001-01-01T00:00:00Z", @@ -172,7 +178,8 @@ func TestDedupSharedMaps(t *testing.T) { "state": "suppressed", "alertmanager": [ { - "name": "", + "fingerprint": "1", + "name": "am", "cluster": "fakeCluster", "state": "", "startsAt": "0001-01-01T00:00:00Z", @@ -184,7 +191,8 @@ func TestDedupSharedMaps(t *testing.T) { "inhibitedBy": null }, { - "name": "", + "fingerprint": "1", + "name": "am", "cluster": "fakeCluster", "state": "", "startsAt": "0001-01-01T00:00:00Z", From c49a70ef93fe85e28093b03c7ef28dfbe20ff9a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Tue, 30 Jun 2020 21:04:02 +0100 Subject: [PATCH 2/4] feat(ui): add @finterprint filter to the help section --- ui/src/Components/MainModal/Help.js | 13 +++++ .../MainModal/__snapshots__/Help.test.js.snap | 47 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/ui/src/Components/MainModal/Help.js b/ui/src/Components/MainModal/Help.js index 88a1566b1..515cbb261 100644 --- a/ui/src/Components/MainModal/Help.js +++ b/ui/src/Components/MainModal/Help.js @@ -226,6 +226,19 @@ const Help = ({ defaultIsOpen }) => ( + + Match only alert with fingerprint 123456789. + + + Match all alerts except the one with fingerprint{" "} + 123456789. + + + +
+ Supported operators: + + = + + + != + +
+
+ Examples: +
+
    +
  • +
    + + @fingerprint=123456789 + +
    +
    + Match only alert with fingerprint + + 123456789 + + . +
    +
  • +
  • +
    + + @fingerprint!=123456789 + +
    +
    + Match all alerts except the one with fingerprint + + 123456789 + + . +
    +
  • +
+
Match suppressed alerts based on the silence ID
From bd48ab6a5bf4a2f72247c159d3687dbee6056232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Tue, 30 Jun 2020 22:15:15 +0100 Subject: [PATCH 3/4] feat(ui): show modal with list of inhibiting alerts --- ui/src/Common/Query.js | 1 + .../Alert/__snapshots__/index.test.js.snap | 2 +- .../Grid/AlertGrid/AlertGroup/Alert/index.js | 21 ++- .../AlertGrid/AlertGroup/Alert/index.test.js | 17 +++ .../InhibitedByModalContent.js | 38 ++++++ ui/src/Components/InhibitedByModal/index.js | 58 ++++++++ .../Components/InhibitedByModal/index.test.js | 107 +++++++++++++++ ui/src/Components/LabelSetList/index.js | 11 +- ui/src/Components/LabelSetList/index.test.js | 6 +- .../ManagedSilence/DeleteSilence.js | 53 ++------ .../ManagedSilence/DeleteSilence.test.js | 63 --------- ui/src/Components/PaginatedAlertList/index.js | 60 +++++++++ .../PaginatedAlertList/index.test.js | 125 ++++++++++++++++++ .../SilenceModal/SilencePreview/index.js | 51 +------ 14 files changed, 449 insertions(+), 164 deletions(-) create mode 100644 ui/src/Components/InhibitedByModal/InhibitedByModalContent.js create mode 100644 ui/src/Components/InhibitedByModal/index.js create mode 100644 ui/src/Components/InhibitedByModal/index.test.js create mode 100644 ui/src/Components/PaginatedAlertList/index.js create mode 100644 ui/src/Components/PaginatedAlertList/index.test.js diff --git a/ui/src/Common/Query.js b/ui/src/Common/Query.js index dcaea36c0..afb527a05 100644 --- a/ui/src/Common/Query.js +++ b/ui/src/Common/Query.js @@ -8,6 +8,7 @@ const StaticLabels = Object.freeze({ AlertName: "alertname", AlertManager: "@alertmanager", AlertmanagerCluster: "@cluster", + Fingerprint: "@fingerprint", Receiver: "@receiver", State: "@state", SilenceID: "@silence_id", diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap index 3c344eb53..7bf9ce9d5 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap @@ -81,7 +81,7 @@ exports[` matches snapshot when inhibited 1`] = `
- + 0) { - isInhibited = true; + for (const fingerprint of am.inhibitedBy) { + if (!inhibitedBy.includes(fingerprint)) { + inhibitedBy.push(fingerprint); + } } if (!silences[am.cluster]) { silences[am.cluster] = { @@ -87,12 +86,8 @@ const Alert = ({ silenceFormStore={silenceFormStore} setIsMenuOpen={setIsMenuOpen} /> - {isInhibited ? ( - - - - - + {inhibitedBy.length > 0 ? ( + ) : null} {Object.entries(alert.labels).map(([name, value]) => ( ", () => { }); it("renders inhibition icon when inhibited", () => { + const alert = MockedAlert(); + alert.alertmanager[0].inhibitedBy = ["123456"]; + alert.alertmanager.push({ + name: "ha2", + cluster: "HA", + state: "active", + startsAt: "2018-08-14T17:36:40.017867056Z", + source: "localhost/prometheus", + silencedBy: [], + inhibitedBy: ["123456"], + }); + const group = MockAlertGroup({}, [alert], [], {}, {}); + const tree = MountedAlert(alert, group, false, false); + expect(tree.find(".fa-volume-mute")).toHaveLength(1); + }); + + it("inhibition icon passes only unique fingerprints", () => { const alert = MockedAlert(); alert.alertmanager[0].inhibitedBy = ["123456"]; const group = MockAlertGroup({}, [alert], [], {}, {}); diff --git a/ui/src/Components/InhibitedByModal/InhibitedByModalContent.js b/ui/src/Components/InhibitedByModal/InhibitedByModalContent.js new file mode 100644 index 000000000..28780fe1a --- /dev/null +++ b/ui/src/Components/InhibitedByModal/InhibitedByModalContent.js @@ -0,0 +1,38 @@ +import React from "react"; +import PropTypes from "prop-types"; + +import { AlertStore } from "Stores/AlertStore"; +import { FormatQuery, QueryOperators, StaticLabels } from "Common/Query"; +import { PaginatedAlertList } from "Components/PaginatedAlertList"; + +const InhibitedByModalContent = ({ alertStore, fingerprints, onHide }) => { + return ( + +
+
Inhibiting alerts
+ +
+
+ +
+
+ ); +}; +InhibitedByModalContent.propTypes = { + alertStore: PropTypes.instanceOf(AlertStore).isRequired, + fingerprints: PropTypes.arrayOf(PropTypes.string).isRequired, + onHide: PropTypes.func.isRequired, +}; + +export { InhibitedByModalContent }; diff --git a/ui/src/Components/InhibitedByModal/index.js b/ui/src/Components/InhibitedByModal/index.js new file mode 100644 index 000000000..54800bcb0 --- /dev/null +++ b/ui/src/Components/InhibitedByModal/index.js @@ -0,0 +1,58 @@ +import React, { useState, useCallback } from "react"; +import PropTypes from "prop-types"; + +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faSpinner } from "@fortawesome/free-solid-svg-icons/faSpinner"; +import { faVolumeMute } from "@fortawesome/free-solid-svg-icons/faVolumeMute"; + +import { AlertStore } from "Stores/AlertStore"; +import { TooltipWrapper } from "Components/TooltipWrapper"; +import { Modal } from "Components/Modal"; + +// https://github.com/facebook/react/issues/14603 +const InhibitedByModalContent = React.lazy(() => + import("./InhibitedByModalContent").then((module) => ({ + default: module.InhibitedByModalContent, + })) +); + +const InhibitedByModal = ({ alertStore, fingerprints }) => { + const [isVisible, setIsVisible] = useState(false); + + const toggle = useCallback(() => setIsVisible(!isVisible), [isVisible]); + + return ( + + + + + + + + + + + } + > + setIsVisible(false)} + isVisible={isVisible} + fingerprints={fingerprints} + /> + + + + ); +}; +InhibitedByModal.propTypes = { + alertStore: PropTypes.instanceOf(AlertStore).isRequired, + fingerprints: PropTypes.arrayOf(PropTypes.string).isRequired, +}; + +export { InhibitedByModal }; diff --git a/ui/src/Components/InhibitedByModal/index.test.js b/ui/src/Components/InhibitedByModal/index.test.js new file mode 100644 index 000000000..682acaf37 --- /dev/null +++ b/ui/src/Components/InhibitedByModal/index.test.js @@ -0,0 +1,107 @@ +import React from "react"; +import { act } from "react-dom/test-utils"; + +import { mount } from "enzyme"; + +import { AlertStore } from "Stores/AlertStore"; +import { InhibitedByModal } from "."; + +let alertStore; + +beforeAll(() => { + jest.useFakeTimers(); +}); + +beforeEach(() => { + alertStore = new AlertStore([]); +}); + +afterEach(() => { + document.body.className = ""; +}); + +describe("", () => { + it("renders a spinner placeholder while modal content is loading", () => { + const tree = mount( + + ); + const toggle = tree.find("span.badge.badge-light"); + toggle.simulate("click"); + expect(tree.find("InhibitedByModalContent")).toHaveLength(0); + expect(tree.find(".modal-content").find("svg.fa-spinner")).toHaveLength(1); + }); + + it("renders modal content if fallback is not used", () => { + const tree = mount( + + ); + const toggle = tree.find("span.badge.badge-light"); + toggle.simulate("click"); + expect(tree.find(".modal-title").text()).toBe("Inhibiting alerts"); + expect(tree.find(".modal-content").find("svg.fa-spinner")).toHaveLength(0); + }); + + it("hides the modal when toggle() is called twice", () => { + const tree = mount( + + ); + const toggle = tree.find("span.badge.badge-light"); + + toggle.simulate("click"); + act(() => jest.runOnlyPendingTimers()); + tree.update(); + expect(tree.find(".modal-title").text()).toBe("Inhibiting alerts"); + + toggle.simulate("click"); + act(() => jest.runOnlyPendingTimers()); + tree.update(); + expect(tree.find(".modal-title")).toHaveLength(0); + }); + + it("hides the modal when button.close is clicked", () => { + const tree = mount( + + ); + const toggle = tree.find("span.badge.badge-light"); + + toggle.simulate("click"); + expect(tree.find(".modal-title").text()).toBe("Inhibiting alerts"); + + tree.find("button.close").simulate("click"); + act(() => jest.runOnlyPendingTimers()); + tree.update(); + expect(tree.find("InhibitedByModalContent")).toHaveLength(0); + }); + + it("'modal-open' class is appended to body node when modal is visible", () => { + const tree = mount( + + ); + const toggle = tree.find("span.badge.badge-light"); + toggle.simulate("click"); + expect(document.body.className.split(" ")).toContain("modal-open"); + }); + + it("'modal-open' class is removed from body node after modal is hidden", () => { + const tree = mount( + + ); + + tree.find("span.badge.badge-light").simulate("click"); + expect(document.body.className.split(" ")).toContain("modal-open"); + + tree.find("span.badge.badge-light").simulate("click"); + act(() => jest.runOnlyPendingTimers()); + expect(document.body.className.split(" ")).not.toContain("modal-open"); + }); + + it("'modal-open' class is removed from body node after modal is unmounted", () => { + const tree = mount( + + ); + const toggle = tree.find("span.badge.badge-light"); + toggle.simulate("click"); + tree.unmount(); + expect(document.body.className.split(" ")).not.toContain("modal-open"); + }); +}); diff --git a/ui/src/Components/LabelSetList/index.js b/ui/src/Components/LabelSetList/index.js index 019f5ca4f..b0e647520 100644 --- a/ui/src/Components/LabelSetList/index.js +++ b/ui/src/Components/LabelSetList/index.js @@ -26,14 +26,14 @@ const GroupListToUniqueLabelsList = (groups) => { return Object.values(alerts); }; -const LabelSetList = ({ alertStore, labelsList }) => { +const LabelSetList = ({ alertStore, labelsList, title }) => { const [activePage, setActivePage] = useState(1); const maxPerPage = IsMobile() ? 5 : 10; return labelsList.length > 0 ? (
-

Affected alerts

+ {title ?

{title}

: null}
    {labelsList @@ -63,12 +63,17 @@ const LabelSetList = ({ alertStore, labelsList }) => { />
) : ( -

No alerts matched

+
+

+ No alerts matched +

+
); }; LabelSetList.propTypes = { alertStore: PropTypes.instanceOf(AlertStore).isRequired, labelsList: PropTypes.arrayOf(PropTypes.object).isRequired, + title: PropTypes.string, }; export { LabelSetList, GroupListToUniqueLabelsList }; diff --git a/ui/src/Components/LabelSetList/index.test.js b/ui/src/Components/LabelSetList/index.test.js index a92bb4615..787095ad6 100644 --- a/ui/src/Components/LabelSetList/index.test.js +++ b/ui/src/Components/LabelSetList/index.test.js @@ -19,7 +19,11 @@ afterEach(() => { const MountedLabelSetList = (labelsList) => { return mount( - + ); }; diff --git a/ui/src/Components/ManagedSilence/DeleteSilence.js b/ui/src/Components/ManagedSilence/DeleteSilence.js index 14ae12539..5b758af06 100644 --- a/ui/src/Components/ManagedSilence/DeleteSilence.js +++ b/ui/src/Components/ManagedSilence/DeleteSilence.js @@ -8,16 +8,12 @@ import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle"; import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch"; import { APISilence } from "Models/API"; -import { AlertStore, FormatBackendURI, FormatAlertsQ } from "Stores/AlertStore"; +import { AlertStore } from "Stores/AlertStore"; import { SilenceFormStore } from "Stores/SilenceFormStore"; import { FormatQuery, QueryOperators, StaticLabels } from "Common/Query"; -import { useFetchGet } from "Hooks/useFetchGet"; import { useFetchDelete } from "Hooks/useFetchDelete"; import { Modal } from "Components/Modal"; -import { - LabelSetList, - GroupListToUniqueLabelsList, -} from "Components/LabelSetList"; +import { PaginatedAlertList } from "Components/PaginatedAlertList"; const ProgressMessage = () => (
@@ -55,32 +51,6 @@ const SuccessMessage = () => (
); -const DeletePreview = ({ alertStore, silence }) => { - const { response, error, isLoading } = useFetchGet( - FormatBackendURI("alerts.json?") + - FormatAlertsQ([ - FormatQuery(StaticLabels.SilenceID, QueryOperators.Equal, silence.id), - ]) - ); - - return isLoading ? ( - - ) : error ? ( - - ) : ( - - ); -}; -DeletePreview.propTypes = { - alertStore: PropTypes.instanceOf(AlertStore).isRequired, - silence: APISilence.isRequired, -}; - const DeleteResult = ({ alertStore, cluster, silence }) => { const [currentTime, setCurrentTime] = useState(Math.floor(Date.now())); @@ -164,7 +134,17 @@ const DeleteSilenceModalContent = ({ /> ) : ( - +