From e69723e345f9532273e01cccb492ba7b29ae424a Mon Sep 17 00:00:00 2001 From: Ihor Panasiuk Date: Wed, 2 Jul 2025 11:16:36 +0200 Subject: [PATCH 1/5] fix: add parentheses for searchCel and dateRange to prevent broken logic --- keep-ui/widgets/alerts-table/ui/useAlertsTableData.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/keep-ui/widgets/alerts-table/ui/useAlertsTableData.ts b/keep-ui/widgets/alerts-table/ui/useAlertsTableData.ts index a95b2e6b92..88ca5832c7 100644 --- a/keep-ui/widgets/alerts-table/ui/useAlertsTableData.ts +++ b/keep-ui/widgets/alerts-table/ui/useAlertsTableData.ts @@ -141,7 +141,10 @@ export const useAlertsTableData = (query: AlertsTableDataQuery | undefined) => { } const filterArray = [query?.searchCel, dateRangeCel]; - return filterArray.filter(Boolean).join(" && "); + return filterArray + .filter(Boolean) + .map((cel) => `(${cel})`) + .join(" && "); }, [query?.searchCel, dateRangeCel]); useEffect(() => { From 38a5de44ebc00d195779160e2e4e542b9c1673d7 Mon Sep 17 00:00:00 2001 From: Ihor Panasiuk Date: Wed, 2 Jul 2025 12:33:53 +0200 Subject: [PATCH 2/5] add unit test --- .../ui/__tests__/useAlertsTableData.test.ts | 318 ++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 keep-ui/widgets/alerts-table/ui/__tests__/useAlertsTableData.test.ts diff --git a/keep-ui/widgets/alerts-table/ui/__tests__/useAlertsTableData.test.ts b/keep-ui/widgets/alerts-table/ui/__tests__/useAlertsTableData.test.ts new file mode 100644 index 0000000000..4e50e02cd3 --- /dev/null +++ b/keep-ui/widgets/alerts-table/ui/__tests__/useAlertsTableData.test.ts @@ -0,0 +1,318 @@ +import { renderHook, act } from "@testing-library/react"; +import { + useAlertsTableData, + AlertsTableDataQuery, +} from "../useAlertsTableData"; +import { AlertsQuery, useAlerts } from "@/entities/alerts/model"; +import { useAlertPolling } from "@/utils/hooks/useAlertPolling"; +import { + AbsoluteTimeFrame, + AllTimeFrame, +} from "@/components/ui/DateRangePickerV2"; + +jest.useFakeTimers(); +jest.mock("@/entities/alerts/model", () => ({ + useAlerts: jest.fn(), +})); +jest.mock("@/utils/hooks/useAlertPolling", () => ({ + useAlertPolling: jest.fn(), +})); +jest.mock("uuid", () => ({ v4: () => "mock-uuid" })); + +const mockUseLastAlerts = jest.fn(); +(useAlerts as jest.Mock).mockReturnValue({ useLastAlerts: mockUseLastAlerts }); + +const mockMutate = jest.fn(); + +const defaultAlerts = [{ id: 1, name: "Alert 1" }]; +const defaultQuery: AlertsTableDataQuery = { + searchCel: "test", + filterCel: "filter", + limit: 10, + offset: 0, + sortOptions: [{ sortBy: "name", sortDirection: "ASC" }], + timeFrame: { type: "relative", deltaMs: 60000, isPaused: false }, +}; + +beforeEach(() => { + jest.clearAllMocks(); + mockUseLastAlerts.mockReturnValue({ + data: defaultAlerts, + totalCount: 1, + isLoading: false, + mutate: mockMutate, + error: null, + queryTimeInSeconds: 1, + }); + (useAlertPolling as jest.Mock).mockReturnValue({ data: null }); +}); + +describe("useAlertsTableData", () => { + it("returns alerts and related data", () => { + const { result } = renderHook(() => useAlertsTableData(defaultQuery)); + expect(result.current.alerts).toEqual(defaultAlerts); + expect(result.current.totalCount).toBe(1); + expect(result.current.alertsLoading).toBe(false); + expect(result.current.facetsCel).toContain("test"); + expect(result.current.alertsError).toBeNull(); + expect(typeof result.current.mutateAlerts).toBe("function"); + }); + + it("handles undefined query", () => { + const { result } = renderHook(() => useAlertsTableData(undefined)); + expect(mockUseLastAlerts).toHaveBeenCalledWith(undefined, { + revalidateOnFocus: false, + revalidateOnMount: true, + }); + }); + + it("handles error state", () => { + mockUseLastAlerts.mockReturnValue({ + data: undefined, + totalCount: 0, + isLoading: false, + mutate: mockMutate, + error: new Error("Test error"), + queryTimeInSeconds: 1, + }); + const { result } = renderHook(() => useAlertsTableData(defaultQuery)); + expect(result.current.alertsError).toBeInstanceOf(Error); + }); + + it("updates alerts when polling token changes", () => { + (useAlertPolling as jest.Mock).mockReturnValue({ data: "token" }); + const { result } = renderHook(() => useAlertsTableData(defaultQuery)); + expect(result.current.alertsChangeToken).toBe("token"); + }); + + it("generates correct facetsCel for absolute timeFrame", () => { + const { result } = renderHook(() => + useAlertsTableData({ + ...defaultQuery, + searchCel: "name == 'foo'", + filterCel: "description in ['bar', 'baz']", + timeFrame: { + type: "absolute", + start: new Date(0), + end: new Date(1000), + isPaused: false, + } as AbsoluteTimeFrame, + }) + ); + expect(result.current.facetsCel).toBe( + "(name == 'foo') && (lastReceived >= '1970-01-01T00:00:00.000Z' && lastReceived <= '1970-01-01T00:00:01.000Z')" + ); + }); + + it("calls useLastAlerts with correct query", () => { + const query: AlertsTableDataQuery = { + searchCel: "name == 'foo'", + filterCel: "(description in ['bar', 'baz'])", + limit: 10, + offset: 200, + sortOptions: [{ sortBy: "name", sortDirection: "ASC" }], + timeFrame: { + type: "absolute", + start: new Date(0), + end: new Date(1000), + isPaused: false, + } as AbsoluteTimeFrame, + }; + const { result } = renderHook(() => useAlertsTableData(query)); + expect(mockUseLastAlerts).toHaveBeenCalledWith( + { + cel: "(name == 'foo') && (lastReceived >= '1970-01-01T00:00:00.000Z' && lastReceived <= '1970-01-01T00:00:01.000Z') && (description in ['bar', 'baz'])", + limit: 10, + offset: 200, + sortOptions: [{ sortBy: "name", sortDirection: "ASC" }], + } as AlertsQuery, + { revalidateOnFocus: false, revalidateOnMount: true } + ); + }); + + it("handles paused state", () => { + const pausedQuery = { + ...defaultQuery, + timeFrame: { ...defaultQuery.timeFrame, isPaused: true }, + }; + const { result } = renderHook(() => useAlertsTableData(pausedQuery)); + expect(result.current.alerts).toEqual(defaultAlerts); + }); + + it("provides facetsPanelRefreshToken when timeframe changes to AllTimeFrame", () => { + const query: AlertsTableDataQuery = { + ...defaultQuery, + timeFrame: { type: "relative", deltaMs: 60000, isPaused: false }, + }; + const { result, rerender } = renderHook( + ({ query }) => useAlertsTableData(query), + { + initialProps: { query }, + } + ); + + act(() => { + jest.advanceTimersByTime(200); // Simulate time passing + }); + + rerender({ + query: { + ...query, + timeFrame: { + type: "all-time", + isPaused: false, + } as AllTimeFrame, + }, + }); + expect(result.current.facetsPanelRefreshToken).toBe(undefined); // should be still undefined + rerender({ + query: { + ...query, + timeFrame: { + type: "all-time", + isPaused: false, + } as AllTimeFrame, + }, + }); + + expect(result.current.facetsPanelRefreshToken).toBe("mock-uuid"); + }); + + // Additional tests + + it("returns null facetsCel if query or dateRangeCel is null", () => { + const { result } = renderHook(() => useAlertsTableData(undefined)); + expect(result.current.facetsCel).toBeNull(); + }); + + it("calls mutateAlerts when mutateAlerts is invoked", () => { + const { result } = renderHook(() => useAlertsTableData(defaultQuery)); + act(() => { + result.current.mutateAlerts(); + }); + expect(mockMutate).toHaveBeenCalled(); + }); + + it("returns alerts if isPaused and alertsLoading is false", () => { + mockUseLastAlerts.mockReturnValueOnce({ + data: defaultAlerts, + totalCount: 1, + isLoading: false, + mutate: mockMutate, + error: null, + queryTimeInSeconds: 1, + }); + const pausedQuery = { + ...defaultQuery, + timeFrame: { ...defaultQuery.timeFrame, isPaused: true }, + }; + const { result } = renderHook(() => useAlertsTableData(pausedQuery)); + expect(result.current.alerts).toEqual(defaultAlerts); + }); + + it("alertsLoading is false when isLoading is true and polling is triggered", () => { + mockUseLastAlerts.mockReturnValueOnce({ + data: defaultAlerts, + totalCount: 1, + isLoading: true, + mutate: mockMutate, + error: null, + queryTimeInSeconds: 1, + }); + (useAlertPolling as jest.Mock).mockReturnValueOnce({ + data: "polling-token", + }); + const { result } = renderHook(() => useAlertsTableData(defaultQuery)); + + act(() => { + jest.advanceTimersByTime(1000); // Simulate time passing for polling + }); + + // isPolling is set to false after mount, so alertsLoading should be true + expect(result.current.alertsLoading).toBe(false); + }); + + it("alertsLoading is true when isLoading is true and polling has expired", () => { + mockUseLastAlerts.mockReturnValue({ + data: defaultAlerts, + totalCount: 1, + isLoading: true, + mutate: mockMutate, + error: null, + queryTimeInSeconds: 1, + }); + (useAlertPolling as jest.Mock).mockReturnValue({ data: "polling-token" }); + const { result, rerender } = renderHook( + ({ query }) => useAlertsTableData(query), + { + initialProps: { query: defaultQuery }, + } + ); + + act(() => { + jest.advanceTimersByTime(16000); // Simulate time passing for polling + }); + + rerender({ + query: { + ...defaultQuery, + searchCel: "foo", + }, + }); // trigger query change + + expect(result.current.alertsLoading).toBe(true); + }); + + it("returns correct facetsCel with only searchCel", () => { + const { result } = renderHook(() => + useAlertsTableData({ + ...defaultQuery, + timeFrame: { + type: "all-time", + isPaused: false, + } as AllTimeFrame, + searchCel: "name == 'foo'", + filterCel: "", + }) + ); + expect(result.current.facetsCel).toBe("(name == 'foo')"); + }); + + it("returns correct facetsCel with only dateRangeCel", () => { + const { result } = renderHook(() => + useAlertsTableData({ + ...defaultQuery, + timeFrame: { + type: "absolute", + start: new Date("2025-07-02T10:28:27.289Z"), + end: new Date("2025-07-02T10:29:24.640Z"), + isPaused: false, + } as AbsoluteTimeFrame, + searchCel: "", + filterCel: "", + }) + ); + expect(result.current.facetsCel).toBe( + "(lastReceived >= '2025-07-02T10:28:27.289Z' && lastReceived <= '2025-07-02T10:29:24.640Z')" + ); + }); + + it("returns correct facetsCel with both searchCel and dateRangeCel", () => { + const { result } = renderHook(() => + useAlertsTableData({ + ...defaultQuery, + timeFrame: { + type: "absolute", + start: new Date("2025-07-02T10:28:27.289Z"), + end: new Date("2025-07-02T10:29:24.640Z"), + isPaused: false, + } as AbsoluteTimeFrame, + searchCel: "name == 'foo'", + filterCel: "description in ['bar', 'baz']", + }) + ); + expect(result.current.facetsCel).toContain( + "(name == 'foo') && (lastReceived >= '2025-07-02T10:28:27.289Z' && lastReceived <= '2025-07-02T10:29:24.640Z')" + ); + }); +}); From 4be098e03f62b8cd30f058ad0026877bcc521909 Mon Sep 17 00:00:00 2001 From: Ihor Panasiuk Date: Wed, 2 Jul 2025 13:05:18 +0200 Subject: [PATCH 3/5] fix: update filterCel handling in useAlertsTableData for correct query formation --- .../alerts-table/ui/__tests__/useAlertsTableData.test.ts | 6 +++--- keep-ui/widgets/alerts-table/ui/useAlertsTableData.ts | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/keep-ui/widgets/alerts-table/ui/__tests__/useAlertsTableData.test.ts b/keep-ui/widgets/alerts-table/ui/__tests__/useAlertsTableData.test.ts index 4e50e02cd3..3faa901862 100644 --- a/keep-ui/widgets/alerts-table/ui/__tests__/useAlertsTableData.test.ts +++ b/keep-ui/widgets/alerts-table/ui/__tests__/useAlertsTableData.test.ts @@ -107,7 +107,7 @@ describe("useAlertsTableData", () => { it("calls useLastAlerts with correct query", () => { const query: AlertsTableDataQuery = { searchCel: "name == 'foo'", - filterCel: "(description in ['bar', 'baz'])", + filterCel: "description in ['bar', 'baz']", limit: 10, offset: 200, sortOptions: [{ sortBy: "name", sortDirection: "ASC" }], @@ -268,8 +268,8 @@ describe("useAlertsTableData", () => { useAlertsTableData({ ...defaultQuery, timeFrame: { - type: "all-time", - isPaused: false, + type: "all-time", + isPaused: false, } as AllTimeFrame, searchCel: "name == 'foo'", filterCel: "", diff --git a/keep-ui/widgets/alerts-table/ui/useAlertsTableData.ts b/keep-ui/widgets/alerts-table/ui/useAlertsTableData.ts index 88ca5832c7..08d926bc47 100644 --- a/keep-ui/widgets/alerts-table/ui/useAlertsTableData.ts +++ b/keep-ui/widgets/alerts-table/ui/useAlertsTableData.ts @@ -153,11 +153,12 @@ export const useAlertsTableData = (query: AlertsTableDataQuery | undefined) => { return; } + const filterCel = query.filterCel ? `(${query.filterCel})` : ""; const alertsQuery: AlertsQuery = { limit: query.limit, offset: query.offset, sortOptions: query.sortOptions, - cel: [mainCelQuery, query.filterCel].filter(Boolean).join(" && "), + cel: [mainCelQuery, filterCel].filter(Boolean).join(" && "), }; setAlertsQueryState(alertsQuery); From 5602b7f94b0e4144962df3ef7b1a1116ccfe246e Mon Sep 17 00:00:00 2001 From: Ihor Panasiuk Date: Wed, 2 Jul 2025 14:24:32 +0200 Subject: [PATCH 4/5] fix bug when infinite rendering was caused by initial value being always a new object every render --- keep-ui/utils/hooks/useLocalStorage.ts | 19 +-- .../widgets/alerts-table/ui/alert-table.tsx | 114 +++++++++--------- 2 files changed, 69 insertions(+), 64 deletions(-) diff --git a/keep-ui/utils/hooks/useLocalStorage.ts b/keep-ui/utils/hooks/useLocalStorage.ts index b50e7b0808..ba693e148c 100644 --- a/keep-ui/utils/hooks/useLocalStorage.ts +++ b/keep-ui/utils/hooks/useLocalStorage.ts @@ -1,7 +1,7 @@ "use client"; // culled from https://github.com/cpvalente/ontime/blob/master/apps/client/src/common/hooks/useLocalStorage.ts -import { useSyncExternalStore } from "react"; +import { useMemo, useRef, useSyncExternalStore } from "react"; const STORAGE_EVENT = "keephq"; @@ -10,7 +10,7 @@ function getSnapshot(key: string): string | null { if (typeof window === "undefined" || typeof localStorage === "undefined") { return null; } - + try { return localStorage.getItem(`keephq-${key}`); } catch { @@ -30,14 +30,15 @@ function getParsedJson( } export const useLocalStorage = (key: string, initialValue: T) => { - const localStorageValue = useSyncExternalStore(subscribe, () => - getSnapshot(key), + const localStorageValue = useSyncExternalStore( + subscribe, + () => getSnapshot(key), () => JSON.stringify(initialValue) ); - const parsedLocalStorageValue = getParsedJson( - localStorageValue, - initialValue - ); + const initialValueRef = useRef(initialValue); + initialValueRef.current = initialValue; + + const parsedLocalStorageValue = useMemo(() => getParsedJson(localStorageValue, initialValueRef.current), [localStorageValue]); /** * @description Set value to local storage @@ -48,7 +49,7 @@ export const useLocalStorage = (key: string, initialValue: T) => { if (typeof window === "undefined" || typeof localStorage === "undefined") { return; } - + // Allow value to be a function so we have same API as useState const valueToStore = value instanceof Function ? value(parsedLocalStorageValue) : value; diff --git a/keep-ui/widgets/alerts-table/ui/alert-table.tsx b/keep-ui/widgets/alerts-table/ui/alert-table.tsx index 834ec41008..5cfff6d806 100644 --- a/keep-ui/widgets/alerts-table/ui/alert-table.tsx +++ b/keep-ui/widgets/alerts-table/ui/alert-table.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import clsx from "clsx"; import { Card, Table } from "@tremor/react"; import { @@ -200,66 +200,70 @@ export function AlertTable({ const { toggleAll, areAllGroupsExpanded } = groupExpansionState; const isGroupingActive = grouping.length > 0; - const filteredAlerts = alerts.filter((alert) => { - // First apply tab filter - if (!tabs[selectedTab].filter(alert)) { - return false; - } - - // Then apply facet filters - return Object.entries(facetFilters).every(([facetKey, includedValues]) => { - // If no values are included, don't filter - if (includedValues.length === 0) { - return true; + const filteredAlerts = useMemo(() => { + return alerts.filter((alert) => { + // First apply tab filter + if (!tabs[selectedTab].filter(alert)) { + return false; } - let value; - if (facetKey.includes(".")) { - // Handle nested keys like "labels.job" - const [parentKey, childKey] = facetKey.split("."); - const parentValue = alert[parentKey as keyof AlertDto]; - - if ( - typeof parentValue === "object" && - parentValue !== null && - !Array.isArray(parentValue) && - !(parentValue instanceof Date) - ) { - value = (parentValue as Record)[childKey]; - } - } else { - value = alert[facetKey as keyof AlertDto]; - } + // Then apply facet filters + return Object.entries(facetFilters).every( + ([facetKey, includedValues]) => { + // If no values are included, don't filter + if (includedValues.length === 0) { + return true; + } - // Handle source array separately - if (facetKey === "source") { - const sources = value as string[]; + let value; + if (facetKey.includes(".")) { + // Handle nested keys like "labels.job" + const [parentKey, childKey] = facetKey.split("."); + const parentValue = alert[parentKey as keyof AlertDto]; + + if ( + typeof parentValue === "object" && + parentValue !== null && + !Array.isArray(parentValue) && + !(parentValue instanceof Date) + ) { + value = (parentValue as Record)[childKey]; + } + } else { + value = alert[facetKey as keyof AlertDto]; + } - // Check if n/a is selected and sources is empty/null - if (includedValues.includes("n/a")) { - return !sources || sources.length === 0; - } + // Handle source array separately + if (facetKey === "source") { + const sources = value as string[]; - return ( - Array.isArray(sources) && - sources.some((source) => includedValues.includes(source)) - ); - } + // Check if n/a is selected and sources is empty/null + if (includedValues.includes("n/a")) { + return !sources || sources.length === 0; + } - // Handle n/a cases for other facets - if (includedValues.includes("n/a")) { - return value === null || value === undefined || value === ""; - } + return ( + Array.isArray(sources) && + sources.some((source) => includedValues.includes(source)) + ); + } - // For non-n/a cases, convert value to string for comparison - // Skip null/undefined values as they should only match n/a - if (value === null || value === undefined || value === "") { - return false; - } + // Handle n/a cases for other facets + if (includedValues.includes("n/a")) { + return value === null || value === undefined || value === ""; + } + + // For non-n/a cases, convert value to string for comparison + // Skip null/undefined values as they should only match n/a + if (value === null || value === undefined || value === "") { + return false; + } - return includedValues.includes(String(value)); + return includedValues.includes(String(value)); + } + ); }); - }); + }, [alerts, facetFilters, selectedTab, tabs]); const leftPinnedColumns = noisyAlertsEnabled ? ["severity", "checkbox", "status", "source", "name", "noise"] @@ -372,8 +376,8 @@ export function AlertTable({ isCreateIncidentWithAIOpen={isCreateIncidentWithAIOpen} /> ) : ( - + />
From 058f37ddcb9ef20f81a326d582f8a4e1fa23a863 Mon Sep 17 00:00:00 2001 From: Ihor Panasiuk Date: Wed, 2 Jul 2025 14:27:37 +0200 Subject: [PATCH 5/5] Revert "fix bug when infinite rendering was caused by initial value being always a new object every render" This reverts commit 5602b7f94b0e4144962df3ef7b1a1116ccfe246e. --- keep-ui/utils/hooks/useLocalStorage.ts | 19 ++- .../widgets/alerts-table/ui/alert-table.tsx | 114 +++++++++--------- 2 files changed, 64 insertions(+), 69 deletions(-) diff --git a/keep-ui/utils/hooks/useLocalStorage.ts b/keep-ui/utils/hooks/useLocalStorage.ts index ba693e148c..b50e7b0808 100644 --- a/keep-ui/utils/hooks/useLocalStorage.ts +++ b/keep-ui/utils/hooks/useLocalStorage.ts @@ -1,7 +1,7 @@ "use client"; // culled from https://github.com/cpvalente/ontime/blob/master/apps/client/src/common/hooks/useLocalStorage.ts -import { useMemo, useRef, useSyncExternalStore } from "react"; +import { useSyncExternalStore } from "react"; const STORAGE_EVENT = "keephq"; @@ -10,7 +10,7 @@ function getSnapshot(key: string): string | null { if (typeof window === "undefined" || typeof localStorage === "undefined") { return null; } - + try { return localStorage.getItem(`keephq-${key}`); } catch { @@ -30,15 +30,14 @@ function getParsedJson( } export const useLocalStorage = (key: string, initialValue: T) => { - const localStorageValue = useSyncExternalStore( - subscribe, - () => getSnapshot(key), + const localStorageValue = useSyncExternalStore(subscribe, () => + getSnapshot(key), () => JSON.stringify(initialValue) ); - const initialValueRef = useRef(initialValue); - initialValueRef.current = initialValue; - - const parsedLocalStorageValue = useMemo(() => getParsedJson(localStorageValue, initialValueRef.current), [localStorageValue]); + const parsedLocalStorageValue = getParsedJson( + localStorageValue, + initialValue + ); /** * @description Set value to local storage @@ -49,7 +48,7 @@ export const useLocalStorage = (key: string, initialValue: T) => { if (typeof window === "undefined" || typeof localStorage === "undefined") { return; } - + // Allow value to be a function so we have same API as useState const valueToStore = value instanceof Function ? value(parsedLocalStorageValue) : value; diff --git a/keep-ui/widgets/alerts-table/ui/alert-table.tsx b/keep-ui/widgets/alerts-table/ui/alert-table.tsx index 5cfff6d806..834ec41008 100644 --- a/keep-ui/widgets/alerts-table/ui/alert-table.tsx +++ b/keep-ui/widgets/alerts-table/ui/alert-table.tsx @@ -1,4 +1,4 @@ -import { useMemo, useRef, useState } from "react"; +import { useRef, useState } from "react"; import clsx from "clsx"; import { Card, Table } from "@tremor/react"; import { @@ -200,70 +200,66 @@ export function AlertTable({ const { toggleAll, areAllGroupsExpanded } = groupExpansionState; const isGroupingActive = grouping.length > 0; - const filteredAlerts = useMemo(() => { - return alerts.filter((alert) => { - // First apply tab filter - if (!tabs[selectedTab].filter(alert)) { - return false; - } + const filteredAlerts = alerts.filter((alert) => { + // First apply tab filter + if (!tabs[selectedTab].filter(alert)) { + return false; + } - // Then apply facet filters - return Object.entries(facetFilters).every( - ([facetKey, includedValues]) => { - // If no values are included, don't filter - if (includedValues.length === 0) { - return true; - } + // Then apply facet filters + return Object.entries(facetFilters).every(([facetKey, includedValues]) => { + // If no values are included, don't filter + if (includedValues.length === 0) { + return true; + } - let value; - if (facetKey.includes(".")) { - // Handle nested keys like "labels.job" - const [parentKey, childKey] = facetKey.split("."); - const parentValue = alert[parentKey as keyof AlertDto]; - - if ( - typeof parentValue === "object" && - parentValue !== null && - !Array.isArray(parentValue) && - !(parentValue instanceof Date) - ) { - value = (parentValue as Record)[childKey]; - } - } else { - value = alert[facetKey as keyof AlertDto]; - } + let value; + if (facetKey.includes(".")) { + // Handle nested keys like "labels.job" + const [parentKey, childKey] = facetKey.split("."); + const parentValue = alert[parentKey as keyof AlertDto]; + + if ( + typeof parentValue === "object" && + parentValue !== null && + !Array.isArray(parentValue) && + !(parentValue instanceof Date) + ) { + value = (parentValue as Record)[childKey]; + } + } else { + value = alert[facetKey as keyof AlertDto]; + } - // Handle source array separately - if (facetKey === "source") { - const sources = value as string[]; + // Handle source array separately + if (facetKey === "source") { + const sources = value as string[]; - // Check if n/a is selected and sources is empty/null - if (includedValues.includes("n/a")) { - return !sources || sources.length === 0; - } + // Check if n/a is selected and sources is empty/null + if (includedValues.includes("n/a")) { + return !sources || sources.length === 0; + } - return ( - Array.isArray(sources) && - sources.some((source) => includedValues.includes(source)) - ); - } + return ( + Array.isArray(sources) && + sources.some((source) => includedValues.includes(source)) + ); + } - // Handle n/a cases for other facets - if (includedValues.includes("n/a")) { - return value === null || value === undefined || value === ""; - } + // Handle n/a cases for other facets + if (includedValues.includes("n/a")) { + return value === null || value === undefined || value === ""; + } - // For non-n/a cases, convert value to string for comparison - // Skip null/undefined values as they should only match n/a - if (value === null || value === undefined || value === "") { - return false; - } + // For non-n/a cases, convert value to string for comparison + // Skip null/undefined values as they should only match n/a + if (value === null || value === undefined || value === "") { + return false; + } - return includedValues.includes(String(value)); - } - ); + return includedValues.includes(String(value)); }); - }, [alerts, facetFilters, selectedTab, tabs]); + }); const leftPinnedColumns = noisyAlertsEnabled ? ["severity", "checkbox", "status", "source", "name", "noise"] @@ -376,8 +372,8 @@ export function AlertTable({ isCreateIncidentWithAIOpen={isCreateIncidentWithAIOpen} /> ) : ( - + />