feat(ui): paginate api

This commit is contained in:
Łukasz Mierzwa
2021-06-28 12:28:40 +01:00
committed by Łukasz Mierzwa
parent a817fd4701
commit 04379fc3cc
28 changed files with 442 additions and 75 deletions
+4
View File
@@ -7,6 +7,10 @@
- Use [uber-go/automaxprocs](https://github.com/uber-go/automaxprocs)
to automatically adjust `GOMAXPROCS` to match Linux container CPU quota.
Runtime value of `GOMAXPROCS` is now exported as a `go_max_procs` metric.
- API will now return only first 40 alert groups by default, the rest can
be loaded when user clicks on the `Load more` button.
`grid:groupLimit` config option was added to customise how many groups
are returned and displayed in the UI by default.
## v0.86
+23
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"net/http"
"sort"
"strconv"
"strings"
"github.com/prymitive/karma/internal/alertmanager"
@@ -23,6 +24,28 @@ func lookupQueryStringSlice(r *http.Request, key string) ([]string, bool) {
return vals, found
}
func lookupLoadLimits(r *http.Request, key string) map[string]int {
limits := map[string]int{}
vals, found := lookupQueryStringSlice(r, key)
if !found {
return limits
}
var limit int
var err error
for _, val := range vals {
valSlice := strings.SplitN(val, "=", 2)
if len(valSlice) != 2 {
continue
}
limit, err = strconv.Atoi(valSlice[1])
if err != nil {
continue
}
limits[valSlice[0]] = limit
}
return limits
}
// knownLabelNames allows querying known label names
func knownLabelNames(w http.ResponseWriter, r *http.Request) {
noCache(w)
@@ -34,6 +34,7 @@ env GRID_SORTING_REVERSE=false
env GRID_SORTING_LABEL=severity
env GRID_AUTO_IGNORE="region instance"
env GRID_AUTO_ORDER="severity cluster"
env GRID_GROUPLIMIT=1
env KARMA_NAME=karma-demo
@@ -153,6 +154,7 @@ level=info msg=" ignore:"
level=info msg=" - '\"region'"
level=info msg=" order:"
level=info msg=" - '\"severity'"
level=info msg=" groupLimit: 1"
level=info msg="history:"
level=info msg=" enabled: true"
level=info msg=" workers: 30"
@@ -167,6 +167,7 @@ level=info msg=" - instance"
level=info msg=" order:"
level=info msg=" - severity"
level=info msg=" - cluster"
level=info msg=" groupLimit: 1"
level=info msg="history:"
level=info msg=" enabled: false"
level=info msg=" workers: 123"
@@ -367,6 +368,7 @@ grid:
order:
- severity
- cluster
groupLimit: 1
history:
enabled: false
workers: 123
@@ -70,6 +70,7 @@ level=info msg=" labels: {}"
level=info msg=" auto:"
level=info msg=" ignore: []"
level=info msg=" order: []"
level=info msg=" groupLimit: 40"
level=info msg="history:"
level=info msg=" enabled: true"
level=info msg=" workers: 30"
+1
View File
@@ -70,6 +70,7 @@ level=info msg=" labels: {}"
level=info msg=" auto:"
level=info msg=" ignore: []"
level=info msg=" order: []"
level=info msg=" groupLimit: 40"
level=info msg="history:"
level=info msg=" enabled: true"
level=info msg=" workers: 30"
@@ -70,6 +70,7 @@ level=info msg=" labels: {}"
level=info msg=" auto:"
level=info msg=" ignore: []"
level=info msg=" order: []"
level=info msg=" groupLimit: 40"
level=info msg="history:"
level=info msg=" enabled: true"
level=info msg=" workers: 30"
@@ -72,6 +72,7 @@ level=info msg=" labels: {}"
level=info msg=" auto:"
level=info msg=" ignore: []"
level=info msg=" order: []"
level=info msg=" groupLimit: 40"
level=info msg="history:"
level=info msg=" enabled: true"
level=info msg=" workers: 30"
@@ -70,6 +70,7 @@ level=info msg=" labels: {}"
level=info msg=" auto:"
level=info msg=" ignore: []"
level=info msg=" order: []"
level=info msg=" groupLimit: 40"
level=info msg="history:"
level=info msg=" enabled: true"
level=info msg=" workers: 30"
@@ -70,6 +70,7 @@ level=info msg=" labels: {}"
level=info msg=" auto:"
level=info msg=" ignore: []"
level=info msg=" order: []"
level=info msg=" groupLimit: 40"
level=info msg="history:"
level=info msg=" enabled: true"
level=info msg=" workers: 30"
+25 -5
View File
@@ -182,6 +182,7 @@ func alerts(w http.ResponseWriter, r *http.Request) {
Comment: config.Config.AlertAcknowledgement.Comment,
},
HistoryEnabled: config.Config.History.Enabled,
GridGroupLimit: config.Config.Grid.GroupLimit,
}
resp.Authentication = models.AuthenticationInfo{
Enabled: config.Config.Authentication.Enabled,
@@ -215,6 +216,7 @@ func alerts(w http.ResponseWriter, r *http.Request) {
gridLabel, _ := lookupQueryString(r, "gridLabel")
q, _ := lookupQueryStringSlice(r, "q")
matchFilters := getFiltersFromQuery(q)
limits := lookupLoadLimits(r, "limit")
grids := map[string]models.APIGrid{}
colors := models.LabelsColorMap{}
@@ -438,16 +440,34 @@ func alerts(w http.ResponseWriter, r *http.Request) {
}
}
//resp.AlertGroups = sortAlertGroups(c, alerts)
v, _ := lookupQueryString(r, "gridSortReverse")
gridSortReverse := v == "1"
sortedGrids := sortGrids(r, gridLabel, grids, gridSortReverse)
for i := 0; i < len(sortedGrids); i++ {
sortedGrids[i].TotalGroups = len(sortedGrids[i].AlertGroups)
limit, found := limits[sortedGrids[i].LabelValue]
if !found {
limit = config.Config.Grid.GroupLimit
}
l := sortedGrids[i].TotalGroups
if limit < l {
l = limit
}
if l < 1 {
l = 1
}
sortedGrids[i].AlertGroups = sortedGrids[i].AlertGroups[:l]
}
for _, filter := range matchFilters {
if filter.GetValue() != "" && filter.GetMatcher() == "=" {
transform.ColorLabel(colors, filter.GetName(), filter.GetValue())
}
}
//resp.AlertGroups = sortAlertGroups(c, alerts)
v, _ := lookupQueryString(r, "gridSortReverse")
gridSortReverse := v == "1"
receivers := []string{}
for k := range allReceivers {
k := k
@@ -455,7 +475,7 @@ func alerts(w http.ResponseWriter, r *http.Request) {
}
sort.Strings(receivers)
resp.Grids = sortGrids(r, gridLabel, grids, gridSortReverse)
resp.Grids = sortedGrids
resp.Silences = silences
resp.Colors = colors
resp.Counters = countersToLabelStats(counters)
+150
View File
@@ -965,6 +965,7 @@ func TestEmptySettings(t *testing.T) {
Comment: "ACK! This alert was acknowledged using karma on %NOW%",
},
HistoryEnabled: true,
GridGroupLimit: 40,
}
if diff := cmp.Diff(expectedSettings, ur.Settings); diff != "" {
@@ -2845,3 +2846,152 @@ func TestAutoGrid(t *testing.T) {
}
}
}
func TestGridLimit(t *testing.T) {
type testCaseT struct {
groupLimit int
q string
groups map[string][]int
}
testCases := []testCaseT{
{
q: "",
groups: map[string][]int{
"": {10, 10},
},
},
{
groupLimit: 5,
q: "",
groups: map[string][]int{
"": {10, 5},
},
},
{
groupLimit: 15,
q: "",
groups: map[string][]int{
"": {10, 10},
},
},
{
q: "limit==1",
groups: map[string][]int{
"": {10, 1},
},
},
{
q: "limit",
groups: map[string][]int{
"": {10, 10},
},
},
{
q: "limit=",
groups: map[string][]int{
"": {10, 10},
},
},
{
q: "gridLabel=job&limit=node_exporter=1",
groups: map[string][]int{
"node_exporter": {6, 1},
"node_ping": {4, 4},
},
},
{
q: "gridLabel=job&limit=node_exporter=a&limit=node_ping=1",
groups: map[string][]int{
"node_exporter": {6, 6},
"node_ping": {4, 1},
},
},
{
q: "gridLabel=job&limit=node_exporter=0&limit=node_ping=2",
groups: map[string][]int{
"node_exporter": {6, 1},
"node_ping": {4, 2},
},
},
{
q: "gridLabel=job&limit=node_exporter=0&limit=node_ping=20",
groups: map[string][]int{
"node_exporter": {6, 1},
"node_ping": {4, 4},
},
},
}
defer func() {
config.Config.Grid.GroupLimit = 50
}()
mockConfig()
for _, tc := range testCases {
if tc.groupLimit > 0 {
config.Config.Grid.GroupLimit = tc.groupLimit
} else {
config.Config.Grid.GroupLimit = 50
}
for _, version := range mock.ListAllMocks() {
t.Logf("Testing grids using mock files from Alertmanager %s", version)
mockAlerts(version)
r := testRouter()
setupRouter(r, nil)
// re-run a few times to test the cache
for i := 1; i <= 3; i++ {
req := httptest.NewRequest("GET", fmt.Sprintf("/alerts.json?%s", tc.q), nil)
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != http.StatusOK {
t.Errorf("GET /alerts.json returned status %d", resp.Code)
}
ur := models.AlertsResponse{}
err := json.Unmarshal(resp.Body.Bytes(), &ur)
if err != nil {
t.Errorf("Failed to unmarshal response: %s", err)
}
if len(ur.Grids) == 0 {
t.Errorf("[%s] Got empty grid list", tc.q)
}
for _, grid := range ur.Grids {
if grid.TotalGroups == 0 {
t.Errorf("[%s] got empty grid for %s=%s", tc.q, grid.LabelName, grid.LabelValue)
}
found := false
for labelValue := range tc.groups {
if grid.LabelValue == labelValue {
found = true
break
}
}
if !found {
t.Errorf("[%s] got extra grid %s=%s", tc.q, grid.LabelName, grid.LabelValue)
}
}
for labelValue, totals := range tc.groups {
totalGroups := totals[0]
presentGroups := totals[1]
found := false
for _, grid := range ur.Grids {
if grid.LabelValue == labelValue {
found = true
if grid.TotalGroups != totalGroups {
t.Errorf("[%s] grid for label %s=%s returned totalGroups=%d, expected %d", tc.q, grid.LabelName, grid.LabelValue, grid.TotalGroups, totalGroups)
}
if len(grid.AlertGroups) != presentGroups {
t.Errorf("[%s] grid for label %s=%s returned %d alert groups, expected %d", tc.q, grid.LabelName, grid.LabelValue, len(grid.AlertGroups), presentGroups)
}
break
}
}
if !found {
t.Errorf("[%s] grid with label value %s missing", tc.q, labelValue)
}
}
}
}
}
}
+4
View File
@@ -648,6 +648,7 @@ grid:
auto:
ignore: list of strings
order: list of strings
groupLimit: integer
```
- `sorting:order` - default sort order for alert grid, valid values are:
@@ -678,6 +679,8 @@ grid:
source label when multi-grid is configured to `Automatic selection` in the UI
or when `ui:multiGridLabel` is set to `@auto`. If a label name is not present
in this list labels with equal weight will be picked in alphabetic order.
- `groupLimit` - default number of alert groups to show in the UI, loading more
will require user to click on `Load more` button.
Defaults:
@@ -692,6 +695,7 @@ grid:
auto:
ignore: []
order: []
groupLimit: 40
```
Example with sorting using `severity` label and value mappings for it:
+3
View File
@@ -97,6 +97,7 @@ func SetupFlags(f *pflag.FlagSet) {
f.String("grid.sorting.label", "alertname", "Label name to use when sorting alert grid by label")
f.StringSlice("grid.auto.ignore", []string{}, "List of label names not allowed for automatic multi-grid")
f.StringSlice("grid.auto.order", []string{}, "Order of preference for selecting label names for automatic multi-grid")
f.Int("grid.groupLimit", 40, "Default number of groups to show for each grid")
f.Bool("history.enabled", true, "Enable alert history queries")
f.Duration("history.timeout", time.Second*20, "Timeout for history queries against source Prometheus servers")
@@ -211,6 +212,8 @@ func readEnvVariables(k *koanf.Koanf) {
return "annotations.enableInsecureHTML"
case "AUTHENTICATION_HEADER_VALUE_RE":
return "authentication.header.value_re"
case "GRID_GROUPLIMIT":
return "grid.groupLimit"
case "SILENCEFORM_STRIP_LABELS":
return "silenceForm.strip.labels"
case "UI_HIDEFILTERSWHENIDLE":
+1
View File
@@ -88,6 +88,7 @@ grid:
auto:
ignore: []
order: []
groupLimit: 40
history:
enabled: true
workers: 30
+1
View File
@@ -133,6 +133,7 @@ type configSchema struct {
Ignore []string
Order []string
}
GroupLimit int `yaml:"groupLimit"`
} `yaml:"grid"`
History struct {
Enabled bool
+2
View File
@@ -324,6 +324,7 @@ type Settings struct {
SilenceForm SilenceFormSettings `json:"silenceForm"`
AlertAcknowledgement AlertAcknowledgementSettings `json:"alertAcknowledgement"`
HistoryEnabled bool `json:"historyEnabled"`
GridGroupLimit int `json:"gridGroupLimit"`
}
type AuthenticationInfo struct {
@@ -335,6 +336,7 @@ type APIGrid struct {
LabelName string `json:"labelName"`
LabelValue string `json:"labelValue"`
AlertGroups []APIAlertGroup `json:"alertGroups"`
TotalGroups int `json:"totalGroups"`
StateCount map[string]int `json:"stateCount"`
}
+37 -11
View File
@@ -149,7 +149,7 @@ describe("<Fetcher />", () => {
const fetchSpy = jest.spyOn(alertStore, "fetchWithThrottle");
settingsStore.gridConfig.setSortOrder("default");
mount(<Fetcher alertStore={alertStore} settingsStore={settingsStore} />);
expect(fetchSpy).toHaveBeenCalledWith("", false, "", "", "");
expect(fetchSpy).toHaveBeenCalledWith("", false, "", "", "", {});
});
it("calls alertStore.fetchWithThrottle with correct sort arguments when sortOrder=disabled reverseSort=false", () => {
@@ -158,7 +158,7 @@ describe("<Fetcher />", () => {
settingsStore.gridConfig.setSortOrder("disabled");
settingsStore.gridConfig.setSortReverse(false);
mount(<Fetcher alertStore={alertStore} settingsStore={settingsStore} />);
expect(fetchSpy).toHaveBeenCalledWith("", false, "disabled", "", "");
expect(fetchSpy).toHaveBeenCalledWith("", false, "disabled", "", "", {});
});
it("calls alertStore.fetchWithThrottle with correct sort arguments when sortOrder=disabled reverseSort=true", () => {
@@ -167,7 +167,7 @@ describe("<Fetcher />", () => {
settingsStore.gridConfig.setSortOrder("disabled");
settingsStore.gridConfig.setSortReverse(true);
mount(<Fetcher alertStore={alertStore} settingsStore={settingsStore} />);
expect(fetchSpy).toHaveBeenCalledWith("", false, "disabled", "", "");
expect(fetchSpy).toHaveBeenCalledWith("", false, "disabled", "", "", {});
});
it("calls alertStore.fetchWithThrottle with correct sort arguments when sortOrder=startsAt reverseSort=false", () => {
@@ -176,7 +176,7 @@ describe("<Fetcher />", () => {
settingsStore.gridConfig.setSortOrder("startsAt");
settingsStore.gridConfig.setSortReverse(false);
mount(<Fetcher alertStore={alertStore} settingsStore={settingsStore} />);
expect(fetchSpy).toHaveBeenCalledWith("", false, "startsAt", "", "0");
expect(fetchSpy).toHaveBeenCalledWith("", false, "startsAt", "", "0", {});
});
it("calls alertStore.fetchWithThrottle with correct sort arguments when sortOrder=startsAt reverseSort=true", () => {
@@ -185,7 +185,7 @@ describe("<Fetcher />", () => {
settingsStore.gridConfig.setSortOrder("startsAt");
settingsStore.gridConfig.setSortReverse(true);
mount(<Fetcher alertStore={alertStore} settingsStore={settingsStore} />);
expect(fetchSpy).toHaveBeenCalledWith("", false, "startsAt", "", "1");
expect(fetchSpy).toHaveBeenCalledWith("", false, "startsAt", "", "1", {});
});
it("calls alertStore.fetchWithThrottle with correct sort arguments when sortOrder=label sortLabel=cluster reverseSort=false", () => {
@@ -195,7 +195,14 @@ describe("<Fetcher />", () => {
settingsStore.gridConfig.setSortLabel("cluster");
settingsStore.gridConfig.setSortReverse(false);
mount(<Fetcher alertStore={alertStore} settingsStore={settingsStore} />);
expect(fetchSpy).toHaveBeenCalledWith("", false, "label", "cluster", "0");
expect(fetchSpy).toHaveBeenCalledWith(
"",
false,
"label",
"cluster",
"0",
{}
);
});
it("calls alertStore.fetchWithThrottle with correct sort arguments when sortOrder=label sortLabel=job reverseSort=true", () => {
@@ -205,7 +212,7 @@ describe("<Fetcher />", () => {
settingsStore.gridConfig.setSortLabel("job");
settingsStore.gridConfig.setSortReverse(true);
mount(<Fetcher alertStore={alertStore} settingsStore={settingsStore} />);
expect(fetchSpy).toHaveBeenCalledWith("", false, "label", "job", "1");
expect(fetchSpy).toHaveBeenCalledWith("", false, "label", "job", "1", {});
});
it("calls alertStore.fetchWithThrottle with correct sort arguments when sortOrder=label sortLabel=instance reverseSort=null", () => {
@@ -215,7 +222,14 @@ describe("<Fetcher />", () => {
settingsStore.gridConfig.setSortLabel("instance");
settingsStore.gridConfig.setSortReverse(null);
mount(<Fetcher alertStore={alertStore} settingsStore={settingsStore} />);
expect(fetchSpy).toHaveBeenCalledWith("", false, "label", "instance", "");
expect(fetchSpy).toHaveBeenCalledWith(
"",
false,
"label",
"instance",
"",
{}
);
});
it("calls alertStore.fetchWithThrottle with gridLabel=cluster gridSortReverse=false", () => {
@@ -225,7 +239,7 @@ describe("<Fetcher />", () => {
settingsStore.multiGridConfig.setGridLabel("cluster");
settingsStore.multiGridConfig.setGridSortReverse(false);
mount(<Fetcher alertStore={alertStore} settingsStore={settingsStore} />);
expect(fetchSpy).toHaveBeenCalledWith("cluster", false, "", "", "");
expect(fetchSpy).toHaveBeenCalledWith("cluster", false, "", "", "", {});
});
it("calls alertStore.fetchWithThrottle with gridLabel=cluster gridSortReverse=true", () => {
@@ -235,7 +249,7 @@ describe("<Fetcher />", () => {
settingsStore.multiGridConfig.setGridLabel("cluster");
settingsStore.multiGridConfig.setGridSortReverse(true);
mount(<Fetcher alertStore={alertStore} settingsStore={settingsStore} />);
expect(fetchSpy).toHaveBeenCalledWith("cluster", true, "", "", "");
expect(fetchSpy).toHaveBeenCalledWith("cluster", true, "", "", "", {});
});
it("calls alertStore.fetchWithThrottle with gridLabel= gridSortReverse=true", () => {
@@ -245,7 +259,19 @@ describe("<Fetcher />", () => {
settingsStore.multiGridConfig.setGridLabel("");
settingsStore.multiGridConfig.setGridSortReverse(true);
mount(<Fetcher alertStore={alertStore} settingsStore={settingsStore} />);
expect(fetchSpy).toHaveBeenCalledWith("", true, "", "", "");
expect(fetchSpy).toHaveBeenCalledWith("", true, "", "", "", {});
});
it("calls alertStore.fetchWithThrottle with limits set", () => {
MockEmptyAPIResponseWithoutFilters();
const fetchSpy = jest.spyOn(alertStore, "fetchWithThrottle");
settingsStore.gridConfig.setSortOrder("default");
settingsStore.multiGridConfig.setGridLabel("");
settingsStore.multiGridConfig.setGridSortReverse(false);
alertStore.ui.setLimit("old", "bar", 10);
alertStore.ui.setLimit("foo", "bar", 5);
mount(<Fetcher alertStore={alertStore} settingsStore={settingsStore} />);
expect(fetchSpy).toHaveBeenCalledWith("", false, "", "", "", { bar: 5 });
});
it("internal timer is null after unmount", () => {
+13 -8
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState, FC } from "react";
import { reaction } from "mobx";
import { reaction, toJS } from "mobx";
import { observer } from "mobx-react-lite";
import addSeconds from "date-fns/addSeconds";
@@ -175,7 +175,10 @@ const Fetcher: FC<{
settingsStore.multiGridConfig.config.gridSortReverse,
sortSettings.sortOrder,
sortSettings.sortLabel,
sortSettings.sortReverse
sortSettings.sortReverse,
Object.values(alertStore.ui.limits).length > 0
? toJS(Object.values(alertStore.ui.limits)[0])
: {}
);
};
@@ -192,14 +195,16 @@ const Fetcher: FC<{
.map((f: { raw: string }) => f.raw)
.join(" "),
grid: {
sortOrder: settingsStore.gridConfig.config.sortOrder,
sortLabel: settingsStore.gridConfig.config.sortLabel,
sortOrder: toJS(settingsStore.gridConfig.config.sortOrder),
sortLabel: toJS(settingsStore.gridConfig.config.sortLabel),
limits: toJS(alertStore.ui.limits),
},
multigrid: {
gridLabel: settingsStore.multiGridConfig.config.gridLabel,
gridSortReverse:
settingsStore.multiGridConfig.config.gridSortReverse,
reverseSort: settingsStore.gridConfig.config.reverseSort,
gridLabel: toJS(settingsStore.multiGridConfig.config.gridLabel),
gridSortReverse: toJS(
settingsStore.multiGridConfig.config.gridSortReverse
),
reverseSort: toJS(settingsStore.gridConfig.config.reverseSort),
},
}),
() => {
+10 -15
View File
@@ -52,8 +52,6 @@ const Grid: FC<{
const { ref, repack } = useGrid(gridSizesConfig);
const debouncedRepack = useMemo(() => debounce(() => repack(), 10), [repack]);
const [groupsToRender, setGroupsToRender] = useState<number>(50);
const [isExpanded, setIsExpanded] = useState<boolean>(
!DefaultDetailsCollapseValue(settingsStore)
);
@@ -105,12 +103,6 @@ const Grid: FC<{
};
}, [debouncedRepack, onAlertGridCollapseEvent]);
useEffect(() => {
if (groupsToRender > grid.alertGroups.length) {
setGroupsToRender(Math.max(50, grid.alertGroups.length));
}
}, [grid.alertGroups.length, groupsToRender]);
useEffect(() => {
repack();
});
@@ -144,7 +136,7 @@ const Grid: FC<{
>
<TransitionGroup component={null} appear enter exit>
{isExpanded || grid.labelName === ""
? grid.alertGroups.slice(0, groupsToRender).map((group) => (
? grid.alertGroups.map((group) => (
<CSSTransition
key={group.id}
classNames={
@@ -175,7 +167,7 @@ const Grid: FC<{
</TransitionGroup>
</div>
<TransitionGroup component={null} enter exit>
{isExpanded && grid.alertGroups.length > groupsToRender && (
{isExpanded && grid.totalGroups > grid.alertGroups.length && (
<CSSTransition
classNames="components-animation-fade"
timeout={context.animations.duration}
@@ -186,11 +178,14 @@ const Grid: FC<{
<button
type="button"
className="btn btn-secondary mb-3"
onClick={() =>
setGroupsToRender(
Math.min(groupsToRender + 30, grid.alertGroups.length)
)
}
onClick={() => {
alertStore.ui.setLimit(
grid.labelName,
grid.labelValue,
grid.alertGroups.length +
alertStore.settings.values.gridGroupLimit
);
}}
>
<FontAwesomeIcon className="me-2" icon={faAngleDoubleDown} />
Load more
@@ -26,6 +26,7 @@ beforeEach(() => {
labelName: "foo",
labelValue: "bar",
alertGroups: [],
totalGroups: 0,
stateCount: {
active: 0,
suppressed: 0,
@@ -94,6 +94,9 @@ const MockGrid = () => ({
alertGroups: alertStore.data.grids.length
? alertStore.data.grids[0].alertGroups
: [],
totalGroups: alertStore.data.grids.length
? alertStore.data.grids[0].alertGroups.length
: 0,
stateCount: {
unprocessed: 1,
suppressed: 2,
@@ -148,7 +151,11 @@ const MockGroup = (groupName: string, alertCount: number) => {
return group;
};
const MockGroupList = (count: number, alertPerGroup: number) => {
const MockGroupList = (
count: number,
alertPerGroup: number,
totalGroups?: number
) => {
const groups = [];
for (let i = 1; i <= count; i++) {
const id = `id${i}`;
@@ -179,6 +186,7 @@ const MockGroupList = (count: number, alertPerGroup: number) => {
labelName: "",
labelValue: "",
alertGroups: groups,
totalGroups: totalGroups ? totalGroups : groups.length,
stateCount: {
unprocessed: 1,
suppressed: 2,
@@ -208,19 +216,69 @@ describe("<Grid />", () => {
).not.toMatch(/animate components-animation-fade-appear/);
});
it("renders only first 50 alert groups", () => {
it("renders all alert groups", () => {
MockGroupList(55, 5);
const tree = MountedGrid();
const alertGroups = tree.find("AlertGroup");
expect(alertGroups).toHaveLength(50);
expect(alertGroups).toHaveLength(55);
});
it("appends 30 groups after clicking 'Load More' button", () => {
MockGroupList(85, 5);
const tree = MountedGrid();
it("appends more groups after clicking 'Load More' button", () => {
MockGroupList(40, 5, 70);
const tree = mount(
<Grid
alertStore={alertStore}
silenceFormStore={silenceFormStore}
settingsStore={settingsStore}
gridSizesConfig={GridSizesConfig(420)}
groupWidth={420}
grid={alertStore.data.grids[0]}
outerPadding={0}
/>,
{
wrappingComponent: ThemeContext.Provider,
wrappingComponentProps: { value: MockThemeContext },
}
);
tree.find("button").simulate("click");
const alertGroups = tree.find("AlertGroup");
expect(alertGroups).toHaveLength(80);
expect(alertStore.ui.limits).toStrictEqual({
"": { "": 40 + alertStore.settings.values.gridGroupLimit },
});
});
it("sets correct limits after clicking 'Load More' button", () => {
MockGroupList(50, 5, 60);
alertStore.settings.setValues({
...alertStore.settings.values,
gridGroupLimit: 20,
});
alertStore.data.setGrids([
{
...alertStore.data.grids[0],
labelName: "foo",
labelValue: "bar",
totalGroups: 69,
},
]);
const tree = mount(
<Grid
alertStore={alertStore}
silenceFormStore={silenceFormStore}
settingsStore={settingsStore}
gridSizesConfig={GridSizesConfig(420)}
groupWidth={420}
grid={alertStore.data.grids[0]}
outerPadding={0}
/>,
{
wrappingComponent: ThemeContext.Provider,
wrappingComponentProps: { value: MockThemeContext },
}
);
tree.find("button").simulate("click");
expect(alertStore.ui.limits).toStrictEqual({
foo: { bar: 70 },
});
});
it("doesn't sort groups when sorting is set to 'disabled'", () => {
@@ -338,6 +396,7 @@ describe("<Grid />", () => {
labelName: "foo",
labelValue: "bar",
alertGroups: groups.slice(0, 10),
totalGroups: groups.slice(0, 10).length,
stateCount: {
unprocessed: 1,
suppressed: 2,
@@ -348,6 +407,7 @@ describe("<Grid />", () => {
labelName: "foo",
labelValue: "",
alertGroups: groups.slice(10, 20),
totalGroups: groups.slice(10, 20).length,
stateCount: {
unprocessed: 1,
suppressed: 2,
@@ -591,6 +651,7 @@ describe("<AlertGrid />", () => {
labelName: "foo",
labelValue: "bar",
alertGroups: groups,
totalGroups: groups.length,
stateCount: {
unprocessed: 1,
suppressed: 2,
@@ -601,6 +662,7 @@ describe("<AlertGrid />", () => {
labelName: "foo",
labelValue: "",
alertGroups: groups,
totalGroups: groups.length,
stateCount: {
unprocessed: 1,
suppressed: 2,
@@ -629,6 +691,7 @@ describe("<AlertGrid />", () => {
labelName: "foo",
labelValue: "bar",
alertGroups: groups,
totalGroups: groups.length,
stateCount: {
unprocessed: 0,
suppressed: 0,
@@ -639,6 +702,7 @@ describe("<AlertGrid />", () => {
labelName: "foo",
labelValue: "",
alertGroups: groups,
totalGroups: groups.length,
stateCount: {
unprocessed: 0,
suppressed: 0,
@@ -672,6 +736,7 @@ describe("<AlertGrid />", () => {
labelName: "",
labelValue: "",
alertGroups: groups,
totalGroups: groups.length,
stateCount: {
unprocessed: 0,
suppressed: 0,
@@ -56,6 +56,7 @@ const MockAPIResponse = () => {
{}
),
],
totalGroups: 2,
stateCount: {
unprocessed: 1,
suppressed: 2,
+2
View File
@@ -91,6 +91,7 @@ export interface APIGridT {
labelName: string;
labelValue: string;
alertGroups: APIAlertGroupT[];
totalGroups: number;
stateCount: StateCountT;
}
@@ -189,6 +190,7 @@ export interface APISettingsT {
comment: string;
};
historyEnabled: boolean;
gridGroupLimit: number;
}
export interface APIAlertsResponseT {
+63 -25
View File
@@ -544,7 +544,9 @@ describe("AlertStore.fetch", () => {
});
const store = new AlertStore(["label=value"]);
await expect(store.fetch("", false, "", "", "")).resolves.toBeUndefined();
await expect(
store.fetch("", false, "", "", "", {})
).resolves.toBeUndefined();
expect(fetchMock.calls()).toHaveLength(1);
expect(store.status.value).toEqual(AlertStoreStatuses.Idle);
@@ -558,7 +560,9 @@ describe("AlertStore.fetch", () => {
});
const store = new AlertStore([]);
await expect(store.fetch("", false, "", "", "")).resolves.toBeUndefined();
await expect(
store.fetch("", false, "", "", "", {})
).resolves.toBeUndefined();
expect(fetchMock.calls()).toHaveLength(1);
expect(store.status.value).toEqual(AlertStoreStatuses.Failure);
@@ -575,9 +579,9 @@ describe("AlertStore.fetch", () => {
});
const store = new AlertStore([]);
await expect(store.fetch("", false, "", "", "")).resolves.toHaveProperty(
"error"
);
await expect(
store.fetch("", false, "", "", "", {})
).resolves.toHaveProperty("error");
expect(fetchMock.calls()).toHaveLength(10);
expect(store.status.value).toEqual(AlertStoreStatuses.Failure);
@@ -595,9 +599,9 @@ describe("AlertStore.fetch", () => {
throws: new Error("fetch error"),
});
await expect(store.fetch("", false, "", "", "")).resolves.toHaveProperty(
"error"
);
await expect(
store.fetch("", false, "", "", "", {})
).resolves.toHaveProperty("error");
expect(fetchMock.calls()).toHaveLength(10);
});
@@ -610,9 +614,9 @@ describe("AlertStore.fetch", () => {
throws: new Error("fetch error"),
});
await expect(store.fetch("", false, "", "", "")).resolves.toHaveProperty(
"error"
);
await expect(
store.fetch("", false, "", "", "", {})
).resolves.toHaveProperty("error");
expect(fetchMock.calls()).toHaveLength(10);
const response = EmptyAPIResponse();
@@ -621,7 +625,9 @@ describe("AlertStore.fetch", () => {
body: JSON.stringify(response),
});
await expect(store.fetch("", false, "", "", "")).resolves.toBeUndefined();
await expect(
store.fetch("", false, "", "", "", {})
).resolves.toBeUndefined();
expect(fetchMock.calls()).toHaveLength(1);
fetchMock.reset();
@@ -629,9 +635,9 @@ describe("AlertStore.fetch", () => {
throws: new Error("fetch error"),
});
await expect(store.fetch("", false, "", "", "")).resolves.toHaveProperty(
"error"
);
await expect(
store.fetch("", false, "", "", "", {})
).resolves.toHaveProperty("error");
expect(fetchMock.calls()).toHaveLength(10);
});
@@ -649,7 +655,9 @@ describe("AlertStore.fetch", () => {
}) as any
);
await expect(store.fetch("", false, "", "", "")).resolves.toBeUndefined();
await expect(
store.fetch("", false, "", "", "", {})
).resolves.toBeUndefined();
expect(store.info.reloadNeeded).toBe(true);
});
@@ -664,9 +672,9 @@ describe("AlertStore.fetch", () => {
throws: new Error("fetch error"),
});
await expect(store.fetch("", false, "", "", "")).resolves.toHaveProperty(
"error"
);
await expect(
store.fetch("", false, "", "", "", {})
).resolves.toHaveProperty("error");
expect(store.filters.values[0].applied).toBe(true);
});
@@ -681,7 +689,9 @@ describe("AlertStore.fetch", () => {
// initial fetch, should update settings
store.settings.setValues({ foo: "bar" } as any);
await expect(store.fetch("", false, "", "", "")).resolves.toBeUndefined();
await expect(
store.fetch("", false, "", "", "", {})
).resolves.toBeUndefined();
expect(store.settings.values).toMatchObject({
staticColorLabels: ["job"],
annotationsDefaultHidden: false,
@@ -691,7 +701,9 @@ describe("AlertStore.fetch", () => {
});
// second fetch, should keep same settings
await expect(store.fetch("", false, "", "", "")).resolves.toBeUndefined();
await expect(
store.fetch("", false, "", "", "", {})
).resolves.toBeUndefined();
expect(store.settings.values).toMatchObject({
staticColorLabels: ["job"],
annotationsDefaultHidden: false,
@@ -708,7 +720,9 @@ describe("AlertStore.fetch", () => {
body: JSON.stringify(response),
});
const store = new AlertStore(["label=value"]);
await expect(store.fetch("", false, "", "", "")).resolves.toBeUndefined();
await expect(
store.fetch("", false, "", "", "", {})
).resolves.toBeUndefined();
expect(store.info.upgradeReady).toBe(false);
response.version = "newFakeVersion";
@@ -716,7 +730,9 @@ describe("AlertStore.fetch", () => {
fetchMock.mock("*", {
body: JSON.stringify(response),
});
await expect(store.fetch("", false, "", "", "")).resolves.toBeUndefined();
await expect(
store.fetch("", false, "", "", "", {})
).resolves.toBeUndefined();
expect(store.info.upgradeReady).toBe(true);
});
@@ -729,6 +745,7 @@ describe("AlertStore.fetch", () => {
labelName: "",
labelValue: "",
alertGroups: [g1, g2],
totalGroups: 2,
stateCount: { unprocessed: 0, active: 2, suppressed: 0 },
},
];
@@ -749,6 +766,7 @@ describe("AlertStore.fetch", () => {
labelName: "",
labelValue: "",
alertGroups: [g1, g2, g3],
totalGroups: 3,
stateCount: { unprocessed: 0, active: 3, suppressed: 0 },
},
]);
@@ -761,6 +779,7 @@ describe("AlertStore.fetch", () => {
labelName: "",
labelValue: "",
alertGroups: [g1, g3],
totalGroups: 2,
stateCount: { unprocessed: 0, active: 2, suppressed: 0 },
},
];
@@ -778,7 +797,7 @@ describe("AlertStore.fetch", () => {
});
const store = new AlertStore(["label=value"]);
await expect(
store.fetch("", false, "sortOrder", "sortLabel", "sortReverse")
store.fetch("", false, "sortOrder", "sortLabel", "sortReverse", {})
).resolves.toBeUndefined();
expect(fetchMock.calls().length).toEqual(1);
expect(fetchMock.calls()[0][0]).toBe(
@@ -794,11 +813,30 @@ describe("AlertStore.fetch", () => {
});
const store = new AlertStore(["label=value"]);
await expect(
store.fetch("cluster", true, "sortOrder", "sortLabel", "sortReverse")
store.fetch("cluster", true, "sortOrder", "sortLabel", "sortReverse", {})
).resolves.toBeUndefined();
expect(fetchMock.calls().length).toEqual(1);
expect(fetchMock.calls()[0][0]).toBe(
"/alerts.json?&gridLabel=cluster&gridSortReverse=1&sortOrder=sortOrder&sortLabel=sortLabel&sortReverse=sortReverse&q=label%3Dvalue"
);
});
it("uses correct query args with limits", async () => {
const response = EmptyAPIResponse();
fetchMock.reset();
fetchMock.mock("*", {
body: JSON.stringify(response),
});
const store = new AlertStore(["label=value"]);
store.ui.setLimit("foo", "bar", 7);
await expect(
store.fetch("cluster", true, "sortOrder", "sortLabel", "sortReverse", {
bar: 7,
})
).resolves.toBeUndefined();
expect(fetchMock.calls().length).toEqual(1);
expect(fetchMock.calls()[0][0]).toBe(
"/alerts.json?&gridLabel=cluster&gridSortReverse=1&sortOrder=sortOrder&sortLabel=sortLabel&sortReverse=sortReverse&limit=bar=7&q=label%3Dvalue"
);
});
});
+15 -3
View File
@@ -207,6 +207,8 @@ interface AlertStoreStatusT {
interface AlertStoreUIT {
isIdle: boolean;
setIsIdle: (val: boolean) => void;
limits: { [key: string]: { [val: string]: number } };
setLimit: (key: string, val: string, limit: number) => void;
}
class AlertStore {
@@ -474,6 +476,7 @@ class AlertStore {
comment: "ACK! This alert was acknowledged using karma",
},
historyEnabled: true,
gridGroupLimit: 40,
} as APISettingsT,
setValues(v: APISettingsT) {
this.values = v;
@@ -548,9 +551,14 @@ class AlertStore {
setIsIdle(val: boolean) {
this.isIdle = val;
},
limits: {} as { [key: string]: { [val: string]: number } },
setLimit(key: string, val: string, limit: number) {
this.limits = { [key]: { ...this.limits[key], [val]: limit } };
},
},
{
setIsIdle: action.bound,
setLimit: action.bound,
}
);
@@ -563,17 +571,21 @@ class AlertStore {
gridSortReverse: boolean,
sortOrder: string,
sortLabel: string,
sortReverse: string
sortReverse: string,
limits: { [key: string]: number }
) => {
this.status.setFetching();
const args = [
const args: string[] = [
`gridLabel=${gridLabel}`,
`gridSortReverse=${gridSortReverse ? "1" : "0"}`,
`sortOrder=${sortOrder}`,
`sortLabel=${sortLabel}`,
`sortReverse=${sortReverse}`,
];
Object.entries(limits)
.map(([key, value]) => `limit=${key}=${value}`)
.join("&"),
].filter((arg) => arg !== "");
const alertsURI =
FormatBackendURI(`alerts.json?&${args.join("&")}&`) +
+2
View File
@@ -81,6 +81,7 @@ const EmptyAPIResponse = (): APIAlertsResponseT => ({
annotationsVisible: [],
annotationsEnableHTML: false,
historyEnabled: true,
gridGroupLimit: 40,
},
authentication: {
username: "",
@@ -103,6 +104,7 @@ const MockAPIResponse = (): APIAlertsResponseT => {
{}
),
],
totalGroups: 1,
stateCount: {
unprocessed: 1,
suppressed: 2,
+2
View File
@@ -301,6 +301,7 @@ const MockGrid = (alertStore: AlertStore): void => {
labelName: "cluster",
labelValue: "prod",
alertGroups: groups.slice(0, 7),
totalGroups: groups.slice(0, 7).length,
stateCount: {
unprocessed: 1,
suppressed: 2,
@@ -311,6 +312,7 @@ const MockGrid = (alertStore: AlertStore): void => {
labelName: "cluster",
labelValue: "",
alertGroups: groups.slice(7, 11),
totalGroups: groups.slice(7, 11).length,
stateCount: {
unprocessed: 0,
suppressed: 10,