Improve reusability of event definition wizard. (#18731)

* Unify event definition type.

* Remove redundant event definition type.

* Create component for event definition form buttons.

* Make event definition form controls pluggable.

* Fixing linter hint.

* Default to first step when in security mode

* Fix duplicate in event definition form (#18722)

* register event definition export globally

- EventDefinitionsBindings, EventNotificationsBindings, FieldValueProvidersBindings

* fix types

* Fix search query being erased while typing

* fix test

* fix types

* Make initial step configurable.

* Only set URL query param `step` when necessary.

* Refactor from controls.

* Update props:

* Reimplement `onChange` in `FilterForm` to fix problem with params.

* make embryonic only constrained to FilterForm

* Fixing linter hints.

---------

Co-authored-by: Simon Huang <simon.huang@graylog.com>
Co-authored-by: Ousmane SAMBA <ousmane@graylog.com>
Co-authored-by: Ousmane Samba <ousmanesamba@gmail.com>
This commit is contained in:
Linus Pahl
2024-03-21 16:12:11 +01:00
committed by GitHub
parent 475f722908
commit d6ebb2680e
29 changed files with 307 additions and 244 deletions

View File

@@ -137,13 +137,30 @@ type DataTiering = {
}>,
}
type FieldValueProvider = {
type: string,
displayName: string,
formComponent: React.ComponentType,
summaryComponent: React.ComponentType,
defaultConfig: {
template?: string,
table_name?: string,
key_field?: string,
},
requiredFields: {
template?: string,
table_name?: string,
key_field?: string,
},
}
declare module 'graylog-web-plugin/plugin' {
interface PluginExports {
navigation?: Array<PluginNavigation>;
dataTiering?: Array<DataTiering>
defaultNavigation?: Array<PluginNavigation>;
navigationItems?: Array<PluginNavigationItems>;
globalNotifications?: Array<GlobalNotification>
globalNotifications?: Array<GlobalNotification>;
fieldValueProviders?:Array<FieldValueProvider>;
// Global context providers allow to fetch and process data once
// and provide the result for all components in your plugin.
globalContextProviders?: Array<React.ComponentType<React.PropsWithChildrean<{}>>>,

View File

@@ -25,10 +25,10 @@ import { Select } from 'components/common';
import { Clearfix, Col, ControlLabel, FormGroup, HelpBlock, Row } from 'components/bootstrap';
import { HelpPanel } from 'components/event-definitions/common/HelpPanel';
import type User from 'logic/users/User';
import { getPathnameWithoutId } from 'util/URLUtils';
import useSendTelemetry from 'logic/telemetry/useSendTelemetry';
import useLocation from 'routing/useLocation';
import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants';
import { getPathnameWithoutId } from 'util/URLUtils';
import styles from './EventConditionForm.css';
@@ -143,9 +143,10 @@ const EventConditionForm = ({ action, entityTypes, eventDefinition, validation,
Configure how Graylog should create Events of this kind. You can later use those Events as input on other
Conditions, making it possible to build powerful Conditions based on others.
</p>
<FormGroup controlId="event-definition-priority" validationState={validation.errors.config ? 'error' : null}>
<ControlLabel>Condition Type</ControlLabel>
<FormGroup validationState={validation.errors.config ? 'error' : null}>
<ControlLabel htmlFor="event-condition-type-select">Condition Type</ControlLabel>
<Select placeholder="Select a Condition Type"
inputId="event-condition-type-select"
options={formattedEventDefinitionTypes()}
value={eventDefinition.config.type}
onChange={handleEventDefinitionTypeChange}

View File

@@ -15,26 +15,22 @@
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import type { SyntheticEvent } from 'react';
import React, { useEffect, useState } from 'react';
import * as React from 'react';
import PropTypes from 'prop-types';
import last from 'lodash/last';
import defaultTo from 'lodash/defaultTo';
import { PluginStore } from 'graylog-web-plugin/plugin';
import styled from 'styled-components';
import URI from 'urijs';
import QS from 'qs';
import { getPathnameWithoutId } from 'util/URLUtils';
import { Button, Col, Row } from 'components/bootstrap';
import { ModalSubmit, Wizard } from 'components/common';
import { Col, Row } from 'components/bootstrap';
import { Wizard } from 'components/common';
import type { EventNotification } from 'stores/event-notifications/EventNotificationsStore';
import type { EventDefinition } from 'components/event-definitions/event-definitions-types';
import type { EventDefinition, EventDefinitionFormControlsProps } from 'components/event-definitions/event-definitions-types';
import type User from 'logic/users/User';
import useQuery from 'routing/useQuery';
import useHistory from 'routing/useHistory';
import useSendTelemetry from 'logic/telemetry/useSendTelemetry';
import useLocation from 'routing/useLocation';
import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants';
import EventDefinitionFormControls from 'components/event-definitions/event-definition-form/EventDefinitionFormControls';
import EventDetailsForm from './EventDetailsForm';
import EventConditionForm from './EventConditionForm';
@@ -42,7 +38,10 @@ import FieldsForm from './FieldsForm';
import NotificationsForm from './NotificationsForm';
import EventDefinitionSummary from './EventDefinitionSummary';
const STEP_KEYS = ['event-details', 'condition', 'fields', 'notifications', 'summary'];
const WizardContainer = styled.div`
margin-bottom: 10px;
`;
export const STEP_KEYS = ['event-details', 'condition', 'fields', 'notifications', 'summary'];
const STEP_TELEMETRY_KEYS = [
TELEMETRY_EVENT_TYPE.EVENTDEFINITION_DETAILS.STEP_CLICKED,
TELEMETRY_EVENT_TYPE.EVENTDEFINITION_CONDITION.STEP_CLICKED,
@@ -59,11 +58,8 @@ const getConditionPlugin = (type: string | undefined) => {
return PluginStore.exports('eventDefinitionTypes').find((edt) => edt.type === type) || {};
};
const WizardContainer = styled.div`
margin-bottom: 10px;
`;
type Props = {
activeStep: string,
action: 'edit' | 'create',
eventDefinition: EventDefinition,
currentUser: User,
@@ -77,48 +73,40 @@ type Props = {
notifications: Array<EventNotification>,
defaults: { default_backlog_size: number },
onChange: (key: string, value: unknown) => void,
onChangeStep: (step: string) => void,
onCancel: () => void,
onSubmit: () => void
canEdit: boolean,
formControls?: React.ComponentType<EventDefinitionFormControlsProps>
}
const EventDefinitionForm = ({
action,
eventDefinition,
currentUser,
validation,
entityTypes,
notifications,
defaults,
onChange,
onCancel,
onSubmit,
activeStep,
canEdit,
currentUser,
defaults,
entityTypes,
eventDefinition,
formControls: FormControls,
notifications,
onCancel,
onChange,
onChangeStep,
onSubmit,
validation,
}: Props) => {
const { step } = useQuery();
const [activeStep, setActiveStep] = useState(step as string || STEP_KEYS[0]);
const history = useHistory();
const { pathname } = useLocation();
const sendTelemetry = useSendTelemetry();
useEffect(() => {
const currentUrl = new URI(window.location.href);
const queryParameters = QS.parse(currentUrl.query());
if (queryParameters.step !== activeStep) {
const newUrl = currentUrl.removeSearch('step').addQuery('step', activeStep);
history.replace(newUrl.resource());
}
}, [activeStep, history]);
const activeStepIndex = STEP_KEYS.indexOf(activeStep);
const handleSubmit = (event: SyntheticEvent) => {
if (event) {
event.preventDefault();
}
if (activeStep === last(STEP_KEYS)) {
onSubmit();
}
onSubmit();
};
const defaultStepProps = {
@@ -170,7 +158,7 @@ const EventDefinitionForm = ({
},
];
const handleStepChange = (nextStep) => {
const handleStepChange = (nextStep: string) => {
sendTelemetry(STEP_TELEMETRY_KEYS[STEP_KEYS.indexOf(nextStep)], {
app_pathname: getPathnameWithoutId(pathname),
app_section: (action === 'create') ? 'new-event-definition' : 'edit-event-definition',
@@ -178,59 +166,31 @@ const EventDefinitionForm = ({
current_step: steps[STEP_KEYS.indexOf(activeStep)].title,
});
setActiveStep(nextStep);
onChangeStep(nextStep);
};
const renderButtons = () => {
if (activeStep === last(STEP_KEYS)) {
return (
<ModalSubmit onCancel={onCancel}
onSubmit={handleSubmit}
submitButtonText={`${eventDefinition.id ? 'Update' : 'Create'} event definition`} />
);
}
const openPrevPage = () => {
sendTelemetry(TELEMETRY_EVENT_TYPE.EVENTDEFINITION_PREVIOUS_CLICKED, {
app_pathname: getPathnameWithoutId(pathname),
app_section: (action === 'create') ? 'new-event-definition' : 'edit-event-definition',
app_action_value: 'previous-button',
current_step: steps[activeStepIndex].title,
});
const activeStepIndex = STEP_KEYS.indexOf(activeStep);
const previousStep = activeStepIndex > 0 ? STEP_KEYS[activeStepIndex - 1] : undefined;
onChangeStep(previousStep);
};
const handlePreviousClick = () => {
sendTelemetry(TELEMETRY_EVENT_TYPE.EVENTDEFINITION_PREVIOUS_CLICKED, {
app_pathname: getPathnameWithoutId(pathname),
app_section: (action === 'create') ? 'new-event-definition' : 'edit-event-definition',
app_action_value: 'previous-button',
current_step: steps[activeStepIndex].title,
});
const openNextPage = () => {
sendTelemetry(TELEMETRY_EVENT_TYPE.EVENTDEFINITION_NEXT_CLICKED, {
app_pathname: getPathnameWithoutId(pathname),
app_section: (action === 'create') ? 'new-event-definition' : 'edit-event-definition',
app_action_value: 'next-button',
current_step: steps[activeStepIndex].title,
});
const previousStep = activeStepIndex > 0 ? STEP_KEYS[activeStepIndex - 1] : undefined;
setActiveStep(previousStep);
};
const handleNextClick = () => {
sendTelemetry(TELEMETRY_EVENT_TYPE.EVENTDEFINITION_NEXT_CLICKED, {
app_pathname: getPathnameWithoutId(pathname),
app_section: (action === 'create') ? 'new-event-definition' : 'edit-event-definition',
app_action_value: 'next-button',
current_step: steps[activeStepIndex].title,
});
const nextStep = STEP_KEYS[activeStepIndex + 1];
setActiveStep(nextStep);
};
return (
<div>
<Button bsStyle="info"
onClick={handlePreviousClick}
disabled={activeStepIndex === 0}>
Previous
</Button>
<div className="pull-right">
<Button bsStyle="info"
onClick={handleNextClick}>
Next
</Button>
</div>
</div>
);
const nextStep = STEP_KEYS[activeStepIndex + 1];
onChangeStep(nextStep);
};
return (
@@ -245,7 +205,13 @@ const EventDefinitionForm = ({
containerClassName=""
hidePreviousNextButtons />
</WizardContainer>
{renderButtons()}
<FormControls activeStepIndex={activeStepIndex}
action={action}
onOpenPrevPage={openPrevPage}
onOpenNextPage={openNextPage}
steps={steps}
onSubmit={handleSubmit}
onCancel={onCancel} />
</Col>
</Row>
);
@@ -266,6 +232,7 @@ EventDefinitionForm.propTypes = {
EventDefinitionForm.defaultProps = {
action: 'edit',
formControls: EventDefinitionFormControls,
};
export default EventDefinitionForm;

View File

@@ -194,6 +194,12 @@ jest.mock('logic/telemetry/useSendTelemetry');
jest.mock('hooks/useScopePermissions');
jest.mock('hooks/useCurrentUser');
jest.mock('components/perspectives/hooks/useActivePerspective', () => () => ({
id: 'security',
title: 'Security',
welcomeRoute: '/security',
}));
describe('EventDefinitionFormContainer', () => {
beforeEach(() => {
asMock(useLocation).mockImplementation(() => ({ pathname: '/event-definitions', search: '', hash: '', state: null, key: 'mock-key' }));
@@ -226,18 +232,18 @@ describe('EventDefinitionFormContainer', () => {
it('should render Filters & Aggregation form enabled', async () => {
render(<EventDefinitionFormContainer action="edit" eventDefinition={mockAggregationEventDefinition} />);
const tab = await screen.findByRole('button', { name: /filter & aggregation/i });
const tab = await screen.findByRole('button', { name: /condition/i });
userEvent.click(tab);
const conditionTypeSelect = await screen.findByLabelText('Condition Type');
expect(screen.getByRole('textbox', { name: /search query/i })).toBeEnabled();
expect(conditionTypeSelect).toBeInTheDocument();
});
it('Filters & Aggregation should not be accessible for immutable entities', async () => {
asMock(useScopePermissions).mockImplementation(() => exampleEntityScopeImmutable);
render(<EventDefinitionFormContainer action="edit" eventDefinition={mockAggregationEventDefinition} />);
const tab = await screen.findByRole('button', { name: /filter & aggregation/i });
const tab = await screen.findByRole('button', { name: /condition/i });
userEvent.click(tab);
expect(screen.getByText(/cannot be edited/i)).toBeVisible();

View File

@@ -16,11 +16,9 @@
*/
import React, { useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useNavigate } from 'react-router-dom';
import { PluginStore } from 'graylog-web-plugin/plugin';
import cloneDeep from 'lodash/cloneDeep';
import Routes from 'routing/Routes';
import { useStore } from 'stores/connect';
import EventDefinitionPriorityEnum from 'logic/alerts/EventDefinitionPriorityEnum';
import { ConfirmLeaveDialog, Spinner } from 'components/common';
@@ -28,8 +26,7 @@ import { AvailableEventDefinitionTypesStore } from 'stores/event-definitions/Ava
import { ConfigurationsActions } from 'stores/configurations/ConfigurationsStore';
import { EventDefinitionsActions } from 'stores/event-definitions/EventDefinitionsStore';
import { EventNotificationsActions, EventNotificationsStore } from 'stores/event-notifications/EventNotificationsStore';
import 'components/event-notifications/event-notification-types';
import type { EventDefinition } from 'components/event-definitions/event-definitions-types';
import type { EventDefinition, EventDefinitionFormControlsProps } from 'components/event-definitions/event-definitions-types';
import useCurrentUser from 'hooks/useCurrentUser';
import useEventDefinitionConfigFromLocalStorage from 'components/event-definitions/hooks/useEventDefinitionConfigFromLocalStorage';
import { getPathnameWithoutId } from 'util/URLUtils';
@@ -38,7 +35,7 @@ import useLocation from 'routing/useLocation';
import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants';
import useScopePermissions from 'hooks/useScopePermissions';
import EventDefinitionForm from './EventDefinitionForm';
import EventDefinitionForm, { STEP_KEYS } from './EventDefinitionForm';
const fetchNotifications = () => {
EventNotificationsActions.listAll();
@@ -47,7 +44,12 @@ const fetchNotifications = () => {
type Props = {
action: 'edit' | 'create',
eventDefinition: EventDefinition,
formControls?: React.ComponentType<EventDefinitionFormControlsProps>,
initialStep?: string,
onCancel?: () => void
onChangeStep?: (step: string) => void,
onEventDefinitionChange: (nextEventDefinition: EventDefinition) => void,
onSubmit?: () => void,
}
const getConditionPlugin = (edType): any => {
@@ -58,7 +60,17 @@ const getConditionPlugin = (edType): any => {
return PluginStore.exports('eventDefinitionTypes').find((eventDefinitionType) => eventDefinitionType.type === edType) || {};
};
const EventDefinitionFormContainer = ({ action, eventDefinition: eventDefinitionInitial, onEventDefinitionChange }: Props) => {
const EventDefinitionFormContainer = ({
action,
eventDefinition: eventDefinitionInitial,
formControls,
initialStep,
onCancel,
onChangeStep,
onEventDefinitionChange,
onSubmit,
}: Props) => {
const [activeStep, setActiveStep] = useState(initialStep);
const [eventDefinition, setEventDefinition] = useState(eventDefinitionInitial);
const [validation, setValidation] = useState({ errors: {} });
const [eventsClusterConfig, setEventsClusterConfig] = useState(undefined);
@@ -69,7 +81,6 @@ const EventDefinitionFormContainer = ({ action, eventDefinition: eventDefinition
const entityTypes = useStore(AvailableEventDefinitionTypesStore);
const notifications = useStore(EventNotificationsStore);
const currentUser = useCurrentUser();
const navigate = useNavigate();
const { pathname } = useLocation();
const sendTelemetry = useSendTelemetry();
@@ -111,7 +122,13 @@ const EventDefinitionFormContainer = ({ action, eventDefinition: eventDefinition
const handleSubmitSuccessResponse = () => {
setIsDirty(false);
navigate(Routes.ALERTS.DEFINITIONS.LIST);
onSubmit();
};
const showValidationErrors = (errors: { errors: unknown }) => {
setValidation(errors);
setActiveStep(STEP_KEYS[STEP_KEYS.length - 1]);
};
const handleSubmitFailureResponse = (errorResponse) => {
@@ -119,7 +136,7 @@ const EventDefinitionFormContainer = ({ action, eventDefinition: eventDefinition
if (errorResponse.status === 400) {
if (body && body.failed) {
setValidation(body);
showValidationErrors(body);
return;
}
@@ -128,7 +145,7 @@ const EventDefinitionFormContainer = ({ action, eventDefinition: eventDefinition
if (body.message.includes('org.graylog.events.conditions.Expression')
|| body.message.includes('org.graylog.events.conditions.Expr')
|| body.message.includes('org.graylog.events.processor.aggregation.AggregationSeries')) {
setValidation({
showValidationErrors({
errors: { conditions: ['Aggregation condition is not valid'] },
});
@@ -136,7 +153,7 @@ const EventDefinitionFormContainer = ({ action, eventDefinition: eventDefinition
}
if (body.message.includes('embryonic')) {
setValidation({
showValidationErrors({
errors: { query_parameters: ['Query parameters must be declared'] },
});
}
@@ -173,7 +190,12 @@ const EventDefinitionFormContainer = ({ action, eventDefinition: eventDefinition
app_action_value: 'cancel-button',
});
navigate(Routes.ALERTS.DEFINITIONS.LIST);
onCancel();
};
const changeStep = (step: string) => {
onChangeStep?.(step);
setActiveStep(step);
};
if (isLoading || loadingScopePermissions) {
@@ -186,16 +208,19 @@ const EventDefinitionFormContainer = ({ action, eventDefinition: eventDefinition
<ConfirmLeaveDialog question="Do you really want to abandon this page and lose your changes? This action cannot be undone." />
)}
<EventDefinitionForm action={action}
eventDefinition={eventDefinition}
canEdit={scopePermissions.is_mutable}
currentUser={currentUser}
validation={validation}
entityTypes={entityTypes}
notifications={notifications.all}
defaults={defaults}
onChange={handleChange}
activeStep={activeStep}
entityTypes={entityTypes}
eventDefinition={eventDefinition}
formControls={formControls}
notifications={notifications.all}
onCancel={handleCancel}
onChange={handleChange}
onChangeStep={changeStep}
onSubmit={handleSubmit}
canEdit={scopePermissions.is_mutable} />
validation={validation} />
</>
);
};
@@ -223,6 +248,11 @@ EventDefinitionFormContainer.defaultProps = {
notifications: [],
alert: false,
},
formControls: undefined,
initialStep: STEP_KEYS[0],
onCancel: undefined,
onSubmit: undefined,
onChangeStep: undefined,
onEventDefinitionChange: () => {},
};

View File

@@ -0,0 +1,58 @@
/*
* 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 React from 'react';
import { ModalSubmit } from 'components/common';
import { Button } from 'components/bootstrap';
import type { EventDefinitionFormControlsProps } from 'components/event-definitions/event-definitions-types';
const EventDefinitionFormControls = ({
action,
activeStepIndex,
onCancel,
onOpenNextPage,
onOpenPrevPage,
onSubmit,
steps,
}: EventDefinitionFormControlsProps) => {
if (activeStepIndex === steps.length - 1) {
return (
<ModalSubmit onCancel={onCancel}
onSubmit={onSubmit}
submitButtonText={`${action === 'edit' ? 'Update' : 'Create'} event definition`} />
);
}
return (
<div>
<Button bsStyle="info"
onClick={onOpenPrevPage}
disabled={activeStepIndex === 0}>
Previous
</Button>
<div className="pull-right">
<Button bsStyle="info"
onClick={onOpenNextPage}>
Next
</Button>
</div>
</div>
);
};
export default EventDefinitionFormControls;

View File

@@ -35,10 +35,6 @@ import type { EventDefinition } from '../event-definitions-types';
import commonStyles from '../common/commonStyles.css';
import { SYSTEM_EVENT_DEFINITION_TYPE } from '../constants';
// Import built-in plugins
import 'components/event-definitions/event-definition-types';
import 'components/event-notifications/event-notification-types';
type Props = {
eventDefinition: Omit<EventDefinition, 'id'>,
notifications: Array<any>,

View File

@@ -29,9 +29,6 @@ import HoverForHelp from 'components/common/HoverForHelp';
import FieldForm from './FieldForm';
import FieldsList from './FieldsList';
// Import built-in Field Value Providers
import './field-value-providers';
import type { EventDefinition } from '../event-definitions-types';
import commonStyles from '../common/commonStyles.css';

View File

@@ -14,15 +14,13 @@
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import { PluginManifest, PluginStore } from 'graylog-web-plugin/plugin';
import TemplateFieldValueProviderForm from './TemplateFieldValueProviderForm';
import TemplateFieldValueProviderSummary from './TemplateFieldValueProviderSummary';
import LookupTableFieldValueProviderForm from './LookupTableFieldValueProviderForm';
import LookupTableFieldValueProviderFormContainer from './LookupTableFieldValueProviderFormContainer';
import LookupTableFieldValueProviderSummary from './LookupTableFieldValueProviderSummary';
PluginStore.register(new PluginManifest({}, {
export default {
fieldValueProviders: [
{
type: TemplateFieldValueProviderForm.type,
@@ -41,4 +39,4 @@ PluginStore.register(new PluginManifest({}, {
requiredFields: LookupTableFieldValueProviderForm.requiredFields,
},
],
}));
};

View File

@@ -14,6 +14,7 @@
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import * as React from 'react';
import { useCallback, useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
@@ -30,6 +31,10 @@ import union from 'lodash/union';
import moment from 'moment';
import { OrderedMap } from 'immutable';
import { getPathnameWithoutId } from 'util/URLUtils';
import { isPermitted } from 'util/PermissionsMixin';
import * as FormsUtils from 'util/FormsUtils';
import { naturalSortIgnoreCase } from 'util/SortUtils';
import FormWarningsContext from 'contexts/FormWarningsContext';
import { useStore } from 'stores/connect';
import Store from 'logic/local-storage/Store';
@@ -40,21 +45,18 @@ import Search from 'views/logic/search/Search';
import { extractDurationAndUnit } from 'components/common/TimeUnitInput';
import { Alert, ButtonToolbar, ControlLabel, FormGroup, HelpBlock, Input } from 'components/bootstrap';
import RelativeTime from 'components/common/RelativeTime';
import { naturalSortIgnoreCase } from 'util/SortUtils';
import * as FormsUtils from 'util/FormsUtils';
import { isPermitted } from 'util/PermissionsMixin';
import type { LookupTableParameterJson } from 'views/logic/parameters/LookupTableParameter';
import LookupTableParameter from 'views/logic/parameters/LookupTableParameter';
import { LookupTablesActions, LookupTablesStore } from 'stores/lookup-tables/LookupTablesStore';
import validateQuery from 'views/components/searchbar/queryvalidation/validateQuery';
import generateId from 'logic/generateId';
import parseSearch from 'views/logic/slices/parseSearch';
import useLocation from 'routing/useLocation';
import { getPathnameWithoutId } from 'util/URLUtils';
import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants';
import useSendTelemetry from 'logic/telemetry/useSendTelemetry';
import type User from 'logic/users/User';
import useUserDateTime from 'hooks/useUserDateTime';
import type { EventDefinition } from 'components/event-definitions/event-definition-types';
import type { EventDefinition } from 'components/event-definitions/event-definitions-types';
import type { Stream } from 'stores/streams/StreamsStore';
import type { QueryValidationState } from 'views/components/searchbar/queryvalidation/types';
import { indicesInWarmTier, isSearchingWarmTier } from 'views/components/searchbar/queryvalidation/warmTierValidation';
@@ -64,18 +66,19 @@ import commonStyles from '../common/commonStyles.css';
export const PLUGGABLE_CONTROLS_HIDDEN_KEY = 'pluggableSearchBarControlsAreHidden';
export const TIME_UNITS = ['HOURS', 'MINUTES', 'SECONDS'];
type LookupTableParameterJsonEmbryonic = Partial<LookupTableParameterJson> & {
embryonic?: boolean,
}
const LOOKUP_PERMISSIONS = [
'lookuptables:read',
];
const buildNewParameter = (name: string) => ({
const buildNewParameter = (name: string): LookupTableParameterJsonEmbryonic => ({
name: name,
embryonic: true,
type: 'lut-parameter-v1',
data_type: 'any',
title: 'new title',
// has no binding, no need to set binding property
});
type Props = {
@@ -353,7 +356,7 @@ const FilterForm = ({
const parameterButtons = queryParameters.map((queryParam) => (
<EditQueryParameterModal key={queryParam.name}
queryParameter={LookupTableParameter.fromJSON(queryParam)}
embryonic={!!queryParam.embryonic}
embryonic={!!(queryParam as LookupTableParameterJsonEmbryonic).embryonic}
queryParameters={queryParameters}
lookupTables={Object.values(tables)}
onChange={onChangeQueryParameters} />
@@ -363,7 +366,7 @@ const FilterForm = ({
return null;
}
const hasEmbryonicParameters = !isEmpty(queryParameters.filter((param) => (param.embryonic)));
const hasEmbryonicParameters = !isEmpty(queryParameters.filter((param : LookupTableParameterJsonEmbryonic) => (param.embryonic)));
return (
<FormGroup validationState={validation.errors.query_parameters ? 'error' : null}>

View File

@@ -14,13 +14,12 @@
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import { PluginManifest, PluginStore } from 'graylog-web-plugin/plugin';
import FilterAggregationFormContainer from './FilterAggregationFormContainer';
import FilterAggregationForm from './FilterAggregationForm';
import FilterAggregationSummary from './FilterAggregationSummary';
PluginStore.register(new PluginManifest({}, {
export default {
eventDefinitionTypes: [
{
type: 'aggregation-v1',
@@ -33,4 +32,4 @@ PluginStore.register(new PluginManifest({}, {
defaultConfig: FilterAggregationForm.defaultConfig,
},
],
}));
};

View File

@@ -14,6 +14,10 @@
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import { type SyntheticEvent } from 'react';
import type { Steps } from 'components/common/Wizard';
import type { LookupTableParameterJson } from 'views/logic/parameters/LookupTableParameter';
type Provider = {
type: string,
@@ -29,8 +33,9 @@ type FieldSpec = {
};
type Notification = {
notification_id: number,
};
notification_id: string,
notification_parameters: string
}
export type Scheduler = {
data: {
@@ -57,29 +62,51 @@ export type SearchFilter = {
};
export type EventDefinition = {
_scope: string,
id: string,
config?: {
type: string,
execute_every_ms?: number,
search_within_ms?: number,
event_limit?: number,
sigma_rule_id?: string,
streams?: Array<string>
filters?: Array<SearchFilter>,
},
title: string,
description?: string,
matched_at?: string,
priority?: number,
key_spec?: Array<string>
field_spec?: FieldSpec,
notification_settings?: {
backlog_size: number,
grace_period_ms: number,
}
notifications?: Array<Notification>,
_scope?: string,
scheduler?: Scheduler,
description: string,
priority: number,
alert: boolean,
state?: 'ENABLED' | 'DISABLED',
config: {
type: string,
query: string,
query_parameters: LookupTableParameterJson[],
filters: SearchFilter[],
streams: string[],
group_by: string[],
_is_scheduled: boolean,
series: Array<{field: string, id: string, type: string}>,
conditions: {
expression: string | null | {},
},
search_within_ms: number,
execute_every_ms: number,
},
field_spec: FieldSpec,
key_spec: string[],
notification_settings: {
grace_period_ms: number,
backlog_size: number,
},
notifications: Array<Notification>,
remediation_steps?: string,
};
storage: Array<{
type: string,
streams: number[] | string[],
}>,
updated_at: string | null,
matched_at?: string,
scheduler?: Scheduler,
}
export type EventDefinitionFormControlsProps = {
action: 'edit' | 'create',
activeStepIndex: number,
onCancel: () => void,
onOpenNextPage: () => void,
onOpenPrevPage: () => void,
onSubmit: (event: SyntheticEvent) => void,
steps: Steps,
}

View File

@@ -20,7 +20,6 @@ import { useState, useMemo, useCallback } from 'react';
import { EntityDataTable, NoSearchResult, PaginatedList, QueryHelper, RelativeTime, SearchForm, Spinner } from 'components/common';
import type { Sort } from 'stores/PaginationTypes';
import usePaginationQueryParameter from 'hooks/usePaginationQueryParameter';
import 'components/event-definitions/event-definition-types';
import { Link } from 'components/common/router';
import Routes from 'routing/Routes';
import type { ColumnRenderers } from 'components/common/EntityDataTable';

View File

@@ -26,10 +26,6 @@ import withHistory from 'routing/withHistory';
import EventNotificationForm from './EventNotificationForm';
// Import built-in Event Notification Types
import '../event-notification-types';
const initialValidation = {
errors: {},
};

View File

@@ -14,8 +14,6 @@
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import { PluginManifest, PluginStore } from 'graylog-web-plugin/plugin';
import EmailNotificationFormContainer from './EmailNotificationFormContainer';
import EmailNotificationForm from './EmailNotificationForm';
import EmailNotificationSummary from './EmailNotificationSummary';
@@ -32,7 +30,7 @@ import HttpNotificationDetails from '../event-notification-details/HttpNotificat
import LegacyNotificationDetails from '../event-notification-details/LegacyNotificationDetails';
import HttpNotificationDetailsV2 from '../event-notification-details/HttpNotificationDetailsV2';
PluginStore.register(new PluginManifest({}, {
export default {
eventNotificationTypes: [
{
type: 'email-notification-v1',
@@ -67,4 +65,4 @@ PluginStore.register(new PluginManifest({}, {
defaultConfig: LegacyNotificationForm.defaultConfig,
},
],
}));
};

View File

@@ -38,9 +38,6 @@ import BulkActions from './BulkActions';
import useEventNotifications from '../hooks/useEventNotifications';
import useNotificationTest from '../hooks/useNotificationTest';
// Import built-in Event Notification Types
import '../event-notification-types';
const customColumnRenderers = (testResults: TestResults): ColumnRenderers<EventNotification> => ({
attributes: {
title: {

View File

@@ -30,8 +30,6 @@ import withPaginationQueryParameter from 'components/common/withPaginationQueryP
import Events, { PAGE_SIZES, EVENTS_MAX_OFFSET_LIMIT } from './Events';
import 'components/event-definitions/event-definition-types';
const LOCAL_STORAGE_ITEM = 'events-last-search';
const SEARCH_DEBOUNCE_THRESHOLD = 500;

View File

@@ -16,10 +16,10 @@
*/
import { useQuery } from '@tanstack/react-query';
import type { EventDefinition } from 'logic/alerts/types';
import fetch from 'logic/rest/FetchProvider';
import { qualifyUrl } from 'util/URLUtils';
import UserNotification from 'preflight/util/UserNotification';
import type { EventDefinition } from 'components/event-definitions/event-definitions-types';
export const fetchEventDefinitionDetails = async (eventDefinitionId: string): Promise<EventDefinition> => (
fetch('GET', qualifyUrl(`/events/definitions/${eventDefinitionId}`))

View File

@@ -23,7 +23,7 @@ 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 type { EventDefinition } from 'logic/alerts/types';
import type { EventDefinition } from 'components/event-definitions/event-definitions-types';
const useAlertAndEventDefinitionData = () => {
const { pathname: path } = useLocation();

View File

@@ -21,7 +21,7 @@ import isEqual from 'lodash/isEqual';
import UserNotification from 'util/UserNotification';
import fetch from 'logic/rest/FetchProvider';
import { qualifyUrl } from 'util/URLUtils';
import type { EventDefinition } from 'logic/alerts/types';
import type { EventDefinition } from 'components/event-definitions/event-definitions-types';
import type FetchError from 'logic/errors/FetchError';
export type ValueExpr = '>' | '<' | '>=' | '<=' | '==';

View File

@@ -28,6 +28,9 @@ import ViewsBindings from 'views/bindings';
import ThreatIntelBindings from 'threatintel/bindings';
import AwsBindings from 'aws/bindings';
import IntegrationsBindings from 'integrations/bindings';
import EventDefinitionsBindings from 'components/event-definitions/event-definition-types/bindings';
import EventNotificationsBindings from 'components/event-notifications/event-notification-types/bindings';
import FieldValueProvidersBindings from 'components/event-definitions/event-definition-form/field-value-providers/bindings';
import CancellablePromise from 'logic/rest/CancellablePromise';
import TelemetryInit from 'logic/telemetry/TelemetryInit';
import LoginQueryClientProvider from 'contexts/LoginQueryClientProvider';
@@ -44,6 +47,9 @@ PluginStore.register(new PluginManifest({}, ViewsBindings));
PluginStore.register(new PluginManifest({}, ThreatIntelBindings));
PluginStore.register(new PluginManifest({}, AwsBindings));
PluginStore.register(new PluginManifest({}, IntegrationsBindings));
PluginStore.register(new PluginManifest({}, EventDefinitionsBindings));
PluginStore.register(new PluginManifest({}, EventNotificationsBindings));
PluginStore.register(new PluginManifest({}, FieldValueProvidersBindings));
PluginStore.register(new PluginManifest({}, PerspectivesBindings));
PluginStore.register(new PluginManifest({}, NavigationBindings));
PluginStore.register(new PluginManifest({}, SecurityBindings));

View File

@@ -1,56 +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
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import type { ParameterJson } from 'views/logic/parameters/Parameter';
import type { SearchFilter } from 'components/event-definitions/event-definitions-types';
export type EventDefinition = {
_scope: string,
id: string,
title: string,
description: string,
priority: number,
alert: boolean,
config: {
type: string,
query: string,
query_parameters: ParameterJson[],
filters: SearchFilter[],
streams: string[],
group_by: string[],
series: Array<{field: string, id: string, type: string}>,
conditions: {
expression: string | null | {},
},
search_within_ms: number,
execute_every_ms: number,
},
field_spec: {},
key_spec: string[],
notification_settings: {
grace_period_ms: number,
backlog_size: number,
},
notifications: Array<{ notification_id: string, notification_parameters: string}>,
storage: [
{
type: string,
streams: number[] | string[],
}
],
updated_at: string | null
}

View File

@@ -40,6 +40,10 @@ const CreateEventDefinitionPage = () => {
const pageTitle = useMemo(() => (eventDefinitionTitle ? `New Event Definition "${eventDefinitionTitle}"` : 'New Event Definition'), [eventDefinitionTitle]);
const goToOverview = useCallback(() => {
navigate(Routes.ALERTS.DEFINITIONS.LIST);
}, [navigate]);
useEffect(() => {
if (!isPermitted(currentUser.permissions, 'eventdefinitions:create')) {
navigate(Routes.NOTFOUND);
@@ -62,7 +66,10 @@ const CreateEventDefinitionPage = () => {
<Row className="content">
<Col md={12}>
<EventDefinitionFormContainer action="create" onEventDefinitionChange={handleEventDefinitionChange} />
<EventDefinitionFormContainer action="create"
onEventDefinitionChange={handleEventDefinitionChange}
onSubmit={goToOverview}
onCancel={goToOverview} />
</Col>
</Row>

View File

@@ -14,8 +14,9 @@
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import React from 'react';
import { useParams } from 'react-router-dom';
import React, { useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import URI from 'urijs';
import EventsPageNavigation from 'components/events/EventsPageNavigation';
import { Col, Row } from 'components/bootstrap';
@@ -28,14 +29,21 @@ import useCurrentUser from 'hooks/useCurrentUser';
import { EventDefinitionsActions } from 'stores/event-definitions/EventDefinitionsStore';
import type { EventDefinition } from 'components/event-definitions/event-definitions-types';
import useHistory from 'routing/useHistory';
import useQuery from 'routing/useQuery';
import StreamPermissionErrorPage from './StreamPermissionErrorPage';
const EditEventDefinitionPage = () => {
const params = useParams<{definitionId?: string}>();
const { step } = useQuery();
const currentUser = useCurrentUser();
const [eventDefinition, setEventDefinition] = React.useState<EventDefinition>(undefined);
const history = useHistory();
const navigate = useNavigate();
const goToOverview = useCallback(() => {
navigate(Routes.ALERTS.DEFINITIONS.LIST);
}, [navigate]);
React.useEffect(() => {
if (isPermitted(currentUser.permissions, `eventdefinitions:edit:${params.definitionId}`)) {
@@ -71,6 +79,11 @@ const EditEventDefinitionPage = () => {
const missingStreams = streamsWithMissingPermissions();
const updateURLStepQueryParam = (newStep: string) => {
const newUrl = new URI(window.location.href).removeSearch('step').addQuery('step', newStep);
history.replace(newUrl.resource());
};
if (missingStreams.length > 0) {
return <StreamPermissionErrorPage error={null} missingStreamIds={missingStreams} />;
}
@@ -101,7 +114,12 @@ const EditEventDefinitionPage = () => {
</PageHeader>
<Row className="content">
<Col md={12}>
<EventDefinitionFormContainer action="edit" eventDefinition={eventDefinition} />
<EventDefinitionFormContainer action="edit"
initialStep={step as string}
onChangeStep={updateURLStepQueryParam}
eventDefinition={eventDefinition}
onSubmit={goToOverview}
onCancel={goToOverview} />
</Col>
</Row>
</DocumentTitle>

View File

@@ -29,7 +29,6 @@ import EventNotificationDetails from 'components/event-notifications/event-notif
import EventNotificationActionLinks from 'components/event-notifications/event-notification-details/EventNotificationActionLinks';
import type { EventNotification } from 'stores/event-notifications/EventNotificationsStore';
import { EventNotificationsActions } from 'stores/event-notifications/EventNotificationsStore';
import 'components/event-notifications/event-notification-types';
import EventsPageNavigation from 'components/events/EventsPageNavigation';
import useHistory from '../routing/useHistory';

View File

@@ -21,7 +21,7 @@ import uniq from 'lodash/uniq';
import View from 'views/logic/views/View';
import type { AbsoluteTimeRange, ElasticsearchQueryString, RelativeTimeRangeStartOnly } from 'views/logic/queries/Query';
import type { Event } from 'components/events/events/types';
import type { EventDefinition } from 'logic/alerts/types';
import type { EventDefinition, SearchFilter } from 'components/event-definitions/event-definitions-types';
import QueryGenerator from 'views/logic/queries/QueryGenerator';
import Search from 'views/logic/search/Search';
import { matchesDecoratorStream } from 'views/logic/views/ViewStateGenerator';
@@ -46,7 +46,6 @@ import { concatQueryStrings, escape } from 'views/logic/queries/QueryHelper';
import HighlightingRule, { randomColor } from 'views/logic/views/formatting/highlighting/HighlightingRule';
import { exprToConditionMapper } from 'views/logic/ExpressionConditionMappers';
import FormattingSettings from 'views/logic/views/formatting/FormattingSettings';
import type { SearchFilter } from 'components/event-definitions/event-definitions-types';
const AGGREGATION_WIDGET_HEIGHT = 3;

View File

@@ -17,7 +17,7 @@
import { useMemo } from 'react';
import type { EventDefinition } from 'logic/alerts/types';
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';

View File

@@ -29,7 +29,9 @@ export const simpleEventDefinition = {
streams: ['stream-id-1'],
type: 'aggregation-v1',
filters: [],
_is_scheduled: true,
},
updated_at: '2024-02-26T15:32:24.666Z',
description: '',
field_spec: {},
id: 'event-definition-1-id',

View File

@@ -17,7 +17,7 @@
import Immutable from 'immutable';
import type { Event } from 'components/events/events/types';
import type { EventDefinition } from 'logic/alerts/types';
import type { EventDefinition } from 'components/event-definitions/event-definitions-types';
import type { EventDefinitionAggregation } from 'hooks/useEventDefinition';
import QueryGenerator from 'views/logic/queries/QueryGenerator';
import Search from 'views/logic/search/Search';
@@ -84,6 +84,7 @@ export const mockEventDefinitionTwoAggregations:EventDefinition = {
type: 'aggregation-v1',
query: 'http_method: GET',
query_parameters: [],
_is_scheduled: true,
streams: [
'001',
],