feat(ui): use alertList endpoint for silence previews

This commit is contained in:
Łukasz Mierzwa
2021-07-02 21:54:37 +01:00
committed by Łukasz Mierzwa
parent f13cd9a112
commit daffb98381
11 changed files with 184 additions and 179 deletions

View File

@@ -1,5 +1,11 @@
# Changelog
## [unreleased]
### Added
- Added dedicated API endpoint for silence previews.
## v0.87
### Added

View File

@@ -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 };

View File

@@ -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("<PaginatedAlertList />", () => {
});
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(
<PaginatedAlertList alertStore={alertStore} filters={["foo=bar"]} />
);
@@ -85,7 +101,7 @@ describe("<PaginatedAlertList />", () => {
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("<PaginatedAlertList />", () => {
});
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(
<PaginatedAlertList
alertStore={alertStore}
@@ -119,7 +152,7 @@ describe("<PaginatedAlertList />", () => {
it("handles empty grid response correctly", () => {
useFetchGetMock.fetch.setMockedData({
response: EmptyAPIResponse(),
response: { alerts: [] },
error: null,
isLoading: false,
isRetrying: false,

View File

@@ -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<APIAlertsResponseT>(
FormatBackendURI("alerts.json?") + FormatAlertsQ(filters)
const { response, error, isLoading } = useFetchGet<AlertListResponseT>(
FormatBackendURI("alertList.json?") + FormatAlertsQ(filters)
);
return error ? (
@@ -45,9 +42,7 @@ const PaginatedAlertList: FC<{
) : (
<LabelSetList
alertStore={alertStore}
labelsList={GroupListToUniqueLabelsList(
response.grids.length ? response.grids[0].alertGroups : []
)}
labelsList={response.alerts}
title={title}
/>
);

View File

@@ -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("<MatchCounter />", () => {
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("<MatchCounter />", () => {
});
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("<MatchCounter />", () => {
});
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("<MatchCounter />", () => {
MountedMatchCounter();
expect(
(useFetchGet as jest.MockedFunction<typeof useFetchGet>).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("<MatchCounter />", () => {
MountedMatchCounter();
expect(
(useFetchGet as jest.MockedFunction<typeof useFetchGet>).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("<MatchCounter />", () => {
MountedMatchCounter();
expect(
(useFetchGet as jest.MockedFunction<typeof useFetchGet>).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("<MatchCounter />", () => {
MountedMatchCounter();
expect(
(useFetchGet as jest.MockedFunction<typeof useFetchGet>).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("<MatchCounter />", () => {
expect(
(useFetchGet as jest.MockedFunction<typeof useFetchGet>).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"
);
});
});

View File

@@ -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<APIAlertsResponseT>(
FormatBackendURI("alerts.json?") + FormatAlertsQ(filters)
useFetchGet<AlertListResponseT>(
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
)}
</span>
</TooltipWrapper>

View File

@@ -9,87 +9,79 @@ exports[`<SilencePreview /> matches snapshot 1`] = `
</p>
<div>
<ul class=\\"list-group list-group-flush mb-3\\">
<li class=\\"list-group-item px-0 pt-2 pb-1\\">
<span class=\\"components-label badge bg-dark components-label-dark \\">
<span class=\\"components-label-name\\">
alertname:
</span>
<span class=\\"components-label-value\\">
foo
</span>
</span>
<span class=\\"components-label badge bg-default components-label-dark \\">
<span class=\\"components-label-name\\">
job:
</span>
<span class=\\"components-label-value\\">
foo
</span>
</span>
<span class=\\"components-label badge bg-default components-label-dark \\">
<span class=\\"components-label-name\\">
instance:
</span>
<span class=\\"components-label-value\\">
foo1
</span>
</span>
</li>
<li class=\\"list-group-item px-0 pt-2 pb-1\\">
<span class=\\"components-label badge bg-dark components-label-dark \\">
<span class=\\"components-label-name\\">
alertname:
</span>
<span class=\\"components-label-value\\">
bar
</span>
</span>
<span class=\\"components-label badge bg-default components-label-dark \\">
<span class=\\"components-label-name\\">
job:
</span>
<span class=\\"components-label-value\\">
bar
</span>
</span>
<span class=\\"components-label badge bg-default components-label-dark \\">
<span class=\\"components-label-name\\">
instance:
</span>
<span class=\\"components-label-value\\">
bar1
</span>
</span>
</li>
<li class=\\"list-group-item px-0 pt-2 pb-1\\">
<span class=\\"components-label badge bg-dark components-label-dark \\">
<span class=\\"components-label-name\\">
alertname:
</span>
<span class=\\"components-label-value\\">
bar
</span>
</span>
<span class=\\"components-label badge bg-default components-label-dark \\">
<span class=\\"components-label-name\\">
job:
</span>
<span class=\\"components-label-value\\">
bar
</span>
</span>
<span class=\\"components-label badge bg-default components-label-dark \\">
<span class=\\"components-label-name\\">
instance:
</span>
<span class=\\"components-label-value\\">
bar2
</span>
</span>
</li>
</ul>
</div>
<div class=\\"components-pagination\\">
<div class=\\"mt-3\\">
<ul class=\\"pagination justify-content-center\\">
<li class=\\"page-item disabled\\">
<a class=\\"page-link\\"
href=\\"#\\"
aria-label=\\"Go to previous page\\"
>
<svg aria-hidden=\\"true\\"
focusable=\\"false\\"
data-prefix=\\"fas\\"
data-icon=\\"angle-left\\"
class=\\"svg-inline--fa fa-angle-left fa-w-8 \\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 256 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M31.7 239l136-136c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9L127.9 256l96.4 96.4c9.4 9.4 9.4 24.6 0 33.9L201.7 409c-9.4 9.4-24.6 9.4-33.9 0l-136-136c-9.5-9.4-9.5-24.6-.1-34z\\"
>
</path>
</svg>
</a>
</li>
<li class=\\"page-item active\\">
<a class=\\"page-link font-weight-bold\\"
href=\\"#\\"
aria-label=\\"Go to page number 1\\"
>
1
</a>
</li>
<li class=\\"page-item\\">
<a class=\\"page-link\\"
href=\\"#\\"
aria-label=\\"Go to page number 2\\"
>
2
</a>
</li>
<li class=\\"page-item\\">
<a class=\\"page-link\\"
href=\\"#\\"
aria-label=\\"Go to page number 3\\"
>
3
</a>
</li>
<li class=\\"page-item\\">
<a class=\\"page-link\\"
href=\\"#\\"
aria-label=\\"Go to next page\\"
>
<svg aria-hidden=\\"true\\"
focusable=\\"false\\"
data-prefix=\\"fas\\"
data-icon=\\"angle-right\\"
class=\\"svg-inline--fa fa-angle-right fa-w-8 \\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 256 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M224.3 273l-136 136c-9.4 9.4-24.6 9.4-33.9 0l-22.6-22.6c-9.4-9.4-9.4-24.6 0-33.9l96.4-96.4-96.4-96.4c-9.4-9.4-9.4-24.6 0-33.9L54.3 103c9.4-9.4 24.6-9.4 33.9 0l136 136c9.5 9.4 9.5 24.6.1 34z\\"
>
</path>
</svg>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>

View File

@@ -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<typeof useFetchGetMock>).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(
<SilencePreview
@@ -88,7 +49,7 @@ describe("<SilencePreview />", () => {
]);
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("<SilencePreview />", () => {
]);
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("<SilencePreview />", () => {
});
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("<SilencePreview />", () => {
it("handles empty grid response correctly", () => {
useFetchGetMock.fetch.setMockedData({
response: EmptyAPIResponse(),
response: { alerts: [] },
error: undefined,
isLoading: false,
isRetrying: false,

View File

@@ -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,

View File

@@ -219,3 +219,7 @@ export interface HistoryResponseT {
error: string;
samples: HistorySampleT[];
}
export interface AlertListResponseT {
alerts: LabelsT[];
}

View File

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