diff --git a/CHANGELOG.md b/CHANGELOG.md index 88d0037ed..b5d17b97f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [unreleased] + +### Added + +- Added dedicated API endpoint for silence previews. + ## v0.87 ### Added diff --git a/ui/src/Components/LabelSetList/index.tsx b/ui/src/Components/LabelSetList/index.tsx index b26a0953a..7416b931a 100644 --- a/ui/src/Components/LabelSetList/index.tsx +++ b/ui/src/Components/LabelSetList/index.tsx @@ -1,33 +1,10 @@ import { FC, useState } from "react"; import type { AlertStore } from "Stores/AlertStore"; -import type { APIAlertGroupT } from "Models/APITypes"; import { IsMobile } from "Common/Device"; -import { hashObject } from "Common/Hash"; import StaticLabel from "Components/Labels/StaticLabel"; import { PageSelect } from "Components/Pagination"; -// take a list of groups and outputs a list of label sets, this ignores -// the receiver, so we'll end up with only unique alerts -const GroupListToUniqueLabelsList = ( - groups: APIAlertGroupT[] -): { [labelName: string]: string }[] => { - const alerts: { [alertHash: string]: { [labelName: string]: string } } = {}; - for (const group of groups) { - for (const alert of group.alerts) { - const alertLabels = Object.assign( - {}, - group.labels, - group.shared.labels, - alert.labels - ); - const alertHash = hashObject(alertLabels); - alerts[alertHash] = alertLabels; - } - } - return Object.values(alerts); -}; - const LabelSetList: FC<{ alertStore: AlertStore; labelsList: { [labelName: string]: string }[]; @@ -77,4 +54,4 @@ const LabelSetList: FC<{ ); }; -export { LabelSetList, GroupListToUniqueLabelsList }; +export { LabelSetList }; diff --git a/ui/src/Components/PaginatedAlertList/index.test.tsx b/ui/src/Components/PaginatedAlertList/index.test.tsx index 77e7b0f09..1c77b44ac 100644 --- a/ui/src/Components/PaginatedAlertList/index.test.tsx +++ b/ui/src/Components/PaginatedAlertList/index.test.tsx @@ -3,7 +3,6 @@ import { mount } from "enzyme"; import { advanceTo, clear } from "jest-date-mock"; import { useFetchGetMock } from "__fixtures__/useFetchGet"; -import { EmptyAPIResponse } from "__fixtures__/Fetch"; import { AlertStore } from "Stores/AlertStore"; import { useFetchGet } from "Hooks/useFetchGet"; import { PaginatedAlertList } from "."; @@ -76,6 +75,23 @@ describe("", () => { }); it("renders LabelSetList with StaticLabel on mount", () => { + useFetchGetMock.fetch.setMockedData({ + response: { + alerts: [ + { + alertname: "Fake Alert", + foo: "1", + bar: "2", + }, + ], + }, + error: undefined, + isLoading: false, + isRetrying: false, + retryCount: 0, + get: jest.fn(), + cancelGet: jest.fn(), + }); const tree = mount( ); @@ -85,7 +101,7 @@ describe("", () => { it("renders empty LabelSetList with empty response", () => { useFetchGetMock.fetch.setMockedData({ - response: EmptyAPIResponse(), + response: { alerts: [] }, error: null, isLoading: false, isRetrying: false, @@ -106,6 +122,23 @@ describe("", () => { }); it("renders StaticLabel after fetch", () => { + useFetchGetMock.fetch.setMockedData({ + response: { + alerts: [ + { + alertname: "Fake Alert", + foo: "1", + bar: "2", + }, + ], + }, + error: undefined, + isLoading: false, + isRetrying: false, + retryCount: 0, + get: jest.fn(), + cancelGet: jest.fn(), + }); const tree = mount( ", () => { it("handles empty grid response correctly", () => { useFetchGetMock.fetch.setMockedData({ - response: EmptyAPIResponse(), + response: { alerts: [] }, error: null, isLoading: false, isRetrying: false, diff --git a/ui/src/Components/PaginatedAlertList/index.tsx b/ui/src/Components/PaginatedAlertList/index.tsx index 190f57d10..935fcc781 100644 --- a/ui/src/Components/PaginatedAlertList/index.tsx +++ b/ui/src/Components/PaginatedAlertList/index.tsx @@ -4,12 +4,9 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle"; import { faSpinner } from "@fortawesome/free-solid-svg-icons/faSpinner"; -import type { APIAlertsResponseT } from "Models/APITypes"; +import type { AlertListResponseT } from "Models/APITypes"; import { AlertStore, FormatBackendURI, FormatAlertsQ } from "Stores/AlertStore"; -import { - LabelSetList, - GroupListToUniqueLabelsList, -} from "Components/LabelSetList"; +import { LabelSetList } from "Components/LabelSetList"; import { useFetchGet } from "Hooks/useFetchGet"; const FetchError: FC<{ message: ReactNode }> = ({ message }) => ( @@ -34,8 +31,8 @@ const PaginatedAlertList: FC<{ filters: string[]; title?: string; }> = ({ alertStore, filters, title }) => { - const { response, error, isLoading } = useFetchGet( - FormatBackendURI("alerts.json?") + FormatAlertsQ(filters) + const { response, error, isLoading } = useFetchGet( + FormatBackendURI("alertList.json?") + FormatAlertsQ(filters) ); return error ? ( @@ -45,9 +42,7 @@ const PaginatedAlertList: FC<{ ) : ( ); diff --git a/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.tsx b/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.tsx index 836263267..6bfcc5c51 100644 --- a/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.tsx +++ b/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.tsx @@ -3,7 +3,6 @@ import { mount } from "enzyme"; import toDiffableHtml from "diffable-html"; import { useFetchGetMock } from "__fixtures__/useFetchGet"; -import { EmptyAPIResponse } from "__fixtures__/Fetch"; import { SilenceFormStore, NewEmptyMatcher, @@ -35,6 +34,15 @@ const MountedMatchCounter = () => { describe("", () => { it("matches snapshot", () => { + useFetchGetMock.fetch.setMockedData({ + response: { alerts: Array(25).map((i) => ({ alertname: `alert${i}` })) }, + error: null, + isLoading: false, + isRetrying: false, + retryCount: 0, + get: jest.fn(), + cancelGet: jest.fn(), + }); const tree = MountedMatchCounter(); expect(toDiffableHtml(tree.html())).toMatchSnapshot(); }); @@ -86,10 +94,8 @@ describe("", () => { }); it("totalAlerts is 0 after mount", () => { - const r = EmptyAPIResponse(); - r.totalAlerts = 0; useFetchGetMock.fetch.setMockedData({ - response: r, + response: { alerts: [] }, error: null, isLoading: false, isRetrying: false, @@ -103,6 +109,15 @@ describe("", () => { }); it("updates totalAlerts after successful fetch", () => { + useFetchGetMock.fetch.setMockedData({ + response: { alerts: Array(25).map((i) => ({ alertname: `alert${i}` })) }, + error: null, + isLoading: false, + isRetrying: false, + retryCount: 0, + get: jest.fn(), + cancelGet: jest.fn(), + }); const tree = MountedMatchCounter(); expect(tree.text()).toBe("25"); }); @@ -111,7 +126,7 @@ describe("", () => { MountedMatchCounter(); expect( (useFetchGet as jest.MockedFunction).mock.calls[0][0] - ).toBe("./alerts.json?q=foo%3Dbar"); + ).toBe("./alertList.json?q=foo%3Dbar"); }); it("sends correct query string for a 'foo=~bar' matcher", () => { @@ -119,7 +134,7 @@ describe("", () => { MountedMatchCounter(); expect( (useFetchGet as jest.MockedFunction).mock.calls[0][0] - ).toBe("./alerts.json?q=foo%3D~%5Ebar%24"); + ).toBe("./alertList.json?q=foo%3D~%5Ebar%24"); }); it("sends correct query string for a 'foo=~(bar|baz)' matcher", () => { @@ -129,7 +144,7 @@ describe("", () => { MountedMatchCounter(); expect( (useFetchGet as jest.MockedFunction).mock.calls[0][0] - ).toBe("./alerts.json?q=foo%3D~%5E%28bar%7Cbaz%29%24"); + ).toBe("./alertList.json?q=foo%3D~%5E%28bar%7Cbaz%29%24"); }); it("selecting one Alertmanager instance appends it to the filters", () => { @@ -142,7 +157,7 @@ describe("", () => { MountedMatchCounter(); expect( (useFetchGet as jest.MockedFunction).mock.calls[0][0] - ).toBe("./alerts.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28am1%29%24"); + ).toBe("./alertList.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28am1%29%24"); }); it("selecting two Alertmanager instances appends it correctly to the filters", () => { @@ -160,7 +175,7 @@ describe("", () => { expect( (useFetchGet as jest.MockedFunction).mock.calls[0][0] ).toBe( - "./alerts.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28am1%7Cam2%29%24" + "./alertList.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28am1%7Cam2%29%24" ); }); }); diff --git a/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.tsx b/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.tsx index d55f84055..487d09e91 100644 --- a/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.tsx +++ b/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.tsx @@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle"; import { faSpinner } from "@fortawesome/free-solid-svg-icons/faSpinner"; -import type { APIAlertsResponseT } from "Models/APITypes"; +import type { AlertListResponseT } from "Models/APITypes"; import { FormatBackendURI, FormatAlertsQ } from "Stores/AlertStore"; import type { SilenceFormStore, MatcherWithIDT } from "Stores/SilenceFormStore"; import { TooltipWrapper } from "Components/TooltipWrapper"; @@ -23,8 +23,8 @@ const MatchCounter: FC<{ } const { response, error, isLoading, isRetrying } = - useFetchGet( - FormatBackendURI("alerts.json?") + FormatAlertsQ(filters) + useFetchGet( + FormatBackendURI("alertList.json?") + FormatAlertsQ(filters) ); return error ? ( @@ -47,7 +47,7 @@ const MatchCounter: FC<{ className={isRetrying ? "text-danger" : ""} /> ) : ( - Math.max(response.totalAlerts, 0) + response.alerts.length )} diff --git a/ui/src/Components/SilenceModal/SilencePreview/__snapshots__/index.test.tsx.snap b/ui/src/Components/SilenceModal/SilencePreview/__snapshots__/index.test.tsx.snap index cb735a05b..69440a95a 100644 --- a/ui/src/Components/SilenceModal/SilencePreview/__snapshots__/index.test.tsx.snap +++ b/ui/src/Components/SilenceModal/SilencePreview/__snapshots__/index.test.tsx.snap @@ -9,87 +9,79 @@ exports[` matches snapshot 1`] = `

    -
  • - - - alertname: - - - foo - - - - - job: - - - foo - - - - - instance: - - - foo1 - - -
  • -
  • - - - alertname: - - - bar - - - - - job: - - - bar - - - - - instance: - - - bar1 - - -
  • -
  • - - - alertname: - - - bar - - - - - job: - - - bar - - - - - instance: - - - bar2 - - -
