From bd25c6c178f36d3e320cc23ca3cb817c5b037a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Tue, 11 May 2021 15:26:44 +0100 Subject: [PATCH] fix(ui): tweak alert history ui --- demo/prometheus.py | 2 +- ui/src/Components/AlertHistory/index.test.tsx | 133 +++++++++++++++++- ui/src/Components/AlertHistory/index.tsx | 52 +++++-- 3 files changed, 172 insertions(+), 15 deletions(-) diff --git a/demo/prometheus.py b/demo/prometheus.py index 1e224b1fa..c02a92c44 100755 --- a/demo/prometheus.py +++ b/demo/prometheus.py @@ -18,7 +18,7 @@ def generateSeries(): for i in range(24): value = 0 if random.randint(0, 100) > 75: - value = random.randint(0, 100) + value = random.randint(0, 10) series.append([now, str(value)]) now = now - 3600 return series diff --git a/ui/src/Components/AlertHistory/index.test.tsx b/ui/src/Components/AlertHistory/index.test.tsx index 86361d928..4080708b6 100644 --- a/ui/src/Components/AlertHistory/index.test.tsx +++ b/ui/src/Components/AlertHistory/index.test.tsx @@ -14,7 +14,7 @@ import { RainbowHistoryResponse, FailedHistoryResponse, } from "__fixtures__/AlertHistory"; -import { APIAlertGroupT } from "Models/APITypes"; +import { APIAlertGroupT, HistoryResponseT } from "Models/APITypes"; import { AlertHistory } from "."; let group: APIAlertGroupT; @@ -160,4 +160,135 @@ describe("", () => { expect(fetchMock.calls()).toHaveLength(2); expect(toDiffableHtml(tree.html())).toMatchSnapshot(); }); + + interface testCasesT { + title: string; + response: HistoryResponseT; + values: string[]; + } + const testCases: testCasesT[] = [ + { + title: "EmptyHistoryResponse", + response: EmptyHistoryResponse, + values: new Array(24).fill("inactive"), + }, + { + title: "RainbowHistoryResponse", + response: RainbowHistoryResponse, + values: [ + "inactive", + "firing firing-1", + "firing firing-2", + "firing firing-3", + "firing firing-4", + "firing firing-5", + "inactive", + "firing firing-1", + "firing firing-2", + "firing firing-3", + "firing firing-4", + "firing firing-5", + "inactive", + "firing firing-1", + "firing firing-2", + "firing firing-3", + "firing firing-4", + "firing firing-5", + "inactive", + "firing firing-1", + "firing firing-2", + "firing firing-3", + "firing firing-4", + "firing firing-5", + ], + }, + { + title: "FailedHistoryResponse", + response: FailedHistoryResponse, + values: ["error"], + }, + { + title: "Single alert", + response: { + error: "", + samples: [ + ...Array(12).fill({ timestamp: "", value: 0 }), + { timestamp: "", value: 1 }, + ...Array(11).fill({ timestamp: "", value: 0 }), + ], + }, + values: [ + ...new Array(12).fill("inactive"), + "firing firing-1", + ...new Array(11).fill("inactive"), + ], + }, + { + title: "2 alerts in a single hour", + response: { + error: "", + samples: [ + { timestamp: "", value: 2 }, + ...Array(23).fill({ timestamp: "", value: 0 }), + ], + }, + values: ["firing firing-2", ...new Array(23).fill("inactive")], + }, + { + title: "5 alerts in a single hour", + response: { + error: "", + samples: [ + { timestamp: "", value: 5 }, + ...Array(23).fill({ timestamp: "", value: 0 }), + ], + }, + values: ["firing firing-5", ...new Array(23).fill("inactive")], + }, + { + title: "20 alerts in a single hour", + response: { + error: "", + samples: [ + { timestamp: "", value: 20 }, + ...Array(23).fill({ timestamp: "", value: 0 }), + ], + }, + values: ["firing firing-5", ...new Array(23).fill("inactive")], + }, + ]; + for (const testCase of testCases) { + const g = MockGroup("fakeGroup"); + for (let i = 1; i <= 5; i++) { + const alert = MockAlert([], { instance: `instance${i}` }, "active"); + const startsAt = new Date(); + alert.startsAt = startsAt.toISOString(); + alert.alertmanager[0].startsAt = startsAt.toISOString(); + g.alerts.push(alert); + } + + it(`${testCase.title}`, async () => { + fetchMock.resetHistory(); + fetchMock.mock( + "*", + { + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(testCase.response), + }, + { + overwriteRoutes: true, + } + ); + + const tree = mount(); + await act(async () => { + await fetchMock.flush(true); + }); + tree.update(); + + const rects = tree.find("rect").map((r) => r.props().className); + expect(rects).toStrictEqual(testCase.values); + tree.unmount(); + }); + } }); diff --git a/ui/src/Components/AlertHistory/index.tsx b/ui/src/Components/AlertHistory/index.tsx index 5457f2bc2..d0bd8a4e4 100644 --- a/ui/src/Components/AlertHistory/index.tsx +++ b/ui/src/Components/AlertHistory/index.tsx @@ -7,26 +7,48 @@ import { APIAlertGroupT, HistoryResponseT } from "Models/APITypes"; import { useFetchAny, UpstreamT } from "Hooks/useFetchAny"; import { TooltipWrapper } from "Components/TooltipWrapper"; +interface minMaxT { + minValue: number; + maxValue: number; +} + const responseStub: HistoryResponseT = { error: "", samples: Array(24).fill({ timestamp: "", value: 0 }), }; -const promURIRe = new RegExp(/(https?:\/\/.+)\/graph?.+/); +const promURIRe = new RegExp(/^(https?:\/\/.+)\//); export const AlertHistory: FC<{ group: APIAlertGroupT }> = ({ group }) => { const [ref, inView] = useInView({ triggerOnce: true }); + const [epoch, setEpoch] = useState(0); const [sources, setSources] = useState([]); const [upstreams, setUpstreams] = useState([]); const [labels] = useState({ ...group.labels, ...group.shared.labels }); - const { response, error, inProgress } = - useFetchAny(upstreams); - const [maxValue, setMaxValue] = useState(0); + const { response, error } = useFetchAny(upstreams); + const [cachedResponse, setCachedResponse] = + useState(null); + const [minMaxValue, setMinMaxValue] = useState({ + minValue: 0, + maxValue: 0, + }); + + useEffect(() => { + const timer = window.setInterval(() => { + setEpoch((val) => val + 1); + }, 5 * 60 * 1000); + return () => clearInterval(timer); + }, [inView]); useEffect(() => { if (response !== null) { - setMaxValue(Math.max(...response.samples.map((s) => s.value))); + setCachedResponse(response); + const max = Math.max(...response.samples.map((s) => s.value)); + const min = Math.min( + ...response.samples.filter((s) => s.value > 0).map((s) => s.value) + ); + setMinMaxValue({ minValue: min === Infinity ? 0 : min, maxValue: max }); } }, [response]); @@ -60,7 +82,7 @@ export const AlertHistory: FC<{ group: APIAlertGroupT }> = ({ group }) => { }, }, ]); - }, [inView, labels, sources]); + }, [inView, labels, sources, epoch]); return (
@@ -68,9 +90,9 @@ export const AlertHistory: FC<{ group: APIAlertGroupT }> = ({ group }) => { ref={ref} className="w-100 d-flex justify-content-between align-self-center" > - {error || (response && response.error !== "") ? ( + {error || (cachedResponse && cachedResponse.error !== "") ? ( @@ -78,18 +100,22 @@ export const AlertHistory: FC<{ group: APIAlertGroupT }> = ({ group }) => { ) : ( - (response || responseStub).samples.map((sample, i) => ( + (cachedResponse || responseStub).samples.map((sample, i) => ( 0 - ? `firing firing-${Math.round( - (sample.value / maxValue) * 5 - )}` + ? `firing firing-${ + minMaxValue.minValue === minMaxValue.maxValue + ? Math.min(minMaxValue.maxValue, 5) + : Math.round( + (sample.value / minMaxValue.maxValue) * 5 + ) + }` : "inactive" } >