diff --git a/ui/src/Components/AlertHistory/index.test.tsx b/ui/src/Components/AlertHistory/index.test.tsx index 54f1232f9..4361e5e69 100644 --- a/ui/src/Components/AlertHistory/index.test.tsx +++ b/ui/src/Components/AlertHistory/index.test.tsx @@ -16,10 +16,15 @@ import { RainbowHistoryResponse, FailedHistoryResponse, } from "__fixtures__/AlertHistory"; -import type { APIAlertGroupT, HistoryResponseT } from "Models/APITypes"; +import type { + APIAlertGroupT, + APIGridT, + HistoryResponseT, +} from "Models/APITypes"; import { AlertHistory } from "."; let group: APIAlertGroupT; +let grid: APIGridT; const MockGroup = (groupName: string) => { const group = MockAlertGroup( @@ -49,14 +54,123 @@ beforeEach(() => { jest.useFakeTimers(); advanceTo(new Date(Date.UTC(2000, 1, 1, 0, 0, 0))); group = MockGroup("fakeGroup"); + grid = { + labelName: "foo", + labelValue: "bar", + alertGroups: [], + totalGroups: 0, + stateCount: { + active: 0, + suppressed: 0, + unprocessed: 0, + }, + }; }); afterEach(() => { + fetchMock.resetHistory(); fetchMock.reset(); clear(); }); describe("", () => { + it("send a correct payload with empty grid", async () => { + fetchMock.resetHistory(); + fetchMock.mock( + "*", + { + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(EmptyHistoryResponse), + }, + { + overwriteRoutes: true, + } + ); + + grid.labelName = ""; + grid.labelValue = ""; + MockAlerts(3); + const tree = mount(); + await act(async () => { + await fetchMock.flush(true); + }); + expect(fetchMock.calls()).toHaveLength(1); + expect(fetchMock.calls()[0][1]?.body).toStrictEqual( + JSON.stringify({ + sources: [ + "https://secure.example.com/graph", + "http://plain.example.com/", + ], + labels: { alertname: "Fake Alert", groupName: "fakeGroup" }, + }) + ); + tree.unmount(); + }); + + it("send a correct payload with non-empty grid", async () => { + fetchMock.resetHistory(); + fetchMock.mock( + "*", + { + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(EmptyHistoryResponse), + }, + { + overwriteRoutes: true, + } + ); + + MockAlerts(3); + const tree = mount(); + await act(async () => { + await fetchMock.flush(true); + }); + expect(fetchMock.calls()).toHaveLength(1); + expect(fetchMock.calls()[0][1]?.body).toStrictEqual( + JSON.stringify({ + sources: [ + "https://secure.example.com/graph", + "http://plain.example.com/", + ], + labels: { alertname: "Fake Alert", groupName: "fakeGroup", foo: "bar" }, + }) + ); + tree.unmount(); + }); + + it("send a correct payload with @cluster grid", async () => { + fetchMock.resetHistory(); + fetchMock.mock( + "*", + { + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(EmptyHistoryResponse), + }, + { + overwriteRoutes: true, + } + ); + + grid.labelName = "@cluster"; + grid.labelValue = "prod"; + MockAlerts(3); + const tree = mount(); + await act(async () => { + await fetchMock.flush(true); + }); + expect(fetchMock.calls()).toHaveLength(1); + expect(fetchMock.calls()[0][1]?.body).toStrictEqual( + JSON.stringify({ + sources: [ + "https://secure.example.com/graph", + "http://plain.example.com/", + ], + labels: { alertname: "Fake Alert", groupName: "fakeGroup" }, + }) + ); + tree.unmount(); + }); + it("matches snapshot with empty response", async () => { fetchMock.resetHistory(); fetchMock.mock( @@ -71,7 +185,7 @@ describe("", () => { ); MockAlerts(3); - const tree = mount(); + const tree = mount(); await act(async () => { await fetchMock.flush(true); }); @@ -94,7 +208,7 @@ describe("", () => { ); MockAlerts(3); - const tree = mount(); + const tree = mount(); await act(async () => { await fetchMock.flush(true); }); @@ -122,7 +236,7 @@ describe("", () => { ] as any); MockAlerts(3); - const tree = mount(); + const tree = mount(); await act(async () => { await fetchMock.flush(true); }); @@ -151,7 +265,7 @@ describe("", () => { ] as any); MockAlerts(3); - const tree = mount(); + const tree = mount(); await act(async () => { await fetchMock.flush(true); }); @@ -188,7 +302,7 @@ describe("", () => { ); MockAlerts(3); - const tree = mount(); + const tree = mount(); await act(async () => { await fetchMock.flush(true); }); @@ -211,7 +325,7 @@ describe("", () => { ); MockAlerts(3); - const tree = mount(); + const tree = mount(); await act(async () => { await fetchMock.flush(true); }); @@ -329,6 +443,17 @@ describe("", () => { } g.alerts.push(alert); } + const gr = { + labelName: "foo", + labelValue: "bar", + alertGroups: [], + totalGroups: 0, + stateCount: { + active: 0, + suppressed: 0, + unprocessed: 0, + }, + }; it(`${testCase.title}`, async () => { fetchMock.resetHistory(); @@ -343,7 +468,7 @@ describe("", () => { } ); - const tree = mount(); + const tree = mount(); await act(async () => { await fetchMock.flush(true); }); diff --git a/ui/src/Components/AlertHistory/index.tsx b/ui/src/Components/AlertHistory/index.tsx index 3bf3e5651..2d52b046c 100644 --- a/ui/src/Components/AlertHistory/index.tsx +++ b/ui/src/Components/AlertHistory/index.tsx @@ -3,7 +3,11 @@ import { FC, useEffect, useState } from "react"; import { useInView } from "react-intersection-observer"; import { FormatBackendURI } from "Stores/AlertStore"; -import type { APIAlertGroupT, HistoryResponseT } from "Models/APITypes"; +import type { + APIAlertGroupT, + APIGridT, + HistoryResponseT, +} from "Models/APITypes"; import { useFetchAny, UpstreamT } from "Hooks/useFetchAny"; import { TooltipWrapper } from "Components/TooltipWrapper"; @@ -22,12 +26,21 @@ const GetUTCSeconds = (): number => { return (now.getTime() + now.getTimezoneOffset()) / 1000; }; -export const AlertHistory: FC<{ group: APIAlertGroupT }> = ({ group }) => { +export const AlertHistory: FC<{ group: APIAlertGroupT; grid: APIGridT }> = ({ + group, + grid, +}) => { const [ref, inView] = useInView({ triggerOnce: true }); const [lastUpdate, setLastUpdate] = useState(GetUTCSeconds()); const [upstreams, setUpstreams] = useState([]); - const [labels] = useState({ ...group.labels, ...group.shared.labels }); + const [labels] = useState({ + ...group.labels, + ...group.shared.labels, + ...(grid.labelName !== "" && grid.labelName[0] !== "@" + ? { [grid.labelName]: grid.labelValue } + : {}), + }); const [sources] = useState(group.shared.sources); const { response, error } = useFetchAny(upstreams); const [cachedResponse, setCachedResponse] = useState( diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.tsx index c48d8f334..a17a358a9 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.tsx @@ -170,7 +170,7 @@ const AlertGroup: FC<{ {isCollapsed ? null : ( {alertStore.settings.values.historyEnabled ? ( - + ) : null} {group.alerts