fix(ui): rewrite MatchCounter component with hooks

This commit is contained in:
Łukasz Mierzwa
2020-05-01 11:28:12 +01:00
committed by Łukasz Mierzwa
parent 1072e367c5
commit f9c007d3bd
2 changed files with 108 additions and 116 deletions

View File

@@ -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 (
<TooltipWrapper
title={`Failed to fetch alerts matching this label: ${this.matchedAlerts.error}`}
>
<FontAwesomeIcon
className="text-danger"
icon={faExclamationCircle}
/>
</TooltipWrapper>
);
}
return (
<TooltipWrapper title="Number of alerts matching this label">
<span
className="badge badge-light badge-pill d-block"
style={{ fontSize: "85%", lineHeight: "1rem" }}
>
{this.matchedAlerts.total === null ? (
<FontAwesomeIcon icon={faSpinner} spin />
) : (
this.matchedAlerts.total
)}
</span>
</TooltipWrapper>
);
}
}
);
return useObserver(() =>
matchedAlerts.error !== null ? (
<TooltipWrapper
title={`Failed to fetch alerts matching this label: ${matchedAlerts.error}`}
>
<FontAwesomeIcon className="text-danger" icon={faExclamationCircle} />
</TooltipWrapper>
) : (
<TooltipWrapper title="Number of alerts matching this label">
<span
className="badge badge-light badge-pill d-block"
style={{ fontSize: "85%", lineHeight: "1rem" }}
>
{matchedAlerts.total === null ? (
<FontAwesomeIcon icon={faSpinner} spin />
) : (
matchedAlerts.total
)}
</span>
</TooltipWrapper>
)
);
};
MatchCounter.propTypes = {
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
matcher: SilenceFormMatcher.isRequired,
};
export { MatchCounter };

View File

@@ -38,7 +38,7 @@ describe("<MatchCounter />", () => {
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("<MatchCounter />", () => {
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("<MatchCounter />", () => {
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("<MatchCounter />", () => {
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("<MatchCounter />", () => {
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("<MatchCounter />", () => {
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("<MatchCounter />", () => {
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);
});
});