From f9c007d3bd33b783cb94c6010bda4bd91d16805c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 1 May 2020 11:28:12 +0100 Subject: [PATCH] fix(ui): rewrite MatchCounter component with hooks --- .../SilenceModal/SilenceMatch/MatchCounter.js | 136 ++++++++---------- .../SilenceMatch/MatchCounter.test.js | 88 +++++++----- 2 files changed, 108 insertions(+), 116 deletions(-) diff --git a/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.js b/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.js index 2f1f2734a..bf9b6669e 100644 --- a/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.js +++ b/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.js @@ -1,8 +1,7 @@ -import React, { Component } from "react"; +import React, { useEffect, useCallback } from "react"; import PropTypes from "prop-types"; -import { observable, action } from "mobx"; -import { observer } from "mobx-react"; +import { useObserver, useLocalStore } from "mobx-react"; import throttle from "lodash/throttle"; @@ -17,34 +16,20 @@ import { TooltipWrapper } from "Components/TooltipWrapper"; import { FetchGet } from "Common/Fetch"; import { MatcherToFilter, AlertManagersToFilter } from "../Matchers"; -const MatchCounter = observer( - class MatchCounter extends Component { - static propTypes = { - silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired, - matcher: SilenceFormMatcher.isRequired, - }; - - matchedAlerts = observable( - { - total: null, - error: null, - fetch: null, - setTotal(value) { - this.total = value; - }, - setError(value) { - this.error = value; - }, - }, - { - setTotal: action, - setError: action, - } - ); - - onFetch = throttle(() => { - const { silenceFormStore, matcher } = this.props; +const MatchCounter = ({ silenceFormStore, matcher }) => { + const matchedAlerts = useLocalStore(() => ({ + total: null, + error: null, + setTotal(value) { + this.total = value; + }, + setError(value) { + this.error = value; + }, + })); + const onFetch = useCallback( + throttle(() => { const filters = [MatcherToFilter(matcher)]; if (silenceFormStore.data.alertmanagers.length) { filters.push( @@ -55,66 +40,57 @@ const MatchCounter = observer( const alertsURI = FormatBackendURI("alerts.json?") + FormatAlertsQ(filters); - this.matchedAlerts.fetch = FetchGet(alertsURI, {}) + FetchGet(alertsURI, {}) .then((result) => { return result.json(); }) .then((result) => { - this.matchedAlerts.setTotal(result.totalAlerts); - this.matchedAlerts.setError(null); + matchedAlerts.setTotal(result.totalAlerts); + matchedAlerts.setError(null); }) .catch((err) => { console.trace(err); - return this.matchedAlerts.setError(err.message); + return matchedAlerts.setError(err.message); }); - }, 300); + }, 300), + [matcher.name] + ); - onUpdateCounter = () => { - const { matcher } = this.props; - - if (matcher.name === "" || matcher.values.length === 0) { - this.matchedAlerts.setTotal(0); - this.matchedAlerts.setError(null); - return; - } - - this.onFetch(); - }; - - componentDidMount() { - this.onUpdateCounter(); + useEffect(() => { + if (matcher.name === "" || matcher.values.length === 0) { + matchedAlerts.setTotal(0); + matchedAlerts.setError(null); + } else { + onFetch(); } + }, [matchedAlerts, matcher.name, matcher.values.length, onFetch]); - render() { - if (this.matchedAlerts.error !== null) { - return ( - - - - ); - } - - return ( - - - {this.matchedAlerts.total === null ? ( - - ) : ( - this.matchedAlerts.total - )} - - - ); - } - } -); + return useObserver(() => + matchedAlerts.error !== null ? ( + + + + ) : ( + + + {matchedAlerts.total === null ? ( + + ) : ( + matchedAlerts.total + )} + + + ) + ); +}; +MatchCounter.propTypes = { + silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired, + matcher: SilenceFormMatcher.isRequired, +}; export { MatchCounter }; diff --git a/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.js b/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.js index de403f55b..e1667e185 100644 --- a/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.js +++ b/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.js @@ -38,7 +38,7 @@ describe("", () => { expect(toDiffableHtml(tree.html())).toMatchSnapshot(); }); - it("logs a trace on failed fetch", async () => { + it("logs a trace on failed fetch", (done) => { const consoleSpy = jest .spyOn(console, "trace") .mockImplementation(() => {}); @@ -48,12 +48,14 @@ describe("", () => { matcher.name = "foo"; matcher.values = [MatcherValueToObject("bar")]; - const tree = MountedMatchCounter(); - await expect(tree.instance().matchedAlerts.fetch).resolves.toBeUndefined(); - expect(consoleSpy).toHaveBeenCalled(); + MountedMatchCounter(); + setTimeout(() => { + expect(consoleSpy).toHaveBeenCalled(); + done(); + }, 200); }); - it("renders error icon on failed fetch", async () => { + it("renders error icon on failed fetch", (done) => { jest.spyOn(console, "trace").mockImplementation(() => {}); fetch.mockReject(new Error("Fetch error")); @@ -62,8 +64,10 @@ describe("", () => { matcher.values = [MatcherValueToObject("bar")]; const tree = MountedMatchCounter(); - await expect(tree.instance().matchedAlerts.fetch).resolves.toBeUndefined(); - expect(toDiffableHtml(tree.html())).toMatch(/exclamation-circle/); + setTimeout(() => { + expect(toDiffableHtml(tree.html())).toMatch(/exclamation-circle/); + done(); + }, 200); }); it("totalAlerts is 0 after mount", async () => { @@ -72,7 +76,7 @@ describe("", () => { expect(tree.text()).toBe("0"); }); - it("updates totalAlerts after successful fetch", async () => { + it("updates totalAlerts after successful fetch", (done) => { fetch.mockResponse(JSON.stringify({ totalAlerts: 123 })); // we need to set name & value to trigger fetch @@ -81,49 +85,57 @@ describe("", () => { const tree = MountedMatchCounter(); expect(tree.text()).toBe(""); - await expect(tree.instance().matchedAlerts.fetch).resolves.toBeUndefined(); - expect(tree.text()).toBe("123"); + setTimeout(() => { + expect(tree.text()).toBe("123"); + done(); + }, 200); }); - it("sends correct query string for a 'foo=bar' matcher", async () => { + it("sends correct query string for a 'foo=bar' matcher", (done) => { fetch.mockResponse(JSON.stringify({ totalAlerts: 0 })); matcher.name = "foo"; matcher.values = [MatcherValueToObject("bar")]; matcher.isRegex = false; - const tree = MountedMatchCounter(); - await expect(tree.instance().matchedAlerts.fetch).resolves.toBeUndefined(); - expect(fetch.mock.calls[0][0]).toBe("./alerts.json?q=foo%3Dbar"); + MountedMatchCounter(); + setTimeout(() => { + expect(fetch.mock.calls[0][0]).toBe("./alerts.json?q=foo%3Dbar"); + done(); + }, 200); }); - it("sends correct query string for a 'foo=~bar' matcher", async () => { + it("sends correct query string for a 'foo=~bar' matcher", (done) => { fetch.mockResponse(JSON.stringify({ totalAlerts: 0 })); matcher.name = "foo"; matcher.values = [MatcherValueToObject("bar")]; matcher.isRegex = true; - const tree = MountedMatchCounter(); - await expect(tree.instance().matchedAlerts.fetch).resolves.toBeUndefined(); - expect(fetch.mock.calls[0][0]).toBe("./alerts.json?q=foo%3D~%5Ebar%24"); + MountedMatchCounter(); + setTimeout(() => { + expect(fetch.mock.calls[0][0]).toBe("./alerts.json?q=foo%3D~%5Ebar%24"); + done(); + }, 200); }); - it("sends correct query string for a 'foo=~(bar|baz)' matcher", async () => { + it("sends correct query string for a 'foo=~(bar|baz)' matcher", (done) => { fetch.mockResponse(JSON.stringify({ totalAlerts: 0 })); matcher.name = "foo"; matcher.values = [MatcherValueToObject("bar"), MatcherValueToObject("baz")]; matcher.isRegex = true; - const tree = MountedMatchCounter(); - await expect(tree.instance().matchedAlerts.fetch).resolves.toBeUndefined(); - expect(fetch.mock.calls[0][0]).toBe( - "./alerts.json?q=foo%3D~%5E%28bar%7Cbaz%29%24" - ); + MountedMatchCounter(); + setTimeout(() => { + expect(fetch.mock.calls[0][0]).toBe( + "./alerts.json?q=foo%3D~%5E%28bar%7Cbaz%29%24" + ); + done(); + }, 200); }); - it("selecting one Alertmanager instance appends it to the filters", async () => { + it("selecting one Alertmanager instance appends it to the filters", (done) => { fetch.mockResponse(JSON.stringify({ totalAlerts: 0 })); silenceFormStore.data.alertmanagers = [MatcherValueToObject("am1")]; @@ -131,14 +143,16 @@ describe("", () => { matcher.values = [MatcherValueToObject("bar")]; matcher.isRegex = false; - const tree = MountedMatchCounter(); - await expect(tree.instance().matchedAlerts.fetch).resolves.toBeUndefined(); - expect(fetch.mock.calls[0][0]).toBe( - "./alerts.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28am1%29%24" - ); + MountedMatchCounter(); + setTimeout(() => { + expect(fetch.mock.calls[0][0]).toBe( + "./alerts.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28am1%29%24" + ); + done(); + }, 200); }); - it("selecting two Alertmanager instances appends it correctly to the filters", async () => { + it("selecting two Alertmanager instances appends it correctly to the filters", (done) => { fetch.mockResponse(JSON.stringify({ totalAlerts: 0 })); silenceFormStore.data.alertmanagers = [ @@ -149,10 +163,12 @@ describe("", () => { matcher.values = [MatcherValueToObject("bar")]; matcher.isRegex = false; - const tree = MountedMatchCounter(); - await expect(tree.instance().matchedAlerts.fetch).resolves.toBeUndefined(); - expect(fetch.mock.calls[0][0]).toBe( - "./alerts.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28am1%7Cam1%29%24" - ); + MountedMatchCounter(); + setTimeout(() => { + expect(fetch.mock.calls[0][0]).toBe( + "./alerts.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28am1%7Cam1%29%24" + ); + done(); + }, 200); }); });