From 31c9468368ca0e8d9a0e8aabbe8cb05cc6882a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 29 Oct 2021 13:14:46 +0100 Subject: [PATCH] fix(ui): use slices for labels instead of maps --- ui/src/Components/AlertAck/index.test.tsx | 14 ++- ui/src/Components/AlertHistory/index.test.tsx | 63 +++++++++- ui/src/Components/AlertHistory/index.tsx | 6 +- .../AlertGroup/Alert/AlertMenu.test.tsx | 16 ++- .../AlertGrid/AlertGroup/Alert/index.test.tsx | 51 ++++---- .../Grid/AlertGrid/AlertGroup/Alert/index.tsx | 8 +- .../AlertGroup/GroupFooter/index.test.tsx | 13 ++- .../AlertGroup/GroupFooter/index.tsx | 8 +- .../AlertGroup/GroupHeader/GroupMenu.test.tsx | 60 ++++++++-- .../AlertGroup/GroupHeader/GroupMenu.tsx | 4 +- .../AlertGroup/GroupHeader/index.tsx | 8 +- .../Grid/AlertGrid/AlertGroup/index.test.tsx | 13 ++- .../Components/Grid/AlertGrid/index.test.tsx | 11 +- ui/src/Components/LabelSetList/index.test.tsx | 30 +++-- ui/src/Components/LabelSetList/index.tsx | 11 +- .../PaginatedAlertList/index.test.tsx | 20 ++-- .../SilenceMatch/MatchCounter.test.tsx | 12 +- .../SilencePreview/index.test.tsx | 16 ++- .../Components/SilenceModal/index.stories.tsx | 2 +- ui/src/Models/APITypes.ts | 7 +- ui/src/Stores/SilenceFormStore.test.ts | 109 ++++++++++++------ ui/src/Stores/SilenceFormStore.ts | 21 ++-- ui/src/__fixtures__/Alerts.ts | 2 +- ui/src/__fixtures__/Fetch.ts | 6 +- ui/src/__fixtures__/Stories.ts | 17 +-- ui/src/__fixtures__/useFetchGet.ts | 2 +- 26 files changed, 367 insertions(+), 163 deletions(-) diff --git a/ui/src/Components/AlertAck/index.test.tsx b/ui/src/Components/AlertAck/index.test.tsx index 65bb02455..e49ab36cc 100644 --- a/ui/src/Components/AlertAck/index.test.tsx +++ b/ui/src/Components/AlertAck/index.test.tsx @@ -58,11 +58,17 @@ beforeEach(() => { }); alerts = [ - MockAlert([], { foo: "bar" }, "active"), - MockAlert([], { foo: "baz" }, "active"), - MockAlert([], { foo: "ignore" }, "suppressed"), + MockAlert([], [{ name: "foo", value: "bar" }], "active"), + MockAlert([], [{ name: "foo", value: "baz" }], "active"), + MockAlert([], [{ name: "foo", value: "ignore" }], "suppressed"), ]; - group = MockAlertGroup({ alertname: "Fake Alert" }, alerts, [], {}, {}); + group = MockAlertGroup( + [{ name: "alertname", value: "Fake Alert" }], + alerts, + [], + [], + {} + ); group.allLabels.active = { alertname: ["Fake Alert"], foo: ["bar", "baz"], diff --git a/ui/src/Components/AlertHistory/index.test.tsx b/ui/src/Components/AlertHistory/index.test.tsx index 4cb4c5ad8..f71c7046b 100644 --- a/ui/src/Components/AlertHistory/index.test.tsx +++ b/ui/src/Components/AlertHistory/index.test.tsx @@ -20,18 +20,22 @@ import type { APIAlertGroupT, APIGridT, HistoryResponseT, + LabelsT, } from "Models/APITypes"; import { AlertHistory } from "."; let group: APIAlertGroupT; let grid: APIGridT; -const MockGroup = (groupName: string) => { +const MockGroup = (groupName: string, sharedLabels: LabelsT = []) => { const group = MockAlertGroup( - { alertname: "Fake Alert", groupName: groupName }, + [ + { name: "alertname", value: "Fake Alert" }, + { name: "groupName", value: groupName }, + ], [], [], - {}, + sharedLabels, {} ); return group; @@ -39,7 +43,11 @@ const MockGroup = (groupName: string) => { const MockAlerts = (alertCount: number) => { for (let i = 1; i <= alertCount; i++) { - const alert = MockAlert([], { instance: `instance${i}` }, "active"); + const alert = MockAlert( + [], + [{ name: "instance", value: `instance${i}` }], + "active" + ); const startsAt = new Date(); alert.startsAt = startsAt.toISOString(); for (let j = 0; j < alert.alertmanager.length; j++) { @@ -171,6 +179,47 @@ describe("", () => { tree.unmount(); }); + it("send a correct payload with shared labels", async () => { + fetchMock.resetHistory(); + fetchMock.mock( + "*", + { + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(EmptyHistoryResponse), + }, + { + overwriteRoutes: true, + } + ); + + MockAlerts(3); + group = MockGroup("fakeGroup", [ + { name: "shared1", value: "value1" }, + { name: "shared2", value: "value2" }, + ]); + 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", + shared1: "value1", + shared2: "value2", + foo: "bar", + }, + }) + ); + tree.unmount(); + }); + it("matches snapshot with empty response", async () => { fetchMock.resetHistory(); fetchMock.mock( @@ -433,7 +482,11 @@ describe("", () => { for (const testCase of testCases) { const g = MockGroup("fakeGroup"); for (let i = 1; i <= 5; i++) { - const alert = MockAlert([], { instance: `instance${i}` }, "active"); + const alert = MockAlert( + [], + [{ name: "instance", value: `instance${i}` }], + "active" + ); const startsAt = new Date(); alert.startsAt = startsAt.toISOString(); alert.alertmanager.push(alert.alertmanager[0]); diff --git a/ui/src/Components/AlertHistory/index.tsx b/ui/src/Components/AlertHistory/index.tsx index 2d52b046c..fc72dbd01 100644 --- a/ui/src/Components/AlertHistory/index.tsx +++ b/ui/src/Components/AlertHistory/index.tsx @@ -34,9 +34,9 @@ export const AlertHistory: FC<{ group: APIAlertGroupT; grid: APIGridT }> = ({ const [lastUpdate, setLastUpdate] = useState(GetUTCSeconds()); const [upstreams, setUpstreams] = useState([]); - const [labels] = useState({ - ...group.labels, - ...group.shared.labels, + const [labels] = useState<{ [key: string]: string }>({ + ...Object.fromEntries(group.labels.map((l) => [l.name, l.value])), + ...Object.fromEntries(group.shared.labels.map((l) => [l.name, l.value])), ...(grid.labelName !== "" && grid.labelName[0] !== "@" ? { [grid.labelName]: grid.labelValue } : {}), diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.tsx index f37a3b6d5..001b6dd7b 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.tsx @@ -74,8 +74,14 @@ beforeEach(() => { MockAfterClick = jest.fn(); MockSetIsMenuOpen = jest.fn(); - alert = MockAlert([], { foo: "bar" }, "active"); - group = MockAlertGroup({ alertname: "Fake Alert" }, [alert], [], {}, {}); + alert = MockAlert([], [{ name: "foo", value: "bar Alert" }], "active"); + group = MockAlertGroup( + [{ name: "alertname", value: "Fake Alert" }], + [alert], + [], + [], + {} + ); grid = { labelName: "foo", labelValue: "bar", @@ -233,11 +239,11 @@ describe("", () => { isAction: false, }, ], - { foo: "bar" }, + [{ name: "foo", value: "bar" }], "active" ); group = MockAlertGroup( - { alertname: "Fake Alert" }, + [{ name: "alertname", value: "Fake Alert" }], [alert], [ { @@ -262,7 +268,7 @@ describe("", () => { isAction: false, }, ], - {}, + [], {} ); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.test.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.test.tsx index 7308abba7..4acb31f17 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.test.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.test.tsx @@ -58,7 +58,10 @@ const MockedAlert = () => { MockAnnotation("hidden", "some hidden text", false, false, false), MockAnnotation("link", "http://localhost", true, true, false), ], - { job: "node_exporter", cluster: "dev" }, + [ + { name: "job", value: "node_exporter" }, + { name: "cluster", value: "dev" }, + ], "active" ); }; @@ -91,7 +94,7 @@ const MountedAlert = ( describe("", () => { it("matches snapshot with showAlertmanagers=false showReceiver=false", () => { const alert = MockedAlert(); - const group = MockAlertGroup({}, [alert], [], {}, {}); + const group = MockAlertGroup([], [alert], [], [], {}); group.shared.clusters = ["default"]; const tree = MountedAlert(alert, group, false, false); expect(toDiffableHtml(tree.html())).toMatchSnapshot(); @@ -100,7 +103,7 @@ describe("", () => { it("matches snapshot when inhibited", () => { const alert = MockedAlert(); alert.alertmanager[0].inhibitedBy = ["123456"]; - const group = MockAlertGroup({}, [alert], [], {}, {}); + const group = MockAlertGroup([], [alert], [], [], {}); group.shared.clusters = ["default"]; const tree = MountedAlert(alert, group, false, false); expect(toDiffableHtml(tree.html())).toMatchSnapshot(); @@ -119,7 +122,7 @@ describe("", () => { silencedBy: [], inhibitedBy: ["123456"], }); - const group = MockAlertGroup({}, [alert], [], {}, {}); + const group = MockAlertGroup([], [alert], [], [], {}); const tree = MountedAlert(alert, group, false, false); expect(tree.find(".fa-volume-mute")).toHaveLength(1); }); @@ -127,14 +130,14 @@ describe("", () => { it("inhibition icon passes only unique fingerprints", () => { const alert = MockedAlert(); alert.alertmanager[0].inhibitedBy = ["123456"]; - const group = MockAlertGroup({}, [alert], [], {}, {}); + const group = MockAlertGroup([], [alert], [], [], {}); const tree = MountedAlert(alert, group, false, false); expect(tree.find(".fa-volume-mute")).toHaveLength(1); }); it("renders @cluster label for non-shared clusters", () => { const alert = MockedAlert(); - const group = MockAlertGroup({}, [alert], [], {}, {}); + const group = MockAlertGroup([], [alert], [], [], {}); const tree = MountedAlert(alert, group, false, false); const label = tree .find("FilteringLabel") @@ -164,7 +167,7 @@ describe("", () => { silencedBy: [], inhibitedBy: [], }); - const group = MockAlertGroup({}, [alert], [], {}, {}); + const group = MockAlertGroup([], [alert], [], [], {}); const tree = MountedAlert(alert, group, false, false); const labels = tree .find("FilteringLabel") @@ -176,7 +179,7 @@ describe("", () => { it("renders @receiver label with showReceiver=true", () => { const alert = MockedAlert(); - const group = MockAlertGroup({}, [alert], [], {}, {}); + const group = MockAlertGroup([], [alert], [], [], {}); const tree = MountedAlert(alert, group, true, false); const label = tree .find("FilteringLabel") @@ -192,7 +195,7 @@ describe("", () => { silence123456789: MockSilence(), }, }); - const group = MockAlertGroup({}, [alert], [], {}, { default: [] }); + const group = MockAlertGroup([], [alert], [], [], { default: [] }); const tree = MountedAlert(alert, group, false, false); const silence = tree.find("ManagedSilence"); expect(silence).toHaveLength(1); @@ -207,7 +210,7 @@ describe("", () => { "123": MockSilence(), }, }); - const group = MockAlertGroup({}, [alert], [], {}, { default: [] }); + const group = MockAlertGroup([], [alert], [], [], { default: [] }); const tree = MountedAlert(alert, group, false, false); const silence = tree.find("FallbackSilenceDesciption"); expect(silence).toHaveLength(1); @@ -222,7 +225,7 @@ describe("", () => { "123": MockSilence(), }, }); - const group = MockAlertGroup({}, [alert], [], {}, { default: [] }); + const group = MockAlertGroup([], [alert], [], [], { default: [] }); const tree = MountedAlert(alert, group, false, false); const silence = tree.find("FallbackSilenceDesciption"); expect(silence).toHaveLength(1); @@ -258,7 +261,7 @@ describe("", () => { silence123456789: MockSilence(), }, }); - const group = MockAlertGroup({}, [alert], [], {}, {}); + const group = MockAlertGroup([], [alert], [], [], {}); const tree = MountedAlert(alert, group, false, false); const silence = tree.find("ManagedSilence"); expect(silence).toHaveLength(1); @@ -268,13 +271,9 @@ describe("", () => { it("doesn't render shared silences", () => { const alert = MockedAlert(); alert.alertmanager[0].silencedBy = ["silence123456789"]; - const group = MockAlertGroup( - {}, - [alert], - [], - {}, - { default: ["silence123456789"] } - ); + const group = MockAlertGroup([], [alert], [], [], { + default: ["silence123456789"], + }); const tree = MountedAlert(alert, group, false, false); const silence = tree.find("ManagedSilence"); expect(silence).toHaveLength(0); @@ -298,7 +297,7 @@ describe("", () => { isAction: false, }, ]; - const group = MockAlertGroup({}, [alert], [], {}, {}); + const group = MockAlertGroup([], [alert], [], [], {}); const tree = MountedAlert(alert, group, false, false); const annotations = tree.find("div.components-grid-annotation"); expect(annotations).toHaveLength(2); @@ -322,7 +321,7 @@ describe("", () => { isAction: false, }, ]; - const group = MockAlertGroup({}, [alert], [], {}, {}); + const group = MockAlertGroup([], [alert], [], [], {}); const tree = MountedAlert(alert, group, false, true); const annotations = tree.find("div.components-grid-annotation"); expect(annotations).toHaveLength(1); @@ -331,7 +330,7 @@ describe("", () => { it("uses BorderClassMap.active when @state=active", () => { const alert = MockedAlert(); alert.state = "active"; - const group = MockAlertGroup({}, [alert], [], {}, {}); + const group = MockAlertGroup([], [alert], [], [], {}); const tree = MountedAlert(alert, group, false, false); expect( tree @@ -343,7 +342,7 @@ describe("", () => { it("uses BorderClassMap.suppressed when @state=suppressed", () => { const alert = MockedAlert(); alert.state = "suppressed"; - const group = MockAlertGroup({}, [alert], [], {}, {}); + const group = MockAlertGroup([], [alert], [], [], {}); const tree = MountedAlert(alert, group, false, false); expect( tree @@ -355,7 +354,7 @@ describe("", () => { it("uses BorderClassMap.unprocessed when @state=unprocessed", () => { const alert = MockedAlert(); alert.state = "unprocessed"; - const group = MockAlertGroup({}, [alert], [], {}, {}); + const group = MockAlertGroup([], [alert], [], [], {}); const tree = MountedAlert(alert, group, false, false); expect( tree @@ -369,7 +368,7 @@ describe("", () => { const alert = MockedAlert(); (alert.state as string) = "foobar"; - const group = MockAlertGroup({}, [alert], [], {}, {}); + const group = MockAlertGroup([], [alert], [], [], {}); const tree = MountedAlert(alert, group, false, false); expect( tree @@ -384,7 +383,7 @@ describe("", () => { advanceTo(new Date(Date.UTC(2018, 7, 14, 17, 36, 41))); const alert = MockedAlert(); - const group = MockAlertGroup({}, [alert], [], {}, {}); + const group = MockAlertGroup([], [alert], [], [], {}); const tree = MountedAlert(alert, group, false, false); expect( tree diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.tsx index 357b8c1ff..c8ab41154 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.tsx @@ -112,11 +112,11 @@ const Alert: FC<{ {inhibitedBy.length > 0 ? ( ) : null} - {Object.entries(alert.labels).map(([name, value]) => ( + {alert.labels.map((label) => ( ))} diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.test.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.test.tsx index 4568c79b7..3444c8995 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.test.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.test.tsx @@ -23,11 +23,11 @@ let silenceFormStore: SilenceFormStore; const MockGroup = () => { const group = MockAlertGroup( - { alertname: "Fake Alert" }, + [{ name: "alertname", value: "Fake Alert" }], [ - MockAlert([], {}, "suppressed"), - MockAlert([], {}, "suppressed"), - MockAlert([], {}, "suppressed"), + MockAlert([], [], "suppressed"), + MockAlert([], [], "suppressed"), + MockAlert([], [], "suppressed"), ], [ MockAnnotation("summary", "This is summary", true, false, false), @@ -40,7 +40,10 @@ const MockGroup = () => { ), MockAnnotation("link", "http://link.example.com", true, true, false), ], - { label1: "foo", label2: "bar" }, + [ + { name: "label1", value: "foo" }, + { name: "label2", value: "bar" }, + ], {} ); return group; diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.tsx index dad8edc15..5a5a861c1 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.tsx @@ -43,11 +43,11 @@ const GroupFooter: FC<{ )) : null} - {Object.entries(group.shared.labels).map(([name, value]) => ( + {group.shared.labels.map((label) => ( ))} diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.tsx index 8a35830b6..4630bebee 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.tsx @@ -103,7 +103,13 @@ const MountedGroupMenu = (group: APIAlertGroupT, themed: boolean) => { describe("", () => { it("menu content is hidden by default", () => { - const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {}); + const group = MockAlertGroup( + [{ name: "alertname", value: "Fake Alert" }], + [], + [], + [], + {} + ); const tree = MountedGroupMenu(group, true); expect(tree.find("div.dropdown-menu")).toHaveLength(0); expect(MockSetIsMenuOpen).not.toHaveBeenCalled(); @@ -111,7 +117,13 @@ describe("", () => { it("clicking toggle renders menu content", async () => { const promise = Promise.resolve(); - const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {}); + const group = MockAlertGroup( + [{ name: "alertname", value: "Fake Alert" }], + [], + [], + [], + {} + ); const tree = MountedGroupMenu(group, true); const toggle = tree.find("span.cursor-pointer"); toggle.simulate("click"); @@ -122,7 +134,13 @@ describe("", () => { it("clicking toggle twice hides menu content", async () => { const promise = Promise.resolve(); - const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {}); + const group = MockAlertGroup( + [{ name: "alertname", value: "Fake Alert" }], + [], + [], + [], + {} + ); const tree = MountedGroupMenu(group, true); const toggle = tree.find("span.cursor-pointer"); @@ -145,7 +163,13 @@ describe("", () => { it("clicking menu item hides menu content", async () => { const promise = Promise.resolve(); - const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {}); + const group = MockAlertGroup( + [{ name: "alertname", value: "Fake Alert" }], + [], + [], + [], + {} + ); const tree = MountedGroupMenu(group, true); const toggle = tree.find("span.cursor-pointer"); @@ -180,7 +204,13 @@ const MountedMenuContent = (group: APIAlertGroupT) => { describe("", () => { it("clicking on 'Copy' icon copies the link to clickboard", () => { - const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {}); + const group = MockAlertGroup( + [{ name: "alertname", value: "Fake Alert" }], + [], + [], + [], + {} + ); const tree = MountedMenuContent(group); const button = tree.find(".dropdown-item").at(0); button.simulate("click"); @@ -188,7 +218,13 @@ describe("", () => { }); it("clicking on 'Silence' icon opens the silence form modal", () => { - const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {}); + const group = MockAlertGroup( + [{ name: "alertname", value: "Fake Alert" }], + [], + [], + [], + {} + ); group.alertmanagerCount = { am1: 1, ro: 1 }; const tree = MountedMenuContent(group); const button = tree.find(".dropdown-item").at(1); @@ -205,7 +241,13 @@ describe("", () => { upstreams.instances[2].readonly = true; alertStore.data.setUpstreams(upstreams); - const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {}); + const group = MockAlertGroup( + [{ name: "alertname", value: "Fake Alert" }], + [], + [], + [], + {} + ); const tree = MountedMenuContent(group); const button = tree.find(".dropdown-item").at(1); expect(button.hasClass("disabled")).toBe(true); @@ -215,7 +257,7 @@ describe("", () => { it("renders action annotations when present", () => { const group = MockAlertGroup( - { alertname: "Fake Alert" }, + [{ name: "alertname", value: "Fake Alert" }], [], [ { @@ -240,7 +282,7 @@ describe("", () => { isAction: false, }, ], - {}, + [], {} ); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.tsx index 3e75b5e1e..5c1503e30 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.tsx @@ -72,8 +72,8 @@ const MenuContent: FC<{ alertStore, silenceFormStore, }) => { - const groupFilters = Object.keys(group.labels).map((name) => - FormatQuery(name, QueryOperators.Equal, group.labels[name]) + const groupFilters = group.labels.map((label) => + FormatQuery(label.name, QueryOperators.Equal, label.value) ); groupFilters.push( FormatQuery(StaticLabels.Receiver, QueryOperators.Equal, group.receiver) diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/index.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/index.tsx index f11ff3a76..51894d3c3 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/index.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/index.tsx @@ -66,11 +66,11 @@ const GroupHeader: FC<{ /> - {Object.keys(group.labels).map((name) => ( + {group.labels.map((label) => ( ))} diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.tsx index a9d7d7a8e..d7f365119 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.tsx @@ -26,10 +26,13 @@ let originalInnerWidth: number; const MockGroup = (groupName: string) => { const group = MockAlertGroup( - { alertname: "Fake Alert", groupName: groupName }, + [ + { name: "alertname", value: "Fake Alert" }, + { name: "groupName", value: "groupName" }, + ], + [], [], [], - {}, {} ); return group; @@ -71,7 +74,11 @@ afterEach(() => { const MockAlerts = (alertCount: number, totalAlerts: number) => { for (let i = 1; i <= alertCount; i++) { - const alert = MockAlert([], { instance: `instance${i}` }, "active"); + const alert = MockAlert( + [], + [{ name: "instance", value: `instance${i}` }], + "active" + ); const startsAt = new Date(); alert.startsAt = startsAt.toISOString(); alert.alertmanager[0].startsAt = startsAt.toISOString(); diff --git a/ui/src/Components/Grid/AlertGrid/index.test.tsx b/ui/src/Components/Grid/AlertGrid/index.test.tsx index 44e065fce..13a5b7f51 100644 --- a/ui/src/Components/Grid/AlertGrid/index.test.tsx +++ b/ui/src/Components/Grid/AlertGrid/index.test.tsx @@ -143,13 +143,18 @@ const MountedGrid = (theme?: ThemeCtx) => { const MockGroup = (groupName: string, alertCount: number) => { const alerts = []; for (let i = 1; i <= alertCount; i++) { - alerts.push(MockAlert([], { instance: `instance${i}` }, "active")); + alerts.push( + MockAlert([], [{ name: "instance", value: `instance${i}` }], "active") + ); } const group = MockAlertGroup( - { alertname: "Fake Alert", group: groupName }, + [ + { name: "alertname", value: "Fake Alert" }, + { name: "group", value: "groupName" }, + ], alerts, [], - {}, + [], {} ); return group; diff --git a/ui/src/Components/LabelSetList/index.test.tsx b/ui/src/Components/LabelSetList/index.test.tsx index 2aae602f3..a15b143a7 100644 --- a/ui/src/Components/LabelSetList/index.test.tsx +++ b/ui/src/Components/LabelSetList/index.test.tsx @@ -35,17 +35,17 @@ describe("", () => { }); it("renders labels on populated list", () => { - const tree = MountedLabelSetList([{ foo: "bar" }]); + const tree = MountedLabelSetList([[{ name: "foo", value: "bar" }]]); expect(tree.text()).not.toBe("No alerts matched"); expect(tree.find("ul.list-group").text()).toBe("foo: bar"); }); it("matches snapshot with populated list", () => { const tree = MountedLabelSetList([ - { foo: "bar" }, - { job: "node_exporter" }, - { instance: "server1" }, - { cluster: "prod" }, + [{ name: "foo", value: "bar" }], + [{ name: "job", value: "node_exporter" }], + [{ name: "instance", value: "server1" }], + [{ name: "cluster", value: "prod" }], ]); expect(toDiffableHtml(tree.html())).toMatchSnapshot(); }); @@ -53,7 +53,9 @@ describe("", () => { it("doesn't render pagination when list has 10 elements on desktop", () => { global.window.innerWidth = 1024; const tree = MountedLabelSetList( - Array.from(Array(10), (_, i) => ({ instance: `server${i}` })) + Array.from(Array(10), (_, i) => [ + { name: "instance", value: `server${i}` }, + ]) ); expect(tree.find(".pagination")).toHaveLength(0); }); @@ -61,7 +63,9 @@ describe("", () => { it("doesn't render pagination when list has 5 elements on desktop", () => { global.window.innerWidth = 500; const tree = MountedLabelSetList( - Array.from(Array(5), (_, i) => ({ instance: `server${i}` })) + Array.from(Array(5), (_, i) => [ + { name: "instance", value: `server${i}` }, + ]) ); expect(tree.find(".pagination")).toHaveLength(0); }); @@ -69,7 +73,9 @@ describe("", () => { it("renders pagination when list has 11 elements on desktop", () => { global.window.innerWidth = 1024; const tree = MountedLabelSetList( - Array.from(Array(11), (_, i) => ({ instance: `server${i}` })) + Array.from(Array(11), (_, i) => [ + { name: "instance", value: `server${i}` }, + ]) ); expect(tree.find(".pagination")).toHaveLength(1); }); @@ -77,14 +83,18 @@ describe("", () => { it("renders pagination when list has 6 elements on mobile", () => { global.window.innerWidth = 500; const tree = MountedLabelSetList( - Array.from(Array(6), (_, i) => ({ instance: `server${i}` })) + Array.from(Array(6), (_, i) => [ + { name: "instance", value: `server${i}` }, + ]) ); expect(tree.find(".pagination")).toHaveLength(1); }); it("clicking on pagination changes displayed elements", () => { const tree = MountedLabelSetList( - Array.from(Array(21), (_, i) => ({ instance: `server${i + 1}` })) + Array.from(Array(21), (_, i) => [ + { name: "instance", value: `server${i + 1}` }, + ]) ); const pageLink = tree.find(".page-link").at(3); pageLink.simulate("click"); diff --git a/ui/src/Components/LabelSetList/index.tsx b/ui/src/Components/LabelSetList/index.tsx index 7416b931a..7a4cffa73 100644 --- a/ui/src/Components/LabelSetList/index.tsx +++ b/ui/src/Components/LabelSetList/index.tsx @@ -4,10 +4,11 @@ import type { AlertStore } from "Stores/AlertStore"; import { IsMobile } from "Common/Device"; import StaticLabel from "Components/Labels/StaticLabel"; import { PageSelect } from "Components/Pagination"; +import type { LabelsT } from "Models/APITypes"; const LabelSetList: FC<{ alertStore: AlertStore; - labelsList: { [labelName: string]: string }[]; + labelsList: LabelsT[]; title?: string; }> = ({ alertStore, labelsList, title }) => { const [activePage, setActivePage] = useState(1); @@ -26,12 +27,12 @@ const LabelSetList: FC<{ key={`${index}/${labels.length}`} className="list-group-item px-0 pt-2 pb-1" > - {Object.entries(labels).map(([name, value]) => ( + {labels.map((label) => ( ))} diff --git a/ui/src/Components/PaginatedAlertList/index.test.tsx b/ui/src/Components/PaginatedAlertList/index.test.tsx index 1c77b44ac..8c4ddb64d 100644 --- a/ui/src/Components/PaginatedAlertList/index.test.tsx +++ b/ui/src/Components/PaginatedAlertList/index.test.tsx @@ -78,11 +78,11 @@ describe("", () => { useFetchGetMock.fetch.setMockedData({ response: { alerts: [ - { - alertname: "Fake Alert", - foo: "1", - bar: "2", - }, + [ + { name: "alertname", value: "Fake Alert" }, + { name: "foo", value: "1" }, + { name: "bar", value: "2" }, + ], ], }, error: undefined, @@ -125,11 +125,11 @@ describe("", () => { useFetchGetMock.fetch.setMockedData({ response: { alerts: [ - { - alertname: "Fake Alert", - foo: "1", - bar: "2", - }, + [ + { name: "alertname", value: "Fake Alert" }, + { name: "foo", value: "1" }, + { name: "bar", value: "2" }, + ], ], }, error: undefined, diff --git a/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.tsx b/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.tsx index 6bfcc5c51..164c01863 100644 --- a/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.tsx +++ b/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.tsx @@ -35,7 +35,11 @@ const MountedMatchCounter = () => { describe("", () => { it("matches snapshot", () => { useFetchGetMock.fetch.setMockedData({ - response: { alerts: Array(25).map((i) => ({ alertname: `alert${i}` })) }, + response: { + alerts: Array(25).map((i) => [ + { name: "alertname", value: `alert${i}` }, + ]), + }, error: null, isLoading: false, isRetrying: false, @@ -110,7 +114,11 @@ describe("", () => { it("updates totalAlerts after successful fetch", () => { useFetchGetMock.fetch.setMockedData({ - response: { alerts: Array(25).map((i) => ({ alertname: `alert${i}` })) }, + response: { + alerts: Array(25).map((i) => [ + { name: "alertname", value: `alert${i}` }, + ]), + }, error: null, isLoading: false, isRetrying: false, diff --git a/ui/src/Components/SilenceModal/SilencePreview/index.test.tsx b/ui/src/Components/SilenceModal/SilencePreview/index.test.tsx index 536f17511..74814d7d6 100644 --- a/ui/src/Components/SilenceModal/SilencePreview/index.test.tsx +++ b/ui/src/Components/SilenceModal/SilencePreview/index.test.tsx @@ -65,7 +65,11 @@ describe("", () => { it("matches snapshot", () => { useFetchGetMock.fetch.setMockedData({ - response: { alerts: Array(25).map((i) => ({ alertname: `alert${i}` })) }, + response: { + alerts: Array(25).map((i) => [ + { name: "alertname", value: `alert${i}` }, + ]), + }, error: undefined, isLoading: false, isRetrying: false, @@ -94,7 +98,15 @@ describe("", () => { it("renders StaticLabel after fetch", () => { useFetchGetMock.fetch.setMockedData({ - response: { alerts: [{ alertname: "Fake Alert", foo: "1", bar: "1" }] }, + response: { + alerts: [ + [ + { name: "alertname", value: "Fake Alert" }, + { name: "foo", value: "1" }, + { name: "bar", value: "1" }, + ], + ], + }, error: undefined, isLoading: false, isRetrying: false, diff --git a/ui/src/Components/SilenceModal/index.stories.tsx b/ui/src/Components/SilenceModal/index.stories.tsx index ed7d39f68..03e56a925 100644 --- a/ui/src/Components/SilenceModal/index.stories.tsx +++ b/ui/src/Components/SilenceModal/index.stories.tsx @@ -128,7 +128,7 @@ storiesOf("SilenceModal", module) ); fetchMock.mock( "begin:/alertList.json?q=instance", - { alerts: Array(23).fill(MockAlert([], {}, "active")) }, + { alerts: Array(23).fill(MockAlert([], [], "active")) }, { overwriteRoutes: true, } diff --git a/ui/src/Models/APITypes.ts b/ui/src/Models/APITypes.ts index 32e10ff44..7ada29a7d 100644 --- a/ui/src/Models/APITypes.ts +++ b/ui/src/Models/APITypes.ts @@ -1,6 +1,11 @@ export type AlertStateT = "unprocessed" | "active" | "suppressed"; -export type LabelsT = { [key: string]: string }; +export interface LabelT { + name: string; + value: string; +} + +export type LabelsT = LabelT[]; export interface AlertmanagerSilenceMatcherT { name: string; diff --git a/ui/src/Stores/SilenceFormStore.test.ts b/ui/src/Stores/SilenceFormStore.test.ts index ce026e8c5..7a87e356e 100644 --- a/ui/src/Stores/SilenceFormStore.test.ts +++ b/ui/src/Stores/SilenceFormStore.test.ts @@ -25,17 +25,41 @@ beforeEach(() => { const MockGroup = () => { const alerts = [ - MockAlert([], { instance: "prod1", cluster: "prod" }, "active"), - MockAlert([], { instance: "prod2", cluster: "prod" }, "active"), - MockAlert([], { instance: "dev1", cluster: "dev" }, "active"), + MockAlert( + [], + [ + { name: "instance", value: "prod1" }, + { name: "cluster", value: "prod" }, + ], + "active" + ), + MockAlert( + [], + [ + { name: "instance", value: "prod2" }, + { name: "cluster", value: "prod" }, + ], + "active" + ), + MockAlert( + [], + [ + { name: "instance", value: "dev1" }, + { name: "cluster", value: "dev" }, + ], + "active" + ), ]; const group = MockAlertGroup( - { alertname: "FakeAlert" }, + [{ name: "alertname", value: "FakeAlert" }], alerts, [], - { - job: "mock", - }, + [ + { + name: "job", + value: "mock", + }, + ], {} ); return group; @@ -281,17 +305,27 @@ describe("SilenceFormStore.data", () => { it("fillMatchersFromGroup() creates correct matcher object for a list of alerts with uncommon labels", () => { const alerts = [ - MockAlert([], { instance: "1", banana: "ignore" }, "active"), - MockAlert([], { instance: "2" }, "suppressed"), - MockAlert([], { instance: "3" }, "active"), + MockAlert( + [], + [ + { name: "instance", value: "1" }, + { name: "banana", value: "ignore" }, + ], + "active" + ), + MockAlert([], [{ name: "instance", value: "2" }], "suppressed"), + MockAlert([], [{ name: "instance", value: "3" }], "active"), ]; const group = MockAlertGroup( - { alertname: "FakeAlert" }, + [{ name: "alertname", value: "FakeAlert" }], alerts, [], - { - job: "mock", - }, + [ + { + name: "job", + value: "mock", + }, + ], {} ); group.allLabels.active = { @@ -332,17 +366,15 @@ describe("SilenceFormStore.data", () => { it("fillMatchersFromGroup() creates correct matcher object for a list of alerts with no labels", () => { const alerts = [ - MockAlert([], {}, "active"), - MockAlert([], {}, "suppressed"), - MockAlert([], {}, "active"), + MockAlert([], [], "active"), + MockAlert([], [], "suppressed"), + MockAlert([], [], "active"), ]; const group = MockAlertGroup( - { alertname: "FakeAlert" }, + [{ name: "alertname", value: "FakeAlert" }], alerts, [], - { - job: "mock", - }, + [{ name: "job", value: "mock" }], {} ); group.allLabels.active = { @@ -430,32 +462,37 @@ describe("SilenceFormStore.data", () => { it("fillMatchersFromGroup() handles alerts with different label sets", () => { const group = MockAlertGroup( - { region: "AF" }, + [{ name: "region", value: "AF" }], [ MockAlert( [], - { - alertname: "Alert1", - cluster: "prod", - foo: "bar", - }, + [ + { name: "alertname", value: "Alert1" }, + { name: "cluster", value: "prod" }, + { name: "foo", value: "bar" }, + ], "active" ), MockAlert( [], - { - alertname: "Alert2", - instance: "prod2", - cluster: "prod", - }, + [ + { name: "alertname", value: "Alert2" }, + { name: "cluster", value: "prod2" }, + { name: "foo", value: "prod" }, + ], + "active" + ), + MockAlert( + [], + [ + { name: "alertname", value: "Alert3" }, + { name: "instance", value: "dev1" }, + ], "active" ), - MockAlert([], { alertname: "Alert3", instance: "dev1" }, "active"), ], [], - { - job: "mock", - }, + [{ name: "job", value: "mock" }], {} ); group.allLabels.active = { diff --git a/ui/src/Stores/SilenceFormStore.ts b/ui/src/Stores/SilenceFormStore.ts index 76127f11e..f319ec7ec 100644 --- a/ui/src/Stores/SilenceFormStore.ts +++ b/ui/src/Stores/SilenceFormStore.ts @@ -119,7 +119,11 @@ const MatchersFromAlerts = ( // add matchers for all shared labels in this group for (const [key, value] of Object.entries( - Object.assign({}, group.labels, group.shared.labels) + Object.assign( + {}, + Object.fromEntries(group.labels.map((l) => [l.name, l.value])), + Object.fromEntries(group.shared.labels.map((l) => [l.name, l.value])) + ) )) { if (!stripLabels.includes(key)) { const matcher = NewEmptyMatcher(); @@ -131,7 +135,7 @@ const MatchersFromAlerts = ( // array of arrays with label keys for each alert const allLabelKeys = alerts - .map((alert) => Object.keys(alert.labels)) + .map((alert) => alert.labels.map((l) => l.name)) .filter((a) => a.length > 0); // this is the list of label key that are shared across all alerts in the group @@ -153,12 +157,15 @@ const MatchersFromAlerts = ( // add matchers for all unique labels in this group const labels: { [key: string]: Set } = {}; for (const alert of alerts) { - for (const [key, value] of Object.entries(alert.labels)) { - if (sharedLabelKeys.includes(key) && !stripLabels.includes(key)) { - if (!labels[key]) { - labels[key] = new Set(); + for (const label of alert.labels) { + if ( + sharedLabelKeys.includes(label.name) && + !stripLabels.includes(label.name) + ) { + if (!labels[label.name]) { + labels[label.name] = new Set(); } - labels[key].add(value); + labels[label.name].add(label.value); } } } diff --git a/ui/src/__fixtures__/Alerts.ts b/ui/src/__fixtures__/Alerts.ts index 95e21e248..e26250dd9 100644 --- a/ui/src/__fixtures__/Alerts.ts +++ b/ui/src/__fixtures__/Alerts.ts @@ -63,7 +63,7 @@ const MockAlertGroup = ( suppressed: {}, unprocessed: {}, }, - id: "099c5ca6d1c92f615b13056b935d0c8dee70f18c", + id: "839708582c92ce59088d0af392601eec2d0fc02b", alertmanagerCount: { default: 1, }, diff --git a/ui/src/__fixtures__/Fetch.ts b/ui/src/__fixtures__/Fetch.ts index 800d69928..47bc98446 100644 --- a/ui/src/__fixtures__/Fetch.ts +++ b/ui/src/__fixtures__/Fetch.ts @@ -95,10 +95,10 @@ const MockAPIResponse = (): APIAlertsResponseT => { labelValue: "", alertGroups: [ MockAlertGroup( - { alertname: "foo" }, - [MockAlert([], { instance: "foo" }, "suppressed")], + [{ name: "alertname", value: "foo" }], + [MockAlert([], [{ name: "instance", value: "foo" }], "suppressed")], [], - { cluster: "dev" }, + [{ name: "cluster", value: "dev" }], {} ), ], diff --git a/ui/src/__fixtures__/Stories.ts b/ui/src/__fixtures__/Stories.ts index fa10bdd82..6685ea4a2 100644 --- a/ui/src/__fixtures__/Stories.ts +++ b/ui/src/__fixtures__/Stories.ts @@ -64,17 +64,20 @@ const MockGroup = ( }, ] : [], - { instance: `instance${i}` }, + [{ name: "instance", value: `instance${i}` }], state ); alert.startsAt = subMinutes(new Date(), alertCount).toISOString(); alerts.push(alert); } const group = MockAlertGroup( - { alertname: "Fake Alert", group: groupName }, + [ + { name: "alertname", value: "Fake Alert" }, + { name: "group", value: groupName }, + ], alerts, [], - {}, + [], {} ); return group; @@ -170,10 +173,10 @@ const MockGrid = (alertStore: AlertStore): void => { group.shared.clusters = ["default"]; } if (i < 3) { - group.shared.labels = { - cluster: `prod${i}`, - job: "textfile_exporter", - }; + group.shared.labels = [ + { name: "cluster", value: `prod${i}` }, + { name: "job", value: "textfile_exporter" }, + ]; } if (i < 5) { group.shared.annotations = [ diff --git a/ui/src/__fixtures__/useFetchGet.ts b/ui/src/__fixtures__/useFetchGet.ts index b77db0c36..5408b1f29 100644 --- a/ui/src/__fixtures__/useFetchGet.ts +++ b/ui/src/__fixtures__/useFetchGet.ts @@ -105,7 +105,7 @@ const useFetchGetMock = ( }, { re: /^\.\/alertList\.json\?q=/, - response: { alerts: [{ instance: "foo" }] }, + response: { alerts: [[{ name: "instance", value: "foo" }]] }, }, // silence browser {