diff --git a/ui/src/Components/Fetcher/index.test.tsx b/ui/src/Components/Fetcher/index.test.tsx index d91cd99b8..38567f3e5 100644 --- a/ui/src/Components/Fetcher/index.test.tsx +++ b/ui/src/Components/Fetcher/index.test.tsx @@ -149,7 +149,7 @@ describe("", () => { const fetchSpy = jest.spyOn(alertStore, "fetchWithThrottle"); settingsStore.gridConfig.setSortOrder("default"); mount(); - expect(fetchSpy).toHaveBeenCalledWith("", false, "", "", false, {}); + expect(fetchSpy).toHaveBeenCalledWith("", false, "", "", false, {}, 5, {}); }); it("calls alertStore.fetchWithThrottle with correct sort arguments when sortOrder=disabled reverseSort=false", () => { @@ -158,7 +158,16 @@ describe("", () => { settingsStore.gridConfig.setSortOrder("disabled"); settingsStore.gridConfig.setSortReverse(false); mount(); - expect(fetchSpy).toHaveBeenCalledWith("", false, "disabled", "", false, {}); + expect(fetchSpy).toHaveBeenCalledWith( + "", + false, + "disabled", + "", + false, + {}, + 5, + {} + ); }); it("calls alertStore.fetchWithThrottle with correct sort arguments when sortOrder=disabled reverseSort=true", () => { @@ -167,7 +176,16 @@ describe("", () => { settingsStore.gridConfig.setSortOrder("disabled"); settingsStore.gridConfig.setSortReverse(true); mount(); - expect(fetchSpy).toHaveBeenCalledWith("", false, "disabled", "", false, {}); + expect(fetchSpy).toHaveBeenCalledWith( + "", + false, + "disabled", + "", + false, + {}, + 5, + {} + ); }); it("calls alertStore.fetchWithThrottle with correct sort arguments when sortOrder=startsAt reverseSort=false", () => { @@ -176,7 +194,16 @@ describe("", () => { settingsStore.gridConfig.setSortOrder("startsAt"); settingsStore.gridConfig.setSortReverse(false); mount(); - expect(fetchSpy).toHaveBeenCalledWith("", false, "startsAt", "", false, {}); + expect(fetchSpy).toHaveBeenCalledWith( + "", + false, + "startsAt", + "", + false, + {}, + 5, + {} + ); }); it("calls alertStore.fetchWithThrottle with correct sort arguments when sortOrder=startsAt reverseSort=true", () => { @@ -185,7 +212,16 @@ describe("", () => { settingsStore.gridConfig.setSortOrder("startsAt"); settingsStore.gridConfig.setSortReverse(true); mount(); - expect(fetchSpy).toHaveBeenCalledWith("", false, "startsAt", "", true, {}); + expect(fetchSpy).toHaveBeenCalledWith( + "", + false, + "startsAt", + "", + true, + {}, + 5, + {} + ); }); it("calls alertStore.fetchWithThrottle with correct sort arguments when sortOrder=label sortLabel=cluster reverseSort=false", () => { @@ -201,6 +237,8 @@ describe("", () => { "label", "cluster", false, + {}, + 5, {} ); }); @@ -212,7 +250,16 @@ describe("", () => { settingsStore.gridConfig.setSortLabel("job"); settingsStore.gridConfig.setSortReverse(true); mount(); - expect(fetchSpy).toHaveBeenCalledWith("", false, "label", "job", true, {}); + expect(fetchSpy).toHaveBeenCalledWith( + "", + false, + "label", + "job", + true, + {}, + 5, + {} + ); }); it("calls alertStore.fetchWithThrottle with correct sort arguments when sortOrder=label sortLabel=instance reverseSort=null", () => { @@ -228,6 +275,8 @@ describe("", () => { "label", "instance", false, + {}, + 5, {} ); }); @@ -239,7 +288,16 @@ describe("", () => { settingsStore.multiGridConfig.setGridLabel("cluster"); settingsStore.multiGridConfig.setGridSortReverse(false); mount(); - expect(fetchSpy).toHaveBeenCalledWith("cluster", false, "", "", false, {}); + expect(fetchSpy).toHaveBeenCalledWith( + "cluster", + false, + "", + "", + false, + {}, + 5, + {} + ); }); it("calls alertStore.fetchWithThrottle with gridLabel=cluster gridSortReverse=true", () => { @@ -249,7 +307,16 @@ describe("", () => { settingsStore.multiGridConfig.setGridLabel("cluster"); settingsStore.multiGridConfig.setGridSortReverse(true); mount(); - expect(fetchSpy).toHaveBeenCalledWith("cluster", true, "", "", false, {}); + expect(fetchSpy).toHaveBeenCalledWith( + "cluster", + true, + "", + "", + false, + {}, + 5, + {} + ); }); it("calls alertStore.fetchWithThrottle with gridLabel= gridSortReverse=true", () => { @@ -259,7 +326,7 @@ describe("", () => { settingsStore.multiGridConfig.setGridLabel(""); settingsStore.multiGridConfig.setGridSortReverse(true); mount(); - expect(fetchSpy).toHaveBeenCalledWith("", true, "", "", false, {}); + expect(fetchSpy).toHaveBeenCalledWith("", true, "", "", false, {}, 5, {}); }); it("calls alertStore.fetchWithThrottle with limits set", () => { @@ -268,10 +335,19 @@ describe("", () => { settingsStore.gridConfig.setSortOrder("default"); settingsStore.multiGridConfig.setGridLabel(""); settingsStore.multiGridConfig.setGridSortReverse(false); - alertStore.ui.setLimit("old", "bar", 10); - alertStore.ui.setLimit("foo", "bar", 5); + alertStore.ui.setGridGroupLimit("old", "bar", 10); + alertStore.ui.setGridGroupLimit("foo", "bar", 5); mount(); - expect(fetchSpy).toHaveBeenCalledWith("", false, "", "", false, { bar: 5 }); + expect(fetchSpy).toHaveBeenCalledWith( + "", + false, + "", + "", + false, + { bar: 5 }, + 5, + {} + ); }); it("internal timer is null after unmount", () => { diff --git a/ui/src/Components/Fetcher/index.tsx b/ui/src/Components/Fetcher/index.tsx index 21f1cf893..4faa4e59c 100644 --- a/ui/src/Components/Fetcher/index.tsx +++ b/ui/src/Components/Fetcher/index.tsx @@ -176,9 +176,11 @@ const Fetcher: FC<{ sortSettings.sortOrder, sortSettings.sortLabel, sortSettings.sortReverse === "1", - Object.values(alertStore.ui.limits).length > 0 - ? toJS(Object.values(alertStore.ui.limits)[0]) - : {} + Object.values(alertStore.ui.gridGroupLimits).length > 0 + ? toJS(Object.values(alertStore.ui.gridGroupLimits)[0]) + : {}, + settingsStore.alertGroupConfig.config.defaultRenderCount, + alertStore.ui.groupAlertLimits ); }; @@ -197,7 +199,11 @@ const Fetcher: FC<{ grid: { sortOrder: toJS(settingsStore.gridConfig.config.sortOrder), sortLabel: toJS(settingsStore.gridConfig.config.sortLabel), - limits: toJS(alertStore.ui.limits), + gridGroupLimits: toJS(alertStore.ui.gridGroupLimits), + defaultGroupLimit: toJS( + settingsStore.alertGroupConfig.config.defaultRenderCount + ), + groupAlertLimits: toJS(alertStore.ui.groupAlertLimits), }, multigrid: { gridLabel: toJS(settingsStore.multiGridConfig.config.gridLabel), diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.tsx index a0c076fc5..3f52da16c 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.tsx @@ -56,7 +56,7 @@ afterEach(() => { global.innerWidth = originalInnerWidth; }); -const MockAlerts = (alertCount: number) => { +const MockAlerts = (alertCount: number, totalAlerts: number) => { for (let i = 1; i <= alertCount; i++) { const alert = MockAlert([], { instance: `instance${i}` }, "active"); const startsAt = new Date(); @@ -64,12 +64,12 @@ const MockAlerts = (alertCount: number) => { alert.alertmanager[0].startsAt = startsAt.toISOString(); group.alerts.push(alert); } + group.totalAlerts = totalAlerts; }; const MountedAlertGroup = ( afterUpdate: () => void, showAlertmanagers: boolean, - initialAlertsToRender?: number, theme?: ThemeCtx ) => { return mount( @@ -77,7 +77,6 @@ const MountedAlertGroup = ( afterUpdate={afterUpdate} group={group} showAlertmanagers={showAlertmanagers} - initialAlertsToRender={initialAlertsToRender} settingsStore={settingsStore} alertStore={alertStore} silenceFormStore={silenceFormStore} @@ -100,32 +99,31 @@ const ValidateCollapse = ( settingsStore.alertGroupConfig.setDefaultCollapseState(defaultCollapseState); - MockAlerts(3); + MockAlerts(3, 3); const tree = MountedAlertGroup(jest.fn(), false); expect(tree.find("Alert")).toHaveLength(shouldBeCollapsed ? 0 : 3); }; describe("", () => { it("doesn't crash on unmount", () => { - MockAlerts(5); + MockAlerts(5, 5); const tree = MountedAlertGroup(jest.fn(), true); tree.unmount(); }); it("uses 'animate' class when settingsStore.themeConfig.config.animations is true", () => { - MockAlerts(5); - const tree = MountedAlertGroup(jest.fn(), true, 5, MockThemeContext); + MockAlerts(5, 5); + const tree = MountedAlertGroup(jest.fn(), true, MockThemeContext); expect( tree.find("div.components-grid-alertgrid-alertgroup").hasClass("animate") ).toBe(true); }); it("doesn't use 'animate' class when settingsStore.themeConfig.config.animations is false", () => { - MockAlerts(5); + MockAlerts(5, 5); const tree = MountedAlertGroup( jest.fn(), true, - 5, MockThemeContextWithoutAnimations ); expect( @@ -134,14 +132,14 @@ describe("", () => { }); it("renders Alertmanager cluster labels in footer if shared", () => { - MockAlerts(2); + MockAlerts(2, 2); group.shared.clusters = ["default"]; const tree = MountedAlertGroup(jest.fn(), true).find("AlertGroup"); expect(tree.find("GroupFooter").html()).toMatch(/@cluster/); }); it("only renders one @cluster label per cluster in the footer", () => { - MockAlerts(2); + MockAlerts(2, 2); for (let i = 0; i < group.alerts.length; i++) { group.alerts[i].alertmanager.push({ fingerprint: "123", @@ -174,7 +172,7 @@ describe("", () => { }); it("doesn't render @cluster labels with empty alertmanager array", () => { - MockAlerts(2); + MockAlerts(2, 2); for (let i = 0; i < group.alerts.length; i++) { group.alerts[i].alertmanager = []; } @@ -185,7 +183,7 @@ describe("", () => { }); it("doesn't render @cluster labels in footer when they are unique", () => { - MockAlerts(5); + MockAlerts(5, 5); for (let i = 0; i < group.alerts.length; i++) { group.alerts[i].alertmanager[0].name = `fakeAlertmanager${i}`; } @@ -206,7 +204,7 @@ describe("", () => { }); it("only renders titlebar when collapsed", () => { - MockAlerts(10); + MockAlerts(5, 10); const tree = MountedAlertGroup(jest.fn(), false); tree.find("span.badge.cursor-pointer").at(1).simulate("click"); expect(tree.find("Alert")).toHaveLength(0); @@ -214,9 +212,9 @@ describe("", () => { }); it("renders reduced details when idle", () => { - MockAlerts(10); + MockAlerts(5, 10); alertStore.ui.setIsIdle(true); - const tree = MountedAlertGroup(jest.fn(), true, 10, MockThemeContext); + const tree = MountedAlertGroup(jest.fn(), true, MockThemeContext); expect(tree.find("Alert")).toHaveLength(1); }); @@ -252,40 +250,43 @@ describe("", () => { it("renders @receiver label when alertStore.data.receivers.length > 1", () => { alertStore.data.setReceivers(["foo", "bar"]); - MockAlerts(10); + MockAlerts(5, 10); const tree = MountedAlertGroup(jest.fn(), false); expect(tree.html()).toMatch(/@receiver:/); }); it("doesn't render @receiver label when alertStore.data.receivers.length == 0", () => { alertStore.data.setReceivers([]); - MockAlerts(10); + MockAlerts(5, 10); const tree = MountedAlertGroup(jest.fn(), false); expect(tree.html()).not.toMatch(/@receiver:/); }); }); -const ValidateLoadButtonPresent = (totalAlerts: number, isPresent: boolean) => { - MockAlerts(totalAlerts); +const ValidateLoadButtonPresent = ( + alertCount: number, + totalAlerts: number, + isPresent: boolean +) => { + MockAlerts(alertCount, totalAlerts); const tree = MountedAlertGroup(jest.fn(), false); const buttons = tree.find("button"); expect(buttons).toHaveLength(isPresent ? 2 : 0); }; const ValidateLoadButtonAction = ( + alertCount: number, totalAlerts: number, buttonIndex: number, iconMatch: RegExp, - loadedAlerts: number, - alertsToRenderBeforeClick?: number + loadedAlerts: number ) => { - MockAlerts(totalAlerts); - const tree = MountedAlertGroup(jest.fn(), false, alertsToRenderBeforeClick); + MockAlerts(alertCount, totalAlerts); + const tree = MountedAlertGroup(jest.fn(), false); const loadMore = tree.find("button").at(buttonIndex); expect(loadMore.html()).toMatch(iconMatch); loadMore.simulate("click"); - tree.update(); - expect(tree.find("Alert")).toHaveLength(loadedAlerts); + expect(alertStore.ui.groupAlertLimits[group.id]).toBe(loadedAlerts); }; describe(" renderConfig", () => { @@ -293,61 +294,52 @@ describe(" renderConfig", () => { expect(settingsStore.alertGroupConfig.config.defaultRenderCount).toBe(5); }); - it("renders only up to settingsStore.alertGroupConfig.config.defaultRenderCount alerts", () => { - MockAlerts(50); - const tree = MountedAlertGroup(jest.fn(), false).find("AlertGroup"); - const alerts = tree.find("Alert"); - expect(alerts).toHaveLength( - settingsStore.alertGroupConfig.config.defaultRenderCount - ); - }); - it("load buttons are not rendered for 1 alert", () => { - ValidateLoadButtonPresent(1, false); + ValidateLoadButtonPresent(1, 1, false); }); it("load buttons are not rendered for 5 alerts", () => { - ValidateLoadButtonPresent(5, false); + ValidateLoadButtonPresent(5, 5, false); }); it("load buttons are rendered for 6 alert", () => { - ValidateLoadButtonPresent(6, true); + ValidateLoadButtonPresent(5, 6, true); }); it("clicking - icon hides 1 alert if there's 6 in total", () => { - ValidateLoadButtonAction(6, 0, /fa-minus/, 5, 6); + ValidateLoadButtonAction(6, 6, 0, /fa-minus/, 5); }); it("clicking - icon hides 1 alert if there's 6 in total and we're showing 3", () => { - ValidateLoadButtonAction(6, 0, /fa-minus/, 2, 3); + ValidateLoadButtonAction(3, 6, 0, /fa-minus/, 2); }); it("clicking - icon hides 2 alerts if there's 7 in total and we're showing 7", () => { - ValidateLoadButtonAction(7, 0, /fa-minus/, 5, 7); + ValidateLoadButtonAction(7, 7, 0, /fa-minus/, 5); }); it("clicking - icon hides 5 alerts if there's 10 in total and we're showing 10", () => { - ValidateLoadButtonAction(10, 0, /fa-minus/, 5, 10); + ValidateLoadButtonAction(10, 10, 0, /fa-minus/, 5); }); it("clicking - icon hides 5 alerts if there's 18 in total and we're showing 17", () => { - ValidateLoadButtonAction(18, 0, /fa-minus/, 12, 17); + ValidateLoadButtonAction(17, 18, 0, /fa-minus/, 12); }); it("clicking + icon loads 1 more alert if there's 6 in total", () => { - ValidateLoadButtonAction(6, 1, /fa-plus/, 6); + ValidateLoadButtonAction(5, 6, 1, /fa-plus/, 6); }); it("clicking + icon loads 4 more alert if there's 9 in total", () => { - ValidateLoadButtonAction(9, 1, /fa-plus/, 9); + ValidateLoadButtonAction(5, 9, 1, /fa-plus/, 9); }); it("clicking + icon loads 5 more alert if there's 14 in total", () => { - ValidateLoadButtonAction(14, 1, /fa-plus/, 10); + ValidateLoadButtonAction(5, 14, 1, /fa-plus/, 10); }); it("clicking + icon loads 5 more alert if there's 25 in total and we're showing 16", () => { - ValidateLoadButtonAction(25, 1, /fa-plus/, 22, 17); + ValidateLoadButtonAction(16, 25, 1, /fa-plus/, 21); }); it("uses 'z-index: 100' style after setIsMenuOpen() is called on any Alert", async () => { @@ -355,7 +347,7 @@ describe(" renderConfig", () => { fetchMock.mock("*", { body: "" }); const promise = Promise.resolve(); - MockAlerts(5); + MockAlerts(5, 5); const tree = MountedAlertGroup(jest.fn(), false); tree @@ -373,7 +365,7 @@ describe(" renderConfig", () => { it("uses 'z-index: 100' style after setIsMenuOpen() is called on AlertGroup header menu", async () => { const promise = Promise.resolve(); - MockAlerts(5); + MockAlerts(5, 5); const tree = MountedAlertGroup(jest.fn(), false); tree.find("span.cursor-pointer").at(0).simulate("click"); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.tsx index c3f2e6d30..b8b7983b8 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.tsx @@ -48,7 +48,6 @@ const AlertGroup: FC<{ silenceFormStore: SilenceFormStore; groupWidth: number; gridLabelValue: string; - initialAlertsToRender?: number; }> = ({ group, showAlertmanagers, @@ -58,17 +57,10 @@ const AlertGroup: FC<{ settingsStore, groupWidth, gridLabelValue, - initialAlertsToRender, }) => { const defaultRenderCount = settingsStore.alertGroupConfig.config.defaultRenderCount; - const [alertsToRenderInternal, setAlertsToRender] = useState( - initialAlertsToRender || null - ); - - const alertsToRender = alertsToRenderInternal || defaultRenderCount; - const [isMenuOpen, setIsMenuOpen] = useState(false); const [isCollapsed, setIsCollapsed] = useState( @@ -94,15 +86,19 @@ const AlertGroup: FC<{ }; const loadMore = () => { - const step = getStepSize(group.alerts.length); - // show cur+step, but not more that total alert count - setAlertsToRender(Math.min(alertsToRender + step, group.alerts.length)); + const step = getStepSize(group.totalAlerts); + alertStore.ui.setGroupAlertLimit( + group.id, + Math.min(group.alerts.length + step, group.totalAlerts) + ); }; const loadLess = () => { - const step = getStepSize(group.alerts.length); - // show cur-step, but not less than 1 - setAlertsToRender(Math.max(alertsToRender - step, 1)); + const step = getStepSize(group.totalAlerts); + alertStore.ui.setGroupAlertLimit( + group.id, + Math.max(group.alerts.length - step, 1) + ); }; const onAlertGroupCollapseEvent = useCallback( @@ -177,7 +173,7 @@ const AlertGroup: FC<{ ) : null}
    {group.alerts - .slice(0, alertStore.ui.isIdle ? 1 : alertsToRender) + .slice(0, alertStore.ui.isIdle ? 1 : group.alerts.length) .map((alert) => ( ))} - {group.alerts.length > defaultRenderCount ? ( + {group.totalAlerts > defaultRenderCount ? (
  • - {Math.min(alertsToRender, group.alerts.length)} - {" of "} {group.alerts.length} + {" of "} + {group.totalAlerts} { - alertStore.ui.setLimit( + alertStore.ui.setGridGroupLimit( grid.labelName, grid.labelValue, grid.alertGroups.length + diff --git a/ui/src/Components/Grid/AlertGrid/index.test.tsx b/ui/src/Components/Grid/AlertGrid/index.test.tsx index 5fa909df5..02a35e699 100644 --- a/ui/src/Components/Grid/AlertGrid/index.test.tsx +++ b/ui/src/Components/Grid/AlertGrid/index.test.tsx @@ -241,7 +241,7 @@ describe("", () => { } ); tree.find("button").simulate("click"); - expect(alertStore.ui.limits).toStrictEqual({ + expect(alertStore.ui.gridGroupLimits).toStrictEqual({ "": { "": 40 + alertStore.settings.values.gridGroupLimit }, }); }); @@ -276,7 +276,7 @@ describe("", () => { } ); tree.find("button").simulate("click"); - expect(alertStore.ui.limits).toStrictEqual({ + expect(alertStore.ui.gridGroupLimits).toStrictEqual({ foo: { bar: 70 }, }); }); diff --git a/ui/src/Models/APITypes.ts b/ui/src/Models/APITypes.ts index a1a7e0c2f..b4a4224a0 100644 --- a/ui/src/Models/APITypes.ts +++ b/ui/src/Models/APITypes.ts @@ -58,6 +58,7 @@ export interface APIAlertGroupT { receiver: string; labels: LabelsT; alerts: APIAlertT[]; + totalAlerts: number; alertmanagerCount: { [key: string]: number }; stateCount: StateCountT; shared: { @@ -238,4 +239,6 @@ export interface AlertsRequestT { sortOrder: string; sortLabel: string; sortReverse: boolean; + defaultGroupLimit: number; + groupLimits: { [key: string]: number }; } diff --git a/ui/src/Stores/AlertStore.test.ts b/ui/src/Stores/AlertStore.test.ts index 0a7ffbccb..0297cdea6 100644 --- a/ui/src/Stores/AlertStore.test.ts +++ b/ui/src/Stores/AlertStore.test.ts @@ -545,7 +545,7 @@ describe("AlertStore.fetch", () => { const store = new AlertStore(["label=value"]); await expect( - store.fetch("", false, "", "", false, {}) + store.fetch("", false, "", "", false, {}, 5, {}) ).resolves.toBeUndefined(); expect(fetchMock.calls()).toHaveLength(1); @@ -561,7 +561,7 @@ describe("AlertStore.fetch", () => { const store = new AlertStore([]); await expect( - store.fetch("", false, "", "", false, {}) + store.fetch("", false, "", "", false, {}, 5, {}) ).resolves.toBeUndefined(); expect(fetchMock.calls()).toHaveLength(1); @@ -580,7 +580,7 @@ describe("AlertStore.fetch", () => { const store = new AlertStore([]); await expect( - store.fetch("", false, "", "", false, {}) + store.fetch("", false, "", "", false, {}, 5, {}) ).resolves.toHaveProperty("error"); expect(fetchMock.calls()).toHaveLength(10); @@ -600,7 +600,7 @@ describe("AlertStore.fetch", () => { }); await expect( - store.fetch("", false, "", "", false, {}) + store.fetch("", false, "", "", false, {}, 5, {}) ).resolves.toHaveProperty("error"); expect(fetchMock.calls()).toHaveLength(10); }); @@ -615,7 +615,7 @@ describe("AlertStore.fetch", () => { }); await expect( - store.fetch("", false, "", "", false, {}) + store.fetch("", false, "", "", false, {}, 5, {}) ).resolves.toHaveProperty("error"); expect(fetchMock.calls()).toHaveLength(10); @@ -626,7 +626,7 @@ describe("AlertStore.fetch", () => { }); await expect( - store.fetch("", false, "", "", false, {}) + store.fetch("", false, "", "", false, {}, 5, {}) ).resolves.toBeUndefined(); expect(fetchMock.calls()).toHaveLength(1); @@ -636,7 +636,7 @@ describe("AlertStore.fetch", () => { }); await expect( - store.fetch("", false, "", "", false, {}) + store.fetch("", false, "", "", false, {}, 5, {}) ).resolves.toHaveProperty("error"); expect(fetchMock.calls()).toHaveLength(10); }); @@ -656,7 +656,7 @@ describe("AlertStore.fetch", () => { ); await expect( - store.fetch("", false, "", "", false, {}) + store.fetch("", false, "", "", false, {}, 5, {}) ).resolves.toBeUndefined(); expect(store.info.reloadNeeded).toBe(true); @@ -673,7 +673,7 @@ describe("AlertStore.fetch", () => { }); await expect( - store.fetch("", false, "", "", false, {}) + store.fetch("", false, "", "", false, {}, 5, {}) ).resolves.toHaveProperty("error"); expect(store.filters.values[0].applied).toBe(true); }); @@ -690,7 +690,7 @@ describe("AlertStore.fetch", () => { // initial fetch, should update settings store.settings.setValues({ foo: "bar" } as any); await expect( - store.fetch("", false, "", "", false, {}) + store.fetch("", false, "", "", false, {}, 5, {}) ).resolves.toBeUndefined(); expect(store.settings.values).toMatchObject({ staticColorLabels: ["job"], @@ -702,7 +702,7 @@ describe("AlertStore.fetch", () => { // second fetch, should keep same settings await expect( - store.fetch("", false, "", "", false, {}) + store.fetch("", false, "", "", false, {}, 5, {}) ).resolves.toBeUndefined(); expect(store.settings.values).toMatchObject({ staticColorLabels: ["job"], @@ -721,7 +721,7 @@ describe("AlertStore.fetch", () => { }); const store = new AlertStore(["label=value"]); await expect( - store.fetch("", false, "", "", false, {}) + store.fetch("", false, "", "", false, {}, 5, {}) ).resolves.toBeUndefined(); expect(store.info.upgradeReady).toBe(false); @@ -731,7 +731,7 @@ describe("AlertStore.fetch", () => { body: JSON.stringify(response), }); await expect( - store.fetch("", false, "", "", false, {}) + store.fetch("", false, "", "", false, {}, 5, {}) ).resolves.toBeUndefined(); expect(store.info.upgradeReady).toBe(true); }); @@ -797,7 +797,7 @@ describe("AlertStore.fetch", () => { }); const store = new AlertStore(["label=value"]); await expect( - store.fetch("", false, "sortOrder", "sortLabel", false, {}) + store.fetch("", false, "sortOrder", "sortLabel", false, {}, 5, {}) ).resolves.toBeUndefined(); expect(fetchMock.calls().length).toEqual(1); expect(fetchMock.calls()[0][0]).toBe("/alerts.json"); @@ -809,6 +809,8 @@ describe("AlertStore.fetch", () => { sortOrder: "sortOrder", sortLabel: "sortLabel", sortReverse: false, + defaultGroupLimit: 5, + groupLimits: {}, }); }); @@ -820,7 +822,7 @@ describe("AlertStore.fetch", () => { }); const store = new AlertStore(["label=value"]); await expect( - store.fetch("cluster", true, "sortOrder", "sortLabel", true, {}) + store.fetch("cluster", true, "sortOrder", "sortLabel", true, {}, 5, {}) ).resolves.toBeUndefined(); expect(fetchMock.calls().length).toEqual(1); expect(fetchMock.calls()[0][0]).toBe("/alerts.json"); @@ -832,6 +834,8 @@ describe("AlertStore.fetch", () => { sortOrder: "sortOrder", sortLabel: "sortLabel", sortReverse: true, + defaultGroupLimit: 5, + groupLimits: {}, }); }); @@ -842,11 +846,23 @@ describe("AlertStore.fetch", () => { body: JSON.stringify(response), }); const store = new AlertStore(["label=value"]); - store.ui.setLimit("foo", "bar", 7); + store.ui.setGridGroupLimit("foo", "bar", 7); + store.ui.setGroupAlertLimit("1234567890", 7); + store.ui.setGroupAlertLimit("1234567891", 1); + await expect( - store.fetch("cluster", true, "sortOrder", "sortLabel", false, { - bar: 7, - }) + store.fetch( + "cluster", + true, + "sortOrder", + "sortLabel", + false, + { + bar: 7, + }, + 5, + { "1234567890": 7, "1234567891": 1 } + ) ).resolves.toBeUndefined(); expect(fetchMock.calls().length).toEqual(1); expect(fetchMock.calls()[0][0]).toBe("/alerts.json"); @@ -858,6 +874,66 @@ describe("AlertStore.fetch", () => { sortOrder: "sortOrder", sortLabel: "sortLabel", sortReverse: false, + defaultGroupLimit: 5, + groupLimits: { "1234567890": 7, "1234567891": 1 }, }); }); + + it("purges unknown group limits after fetch", async () => { + const store = new AlertStore(["label=value"]); + store.ui.setGridGroupLimit("foo", "bar", 7); + store.ui.setGroupAlertLimit("g1", 7); + store.ui.setGroupAlertLimit("g2", 1); + store.ui.setGroupAlertLimit("g4", 6); + + const g1 = MockGroup("group1", 1, 1, 0); + g1.id = "g1"; + const g2 = MockGroup("group2", 1, 1, 0); + g2.id = "g2"; + const g3 = MockGroup("group3", 1, 1, 0); + g3.id = "g3"; + const response = EmptyAPIResponse(); + response.grids = [ + { + labelName: "", + labelValue: "", + alertGroups: [g1, g2, g3], + totalGroups: 3, + stateCount: { unprocessed: 0, active: 3, suppressed: 0 }, + }, + ]; + fetchMock.reset(); + fetchMock.mock("*", { + body: JSON.stringify(response), + }); + + await expect( + store.fetch( + "cluster", + true, + "sortOrder", + "sortLabel", + false, + { + bar: 7, + }, + 5, + { g1: 7, g2: 1, g4: 6 } + ) + ).resolves.toBeUndefined(); + expect(fetchMock.calls().length).toEqual(1); + expect(fetchMock.calls()[0][0]).toBe("/alerts.json"); + expect(JSON.parse(fetchMock.calls()[0][1]?.body as string)).toStrictEqual({ + filters: ["label=value"], + gridLabel: "cluster", + gridLimits: { bar: 7 }, + gridSortReverse: true, + sortOrder: "sortOrder", + sortLabel: "sortLabel", + sortReverse: false, + defaultGroupLimit: 5, + groupLimits: { g1: 7, g2: 1, g4: 6 }, + }); + expect(store.ui.groupAlertLimits).toStrictEqual({ g1: 7, g2: 1 }); + }); }); diff --git a/ui/src/Stores/AlertStore.ts b/ui/src/Stores/AlertStore.ts index a22a20fb3..591e2acf6 100644 --- a/ui/src/Stores/AlertStore.ts +++ b/ui/src/Stores/AlertStore.ts @@ -209,8 +209,11 @@ interface AlertStoreStatusT { interface AlertStoreUIT { isIdle: boolean; setIsIdle: (val: boolean) => void; - limits: { [key: string]: { [val: string]: number } }; - setLimit: (key: string, val: string, limit: number) => void; + gridGroupLimits: { [key: string]: { [val: string]: number } }; + setGridGroupLimit: (key: string, val: string, limit: number) => void; + groupAlertLimits: { [gid: string]: number }; + setGroupAlertLimit: (gid: string, limit: number) => void; + purgeGroupAlertLimits: (knownGids: string[]) => void; } class AlertStore { @@ -558,14 +561,30 @@ 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 } }; + gridGroupLimits: {} as { [key: string]: { [val: string]: number } }, + setGridGroupLimit(key: string, val: string, limit: number) { + this.gridGroupLimits = { + [key]: { ...this.gridGroupLimits[key], [val]: limit }, + }; + }, + groupAlertLimits: {} as { [gid: string]: number }, + setGroupAlertLimit(gid: string, limit: number) { + this.groupAlertLimits[gid] = limit; + }, + purgeGroupAlertLimits(knownGids: string[]) { + const newLimits: { [gid: string]: number } = {}; + Object.entries(this.groupAlertLimits) + .filter(([gid, _]) => knownGids.includes(gid)) + .forEach(([gid, limit]) => { + newLimits[gid] = limit; + }); + this.groupAlertLimits = newLimits; }, }, { setIsIdle: action.bound, - setLimit: action.bound, + setGridGroupLimit: action.bound, + setGroupAlertLimit: action.bound, } ); @@ -579,7 +598,9 @@ class AlertStore { sortOrder: string, sortLabel: string, sortReverse: boolean, - limits: { [key: string]: number } + gridGroupLimits: { [key: string]: number }, + defaultGroupLimit: number, + groupAlertLimits: { [key: string]: number } ) => { this.status.setFetching(); @@ -587,10 +608,12 @@ class AlertStore { filters: this.filters.values.map((f) => f.raw), gridLabel: gridLabel, gridSortReverse: gridSortReverse, - gridLimits: limits, + gridLimits: gridGroupLimits, sortOrder: sortOrder, sortLabel: sortLabel, sortReverse: sortReverse, + defaultGroupLimit: defaultGroupLimit, + groupLimits: groupAlertLimits, }; const alertsURI = FormatBackendURI("alerts.json"); @@ -676,6 +699,16 @@ class AlertStore { updates.receivers = result.receivers; this.data = Object.assign(this.data, updates); + const knowGroups: string[] = []; + result.grids.map((grid) => + grid.alertGroups + .map((group) => group.id) + .forEach((id) => { + knowGroups.push(id); + }) + ); + this.ui.purgeGroupAlertLimits(knowGroups); + // before storing new version check if we need to reload if ( this.info.version !== "unknown" && diff --git a/ui/src/__fixtures__/Alerts.ts b/ui/src/__fixtures__/Alerts.ts index 16467f554..8ec8198f2 100644 --- a/ui/src/__fixtures__/Alerts.ts +++ b/ui/src/__fixtures__/Alerts.ts @@ -56,6 +56,7 @@ const MockAlertGroup = ( ): APIAlertGroupT => ({ receiver: "by-name", labels: rootLabels, + totalAlerts: alerts.length, alerts: alerts, id: "099c5ca6d1c92f615b13056b935d0c8dee70f18c", alertmanagerCount: { diff --git a/ui/src/__fixtures__/Stories.ts b/ui/src/__fixtures__/Stories.ts index 4b34db235..fa10bdd82 100644 --- a/ui/src/__fixtures__/Stories.ts +++ b/ui/src/__fixtures__/Stories.ts @@ -164,6 +164,8 @@ const MockGrid = (alertStore: AlertStore): void => { group.stateCount.active = active; group.stateCount.suppressed = suppressed; group.stateCount.unprocessed = unprocessed; + group.totalAlerts = i; + group.alerts = group.alerts.slice(0, Math.min(i, 5)); if (i === 2 || i === 4) { group.shared.clusters = ["default"]; }