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 77% 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..413cfb2959cc 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 @@ -21,15 +21,16 @@ 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'; -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,10 +54,11 @@ 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) => [ + const aggregationsMap = useMemo(() => Object.fromEntries(aggregations.map((agg) => [ `${agg.fnSeries}${agg.expr}${agg.value}`, agg, ])), [aggregations]); @@ -64,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([ { @@ -77,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; 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..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 @@ -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,37 @@ 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, -})); + isLoading: false, +}); jest.mock('views/logic/slices/highlightSelectors', () => ({ selectHighlightingRules: jest.fn(), })); describe('', () => { - const EventInfoComponent = () => ( + const EventInfoComponent = ({ type }: { type: AlertType }) => ( - + + + ); @@ -91,16 +95,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 +126,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 +137,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 +145,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 +160,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 +174,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..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,31 +15,32 @@ * . */ 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 useAlertAndEventDefinitionData from 'hooks/useAlertAndEventDefinitionData'; +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'; -const Notifications = () => { - const { eventDefinition } = useAlertAndEventDefinitionData(); +import useAlertAndEventDefinitionData from './hooks/useAlertAndEventDefinitionData'; - const allNotifications = useStore(EventNotificationsStore, ({ all }) => all.reduce((res, cur) => { - res[cur.id] = cur; +const Notifications = () => { + const { alertId, definitionId } = useReplaySearchContext(); + const { eventDefinition } = useAlertAndEventDefinitionData(alertId, definitionId); - return res; - }, {})); + useEffect(() => { + EventNotificationsActions.listAll(); + }, []); - const notificationList = useMemo(() => eventDefinition.notifications.reduce((res, cur) => { - if (allNotifications[cur.notification_id]) { - res.push((allNotifications[cur.notification_id])); - } + const allNotifications = useStore(EventNotificationsStore, ({ all }) => Object.fromEntries( + (all ?? []).map((notification) => [notification.id, notification]), + )); - 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 new file mode 100644 index 000000000000..06b747c48138 --- /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({ alertId: undefined, definitionId: undefined, type: 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..327b55be3585 --- /dev/null +++ b/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData.test.tsx @@ -0,0 +1,107 @@ +/* + * 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, + isLoading: false, + }); + }); +}); 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 62% 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..5cd2e5118eef 100644 --- a/graylog2-web-interface/src/hooks/useAlertAndEventDefinitionData.tsx +++ b/graylog2-web-interface/src/components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData.tsx @@ -17,44 +17,36 @@ 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 { data: eventData } = useEventById(alertId); - const { data } = useEventDefinition(definitionId ?? eventData?.event_definition_id); +const useAlertAndEventDefinitionData = (alertId: string, definitionId?: string) => { + 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, definitionId: string, definitionTitle: string, - isAlert: boolean, - isEvent: boolean, - isEventDefinition: boolean, eventData: Event, eventDefinition: EventDefinition, aggregations: Array, + isLoading: boolean, }>(() => ({ 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]); + 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 13b8a7fcf4a5..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 @@ -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 }, @@ -46,7 +51,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 }, @@ -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/components/events/ReplaySearch.tsx b/graylog2-web-interface/src/components/events/ReplaySearch.tsx new file mode 100644 index 000000000000..fdfa570fe714 --- /dev/null +++ b/graylog2-web-interface/src/components/events/ReplaySearch.tsx @@ -0,0 +1,101 @@ +/* + * 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 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'; +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'; +import useCreateViewForEvent from 'views/logic/views/UseCreateViewForEvent'; + +type ReplaySearchProps = { + alertId: string, + definitionId: string, + eventDefinition: EventDefinition, + event: Event, + aggregations: EventDefinitionAggregation[], + replayEventDefinition: boolean, + searchPageLayout: Partial, + forceSidebarPinned: boolean, +} + +const defaultSearchPageLayout = {}; + +const ReplaySearch = ({ + alertId, definitionId, eventDefinition, aggregations, event: eventData, replayEventDefinition, searchPageLayout, forceSidebarPinned, +}: ReplaySearchProps) => { + const _view = useCreateViewForEvent({ eventData, eventDefinition, aggregations }); + const view = useCreateSearch(_view); + const _searchPageLayout = useMemo(() => ({ + ...searchPageLayout, + infoBar: { component: EventInfoBar }, + }), [searchPageLayout]); + 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 ( + + + + + + ); +}; + +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/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/components/events/bulk-replay/__tests__/BulkEventReplay.test.tsx b/graylog2-web-interface/src/components/events/bulk-replay/__tests__/BulkEventReplay.test.tsx index 9a16f662df4a..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'; @@ -33,7 +32,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') }); 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/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: '>=', diff --git a/graylog2-web-interface/src/views/logic/views/UseCreateViewForEvent.ts b/graylog2-web-interface/src/views/logic/views/UseCreateViewForEvent.ts index 58b071c4842a..a0fff5c9bd57 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'; @@ -217,26 +222,32 @@ 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 timeRange: AbsoluteTimeRange = { - type: 'absolute', - from: eventData?.replay_info?.timerange_start, - to: eventData?.replay_info?.timerange_end, - }; + 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: 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 || []; + const queryParameters = eventDefinition?.config?.query_parameters ?? []; 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/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/logic/views/UseCreateViewForEventDefinition.test.ts b/graylog2-web-interface/src/views/logic/views/UseCreateViewForEventDefinitions.test.ts similarity index 99% rename from graylog2-web-interface/src/views/logic/views/UseCreateViewForEventDefinition.test.ts rename to graylog2-web-interface/src/views/logic/views/UseCreateViewForEventDefinitions.test.ts index 4fd1d45746c6..f6a587f015cf 100644 --- a/graylog2-web-interface/src/views/logic/views/UseCreateViewForEventDefinition.test.ts +++ b/graylog2-web-interface/src/views/logic/views/UseCreateViewForEventDefinitions.test.ts @@ -22,7 +22,7 @@ import { mockEventDefinitionOneAggregation, mockEventDefinitionTwoAggregations, } from 'helpers/mocking/EventAndEventDefinitions_mock'; -import UseCreateViewForEventDefinition from 'views/logic/views/UseCreateViewForEventDefinition'; +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'; diff --git a/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.test.tsx b/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.test.tsx index b0672283fa9c..72a71a296996 100644 --- a/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.test.tsx +++ b/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.test.tsx @@ -21,20 +21,20 @@ 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'; 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(); @@ -42,12 +42,12 @@ 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'); jest.mock('hooks/useEventDefinition'); -jest.mock('hooks/useAlertAndEventDefinitionData'); +jest.mock('components/event-definitions/replay-search/hooks/useAlertAndEventDefinitionData'); jest.mock('stores/event-notifications/EventNotificationsStore', () => ({ EventNotificationsActions: { @@ -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); @@ -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(); @@ -109,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/EventDefinitionReplaySearchPage.tsx b/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.tsx index bb9f1f6eb425..2454fa1ae078 100644 --- a/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.tsx +++ b/graylog2-web-interface/src/views/pages/EventDefinitionReplaySearchPage.tsx @@ -15,36 +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 '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 { eventDefinition, aggregations } = useAlertAndEventDefinitionData(); - 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) { @@ -54,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(() => { @@ -63,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.test.tsx b/graylog2-web-interface/src/views/pages/EventReplaySearchPage.test.tsx index 2fd36665f68f..8a972cfc9a24 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, @@ -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: { @@ -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(); diff --git a/graylog2-web-interface/src/views/pages/EventReplaySearchPage.tsx b/graylog2-web-interface/src/views/pages/EventReplaySearchPage.tsx index 105322bd6835..c3e05da817a5 100644 --- a/graylog2-web-interface/src/views/pages/EventReplaySearchPage.tsx +++ b/graylog2-web-interface/src/views/pages/EventReplaySearchPage.tsx @@ -15,37 +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 '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 { eventData, eventDefinition, aggregations } = useAlertAndEventDefinitionData(); - 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) { @@ -55,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); @@ -65,7 +44,9 @@ const EventReplaySearchPage = () => { const isLoading = eventIsLoading || EDIsLoading || !eventIsFetched || !EDIsFetched || !isNotificationLoaded; - return isLoading ? : ; + return isLoading + ? + : ; }; export default EventReplaySearchPage;