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\?/,