+
+ +
diff --git a/ui/src/Components/SilenceModal/SilencePreview/index.test.tsx b/ui/src/Components/SilenceModal/SilencePreview/index.test.tsx index 8c675ed19..536f17511 100644 --- a/ui/src/Components/SilenceModal/SilencePreview/index.test.tsx +++ b/ui/src/Components/SilenceModal/SilencePreview/index.test.tsx @@ -2,8 +2,6 @@ import { mount } from "enzyme"; import toDiffableHtml from "diffable-html"; -import { EmptyAPIResponse } from "__fixtures__/Fetch"; -import { MockAlertGroup, MockAlert } from "__fixtures__/Alerts"; import { AlertStore } from "Stores/AlertStore"; import { SilenceFormStore, NewEmptyMatcher } from "Stores/SilenceFormStore"; import { useFetchGet } from "Hooks/useFetchGet"; @@ -30,43 +28,6 @@ afterEach(() => { (useFetchGet as jest.MockedFunction).mockReset(); }); -const MockAPIResponse = () => { - const response = EmptyAPIResponse(); - - response.grids = [ - { - labelName: "", - labelValue: "", - alertGroups: [ - MockAlertGroup( - { alertname: "foo" }, - [MockAlert([], { instance: "foo1" }, "active")], - [], - { job: "foo" }, - {} - ), - MockAlertGroup( - { alertname: "bar" }, - [ - MockAlert([], { instance: "bar1" }, "active"), - MockAlert([], { instance: "bar2" }, "active"), - ], - [], - { job: "bar" }, - {} - ), - ], - totalGroups: 2, - stateCount: { - unprocessed: 1, - suppressed: 2, - active: 3, - }, - }, - ]; - return response; -}; - const MountedSilencePreview = () => { return mount( ", () => { ]); MountedSilencePreview(); expect(useFetchGet).toHaveBeenCalledWith( - "./alerts.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28amValue%29%24" + "./alertList.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28amValue%29%24" ); }); @@ -98,13 +59,13 @@ describe("", () => { ]); MountedSilencePreview(); expect(useFetchGet).toHaveBeenCalledWith( - "./alerts.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28am1%7Cam2%29%24" + "./alertList.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28am1%7Cam2%29%24" ); }); it("matches snapshot", () => { useFetchGetMock.fetch.setMockedData({ - response: MockAPIResponse(), + response: { alerts: Array(25).map((i) => ({ alertname: `alert${i}` })) }, error: undefined, isLoading: false, isRetrying: false, @@ -132,6 +93,15 @@ describe("", () => { }); it("renders StaticLabel after fetch", () => { + useFetchGetMock.fetch.setMockedData({ + response: { alerts: [{ alertname: "Fake Alert", foo: "1", bar: "1" }] }, + error: undefined, + isLoading: false, + isRetrying: false, + retryCount: 0, + get: jest.fn(), + cancelGet: jest.fn(), + }); const tree = MountedSilencePreview(); expect(tree.text()).toMatch(/Affected alerts/); expect(tree.find("StaticLabel")).toHaveLength(3); @@ -139,7 +109,7 @@ describe("", () => { it("handles empty grid response correctly", () => { useFetchGetMock.fetch.setMockedData({ - response: EmptyAPIResponse(), + response: { alerts: [] }, 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 d46e75fff..ed7d39f68 100644 --- a/ui/src/Components/SilenceModal/index.stories.tsx +++ b/ui/src/Components/SilenceModal/index.stories.tsx @@ -7,7 +7,7 @@ import { storiesOf } from "@storybook/react"; import addHours from "date-fns/addHours"; import addDays from "date-fns/addDays"; -import { MockSilence } from "../../__fixtures__/Alerts"; +import { MockSilence, MockAlert } from "../../__fixtures__/Alerts"; import { AlertStore } from "../../Stores/AlertStore"; import { Settings } from "../../Stores/Settings"; import { @@ -120,21 +120,21 @@ storiesOf("SilenceModal", module) silenceFormStore.tab.current = "editor"; fetchMock.mock( - "begin:/alerts.json?q=cluster", - { totalAlerts: 0 }, + "begin:/alertList.json?q=cluster", + { alerts: [] }, { overwriteRoutes: true, } ); fetchMock.mock( - "begin:/alerts.json?q=instance", - { totalAlerts: 23 }, + "begin:/alertList.json?q=instance", + { alerts: Array(23).fill(MockAlert([], {}, "active")) }, { overwriteRoutes: true, } ); fetchMock.mock( - "begin:/alerts.json?q=tooLong", + "begin:/alertList.json?q=tooLong", { body: "error", status: 503, diff --git a/ui/src/Models/APITypes.ts b/ui/src/Models/APITypes.ts index 723dbeba7..0c39b70c6 100644 --- a/ui/src/Models/APITypes.ts +++ b/ui/src/Models/APITypes.ts @@ -219,3 +219,7 @@ export interface HistoryResponseT { error: string; samples: HistorySampleT[]; } + +export interface AlertListResponseT { + alerts: LabelsT[]; +} diff --git a/ui/src/__fixtures__/useFetchGet.ts b/ui/src/__fixtures__/useFetchGet.ts index be030d6d8..e77446e99 100644 --- a/ui/src/__fixtures__/useFetchGet.ts +++ b/ui/src/__fixtures__/useFetchGet.ts @@ -2,9 +2,18 @@ import { useState, useEffect, useCallback } from "react"; import { MockAPIResponse, MockSilenceResponse } from "__fixtures__/Fetch"; import type { FetchGetResultT } from "Hooks/useFetchGet"; -import type { APIAlertsResponseT, APIManagedSilenceT } from "Models/APITypes"; +import type { + APIAlertsResponseT, + APIManagedSilenceT, + AlertListResponseT, +} from "Models/APITypes"; -type responseT = null | string[] | APIAlertsResponseT | APIManagedSilenceT[]; +type responseT = + | null + | string[] + | APIAlertsResponseT + | APIManagedSilenceT[] + | AlertListResponseT; interface mockedDataT { response: undefined | responseT; @@ -92,6 +101,10 @@ const useFetchGetMock = ( re: /^\.\/alerts\.json\?q=/, response: MockAPIResponse(), }, + { + re: /^\.\/alertList\.json\?q=/, + response: { alerts: [{ instance: "foo" }] }, + }, // silence browser { re: /^.\/silences\.json\?/,