Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide ReplaySearchContext for replayed search augmentations. #21291

Merged
merged 13 commits into from
Jan 9, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -53,18 +54,19 @@ 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]);

const changeColor = useCallback(({ rule, newColor, condition }) => {
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([
{
Expand All @@ -77,33 +79,27 @@ const AggregationConditions = () => {
}
}, [aggregationsMap, dispatch]);

const highlightedAggregations = useMemo<Map<string, HighlightingRule>>(() => {
const initial = new Map<string, HighlightingRule>(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 ? (
<List>
{Array.from(highlightedAggregations).map(([condition, rule]) => {
{Object.entries(highlightedAggregations).map(([condition, rule]) => {
const color = rule?.color as StaticColor;
const hexColor = color?.color;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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: {
Expand All @@ -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'),
Expand All @@ -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('<EventInfoBar />', () => {
const EventInfoComponent = () => (
const EventInfoComponent = ({ type }: { type: AlertType }) => (
<TestStoreProvider>
<EventInfoBar />
<ReplaySearchContext.Provider value={{
type,
definitionId: '',
alertId: '',
}}>
<EventInfoBar />
</ReplaySearchContext.Provider>
</TestStoreProvider>
);

Expand All @@ -91,16 +95,15 @@ describe('<EventInfoBar />', () => {
.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(<EventInfoComponent />);
it('Always shows fields: Priority, Execute search every, Search within, Description, Notifications, Aggregation conditions', async () => {
render(<EventInfoComponent type="event" />);

const priority = await screen.findByTitle('Priority');
const execution = await screen.findByTitle('Execute search every');
Expand All @@ -123,11 +126,7 @@ describe('<EventInfoBar />', () => {
});

it('Shows event timestamp and event definition link for event', async () => {
setMockedHookCache({
isEvent: true,
});

render(<EventInfoComponent />);
render(<EventInfoComponent type="event" />);

const timestamp = await screen.findByTitle('Timestamp');
const eventDefinition = await screen.findByTitle('Event definition');
Expand All @@ -138,40 +137,34 @@ describe('<EventInfoBar />', () => {
});

it("Didn't Shows Event definition updated at for event definition which was updated before event", async () => {
setMockedHookCache({
isEvent: true,
});

render(<EventInfoComponent />);
render(<EventInfoComponent type="event" />);

const eventDefinitionUpdated = screen.queryByTitle('Event definition updated at');

expect(eventDefinitionUpdated).not.toBeInTheDocument();
});

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(<EventInfoComponent />);
render(<EventInfoComponent type="event" />);

const eventDefinitionUpdated = await screen.findByTitle('Event definition updated at');

expect(eventDefinitionUpdated).toHaveTextContent('2023-03-21 14:28:09');
});

it('Do not shows event timestamp and event definition link for event definition', async () => {
setMockedHookCache({
isEventDefinition: true,
mockUseAlertAndEventDefinitionData({
eventData: undefined,
});

render(<EventInfoComponent />);
render(<EventInfoComponent type="event_definition" />);

const timestamp = screen.queryByTitle('Timestamp');
const eventDefinition = screen.queryByTitle('Event definition');
Expand All @@ -181,11 +174,7 @@ describe('<EventInfoBar />', () => {
});

it('show and hide data on button click', async () => {
setMockedHookCache({
isEventDefinition: true,
});

render(<EventInfoComponent />);
render(<EventInfoComponent type="event_definition" />);

const hideButton = await screen.findByText('Hide event definition details');
const detailsContainer = await screen.findByTestId('info-container');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -55,7 +56,7 @@ const Value = styled.div`
`;

const EventInfoBar = () => {
const { isEventDefinition, isEvent, isAlert } = useAlertAndEventDefinitionData();
const { type } = useReplaySearchContext();
const [open, setOpen] = useState<boolean>(true);

const toggleOpen = useCallback((e: SyntheticEvent) => {
Expand All @@ -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 (
<FlatContentRow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,32 @@
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
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 ? (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
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<ReplaySearchContextType>({ alertId: undefined, definitionId: undefined, type: undefined });
export default ReplaySearchContext;
Loading
Loading