mirror of
https://github.com/prymitive/karma
synced 2026-05-05 03:16:51 +00:00
fix(ui): rewrite MatchCounter component with hooks
This commit is contained in:
committed by
Łukasz Mierzwa
parent
1072e367c5
commit
f9c007d3bd
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user