From 7b452d202f53e979573bd0156ca1aa7928903e79 Mon Sep 17 00:00:00 2001 From: Dennis Oelkers Date: Mon, 6 Jan 2025 08:35:34 +0100 Subject: [PATCH 01/13] Introducing `ReplaySearchContext` to decouple from URL. --- ...nditions.tsx => AggregationConditions.tsx} | 7 +- .../replay-search/EventInfoBar.test.tsx | 70 +++---- .../replay-search/EventInfoBar.tsx | 18 +- .../replay-search/Notifications.tsx | 7 +- .../replay-search/ReplaySearchContext.tsx | 27 +++ .../useAlertAndEventDefinitionData.test.tsx | 106 +++++++++++ .../hooks/useAlertAndEventDefinitionData.tsx | 15 +- .../hooks/useAttributeComponents.tsx | 17 +- .../hooks/useReplaySearchContext.ts | 22 +++ .../{types.d.ts => types.ts} | 2 + .../useAlertAndEventDefinitionData.test.tsx | 173 ------------------ .../src/views/components/views/ViewHeader.tsx | 23 ++- .../EventDefinitionReplaySearchPage.test.tsx | 2 +- .../pages/EventDefinitionReplaySearchPage.tsx | 6 +- .../pages/EventReplaySearchPage.test.tsx | 2 +- .../src/views/pages/EventReplaySearchPage.tsx | 6 +- 16 files changed, 242 insertions(+), 261 deletions(-) rename graylog2-web-interface/src/components/event-definitions/replay-search/{AggreagtionConditions.tsx => AggregationConditions.tsx} (93%) create mode 100644 graylog2-web-interface/src/components/event-definitions/replay-search/ReplaySearchContext.tsx create mode 100644 graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData.test.tsx rename graylog2-web-interface/src/{ => components/event-definitions/replay-search}/hooks/useAlertAndEventDefinitionData.tsx (67%) create mode 100644 graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useReplaySearchContext.ts rename graylog2-web-interface/src/components/event-definitions/{types.d.ts => types.ts} (96%) delete mode 100644 graylog2-web-interface/src/hooks/useAlertAndEventDefinitionData.test.tsx diff --git a/graylog2-web-interface/src/components/event-definitions/replay-search/AggreagtionConditions.tsx b/graylog2-web-interface/src/components/event-definitions/replay-search/AggregationConditions.tsx similarity index 93% rename from graylog2-web-interface/src/components/event-definitions/replay-search/AggreagtionConditions.tsx rename to graylog2-web-interface/src/components/event-definitions/replay-search/AggregationConditions.tsx index 1d0ab85e3748..b6f2d5232978 100644 --- a/graylog2-web-interface/src/components/event-definitions/replay-search/AggreagtionConditions.tsx +++ b/graylog2-web-interface/src/components/event-definitions/replay-search/AggregationConditions.tsx @@ -25,11 +25,13 @@ import type HighlightingRule from 'views/logic/views/formatting/highlighting/Hig import { conditionToExprMapper, exprToConditionMapper } from 'views/logic/ExpressionConditionMappers'; import useAppSelector from 'stores/useAppSelector'; import { selectHighlightingRules } from 'views/logic/slices/highlightSelectors'; -import useAlertAndEventDefinitionData from 'hooks/useAlertAndEventDefinitionData'; import { updateHighlightingRule, createHighlightingRules } from 'views/logic/slices/highlightActions'; import { randomColor } from 'views/logic/views/formatting/highlighting/HighlightingRule'; import useAppDispatch from 'stores/useAppDispatch'; import NoAttributeProvided from 'components/event-definitions/replay-search/NoAttributeProvided'; +import useReplaySearchContext from 'components/event-definitions/replay-search/hooks/useReplaySearchContext'; + +import useAlertAndEventDefinitionData from './hooks/useAlertAndEventDefinitionData'; const List = styled.div` display: flex; @@ -53,7 +55,8 @@ const useHighlightingRules = () => useAppSelector(selectHighlightingRules); const AggregationConditions = () => { const dispatch = useAppDispatch(); - const { aggregations } = useAlertAndEventDefinitionData(); + const { alertId, definitionId } = useReplaySearchContext(); + const { aggregations } = useAlertAndEventDefinitionData(alertId, definitionId); const highlightingRules = useHighlightingRules(); const aggregationsMap = useMemo(() => new Map(aggregations.map((agg) => [ diff --git a/graylog2-web-interface/src/components/event-definitions/replay-search/EventInfoBar.test.tsx b/graylog2-web-interface/src/components/event-definitions/replay-search/EventInfoBar.test.tsx index 932886b20d77..088d096496b3 100644 --- a/graylog2-web-interface/src/components/event-definitions/replay-search/EventInfoBar.test.tsx +++ b/graylog2-web-interface/src/components/event-definitions/replay-search/EventInfoBar.test.tsx @@ -20,7 +20,6 @@ import { render, screen, fireEvent } from 'wrappedTestingLibrary'; import MockStore from 'helpers/mocking/StoreMock'; import asMock from 'helpers/mocking/AsMock'; import EventInfoBar from 'components/event-definitions/replay-search/EventInfoBar'; -import useAlertAndEventDefinitionData from 'hooks/useAlertAndEventDefinitionData'; import { mockedMappedAggregation, mockEventData, @@ -31,6 +30,10 @@ import { selectHighlightingRules } from 'views/logic/slices/highlightSelectors'; import HighlightingRule from 'views/logic/views/formatting/highlighting/HighlightingRule'; import { StaticColor } from 'views/logic/views/formatting/highlighting/HighlightingColor'; import useViewsPlugin from 'views/test/testViewsPlugin'; +import type { AlertType } from 'components/event-definitions/types'; +import ReplaySearchContext from 'components/event-definitions/replay-search/ReplaySearchContext'; + +import useAlertAndEventDefinitionData from './hooks/useAlertAndEventDefinitionData'; jest.mock('stores/event-notifications/EventNotificationsStore', () => ({ EventNotificationsActions: { @@ -39,7 +42,7 @@ jest.mock('stores/event-notifications/EventNotificationsStore', () => ({ EventNotificationsStore: MockStore((['getInitialState', () => ({ all: [{ id: 'email_notification_id', title: 'Email notification' }] })])), })); -jest.mock('hooks/useAlertAndEventDefinitionData'); +jest.mock('./hooks/useAlertAndEventDefinitionData'); jest.mock('views/logic/Widgets', () => ({ ...jest.requireActual('views/logic/Widgets'), @@ -51,36 +54,36 @@ jest.mock('views/logic/Widgets', () => ({ }), })); -const setMockedHookCache = ({ +const mockUseAlertAndEventDefinitionData = ({ eventData = mockEventData.event, eventDefinition = mockEventDefinitionTwoAggregations, aggregations = mockedMappedAggregation, - isEvent = false, - isEventDefinition = false, - isAlert = false, alertId = mockEventData.event.id, definitionId = mockEventDefinitionTwoAggregations.id, definitionTitle = mockEventDefinitionTwoAggregations.title, -}) => asMock(useAlertAndEventDefinitionData).mockImplementation(() => ({ +}) => asMock(useAlertAndEventDefinitionData).mockReturnValue({ eventData, eventDefinition, aggregations, - isEvent, - isEventDefinition, - isAlert, alertId, definitionId, definitionTitle, -})); +}); jest.mock('views/logic/slices/highlightSelectors', () => ({ selectHighlightingRules: jest.fn(), })); describe('', () => { - const EventInfoComponent = () => ( + const EventInfoComponent = ({ type }: { type: AlertType }) => ( - + + + ); @@ -91,16 +94,15 @@ describe('', () => { .mockReturnValue([ HighlightingRule.create('count(field1)', 500, 'greater', StaticColor.create('#fff')), HighlightingRule.create('count(field2)', 8000, 'less', StaticColor.create('#000')), - ], - ); + ]); }); - it('Always shows fields: Priority, Execute search every, Search within, Description, Notifications, Aggregation conditions', async () => { - setMockedHookCache({ - isEvent: true, - }); + beforeEach(() => { + mockUseAlertAndEventDefinitionData({}); + }); - render(); + it('Always shows fields: Priority, Execute search every, Search within, Description, Notifications, Aggregation conditions', async () => { + render(); const priority = await screen.findByTitle('Priority'); const execution = await screen.findByTitle('Execute search every'); @@ -123,11 +125,7 @@ describe('', () => { }); it('Shows event timestamp and event definition link for event', async () => { - setMockedHookCache({ - isEvent: true, - }); - - render(); + render(); const timestamp = await screen.findByTitle('Timestamp'); const eventDefinition = await screen.findByTitle('Event definition'); @@ -138,11 +136,7 @@ describe('', () => { }); it("Didn't Shows Event definition updated at for event definition which was updated before event", async () => { - setMockedHookCache({ - isEvent: true, - }); - - render(); + render(); const eventDefinitionUpdated = screen.queryByTitle('Event definition updated at'); @@ -150,15 +144,14 @@ describe('', () => { }); it('Shows Event definition updated at for event definition which was updated after event', async () => { - setMockedHookCache({ - isEvent: true, + mockUseAlertAndEventDefinitionData({ eventDefinition: { ...mockEventDefinitionTwoAggregations, updated_at: '2023-03-21T13:28:09.296Z', }, }); - render(); + render(); const eventDefinitionUpdated = await screen.findByTitle('Event definition updated at'); @@ -166,12 +159,11 @@ describe('', () => { }); it('Do not shows event timestamp and event definition link for event definition', async () => { - setMockedHookCache({ - isEventDefinition: true, + mockUseAlertAndEventDefinitionData({ eventData: undefined, }); - render(); + render(); const timestamp = screen.queryByTitle('Timestamp'); const eventDefinition = screen.queryByTitle('Event definition'); @@ -181,11 +173,7 @@ describe('', () => { }); it('show and hide data on button click', async () => { - setMockedHookCache({ - isEventDefinition: true, - }); - - render(); + render(); const hideButton = await screen.findByText('Hide event definition details'); const detailsContainer = await screen.findByTestId('info-container'); diff --git a/graylog2-web-interface/src/components/event-definitions/replay-search/EventInfoBar.tsx b/graylog2-web-interface/src/components/event-definitions/replay-search/EventInfoBar.tsx index 202cc18496b0..3e0c89d5a562 100644 --- a/graylog2-web-interface/src/components/event-definitions/replay-search/EventInfoBar.tsx +++ b/graylog2-web-interface/src/components/event-definitions/replay-search/EventInfoBar.tsx @@ -21,9 +21,10 @@ import styled from 'styled-components'; import { Button } from 'components/bootstrap'; import { FlatContentRow, Icon } from 'components/common'; -import useAlertAndEventDefinitionData from 'hooks/useAlertAndEventDefinitionData'; import useAttributeComponents from 'components/event-definitions/replay-search/hooks/useAttributeComponents'; import NoAttributeProvided from 'components/event-definitions/replay-search/NoAttributeProvided'; +import useReplaySearchContext from 'components/event-definitions/replay-search/hooks/useReplaySearchContext'; +import assertUnreachable from 'logic/assertUnreachable'; const Header = styled.div` display: flex; @@ -55,7 +56,7 @@ const Value = styled.div` `; const EventInfoBar = () => { - const { isEventDefinition, isEvent, isAlert } = useAlertAndEventDefinitionData(); + const { type } = useReplaySearchContext(); const [open, setOpen] = useState(true); const toggleOpen = useCallback((e: SyntheticEvent) => { @@ -66,12 +67,13 @@ const EventInfoBar = () => { const infoAttributes = useAttributeComponents(); const currentTypeText = useMemo(() => { - if (isEventDefinition) return 'event definition'; - if (isAlert) return 'alert'; - if (isEvent) return 'event'; - - return ''; - }, [isAlert, isEvent, isEventDefinition]); + switch (type) { + case 'alert': return 'alert'; + case 'event': return 'event'; + case 'event_definition': return 'event definition'; + default: return assertUnreachable(type, `Invalid replay type: ${type}`); + } + }, [type]); return ( diff --git a/graylog2-web-interface/src/components/event-definitions/replay-search/Notifications.tsx b/graylog2-web-interface/src/components/event-definitions/replay-search/Notifications.tsx index 61806c7a4a8c..72a36a92a624 100644 --- a/graylog2-web-interface/src/components/event-definitions/replay-search/Notifications.tsx +++ b/graylog2-web-interface/src/components/event-definitions/replay-search/Notifications.tsx @@ -21,11 +21,14 @@ import { Link } from 'components/common/router'; import Routes from 'routing/Routes'; import { useStore } from 'stores/connect'; import { EventNotificationsStore } from 'stores/event-notifications/EventNotificationsStore'; -import useAlertAndEventDefinitionData from 'hooks/useAlertAndEventDefinitionData'; import NoAttributeProvided from 'components/event-definitions/replay-search/NoAttributeProvided'; +import useReplaySearchContext from 'components/event-definitions/replay-search/hooks/useReplaySearchContext'; + +import useAlertAndEventDefinitionData from './hooks/useAlertAndEventDefinitionData'; const Notifications = () => { - const { eventDefinition } = useAlertAndEventDefinitionData(); + const { alertId, definitionId } = useReplaySearchContext(); + const { eventDefinition } = useAlertAndEventDefinitionData(alertId, definitionId); const allNotifications = useStore(EventNotificationsStore, ({ all }) => all.reduce((res, cur) => { res[cur.id] = cur; diff --git a/graylog2-web-interface/src/components/event-definitions/replay-search/ReplaySearchContext.tsx b/graylog2-web-interface/src/components/event-definitions/replay-search/ReplaySearchContext.tsx new file mode 100644 index 000000000000..6df8526508f5 --- /dev/null +++ b/graylog2-web-interface/src/components/event-definitions/replay-search/ReplaySearchContext.tsx @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import * as React from 'react'; + +import type { AlertType } from 'components/event-definitions/types'; + +type ReplaySearchContextType = { + alertId: string; + definitionId: string; + type: AlertType; +} +const ReplaySearchContext = React.createContext(undefined); +export default ReplaySearchContext; diff --git a/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData.test.tsx b/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData.test.tsx new file mode 100644 index 000000000000..58ab4ac7cf02 --- /dev/null +++ b/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData.test.tsx @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import { renderHook } from 'wrappedTestingLibrary/hooks'; + +import { + mockedMappedAggregation, + mockEventData, + mockEventDefinitionTwoAggregations, +} from 'helpers/mocking/EventAndEventDefinitions_mock'; +import asMock from 'helpers/mocking/AsMock'; +import useAlertAndEventDefinitionData from 'components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData'; +import useEventById from 'hooks/useEventById'; +import useEventDefinition from 'hooks/useEventDefinition'; + +jest.mock('hooks/useEventById'); +jest.mock('hooks/useEventDefinition'); + +const mockedHookData = { + alertId: mockEventData.event.id, + definitionId: mockEventData.event.event_definition_id, + definitionTitle: mockEventDefinitionTwoAggregations.title, + eventData: mockEventData.event, + eventDefinition: mockEventDefinitionTwoAggregations, + aggregations: mockedMappedAggregation, +}; + +const hookResultBase = { + refetch: () => {}, + isLoading: false, + isFetched: true, +} as const; + +describe('useAlertAndEventDefinitionData', () => { + beforeEach(() => { + asMock(useEventDefinition).mockReturnValue({ + ...hookResultBase, + data: { + eventDefinition: mockEventDefinitionTwoAggregations, + aggregations: mockedMappedAggregation, + }, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return expected data for alert page', async () => { + const eventId = 'event-id-1'; + + asMock(useEventById).mockReturnValue({ + ...hookResultBase, + data: { ...mockEventData.event, id: eventId, alert: true }, + }); + + const { result } = renderHook(() => useAlertAndEventDefinitionData(eventId)); + + await expect(result.current).toEqual(mockedHookData); + }); + + it('should return expected data for event page', async () => { + const eventId = 'event-id-2'; + + asMock(useEventById).mockReturnValue({ + ...hookResultBase, + data: { ...mockEventData.event, id: eventId, alert: false }, + }); + + const { result } = renderHook(() => useAlertAndEventDefinitionData(eventId)); + + await expect(result.current).toEqual({ + ...mockedHookData, + eventData: { ...mockEventData.event, id: eventId, alert: false }, + alertId: eventId, + }); + }); + + it('should return expected data for event definition', async () => { + asMock(useEventById).mockReturnValue({ + ...hookResultBase, + data: undefined, + }); + + const { result } = renderHook(() => useAlertAndEventDefinitionData(undefined, mockEventDefinitionTwoAggregations.id)); + + await expect(result.current).toEqual({ + ...mockedHookData, + eventData: undefined, + alertId: undefined, + }); + }); +}); diff --git a/graylog2-web-interface/src/hooks/useAlertAndEventDefinitionData.tsx b/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData.tsx similarity index 67% rename from graylog2-web-interface/src/hooks/useAlertAndEventDefinitionData.tsx rename to graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData.tsx index 930acc793a73..e7746a8ebdfa 100644 --- a/graylog2-web-interface/src/hooks/useAlertAndEventDefinitionData.tsx +++ b/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData.tsx @@ -17,18 +17,13 @@ import { useMemo } from 'react'; -import useLocation from 'routing/useLocation'; -import Routes from 'routing/Routes'; -import useParams from 'routing/useParams'; import type { Event } from 'components/events/events/types'; import type { EventDefinitionAggregation } from 'hooks/useEventDefinition'; import useEventDefinition from 'hooks/useEventDefinition'; import type { EventDefinition } from 'components/event-definitions/event-definitions-types'; import useEventById from 'hooks/useEventById'; -const useAlertAndEventDefinitionData = () => { - const { pathname: path } = useLocation(); - const { alertId, definitionId } = useParams<{ alertId?: string, definitionId?: string }>(); +const useAlertAndEventDefinitionData = (alertId: string, definitionId?: string) => { const { data: eventData } = useEventById(alertId); const { data } = useEventDefinition(definitionId ?? eventData?.event_definition_id); const eventDefinition = data?.eventDefinition; @@ -38,9 +33,6 @@ const useAlertAndEventDefinitionData = () => { alertId: string, definitionId: string, definitionTitle: string, - isAlert: boolean, - isEvent: boolean, - isEventDefinition: boolean, eventData: Event, eventDefinition: EventDefinition, aggregations: Array, @@ -48,13 +40,10 @@ const useAlertAndEventDefinitionData = () => { alertId, definitionId: eventDefinition?.id, definitionTitle: eventDefinition?.title, - isAlert: (path === Routes.ALERTS.replay_search(alertId)) && eventData && eventData?.alert, - isEvent: !!alertId && (path === Routes.ALERTS.replay_search(alertId)) && eventData && !eventData?.alert, - isEventDefinition: !!definitionId && (path === Routes.ALERTS.DEFINITIONS.replay_search(definitionId)) && !!eventDefinition, eventData, eventDefinition, aggregations, - }), [alertId, eventDefinition, path, eventData, definitionId, aggregations]); + }), [alertId, eventDefinition, eventData, aggregations]); }; export default useAlertAndEventDefinitionData; diff --git a/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAttributeComponents.tsx b/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAttributeComponents.tsx index 13b8a7fcf4a5..2bd8ab8c6ed1 100644 --- a/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAttributeComponents.tsx +++ b/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAttributeComponents.tsx @@ -21,13 +21,15 @@ import upperFirst from 'lodash/upperFirst'; import { TIME_UNITS } from 'components/event-definitions/event-definition-types/FilterForm'; import EventDefinitionPriorityEnum from 'logic/alerts/EventDefinitionPriorityEnum'; -import useAlertAndEventDefinitionData from 'hooks/useAlertAndEventDefinitionData'; import { extractDurationAndUnit } from 'components/common/TimeUnitInput'; import { Timestamp, HoverForHelp } from 'components/common'; import { Link } from 'components/common/router'; import Routes from 'routing/Routes'; +import useReplaySearchContext from 'components/event-definitions/replay-search/hooks/useReplaySearchContext'; -import AggregationConditions from '../AggreagtionConditions'; +import useAlertAndEventDefinitionData from './useAlertAndEventDefinitionData'; + +import AggregationConditions from '../AggregationConditions'; import Notifications from '../Notifications'; const AlertTimestamp = styled(Timestamp)(({ theme }) => css` @@ -35,9 +37,12 @@ const AlertTimestamp = styled(Timestamp)(({ theme }) => css` `); const useAttributeComponents = () => { - const { eventData, eventDefinition, isEventDefinition } = useAlertAndEventDefinitionData(); + const { alertId, definitionId, type } = useReplaySearchContext(); + const { eventData, eventDefinition } = useAlertAndEventDefinitionData(alertId, definitionId); return useMemo(() => { + const isEventDefinition = type === 'event_definition'; + if (!eventDefinition) { return [ { title: 'Timestamp', content: , show: !isEventDefinition }, @@ -89,11 +94,7 @@ const useAttributeComponents = () => { content: , }, ]; - }, [ - eventData?.timestamp, - eventDefinition, - isEventDefinition, - ]); + }, [eventData.timestamp, eventDefinition, type]); }; export default useAttributeComponents; diff --git a/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useReplaySearchContext.ts b/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useReplaySearchContext.ts new file mode 100644 index 000000000000..2d8daa382a19 --- /dev/null +++ b/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useReplaySearchContext.ts @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import { useContext } from 'react'; + +import ReplaySearchContext from '../ReplaySearchContext'; + +const useReplaySearchContext = () => useContext(ReplaySearchContext); +export default useReplaySearchContext; diff --git a/graylog2-web-interface/src/components/event-definitions/types.d.ts b/graylog2-web-interface/src/components/event-definitions/types.ts similarity index 96% rename from graylog2-web-interface/src/components/event-definitions/types.d.ts rename to graylog2-web-interface/src/components/event-definitions/types.ts index a6fc4b9a3725..e27e8c66301a 100644 --- a/graylog2-web-interface/src/components/event-definitions/types.d.ts +++ b/graylog2-web-interface/src/components/event-definitions/types.ts @@ -21,6 +21,8 @@ import type { SearchBarControl } from 'views/types'; import type User from 'logic/users/User'; import type { EventDefinition } from 'components/event-definitions/event-definitions-types'; +export type AlertType = 'alert' | 'event' | 'event_definition'; + export interface EventDefinitionValidation { errors: { config?: unknown, diff --git a/graylog2-web-interface/src/hooks/useAlertAndEventDefinitionData.test.tsx b/graylog2-web-interface/src/hooks/useAlertAndEventDefinitionData.test.tsx deleted file mode 100644 index dd5e2dc1c24e..000000000000 --- a/graylog2-web-interface/src/hooks/useAlertAndEventDefinitionData.test.tsx +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (C) 2020 Graylog, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * . - */ -import React from 'react'; -import { renderHook } from 'wrappedTestingLibrary/hooks'; -import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; -import { useParams } from 'react-router-dom'; -import type { Location } from 'react-router-dom'; - -import useLocation from 'routing/useLocation'; -import { - mockedMappedAggregation, - mockEventData, - mockEventDefinitionTwoAggregations, -} from 'helpers/mocking/EventAndEventDefinitions_mock'; -import asMock from 'helpers/mocking/AsMock'; -import useAlertAndEventDefinitionData from 'hooks/useAlertAndEventDefinitionData'; -import useEventById from 'hooks/useEventById'; - -jest.mock('logic/rest/FetchProvider', () => jest.fn(() => Promise.resolve())); -jest.mock('routing/useLocation', () => jest.fn(() => ({ pathname: '/' }))); - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: jest.fn(() => ({})), -})); - -jest.mock('graylog-web-plugin/plugin', () => ({ - PluginStore: { exports: jest.fn(() => [{ type: 'aggregation', defaults: {} }]) }, -})); - -jest.mock('views/logic/Widgets', () => ({ - ...jest.requireActual('views/logic/Widgets'), - widgetDefinition: () => ({ - searchTypes: () => [{ - type: 'AGGREGATION', - typeDefinition: {}, - }], - }), -})); - -jest.mock('./useEventById'); - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, -}); -const wrapper = ({ children }) => ( - - {children} - -); - -const mockedHookData = { - alertId: mockEventData.event.id, - definitionId: mockEventData.event.event_definition_id, - definitionTitle: mockEventDefinitionTwoAggregations.title, - isAlert: true, - isEvent: false, - isEventDefinition: false, - eventData: mockEventData.event, - eventDefinition: mockEventDefinitionTwoAggregations, - aggregations: mockedMappedAggregation, -}; - -const mockUseRouterForEvent = (id) => asMock(useLocation).mockImplementation(() => ({ - pathname: `/alerts/${id}/replay-search`, -} as Location)); - -const mockUseRouterForEventDefinition = (id) => asMock(useLocation).mockImplementation(() => ({ - pathname: `/alerts/definitions/${id}/replay-search`, -} as Location)); - -const baseEventResponse = { - refetch: () => {}, - isLoading: false, - isFetched: true, -}; - -describe('useAlertAndEventDefinitionData', () => { - beforeEach(() => { - queryClient.clear(); - - queryClient.setQueryData(['event-definition-by-id', mockEventDefinitionTwoAggregations.id], { - eventDefinition: mockEventDefinitionTwoAggregations, - aggregations: mockedMappedAggregation, - }); - - asMock(useEventById).mockReturnValue({ ...baseEventResponse, data: undefined }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should return expected data for alert page', async () => { - const eventId = 'event-id-1'; - mockUseRouterForEvent(eventId); - - asMock(useParams).mockImplementation(() => ({ - alertId: mockEventData.event.id, - })); - - asMock(useEventById).mockReturnValue({ ...baseEventResponse, data: { ...mockEventData.event, id: eventId, alert: true } }); - const { result } = renderHook(() => useAlertAndEventDefinitionData(), { wrapper }); - - await expect(result.current).toEqual(mockedHookData); - }); - - it('should return expected data for event page', async () => { - const eventId = 'event-id-2'; - - asMock(useParams).mockImplementation(() => ({ - alertId: eventId, - })); - - mockUseRouterForEvent(eventId); - - asMock(useEventById).mockReturnValue({ - ...baseEventResponse, - data: { - ...mockEventData.event, - id: eventId, - alert: false, - }, - }); - - const { result } = renderHook(() => useAlertAndEventDefinitionData(), { wrapper }); - - await expect(result.current).toEqual({ - ...mockedHookData, - eventData: { ...mockEventData.event, id: eventId, alert: false }, - alertId: eventId, - isAlert: false, - isEvent: true, - }); - }); - - it('should return expected data for event definition', async () => { - asMock(useParams).mockImplementation(() => ({ - definitionId: mockEventDefinitionTwoAggregations.id, - })); - - mockUseRouterForEventDefinition(mockEventDefinitionTwoAggregations.id); - - const { result } = renderHook(() => useAlertAndEventDefinitionData(), { wrapper }); - - await expect(result.current).toEqual({ - ...mockedHookData, - eventData: undefined, - alertId: undefined, - isAlert: false, - isEvent: false, - isEventDefinition: true, - }); - }); -}); diff --git a/graylog2-web-interface/src/views/components/views/ViewHeader.tsx b/graylog2-web-interface/src/views/components/views/ViewHeader.tsx index 1cd835f75498..3d0ef19fb8a5 100644 --- a/graylog2-web-interface/src/views/components/views/ViewHeader.tsx +++ b/graylog2-web-interface/src/views/components/views/ViewHeader.tsx @@ -29,11 +29,12 @@ import useViewTitle from 'views/hooks/useViewTitle'; import useView from 'views/hooks/useView'; import useAppDispatch from 'stores/useAppDispatch'; import FavoriteIcon from 'views/components/FavoriteIcon'; -import useAlertAndEventDefinitionData from 'hooks/useAlertAndEventDefinitionData'; import { updateView } from 'views/logic/slices/viewSlice'; import useIsNew from 'views/hooks/useIsNew'; import { createGRN } from 'logic/permissions/GRN'; import ExecutionInfo from 'views/components/views/ExecutionInfo'; +import useAlertAndEventDefinitionData from 'components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData'; +import useReplaySearchContext from 'components/event-definitions/replay-search/hooks/useReplaySearchContext'; const links = { [View.Type.Dashboard]: ({ id, title }) => [{ @@ -113,7 +114,7 @@ const StyledIcon = styled(Icon)` font-size: 0.5rem; `; -const CrumbLink = ({ label, link, dataTestId }: { label: string, link: string | undefined, dataTestId?: string}) => ( +const CrumbLink = ({ label, link, dataTestId = undefined }: { label: string, link: string | undefined, dataTestId?: string}) => ( link ? {label} : {label} ); @@ -124,7 +125,8 @@ const ViewHeader = () => { const [showMetadataEdit, setShowMetadataEdit] = useState(false); const toggleMetadataEdit = useCallback(() => setShowMetadataEdit((cur) => !cur), [setShowMetadataEdit]); - const { alertId, definitionId, definitionTitle, isAlert, isEventDefinition, isEvent } = useAlertAndEventDefinitionData(); + const { alertId, definitionId, type } = useReplaySearchContext(); + const { definitionTitle } = useAlertAndEventDefinitionData(alertId, definitionId); const dispatch = useAppDispatch(); const _onSaveView = useCallback(async (updatedView: View) => { await dispatch(onSaveView(updatedView)); @@ -136,11 +138,16 @@ const ViewHeader = () => { const onChangeFavorite = useCallback((newValue: boolean) => dispatch(updateView(view.toBuilder().favorite(newValue).build())), [dispatch, view]); const breadCrumbs = useMemo(() => { - if (isAlert || isEvent) return links.alert({ id: alertId }); - if (isEventDefinition) return links.eventDefinition({ id: definitionId, title: definitionTitle }); - - return links[view.type]({ id: view.id, title }); - }, [alertId, definitionId, definitionTitle, isAlert, isEvent, isEventDefinition, view, title]); + switch (type) { + case 'alert': + case 'event': + return links.alert({ id: alertId }); + case 'event_definition': + return links.eventDefinition({ id: definitionId, title: definitionTitle }); + default: + return links[view.type]({ id: view.id, title }); + } + }, [type, alertId, definitionId, definitionTitle, view.type, view.id, title]); const showExecutionInfo = view.type === 'SEARCH'; diff --git a/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.test.tsx b/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.test.tsx index b0672283fa9c..ec4fdf6269b7 100644 --- a/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.test.tsx +++ b/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.test.tsx @@ -28,13 +28,13 @@ import useViewsPlugin from 'views/test/testViewsPlugin'; import SearchExecutionState from 'views/logic/search/SearchExecutionState'; import EventDefinitionReplaySearchPage, { onErrorHandler } from 'views/pages/EventDefinitionReplaySearchPage'; import useEventDefinition from 'hooks/useEventDefinition'; -import useAlertAndEventDefinitionData from 'hooks/useAlertAndEventDefinitionData'; import { mockedMappedAggregation, mockEventDefinitionTwoAggregations, } from 'helpers/mocking/EventAndEventDefinitions_mock'; import useParams from 'routing/useParams'; import type { Stream } from 'logic/streams/types'; +import useAlertAndEventDefinitionData from 'components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData'; const mockView = createSearch(); diff --git a/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.tsx b/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.tsx index bb9f1f6eb425..6f2fb1169436 100644 --- a/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.tsx +++ b/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.tsx @@ -22,7 +22,7 @@ import useEventDefinition from 'hooks/useEventDefinition'; import { Spinner } from 'components/common'; import SearchPage from 'views/pages/SearchPage'; import { EventNotificationsActions } from 'stores/event-notifications/EventNotificationsStore'; -import useAlertAndEventDefinitionData from 'hooks/useAlertAndEventDefinitionData'; +import useAlertAndEventDefinitionData from 'components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData'; import useCreateViewForEventDefinition from 'views/logic/views/UseCreateViewForEventDefinition'; import EventInfoBar from 'components/event-definitions/replay-search/EventInfoBar'; import { createFromFetchError } from 'logic/errors/ReportedErrors'; @@ -31,7 +31,9 @@ import useCreateSearch from 'views/hooks/useCreateSearch'; import SearchPageLayoutProvider from 'views/components/contexts/SearchPageLayoutProvider'; const EventView = () => { - const { eventDefinition, aggregations } = useAlertAndEventDefinitionData(); + const { alertId, definitionId } = useParams<{ alertId?: string, definitionId?: string }>(); + + const { eventDefinition, aggregations } = useAlertAndEventDefinitionData(alertId, definitionId); const _view = useCreateViewForEventDefinition({ eventDefinition, aggregations }); const view = useCreateSearch(_view); const searchPageLayout = useMemo(() => ({ diff --git a/graylog2-web-interface/src/views/pages/EventReplaySearchPage.test.tsx b/graylog2-web-interface/src/views/pages/EventReplaySearchPage.test.tsx index 2fd36665f68f..26c2c7bf4c40 100644 --- a/graylog2-web-interface/src/views/pages/EventReplaySearchPage.test.tsx +++ b/graylog2-web-interface/src/views/pages/EventReplaySearchPage.test.tsx @@ -29,7 +29,7 @@ import SearchExecutionState from 'views/logic/search/SearchExecutionState'; import EventReplaySearchPage, { onErrorHandler } from 'views/pages/EventReplaySearchPage'; import useEventById from 'hooks/useEventById'; import useEventDefinition from 'hooks/useEventDefinition'; -import useAlertAndEventDefinitionData from 'hooks/useAlertAndEventDefinitionData'; +import useAlertAndEventDefinitionData from 'components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData'; import { mockedMappedAggregation, mockEventData, diff --git a/graylog2-web-interface/src/views/pages/EventReplaySearchPage.tsx b/graylog2-web-interface/src/views/pages/EventReplaySearchPage.tsx index 105322bd6835..eb557306c56c 100644 --- a/graylog2-web-interface/src/views/pages/EventReplaySearchPage.tsx +++ b/graylog2-web-interface/src/views/pages/EventReplaySearchPage.tsx @@ -24,7 +24,7 @@ import { Spinner } from 'components/common'; import SearchPage from 'views/pages/SearchPage'; import { EventNotificationsActions } from 'stores/event-notifications/EventNotificationsStore'; import useCreateViewForEvent from 'views/logic/views/UseCreateViewForEvent'; -import useAlertAndEventDefinitionData from 'hooks/useAlertAndEventDefinitionData'; +import useAlertAndEventDefinitionData from 'components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData'; import EventInfoBar from 'components/event-definitions/replay-search/EventInfoBar'; import { createFromFetchError } from 'logic/errors/ReportedErrors'; import ErrorsActions from 'actions/errors/ErrorsActions'; @@ -32,7 +32,9 @@ import useCreateSearch from 'views/hooks/useCreateSearch'; import SearchPageLayoutProvider from 'views/components/contexts/SearchPageLayoutProvider'; const EventView = () => { - const { eventData, eventDefinition, aggregations } = useAlertAndEventDefinitionData(); + const { alertId, definitionId } = useParams<{ alertId?: string, definitionId?: string }>(); + + const { eventData, eventDefinition, aggregations } = useAlertAndEventDefinitionData(alertId, definitionId); const _view = useCreateViewForEvent({ eventData, eventDefinition, aggregations }); const view = useCreateSearch(_view); const searchPageLayout = useMemo(() => ({ From 7a6e6bd86528f1f9483e24731b2014484e470013 Mon Sep 17 00:00:00 2001 From: Dennis Oelkers Date: Mon, 6 Jan 2025 09:31:23 +0100 Subject: [PATCH 02/13] Extracting common `ReplaySearch` component. --- .../replay-search/Notifications.tsx | 24 +++---- .../replay-search/ReplaySearchContext.tsx | 2 +- .../hooks/useAttributeComponents.tsx | 8 ++- .../src/components/events/ReplaySearch.tsx | 68 +++++++++++++++++++ .../events/bulk-replay/BulkEventReplay.tsx | 17 ++++- .../events/bulk-replay/ReplaySearch.tsx | 58 ---------------- .../pages/EventDefinitionReplaySearchPage.tsx | 33 ++------- .../src/views/pages/EventReplaySearchPage.tsx | 33 ++------- 8 files changed, 113 insertions(+), 130 deletions(-) create mode 100644 graylog2-web-interface/src/components/events/ReplaySearch.tsx delete mode 100644 graylog2-web-interface/src/components/events/bulk-replay/ReplaySearch.tsx diff --git a/graylog2-web-interface/src/components/event-definitions/replay-search/Notifications.tsx b/graylog2-web-interface/src/components/event-definitions/replay-search/Notifications.tsx index 72a36a92a624..00fa42ff9f38 100644 --- a/graylog2-web-interface/src/components/event-definitions/replay-search/Notifications.tsx +++ b/graylog2-web-interface/src/components/event-definitions/replay-search/Notifications.tsx @@ -15,12 +15,12 @@ * . */ import * as React from 'react'; -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { Link } from 'components/common/router'; import Routes from 'routing/Routes'; import { useStore } from 'stores/connect'; -import { EventNotificationsStore } from 'stores/event-notifications/EventNotificationsStore'; +import { EventNotificationsStore, EventNotificationsActions } from 'stores/event-notifications/EventNotificationsStore'; import NoAttributeProvided from 'components/event-definitions/replay-search/NoAttributeProvided'; import useReplaySearchContext from 'components/event-definitions/replay-search/hooks/useReplaySearchContext'; @@ -30,19 +30,17 @@ const Notifications = () => { const { alertId, definitionId } = useReplaySearchContext(); const { eventDefinition } = useAlertAndEventDefinitionData(alertId, definitionId); - const allNotifications = useStore(EventNotificationsStore, ({ all }) => all.reduce((res, cur) => { - res[cur.id] = cur; + useEffect(() => { + EventNotificationsActions.listAll(); + }, []); - return res; - }, {})); + const allNotifications = useStore(EventNotificationsStore, ({ all }) => Object.fromEntries( + (all ?? []).map((notification) => [notification.id, notification]), + )); - const notificationList = useMemo(() => eventDefinition.notifications.reduce((res, cur) => { - if (allNotifications[cur.notification_id]) { - res.push((allNotifications[cur.notification_id])); - } - - return res; - }, []), [eventDefinition, allNotifications]); + const notificationList = useMemo(() => eventDefinition.notifications + .flatMap(({ notification_id }) => (allNotifications[notification_id] ? [allNotifications[notification_id]] : [])), + [allNotifications, eventDefinition.notifications]); return notificationList.length ? ( <> diff --git a/graylog2-web-interface/src/components/event-definitions/replay-search/ReplaySearchContext.tsx b/graylog2-web-interface/src/components/event-definitions/replay-search/ReplaySearchContext.tsx index 6df8526508f5..06b747c48138 100644 --- a/graylog2-web-interface/src/components/event-definitions/replay-search/ReplaySearchContext.tsx +++ b/graylog2-web-interface/src/components/event-definitions/replay-search/ReplaySearchContext.tsx @@ -23,5 +23,5 @@ type ReplaySearchContextType = { definitionId: string; type: AlertType; } -const ReplaySearchContext = React.createContext(undefined); +const ReplaySearchContext = React.createContext({ alertId: undefined, definitionId: undefined, type: undefined }); export default ReplaySearchContext; diff --git a/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAttributeComponents.tsx b/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAttributeComponents.tsx index 2bd8ab8c6ed1..32bd2ab0c5a9 100644 --- a/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAttributeComponents.tsx +++ b/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAttributeComponents.tsx @@ -43,6 +43,10 @@ const useAttributeComponents = () => { return useMemo(() => { const isEventDefinition = type === 'event_definition'; + if (!eventData) { + return []; + } + if (!eventDefinition) { return [ { title: 'Timestamp', content: , show: !isEventDefinition }, @@ -51,7 +55,7 @@ const useAttributeComponents = () => { const searchWithin = extractDurationAndUnit(eventDefinition.config.search_within_ms, TIME_UNITS); const executeEvery = extractDurationAndUnit(eventDefinition.config.execute_every_ms, TIME_UNITS); - const isEDUpdatedAfterEvent = !isEventDefinition && moment(eventDefinition.updated_at).diff(eventData.timestamp) > 0; + const isEDUpdatedAfterEvent = !isEventDefinition && moment(eventDefinition.updated_at).diff(eventData?.timestamp) > 0; return [ { title: 'Timestamp', content: , show: !isEventDefinition }, @@ -94,7 +98,7 @@ const useAttributeComponents = () => { content: , }, ]; - }, [eventData.timestamp, eventDefinition, type]); + }, [eventData?.timestamp, eventDefinition, type]); }; export default useAttributeComponents; diff --git a/graylog2-web-interface/src/components/events/ReplaySearch.tsx b/graylog2-web-interface/src/components/events/ReplaySearch.tsx new file mode 100644 index 000000000000..a04fa0fbab78 --- /dev/null +++ b/graylog2-web-interface/src/components/events/ReplaySearch.tsx @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import * as React from 'react'; +import { useMemo } from 'react'; + +import useAlertAndEventDefinitionData + from 'components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData'; +import useCreateViewForEventDefinition from 'views/logic/views/UseCreateViewForEventDefinition'; +import useCreateSearch from 'views/hooks/useCreateSearch'; +import EventInfoBar from 'components/event-definitions/replay-search/EventInfoBar'; +import SearchPageLayoutProvider from 'views/components/contexts/SearchPageLayoutProvider'; +import SearchPage from 'views/pages/SearchPage'; +import ReplaySearchContext from 'components/event-definitions/replay-search/ReplaySearchContext'; + +type Props = { + alertId: string, + definitionId: string, + replayEventDefinition?: boolean, + searchPageLayout?: React.ComponentProps['value'], + forceSidebarPinned?: boolean, +} + +const defaultSearchPageLayout = {}; + +const ReplaySearch = ({ + alertId, definitionId, replayEventDefinition = false, + searchPageLayout = defaultSearchPageLayout, forceSidebarPinned = false, +}: Props) => { + const { eventDefinition, aggregations, eventData } = useAlertAndEventDefinitionData(alertId, definitionId); + const _view = useCreateViewForEventDefinition({ eventDefinition, aggregations }); + const view = useCreateSearch(_view); + const _searchPageLayout = useMemo(() => ({ + ...searchPageLayout, + infoBar: { component: EventInfoBar }, + }), []); + const replaySearchContext = useMemo(() => ({ + alertId, + definitionId, + // eslint-disable-next-line no-nested-ternary + type: replayEventDefinition ? 'event_definition' : eventData?.alert ? 'alert' : 'event', + } as const), [alertId, definitionId, eventData?.alert, replayEventDefinition]); + + return ( + + + + + + ); +}; + +export default ReplaySearch; diff --git a/graylog2-web-interface/src/components/events/bulk-replay/BulkEventReplay.tsx b/graylog2-web-interface/src/components/events/bulk-replay/BulkEventReplay.tsx index a09218917207..a50b23117379 100644 --- a/graylog2-web-interface/src/components/events/bulk-replay/BulkEventReplay.tsx +++ b/graylog2-web-interface/src/components/events/bulk-replay/BulkEventReplay.tsx @@ -20,13 +20,14 @@ import styled, { css } from 'styled-components'; import EventListItem from 'components/events/bulk-replay/EventListItem'; import useSelectedEvents from 'components/events/bulk-replay/useSelectedEvents'; -import ReplaySearch from 'components/events/bulk-replay/ReplaySearch'; +import ReplaySearch from 'components/events/ReplaySearch'; import type { Event } from 'components/events/events/types'; import Button from 'components/bootstrap/Button'; import DropdownButton from 'components/bootstrap/DropdownButton'; import useEventBulkActions from 'components/events/events/hooks/useEventBulkActions'; import Center from 'components/common/Center'; import ButtonToolbar from 'components/bootstrap/ButtonToolbar'; +import type { LayoutState } from 'views/components/contexts/SearchPageLayoutContext'; const Container = styled.div` display: flex; @@ -100,6 +101,13 @@ const RemainingBulkActions = ({ completed, events }: RemainingBulkActionsProps) ); }; +const searchPageLayout: Partial = { + sidebar: { + isShown: false, + }, + synchronizeUrl: false, +} as const; + const ReplayedSearch = ({ total, completed, selectedEvent }: React.PropsWithChildren<{ total: number; completed: number; @@ -129,7 +137,12 @@ const ReplayedSearch = ({ total, completed, selectedEvent }: React.PropsWithChil ); } - return ; + return ( + + ); }; const Headline = styled.h2` diff --git a/graylog2-web-interface/src/components/events/bulk-replay/ReplaySearch.tsx b/graylog2-web-interface/src/components/events/bulk-replay/ReplaySearch.tsx deleted file mode 100644 index f3de6a2c031a..000000000000 --- a/graylog2-web-interface/src/components/events/bulk-replay/ReplaySearch.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2020 Graylog, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * . - */ -import * as React from 'react'; -import { useMemo } from 'react'; - -import useCreateSearch from 'views/hooks/useCreateSearch'; -import EventInfoBar from 'components/event-definitions/replay-search/EventInfoBar'; -import SearchPageLayoutProvider from 'views/components/contexts/SearchPageLayoutProvider'; -import SearchPage from 'views/pages/SearchPage'; -import useCreateViewForEvent from 'views/logic/views/UseCreateViewForEvent'; -import useEventDefinition from 'hooks/useEventDefinition'; -import Spinner from 'components/common/Spinner'; -import type { Event } from 'components/events/events/types'; - -type Props = { - event: Event; -} - -const ViewReplaySearch = ({ eventData, eventDefinition, aggregations }) => { - const _view = useCreateViewForEvent({ eventData, eventDefinition, aggregations }); - const view = useCreateSearch(_view); - const searchPageLayout = useMemo(() => ({ - sidebar: { - isShown: false, - }, - infoBar: { component: EventInfoBar }, - synchronizeUrl: false, - } as const), []); - - return ( - - - - ); -}; - -const ReplaySearch = ({ event }: Props) => { - const { data, isLoading } = useEventDefinition(event.event_definition_id); - - return isLoading ? - : ; -}; - -export default ReplaySearch; diff --git a/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.tsx b/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.tsx index 6f2fb1169436..2454fa1ae078 100644 --- a/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.tsx +++ b/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.tsx @@ -15,38 +15,15 @@ * . */ -import React, { useEffect, useState, useMemo } from 'react'; +import React, { useEffect, useState } from 'react'; import useParams from 'routing/useParams'; import useEventDefinition from 'hooks/useEventDefinition'; import { Spinner } from 'components/common'; -import SearchPage from 'views/pages/SearchPage'; import { EventNotificationsActions } from 'stores/event-notifications/EventNotificationsStore'; -import useAlertAndEventDefinitionData from 'components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData'; -import useCreateViewForEventDefinition from 'views/logic/views/UseCreateViewForEventDefinition'; -import EventInfoBar from 'components/event-definitions/replay-search/EventInfoBar'; import { createFromFetchError } from 'logic/errors/ReportedErrors'; import ErrorsActions from 'actions/errors/ErrorsActions'; -import useCreateSearch from 'views/hooks/useCreateSearch'; -import SearchPageLayoutProvider from 'views/components/contexts/SearchPageLayoutProvider'; - -const EventView = () => { - const { alertId, definitionId } = useParams<{ alertId?: string, definitionId?: string }>(); - - const { eventDefinition, aggregations } = useAlertAndEventDefinitionData(alertId, definitionId); - const _view = useCreateViewForEventDefinition({ eventDefinition, aggregations }); - const view = useCreateSearch(_view); - const searchPageLayout = useMemo(() => ({ - infoBar: { component: EventInfoBar }, - }), []); - - return ( - - - - ); -}; +import ReplaySearch from 'components/events/ReplaySearch'; export const onErrorHandler = (error) => { if (error.status === 404) { @@ -56,7 +33,7 @@ export const onErrorHandler = (error) => { const EventDefinitionReplaySearchPage = () => { const [isNotificationLoaded, setIsNotificationLoaded] = useState(false); - const { definitionId } = useParams<{ definitionId?: string }>(); + const { alertId, definitionId } = useParams<{ alertId?: string, definitionId?: string }>(); const { isLoading: EDIsLoading, isFetched: EDIsFetched } = useEventDefinition(definitionId, { onErrorHandler }); useEffect(() => { @@ -65,7 +42,9 @@ const EventDefinitionReplaySearchPage = () => { const isLoading = EDIsLoading || !EDIsFetched || !isNotificationLoaded; - return isLoading ? : ; + return isLoading + ? + : ; }; export default EventDefinitionReplaySearchPage; diff --git a/graylog2-web-interface/src/views/pages/EventReplaySearchPage.tsx b/graylog2-web-interface/src/views/pages/EventReplaySearchPage.tsx index eb557306c56c..c3e05da817a5 100644 --- a/graylog2-web-interface/src/views/pages/EventReplaySearchPage.tsx +++ b/graylog2-web-interface/src/views/pages/EventReplaySearchPage.tsx @@ -15,39 +15,16 @@ * . */ -import React, { useEffect, useState, useMemo } from 'react'; +import React, { useEffect, useState } from 'react'; import useParams from 'routing/useParams'; import useEventById from 'hooks/useEventById'; import useEventDefinition from 'hooks/useEventDefinition'; import { Spinner } from 'components/common'; -import SearchPage from 'views/pages/SearchPage'; import { EventNotificationsActions } from 'stores/event-notifications/EventNotificationsStore'; -import useCreateViewForEvent from 'views/logic/views/UseCreateViewForEvent'; -import useAlertAndEventDefinitionData from 'components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData'; -import EventInfoBar from 'components/event-definitions/replay-search/EventInfoBar'; import { createFromFetchError } from 'logic/errors/ReportedErrors'; import ErrorsActions from 'actions/errors/ErrorsActions'; -import useCreateSearch from 'views/hooks/useCreateSearch'; -import SearchPageLayoutProvider from 'views/components/contexts/SearchPageLayoutProvider'; - -const EventView = () => { - const { alertId, definitionId } = useParams<{ alertId?: string, definitionId?: string }>(); - - const { eventData, eventDefinition, aggregations } = useAlertAndEventDefinitionData(alertId, definitionId); - const _view = useCreateViewForEvent({ eventData, eventDefinition, aggregations }); - const view = useCreateSearch(_view); - const searchPageLayout = useMemo(() => ({ - infoBar: { component: EventInfoBar }, - }), []); - - return ( - - - - ); -}; +import ReplaySearch from 'components/events/ReplaySearch'; export const onErrorHandler = (error) => { if (error.status === 404) { @@ -57,7 +34,7 @@ export const onErrorHandler = (error) => { const EventReplaySearchPage = () => { const [isNotificationLoaded, setIsNotificationLoaded] = useState(false); - const { alertId } = useParams<{ alertId?: string }>(); + const { alertId, definitionId } = useParams<{ alertId?: string, definitionId?: string }>(); const { data: eventData, isLoading: eventIsLoading, isFetched: eventIsFetched } = useEventById(alertId, { onErrorHandler }); const { isLoading: EDIsLoading, isFetched: EDIsFetched } = useEventDefinition(eventData?.event_definition_id); @@ -67,7 +44,9 @@ const EventReplaySearchPage = () => { const isLoading = eventIsLoading || EDIsLoading || !eventIsFetched || !EDIsFetched || !isNotificationLoaded; - return isLoading ? : ; + return isLoading + ? + : ; }; export default EventReplaySearchPage; From cb3ce4231dd9a9f9a6a116b220c72a9e2a65b95f Mon Sep 17 00:00:00 2001 From: Dennis Oelkers Date: Mon, 6 Jan 2025 14:10:38 +0100 Subject: [PATCH 03/13] Simplifying highlighting rule retrieval. --- .../replay-search/AggregationConditions.tsx | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/graylog2-web-interface/src/components/event-definitions/replay-search/AggregationConditions.tsx b/graylog2-web-interface/src/components/event-definitions/replay-search/AggregationConditions.tsx index b6f2d5232978..413cfb2959cc 100644 --- a/graylog2-web-interface/src/components/event-definitions/replay-search/AggregationConditions.tsx +++ b/graylog2-web-interface/src/components/event-definitions/replay-search/AggregationConditions.tsx @@ -21,7 +21,6 @@ import { useMemo, useCallback } from 'react'; import { StaticColor } from 'views/logic/views/formatting/highlighting/HighlightingColor'; import { ColorPickerPopover, Icon } from 'components/common'; import { DEFAULT_CUSTOM_HIGHLIGHT_RANGE } from 'views/Constants'; -import type HighlightingRule from 'views/logic/views/formatting/highlighting/HighlightingRule'; import { conditionToExprMapper, exprToConditionMapper } from 'views/logic/ExpressionConditionMappers'; import useAppSelector from 'stores/useAppSelector'; import { selectHighlightingRules } from 'views/logic/slices/highlightSelectors'; @@ -59,7 +58,7 @@ const AggregationConditions = () => { const { aggregations } = useAlertAndEventDefinitionData(alertId, definitionId); const highlightingRules = useHighlightingRules(); - const aggregationsMap = useMemo(() => new Map(aggregations.map((agg) => [ + const aggregationsMap = useMemo(() => Object.fromEntries(aggregations.map((agg) => [ `${agg.fnSeries}${agg.expr}${agg.value}`, agg, ])), [aggregations]); @@ -67,7 +66,7 @@ const AggregationConditions = () => { if (rule) { dispatch(updateHighlightingRule(rule, { color: StaticColor.create(newColor) })); } else { - const { value, fnSeries, expr } = aggregationsMap.get(condition); + const { value, fnSeries, expr } = aggregationsMap[condition]; dispatch(createHighlightingRules([ { @@ -80,33 +79,27 @@ const AggregationConditions = () => { } }, [aggregationsMap, dispatch]); - const highlightedAggregations = useMemo>(() => { - const initial = new Map(aggregations.map( - ({ fnSeries, value, expr }) => [ - `${fnSeries}${expr}${value}`, undefined, - ], - )); + const validAggregations = aggregations.map(({ fnSeries, value, expr }) => `${fnSeries}${expr}${value}`); - return highlightingRules.reduce((acc, rule) => { + const highlightedAggregations = useMemo(() => Object.fromEntries(highlightingRules + .map((rule) => { const { field, value, condition } = rule; - const expr = conditionToExprMapper?.[condition]; - let result = acc; + const expr = conditionToExprMapper[condition]; if (expr) { const key = `${field}${expr}${value}`; - if (acc.has(key)) { - result = result.set(key, rule); - } + return [key, rule] as const; } - return result; - }, initial); - }, [aggregations, highlightingRules]); + return undefined; + }) + .filter((rule) => rule !== undefined && validAggregations.includes(rule[0]))), + [highlightingRules, validAggregations]); - return highlightedAggregations?.size ? ( + return Object.keys(highlightedAggregations).length ? ( - {Array.from(highlightedAggregations).map(([condition, rule]) => { + {Object.entries(highlightedAggregations).map(([condition, rule]) => { const color = rule?.color as StaticColor; const hexColor = color?.color; From 0b1d11267a6dc770b134d01b2e886a23df62038c Mon Sep 17 00:00:00 2001 From: Dennis Oelkers Date: Mon, 6 Jan 2025 14:11:06 +0100 Subject: [PATCH 04/13] Adding barrier to loading event (definition) when replaying search. --- .../hooks/useAlertAndEventDefinitionData.tsx | 9 ++-- .../hooks/useAttributeComponents.tsx | 4 -- .../src/components/events/ReplaySearch.tsx | 53 +++++++++++++++---- .../logic/ExpressionConditionMappers.tsx | 4 +- 4 files changed, 51 insertions(+), 19 deletions(-) diff --git a/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData.tsx b/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData.tsx index e7746a8ebdfa..5cd2e5118eef 100644 --- a/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData.tsx +++ b/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData.tsx @@ -24,10 +24,11 @@ import type { EventDefinition } from 'components/event-definitions/event-definit import useEventById from 'hooks/useEventById'; const useAlertAndEventDefinitionData = (alertId: string, definitionId?: string) => { - const { data: eventData } = useEventById(alertId); - const { data } = useEventDefinition(definitionId ?? eventData?.event_definition_id); + const { data: eventData, isLoading: isLoadingEvent } = useEventById(alertId); + const { data, isLoading: isLoadingEventDefinition } = useEventDefinition(definitionId ?? eventData?.event_definition_id); const eventDefinition = data?.eventDefinition; const aggregations = data?.aggregations; + const isLoading = (alertId && isLoadingEvent) || (definitionId && isLoadingEventDefinition); return useMemo<{ alertId: string, @@ -36,6 +37,7 @@ const useAlertAndEventDefinitionData = (alertId: string, definitionId?: string) eventData: Event, eventDefinition: EventDefinition, aggregations: Array, + isLoading: boolean, }>(() => ({ alertId, definitionId: eventDefinition?.id, @@ -43,7 +45,8 @@ const useAlertAndEventDefinitionData = (alertId: string, definitionId?: string) eventData, eventDefinition, aggregations, - }), [alertId, eventDefinition, eventData, aggregations]); + isLoading, + }), [alertId, eventDefinition, eventData, aggregations, isLoading]); }; export default useAlertAndEventDefinitionData; diff --git a/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAttributeComponents.tsx b/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAttributeComponents.tsx index 32bd2ab0c5a9..7a354b5f7718 100644 --- a/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAttributeComponents.tsx +++ b/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAttributeComponents.tsx @@ -43,10 +43,6 @@ const useAttributeComponents = () => { return useMemo(() => { const isEventDefinition = type === 'event_definition'; - if (!eventData) { - return []; - } - if (!eventDefinition) { return [ { title: 'Timestamp', content: , show: !isEventDefinition }, diff --git a/graylog2-web-interface/src/components/events/ReplaySearch.tsx b/graylog2-web-interface/src/components/events/ReplaySearch.tsx index a04fa0fbab78..d4d9e5e59da3 100644 --- a/graylog2-web-interface/src/components/events/ReplaySearch.tsx +++ b/graylog2-web-interface/src/components/events/ReplaySearch.tsx @@ -25,28 +25,34 @@ import EventInfoBar from 'components/event-definitions/replay-search/EventInfoBa import SearchPageLayoutProvider from 'views/components/contexts/SearchPageLayoutProvider'; import SearchPage from 'views/pages/SearchPage'; import ReplaySearchContext from 'components/event-definitions/replay-search/ReplaySearchContext'; +import type { LayoutState } from 'views/components/contexts/SearchPageLayoutContext'; +import Spinner from 'components/common/Spinner'; +import type { EventDefinition } from 'components/event-definitions/event-definitions-types'; +import type { Event } from 'components/events/events/types'; +import type { EventDefinitionAggregation } from 'hooks/useEventDefinition'; -type Props = { +type ReplaySearchProps = { alertId: string, definitionId: string, - replayEventDefinition?: boolean, - searchPageLayout?: React.ComponentProps['value'], - forceSidebarPinned?: boolean, + eventDefinition: EventDefinition, + event: Event, + aggregations: EventDefinitionAggregation[], + replayEventDefinition: boolean, + searchPageLayout: Partial, + forceSidebarPinned: boolean, } const defaultSearchPageLayout = {}; const ReplaySearch = ({ - alertId, definitionId, replayEventDefinition = false, - searchPageLayout = defaultSearchPageLayout, forceSidebarPinned = false, -}: Props) => { - const { eventDefinition, aggregations, eventData } = useAlertAndEventDefinitionData(alertId, definitionId); + alertId, definitionId, eventDefinition, aggregations, event: eventData, replayEventDefinition, searchPageLayout, forceSidebarPinned, +}: ReplaySearchProps) => { const _view = useCreateViewForEventDefinition({ eventDefinition, aggregations }); const view = useCreateSearch(_view); const _searchPageLayout = useMemo(() => ({ ...searchPageLayout, infoBar: { component: EventInfoBar }, - }), []); + }), [searchPageLayout]); const replaySearchContext = useMemo(() => ({ alertId, definitionId, @@ -65,4 +71,31 @@ const ReplaySearch = ({ ); }; -export default ReplaySearch; +type Props = { + alertId: string, + definitionId: string, + replayEventDefinition?: boolean, + searchPageLayout?: Partial, + forceSidebarPinned?: boolean, +} + +const LoadingBarrier = ({ + alertId, definitionId, replayEventDefinition = false, searchPageLayout = defaultSearchPageLayout, forceSidebarPinned = false, +}: Props) => { + const { eventDefinition, aggregations, eventData, isLoading } = useAlertAndEventDefinitionData(alertId, definitionId); + + return isLoading + ? + : ( + + ); +}; + +export default LoadingBarrier; diff --git a/graylog2-web-interface/src/views/logic/ExpressionConditionMappers.tsx b/graylog2-web-interface/src/views/logic/ExpressionConditionMappers.tsx index ca853b7fc0ab..42d8bff4cf58 100644 --- a/graylog2-web-interface/src/views/logic/ExpressionConditionMappers.tsx +++ b/graylog2-web-interface/src/views/logic/ExpressionConditionMappers.tsx @@ -18,7 +18,7 @@ import type { ValueExpr } from 'hooks/useEventDefinition'; import type { Condition } from 'views/logic/views/formatting/highlighting/HighlightingRule'; -export const exprToConditionMapper: {[name: string]: Condition} = { +export const exprToConditionMapper: { [name: string]: Condition } = { '<': 'less', '<=': 'less_equal', '>=': 'greater_equal', @@ -26,7 +26,7 @@ export const exprToConditionMapper: {[name: string]: Condition} = { '==': 'equal', }; -export const conditionToExprMapper: {[name: string]: ValueExpr} = { +export const conditionToExprMapper: { [name: string]: ValueExpr } = { less: '<', less_equal: '<=', greater_equal: '>=', From 2d35566dd12fe2d36b62d7c6ef1d48201661f58f Mon Sep 17 00:00:00 2001 From: Dennis Oelkers Date: Mon, 6 Jan 2025 17:03:42 +0100 Subject: [PATCH 05/13] Adjusting test. --- .../replay-search/hooks/useAlertAndEventDefinitionData.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData.test.tsx b/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData.test.tsx index 58ab4ac7cf02..327b55be3585 100644 --- a/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData.test.tsx +++ b/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData.test.tsx @@ -101,6 +101,7 @@ describe('useAlertAndEventDefinitionData', () => { ...mockedHookData, eventData: undefined, alertId: undefined, + isLoading: false, }); }); }); From 0bdca1d4669ea3723c9723fcea8e6123c537e77b Mon Sep 17 00:00:00 2001 From: Dennis Oelkers Date: Mon, 6 Jan 2025 17:24:01 +0100 Subject: [PATCH 06/13] Adjusting tests. --- .../replay-search/EventInfoBar.test.tsx | 1 + .../pages/EventDefinitionReplaySearchPage.test.tsx | 10 ++++------ .../src/views/pages/EventReplaySearchPage.test.tsx | 4 +--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/graylog2-web-interface/src/components/event-definitions/replay-search/EventInfoBar.test.tsx b/graylog2-web-interface/src/components/event-definitions/replay-search/EventInfoBar.test.tsx index 088d096496b3..e20ceef5f5b8 100644 --- a/graylog2-web-interface/src/components/event-definitions/replay-search/EventInfoBar.test.tsx +++ b/graylog2-web-interface/src/components/event-definitions/replay-search/EventInfoBar.test.tsx @@ -68,6 +68,7 @@ const mockUseAlertAndEventDefinitionData = ({ alertId, definitionId, definitionTitle, + isLoading: false, }); jest.mock('views/logic/slices/highlightSelectors', () => ({ diff --git a/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.test.tsx b/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.test.tsx index ec4fdf6269b7..3fd241d2fe2c 100644 --- a/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.test.tsx +++ b/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.test.tsx @@ -47,7 +47,7 @@ jest.mock('views/logic/views/UseProcessHooksForView'); jest.mock('views/hooks/useCreateSearch'); jest.mock('hooks/useEventDefinition'); -jest.mock('hooks/useAlertAndEventDefinitionData'); +jest.mock('components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData'); jest.mock('stores/event-notifications/EventNotificationsStore', () => ({ EventNotificationsActions: { @@ -90,17 +90,15 @@ describe('EventDefinitionReplaySearchPage', () => { }); it('should run useEventDefinition, UseCreateViewForEvent with correct parameters', async () => { - asMock(useAlertAndEventDefinitionData).mockImplementation(() => ({ + asMock(useAlertAndEventDefinitionData).mockReturnValue({ eventData: undefined, eventDefinition: mockEventDefinitionTwoAggregations, aggregations: mockedMappedAggregation, - isEvent: false, - isEventDefinition: true, - isAlert: false, alertId: undefined, definitionId: mockEventDefinitionTwoAggregations.id, definitionTitle: mockEventDefinitionTwoAggregations.title, - })); + isLoading: false, + }); render(); diff --git a/graylog2-web-interface/src/views/pages/EventReplaySearchPage.test.tsx b/graylog2-web-interface/src/views/pages/EventReplaySearchPage.test.tsx index 26c2c7bf4c40..05df96475c41 100644 --- a/graylog2-web-interface/src/views/pages/EventReplaySearchPage.test.tsx +++ b/graylog2-web-interface/src/views/pages/EventReplaySearchPage.test.tsx @@ -103,12 +103,10 @@ describe('EventReplaySearchPage', () => { eventData: mockEventData.event, eventDefinition: mockEventDefinitionTwoAggregations, aggregations: mockedMappedAggregation, - isEvent: true, - isEventDefinition: false, - isAlert: false, alertId: mockEventData.event.id, definitionId: mockEventDefinitionTwoAggregations.id, definitionTitle: mockEventDefinitionTwoAggregations.title, + isLoading: false, })); render(); From c0b5cdd2bd364ebf8eb2a495340742815e83d9e9 Mon Sep 17 00:00:00 2001 From: Dennis Oelkers Date: Tue, 7 Jan 2025 14:56:02 +0100 Subject: [PATCH 07/13] Improving icon. --- .../src/components/events/bulk-replay/EventListItem.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/graylog2-web-interface/src/components/events/bulk-replay/EventListItem.tsx b/graylog2-web-interface/src/components/events/bulk-replay/EventListItem.tsx index faec6f570603..4adf8af2e53c 100644 --- a/graylog2-web-interface/src/components/events/bulk-replay/EventListItem.tsx +++ b/graylog2-web-interface/src/components/events/bulk-replay/EventListItem.tsx @@ -87,8 +87,7 @@ const EventListItem = ({ done, event, onClick, selected, removeItem, markItemAsD From f00d767efa5085ee69cce17fc6d9139636c842e0 Mon Sep 17 00:00:00 2001 From: Dennis Oelkers Date: Wed, 8 Jan 2025 17:10:46 +0100 Subject: [PATCH 08/13] Adjusting test. --- .../events/bulk-replay/__tests__/BulkEventReplay.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graylog2-web-interface/src/components/events/bulk-replay/__tests__/BulkEventReplay.test.tsx b/graylog2-web-interface/src/components/events/bulk-replay/__tests__/BulkEventReplay.test.tsx index 9a16f662df4a..8c818351125f 100644 --- a/graylog2-web-interface/src/components/events/bulk-replay/__tests__/BulkEventReplay.test.tsx +++ b/graylog2-web-interface/src/components/events/bulk-replay/__tests__/BulkEventReplay.test.tsx @@ -33,7 +33,7 @@ const initialEventIds = [ '01JH0029TS9PX5ED87TZ1RVRT2', ]; -jest.mock('components/events/bulk-replay/ReplaySearch', () => ({ event }: { event: Event }) => Replaying search for event {event.id}); +jest.mock('components/events/ReplaySearch', () => ({ alertId }: { alertId: string }) => Replaying search for event {alertId}); const markEventAsInvestigated = async (eventId: string) => { const markAsInvestigatedButton = await screen.findByRole('button', { name: new RegExp(`mark event "${eventId}" as investigated`, 'i') }); From 3e92e09e200f10f848f15440c55eaf729bf2dfed Mon Sep 17 00:00:00 2001 From: Dennis Oelkers Date: Thu, 9 Jan 2025 08:24:00 +0100 Subject: [PATCH 09/13] Removing unused import. --- .../events/bulk-replay/__tests__/BulkEventReplay.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/graylog2-web-interface/src/components/events/bulk-replay/__tests__/BulkEventReplay.test.tsx b/graylog2-web-interface/src/components/events/bulk-replay/__tests__/BulkEventReplay.test.tsx index 8c818351125f..559d661a9bb8 100644 --- a/graylog2-web-interface/src/components/events/bulk-replay/__tests__/BulkEventReplay.test.tsx +++ b/graylog2-web-interface/src/components/events/bulk-replay/__tests__/BulkEventReplay.test.tsx @@ -19,7 +19,6 @@ import { render, screen, waitFor } from 'wrappedTestingLibrary'; import userEvent from '@testing-library/user-event'; import { PluginManifest } from 'graylog-web-plugin/plugin'; -import type { Event } from 'components/events/events/types'; import { usePlugin } from 'views/test/testPlugins'; import MenuItem from 'components/bootstrap/menuitem/MenuItem'; From 44951643907cd7428f9869f808f44f2fe95723b4 Mon Sep 17 00:00:00 2001 From: Dennis Oelkers Date: Thu, 9 Jan 2025 09:36:25 +0100 Subject: [PATCH 10/13] Consolidating hooks for view creation based on event (definition). --- .../src/components/events/ReplaySearch.tsx | 4 +- .../logic/views/UseCreateViewForEvent.ts | 12 +- .../UseCreateViewForEventDefinition.test.ts | 133 ------------------ .../views/UseCreateViewForEventDefinition.ts | 55 -------- .../EventDefinitionReplaySearchPage.test.tsx | 8 +- .../pages/EventReplaySearchPage.test.tsx | 2 +- 6 files changed, 14 insertions(+), 200 deletions(-) delete mode 100644 graylog2-web-interface/src/views/logic/views/UseCreateViewForEventDefinition.test.ts delete mode 100644 graylog2-web-interface/src/views/logic/views/UseCreateViewForEventDefinition.ts diff --git a/graylog2-web-interface/src/components/events/ReplaySearch.tsx b/graylog2-web-interface/src/components/events/ReplaySearch.tsx index d4d9e5e59da3..fdfa570fe714 100644 --- a/graylog2-web-interface/src/components/events/ReplaySearch.tsx +++ b/graylog2-web-interface/src/components/events/ReplaySearch.tsx @@ -19,7 +19,6 @@ import { useMemo } from 'react'; import useAlertAndEventDefinitionData from 'components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData'; -import useCreateViewForEventDefinition from 'views/logic/views/UseCreateViewForEventDefinition'; import useCreateSearch from 'views/hooks/useCreateSearch'; import EventInfoBar from 'components/event-definitions/replay-search/EventInfoBar'; import SearchPageLayoutProvider from 'views/components/contexts/SearchPageLayoutProvider'; @@ -30,6 +29,7 @@ import Spinner from 'components/common/Spinner'; import type { EventDefinition } from 'components/event-definitions/event-definitions-types'; import type { Event } from 'components/events/events/types'; import type { EventDefinitionAggregation } from 'hooks/useEventDefinition'; +import useCreateViewForEvent from 'views/logic/views/UseCreateViewForEvent'; type ReplaySearchProps = { alertId: string, @@ -47,7 +47,7 @@ const defaultSearchPageLayout = {}; const ReplaySearch = ({ alertId, definitionId, eventDefinition, aggregations, event: eventData, replayEventDefinition, searchPageLayout, forceSidebarPinned, }: ReplaySearchProps) => { - const _view = useCreateViewForEventDefinition({ eventDefinition, aggregations }); + const _view = useCreateViewForEvent({ eventData, eventDefinition, aggregations }); const view = useCreateSearch(_view); const _searchPageLayout = useMemo(() => ({ ...searchPageLayout, diff --git a/graylog2-web-interface/src/views/logic/views/UseCreateViewForEvent.ts b/graylog2-web-interface/src/views/logic/views/UseCreateViewForEvent.ts index 58b071c4842a..63028e43925c 100644 --- a/graylog2-web-interface/src/views/logic/views/UseCreateViewForEvent.ts +++ b/graylog2-web-interface/src/views/logic/views/UseCreateViewForEvent.ts @@ -217,11 +217,13 @@ export const ViewGenerator = async ({ }; export const UseCreateViewForEvent = ( - { eventData, eventDefinition, aggregations }: { eventData: Event, eventDefinition: EventDefinition, aggregations: Array }, + { eventData, eventDefinition, aggregations }: { eventData?: Event, eventDefinition: EventDefinition, aggregations: Array }, ) => { - const queryStringFromGrouping = concatQueryStrings(Object.entries(eventData.group_by_fields).map(([field, value]) => `${field}:${escape(value)}`), { withBrackets: false }); - const eventQueryString = eventData?.replay_info?.query || ''; - const { streams, stream_categories: streamCategories } = eventData.replay_info; + const queryStringFromGrouping = concatQueryStrings(Object.entries(eventData?.group_by_fields ?? {}) + .map(([field, value]) => `${field}:${escape(value)}`), { withBrackets: false }); + const eventQueryString = eventData?.replay_info?.query ?? eventDefinition?.config?.query ?? ''; + const streams = eventData?.replay_info?.streams ?? eventDefinition?.config?.streams ?? []; + const streamCategories = eventData?.replay_info?.stream_categories ?? eventDefinition?.config?.stream_categories ?? []; const timeRange: AbsoluteTimeRange = { type: 'absolute', from: eventData?.replay_info?.timerange_start, @@ -232,7 +234,7 @@ export const UseCreateViewForEvent = ( query_string: concatQueryStrings([eventQueryString, queryStringFromGrouping]), }; - const queryParameters = eventDefinition?.config?.query_parameters || []; + const queryParameters = eventDefinition?.config?.query_parameters ?? []; const groupBy = eventDefinition?.config?.group_by ?? []; diff --git a/graylog2-web-interface/src/views/logic/views/UseCreateViewForEventDefinition.test.ts b/graylog2-web-interface/src/views/logic/views/UseCreateViewForEventDefinition.test.ts deleted file mode 100644 index 4fd1d45746c6..000000000000 --- a/graylog2-web-interface/src/views/logic/views/UseCreateViewForEventDefinition.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2020 Graylog, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * . - */ -import { renderHook } from 'wrappedTestingLibrary/hooks'; -import ObjectID from 'bson-objectid'; - -import { - mockedMappedAggregation, mockedViewWithOneAggregationED, mockedViewWithTwoAggregationsED, - mockEventDefinitionOneAggregation, - mockEventDefinitionTwoAggregations, -} from 'helpers/mocking/EventAndEventDefinitions_mock'; -import UseCreateViewForEventDefinition from 'views/logic/views/UseCreateViewForEventDefinition'; -import generateId from 'logic/generateId'; -import asMock from 'helpers/mocking/AsMock'; -import type View from 'views/logic/views/View'; -import { StaticColor } from 'views/logic/views/formatting/highlighting/HighlightingColor'; - -const counter = () => { - let index = 0; - - return () => { - const oldIndex = index; - index += 1; - - return oldIndex; - }; -}; - -const generateIdCounterTwoAggregations = counter(); -const objectIdCounterTwoAggregations = counter(); -const generateIdCounterOneAggregation = counter(); -const objectIdCounterOneAggregations = counter(); - -const mockedGenerateIdTwoAggregations = () => { - const idSet = ['query-id', 'mc-widget-id', 'allm-widget-id', 'field1-widget-id', 'field2-widget-id', 'summary-widget-id']; - const index = generateIdCounterTwoAggregations(); - - return idSet[index]; -}; - -const mockedObjectIdTwoAggregations = () => { - const idSet = ['', 'view-id', 'search-id']; - const index = objectIdCounterTwoAggregations(); - - return idSet[index]; -}; - -const mockedObjectIdOneAggregation = () => { - const idSet = ['', 'view-id', 'search-id']; - const index = objectIdCounterOneAggregations(); - - return idSet[index]; -}; - -const mockedGenerateIdOneAggregation = () => { - const idSet = ['query-id', 'mc-widget-id', 'allm-widget-id', 'field1-widget-id']; - const index = generateIdCounterOneAggregation(); - - return idSet[index]; -}; - -jest.mock('graylog-web-plugin/plugin', () => ({ - PluginStore: { exports: jest.fn(() => [{ type: 'aggregation', defaults: {} }]) }, -})); - -jest.mock('logic/generateId', () => jest.fn()); - -jest.mock('bson-objectid', () => jest.fn()); -const mock_color = StaticColor.create('#ffffff'); - -jest.mock('views/logic/views/formatting/highlighting/HighlightingRule', () => ({ - ...jest.requireActual('views/logic/views/formatting/highlighting/HighlightingRule'), - randomColor: jest.fn(() => mock_color), - __esModule: true, -})); - -jest.mock('views/logic/Widgets', () => ({ - ...jest.requireActual('views/logic/Widgets'), - widgetDefinition: () => ({ - searchTypes: () => [{ - type: 'AGGREGATION', - typeDefinition: {}, - }], - }), -})); - -const todayDate = new Date(); -const withCurrentDate = (view: View) => view.toBuilder().createdAt(todayDate).build(); - -describe('UseCreateViewForEventDefinition', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should create view with 2 aggregation widgets and one summary', async () => { - asMock(generateId).mockImplementation(mockedGenerateIdTwoAggregations); - - asMock(ObjectID).mockImplementation(() => (({ - toString: () => mockedObjectIdTwoAggregations(), - }) as ObjectID)); - - const { result } = renderHook(() => UseCreateViewForEventDefinition({ eventDefinition: mockEventDefinitionTwoAggregations, aggregations: mockedMappedAggregation })); - const view = await result.current.then((r) => r); - - expect(withCurrentDate(view)).toEqual(withCurrentDate(mockedViewWithTwoAggregationsED)); - }); - - it('should create view with 1 aggregation widgets and without summary', async () => { - asMock(generateId).mockImplementation(mockedGenerateIdOneAggregation); - - asMock(ObjectID).mockImplementation(() => (({ - toString: () => mockedObjectIdOneAggregation(), - }) as ObjectID)); - - const { result } = renderHook(() => UseCreateViewForEventDefinition({ eventDefinition: mockEventDefinitionOneAggregation, aggregations: [mockedMappedAggregation[0]] })); - const view = await result.current.then((r) => r); - - expect(withCurrentDate(view)).toEqual(withCurrentDate(mockedViewWithOneAggregationED)); - }); -}); diff --git a/graylog2-web-interface/src/views/logic/views/UseCreateViewForEventDefinition.ts b/graylog2-web-interface/src/views/logic/views/UseCreateViewForEventDefinition.ts deleted file mode 100644 index cdc10f9355e3..000000000000 --- a/graylog2-web-interface/src/views/logic/views/UseCreateViewForEventDefinition.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2020 Graylog, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * . - */ - -import { useMemo } from 'react'; - -import type { EventDefinition } from 'components/event-definitions/event-definitions-types'; -import type { EventDefinitionAggregation } from 'hooks/useEventDefinition'; -import type { ElasticsearchQueryString, RelativeTimeRangeStartOnly } from 'views/logic/queries/Query'; -import { ViewGenerator } from 'views/logic/views/UseCreateViewForEvent'; - -const useCreateViewForEventDefinition = ( - { - eventDefinition, - aggregations, - }: { eventDefinition: EventDefinition, aggregations: Array }, -) => { - const streams = eventDefinition?.config?.streams ?? []; - const streamCategories = eventDefinition?.config?.stream_categories ?? []; - const timeRange: RelativeTimeRangeStartOnly = { - type: 'relative', - range: (eventDefinition?.config?.search_within_ms ?? 0) / 1000, - }; - const queryString: ElasticsearchQueryString = { - type: 'elasticsearch', - query_string: eventDefinition?.config?.query || '', - }; - - const queryParameters = eventDefinition?.config?.query_parameters || []; - - const groupBy = eventDefinition?.config?.group_by ?? []; - - const searchFilters = eventDefinition?.config?.filters ?? []; - - return useMemo( - () => ViewGenerator({ streams, streamCategories, timeRange, queryString, aggregations, groupBy, queryParameters, searchFilters }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [], - ); -}; - -export default useCreateViewForEventDefinition; diff --git a/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.test.tsx b/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.test.tsx index 3fd241d2fe2c..72a71a296996 100644 --- a/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.test.tsx +++ b/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.test.tsx @@ -21,7 +21,7 @@ import MockStore from 'helpers/mocking/StoreMock'; import asMock from 'helpers/mocking/AsMock'; import SearchComponent from 'views/components/Search'; import StreamsContext from 'contexts/StreamsContext'; -import UseCreateViewForEventDefinition from 'views/logic/views/UseCreateViewForEventDefinition'; +import UseCreateViewForEvent from 'views/logic/views/UseCreateViewForEvent'; import useProcessHooksForView from 'views/logic/views/UseProcessHooksForView'; import { createSearch } from 'fixtures/searches'; import useViewsPlugin from 'views/test/testViewsPlugin'; @@ -42,7 +42,7 @@ jest.mock('views/components/Search'); jest.mock('routing/useParams'); jest.mock('views/logic/views/Actions'); -jest.mock('views/logic/views/UseCreateViewForEventDefinition'); +jest.mock('views/logic/views/UseCreateViewForEvent'); jest.mock('views/logic/views/UseProcessHooksForView'); jest.mock('views/hooks/useCreateSearch'); @@ -77,7 +77,7 @@ describe('EventDefinitionReplaySearchPage', () => { beforeEach(() => { asMock(useParams).mockReturnValue({ definitionId: mockEventDefinitionTwoAggregations.id }); - asMock(UseCreateViewForEventDefinition).mockReturnValue(Promise.resolve(mockView)); + asMock(UseCreateViewForEvent).mockReturnValue(Promise.resolve(mockView)); asMock(useProcessHooksForView).mockReturnValue({ status: 'loaded', view: mockView, executionState: SearchExecutionState.empty() }); asMock(SearchComponent).mockImplementation(() => Extended Search Page); @@ -107,7 +107,7 @@ describe('EventDefinitionReplaySearchPage', () => { })); await waitFor(() => { - expect(UseCreateViewForEventDefinition).toHaveBeenCalledWith({ + expect(UseCreateViewForEvent).toHaveBeenCalledWith({ eventDefinition: mockEventDefinitionTwoAggregations, aggregations: mockedMappedAggregation, }); }); diff --git a/graylog2-web-interface/src/views/pages/EventReplaySearchPage.test.tsx b/graylog2-web-interface/src/views/pages/EventReplaySearchPage.test.tsx index 05df96475c41..8a972cfc9a24 100644 --- a/graylog2-web-interface/src/views/pages/EventReplaySearchPage.test.tsx +++ b/graylog2-web-interface/src/views/pages/EventReplaySearchPage.test.tsx @@ -49,7 +49,7 @@ jest.mock('views/logic/views/UseProcessHooksForView'); jest.mock('views/hooks/useCreateSearch'); jest.mock('hooks/useEventById'); jest.mock('hooks/useEventDefinition'); -jest.mock('hooks/useAlertAndEventDefinitionData'); +jest.mock('components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData'); jest.mock('stores/event-notifications/EventNotificationsStore', () => ({ EventNotificationsActions: { From 8e389582d41460dda3f02c6edff25f1784feddfd Mon Sep 17 00:00:00 2001 From: Dennis Oelkers Date: Thu, 9 Jan 2025 09:49:24 +0100 Subject: [PATCH 11/13] Fixing linter hint. --- .../src/components/events/bulk-replay/EventListItem.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graylog2-web-interface/src/components/events/bulk-replay/EventListItem.tsx b/graylog2-web-interface/src/components/events/bulk-replay/EventListItem.tsx index 4adf8af2e53c..faec6f570603 100644 --- a/graylog2-web-interface/src/components/events/bulk-replay/EventListItem.tsx +++ b/graylog2-web-interface/src/components/events/bulk-replay/EventListItem.tsx @@ -87,7 +87,8 @@ const EventListItem = ({ done, event, onClick, selected, removeItem, markItemAsD From 6ae0146456c27a803f9404ff203fd0203e99c71b Mon Sep 17 00:00:00 2001 From: Dennis Oelkers Date: Thu, 9 Jan 2025 12:16:42 +0100 Subject: [PATCH 12/13] Falling back to event definition specs when no event is present. --- .../logic/views/UseCreateViewForEvent.ts | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/graylog2-web-interface/src/views/logic/views/UseCreateViewForEvent.ts b/graylog2-web-interface/src/views/logic/views/UseCreateViewForEvent.ts index 63028e43925c..9c3f47e578c5 100644 --- a/graylog2-web-interface/src/views/logic/views/UseCreateViewForEvent.ts +++ b/graylog2-web-interface/src/views/logic/views/UseCreateViewForEvent.ts @@ -19,7 +19,12 @@ import * as Immutable from 'immutable'; import uniq from 'lodash/uniq'; import View from 'views/logic/views/View'; -import type { AbsoluteTimeRange, ElasticsearchQueryString, RelativeTimeRangeStartOnly } from 'views/logic/queries/Query'; +import type { + AbsoluteTimeRange, + ElasticsearchQueryString, + RelativeTimeRangeStartOnly, + TimeRange, +} from 'views/logic/queries/Query'; import type { Event } from 'components/events/events/types'; import type { EventDefinition, SearchFilter } from 'components/event-definitions/event-definitions-types'; import QueryGenerator from 'views/logic/queries/QueryGenerator'; @@ -224,14 +229,18 @@ export const UseCreateViewForEvent = ( const eventQueryString = eventData?.replay_info?.query ?? eventDefinition?.config?.query ?? ''; const streams = eventData?.replay_info?.streams ?? eventDefinition?.config?.streams ?? []; const streamCategories = eventData?.replay_info?.stream_categories ?? eventDefinition?.config?.stream_categories ?? []; - const timeRange: AbsoluteTimeRange = { - type: 'absolute', - from: eventData?.replay_info?.timerange_start, - to: eventData?.replay_info?.timerange_end, - }; + const timeRange: TimeRange = eventData + ? { + type: 'absolute', + from: eventData?.replay_info?.timerange_start, + to: eventData?.replay_info?.timerange_end, + } : { + type: 'relative', + range: (eventDefinition?.config?.search_within_ms ?? 0) / 1000, + }; const queryString: ElasticsearchQueryString = { type: 'elasticsearch', - query_string: concatQueryStrings([eventQueryString, queryStringFromGrouping]), + query_string: eventData ? concatQueryStrings([eventQueryString, queryStringFromGrouping]) : (eventDefinition?.config?.query ?? ''), }; const queryParameters = eventDefinition?.config?.query_parameters ?? []; From cecdb0f6ea21714ec6331b492338809266adf083 Mon Sep 17 00:00:00 2001 From: Dennis Oelkers Date: Thu, 9 Jan 2025 12:27:23 +0100 Subject: [PATCH 13/13] Adding back previous tests for creating a view for event definitions. --- .../logic/views/UseCreateViewForEvent.ts | 2 +- .../UseCreateViewForEventDefinitions.test.ts | 133 ++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 graylog2-web-interface/src/views/logic/views/UseCreateViewForEventDefinitions.test.ts diff --git a/graylog2-web-interface/src/views/logic/views/UseCreateViewForEvent.ts b/graylog2-web-interface/src/views/logic/views/UseCreateViewForEvent.ts index 9c3f47e578c5..a0fff5c9bd57 100644 --- a/graylog2-web-interface/src/views/logic/views/UseCreateViewForEvent.ts +++ b/graylog2-web-interface/src/views/logic/views/UseCreateViewForEvent.ts @@ -247,7 +247,7 @@ export const UseCreateViewForEvent = ( const groupBy = eventDefinition?.config?.group_by ?? []; - const searchFilters = eventDefinition?.config?.filters; + const searchFilters = eventDefinition?.config?.filters ?? []; return useMemo( () => ViewGenerator({ streams, streamCategories, timeRange, queryString, aggregations, groupBy, queryParameters, searchFilters }), diff --git a/graylog2-web-interface/src/views/logic/views/UseCreateViewForEventDefinitions.test.ts b/graylog2-web-interface/src/views/logic/views/UseCreateViewForEventDefinitions.test.ts new file mode 100644 index 000000000000..f6a587f015cf --- /dev/null +++ b/graylog2-web-interface/src/views/logic/views/UseCreateViewForEventDefinitions.test.ts @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import { renderHook } from 'wrappedTestingLibrary/hooks'; +import ObjectID from 'bson-objectid'; + +import { + mockedMappedAggregation, mockedViewWithOneAggregationED, mockedViewWithTwoAggregationsED, + mockEventDefinitionOneAggregation, + mockEventDefinitionTwoAggregations, +} from 'helpers/mocking/EventAndEventDefinitions_mock'; +import UseCreateViewForEventDefinition from 'views/logic/views/UseCreateViewForEvent'; +import generateId from 'logic/generateId'; +import asMock from 'helpers/mocking/AsMock'; +import type View from 'views/logic/views/View'; +import { StaticColor } from 'views/logic/views/formatting/highlighting/HighlightingColor'; + +const counter = () => { + let index = 0; + + return () => { + const oldIndex = index; + index += 1; + + return oldIndex; + }; +}; + +const generateIdCounterTwoAggregations = counter(); +const objectIdCounterTwoAggregations = counter(); +const generateIdCounterOneAggregation = counter(); +const objectIdCounterOneAggregations = counter(); + +const mockedGenerateIdTwoAggregations = () => { + const idSet = ['query-id', 'mc-widget-id', 'allm-widget-id', 'field1-widget-id', 'field2-widget-id', 'summary-widget-id']; + const index = generateIdCounterTwoAggregations(); + + return idSet[index]; +}; + +const mockedObjectIdTwoAggregations = () => { + const idSet = ['', 'view-id', 'search-id']; + const index = objectIdCounterTwoAggregations(); + + return idSet[index]; +}; + +const mockedObjectIdOneAggregation = () => { + const idSet = ['', 'view-id', 'search-id']; + const index = objectIdCounterOneAggregations(); + + return idSet[index]; +}; + +const mockedGenerateIdOneAggregation = () => { + const idSet = ['query-id', 'mc-widget-id', 'allm-widget-id', 'field1-widget-id']; + const index = generateIdCounterOneAggregation(); + + return idSet[index]; +}; + +jest.mock('graylog-web-plugin/plugin', () => ({ + PluginStore: { exports: jest.fn(() => [{ type: 'aggregation', defaults: {} }]) }, +})); + +jest.mock('logic/generateId', () => jest.fn()); + +jest.mock('bson-objectid', () => jest.fn()); +const mock_color = StaticColor.create('#ffffff'); + +jest.mock('views/logic/views/formatting/highlighting/HighlightingRule', () => ({ + ...jest.requireActual('views/logic/views/formatting/highlighting/HighlightingRule'), + randomColor: jest.fn(() => mock_color), + __esModule: true, +})); + +jest.mock('views/logic/Widgets', () => ({ + ...jest.requireActual('views/logic/Widgets'), + widgetDefinition: () => ({ + searchTypes: () => [{ + type: 'AGGREGATION', + typeDefinition: {}, + }], + }), +})); + +const todayDate = new Date(); +const withCurrentDate = (view: View) => view.toBuilder().createdAt(todayDate).build(); + +describe('UseCreateViewForEventDefinition', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should create view with 2 aggregation widgets and one summary', async () => { + asMock(generateId).mockImplementation(mockedGenerateIdTwoAggregations); + + asMock(ObjectID).mockImplementation(() => (({ + toString: () => mockedObjectIdTwoAggregations(), + }) as ObjectID)); + + const { result } = renderHook(() => UseCreateViewForEventDefinition({ eventDefinition: mockEventDefinitionTwoAggregations, aggregations: mockedMappedAggregation })); + const view = await result.current.then((r) => r); + + expect(withCurrentDate(view)).toEqual(withCurrentDate(mockedViewWithTwoAggregationsED)); + }); + + it('should create view with 1 aggregation widgets and without summary', async () => { + asMock(generateId).mockImplementation(mockedGenerateIdOneAggregation); + + asMock(ObjectID).mockImplementation(() => (({ + toString: () => mockedObjectIdOneAggregation(), + }) as ObjectID)); + + const { result } = renderHook(() => UseCreateViewForEventDefinition({ eventDefinition: mockEventDefinitionOneAggregation, aggregations: [mockedMappedAggregation[0]] })); + const view = await result.current.then((r) => r); + + expect(withCurrentDate(view)).toEqual(withCurrentDate(mockedViewWithOneAggregationED)); + }); +});