diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts
index b586e0593ab6f..b26f935612755 100644
--- a/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts
+++ b/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts
@@ -36,11 +36,18 @@ interface Params {
signal?: AbortSignal;
}
-export interface EqlResponseError {
- code: EQL_ERROR_CODES;
- messages?: string[];
- error?: Error;
-}
+export type EqlResponseError =
+ | {
+ code:
+ | EQL_ERROR_CODES.INVALID_SYNTAX
+ | EQL_ERROR_CODES.INVALID_EQL
+ | EQL_ERROR_CODES.MISSING_DATA_SOURCE;
+ messages: string[];
+ }
+ | {
+ code: EQL_ERROR_CODES.FAILED_REQUEST;
+ error: Error;
+ };
export interface ValidateEqlResponse {
valid: boolean;
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/save_with_errors_confirmation/index.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_confirm_validation_errors_modal/confirm_validation_errors_modal.tsx
similarity index 59%
rename from x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/save_with_errors_confirmation/index.tsx
rename to x-pack/plugins/security_solution/public/common/hooks/use_confirm_validation_errors_modal/confirm_validation_errors_modal.tsx
index 3f14945bedadc..52ef0465e39aa 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/save_with_errors_confirmation/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_confirm_validation_errors_modal/confirm_validation_errors_modal.tsx
@@ -5,41 +5,39 @@
* 2.0.
*/
-import React from 'react';
-
+import React, { memo } from 'react';
import { EuiConfirmModal, EuiSpacer, EuiText } from '@elastic/eui';
-
import * as i18n from './translations';
-interface SaveWithErrorsModalProps {
+interface ConfirmValidationErrorsModalProps {
errors: string[];
onCancel: () => void;
onConfirm: () => void;
}
-const SaveWithErrorsModalComponent = ({
+export const ConfirmValidationErrorsModal = memo(function ConfirmValidationErrorsModal({
errors,
onCancel,
onConfirm,
-}: SaveWithErrorsModalProps) => {
+}: ConfirmValidationErrorsModalProps): JSX.Element {
return (
<>
- {i18n.SAVE_WITH_ERRORS_MODAL_MESSAGE(errors.length)}
+ {i18n.SAVE_WITH_ERRORS_MESSAGE(errors.length)}
- {errors.map((validationError, idx) => {
+ {errors.map((error) => {
return (
- -
- {validationError}
+
-
+ {error}
);
})}
@@ -47,7 +45,4 @@ const SaveWithErrorsModalComponent = ({
>
);
-};
-
-export const SaveWithErrorsModal = React.memo(SaveWithErrorsModalComponent);
-SaveWithErrorsModal.displayName = 'SaveWithErrorsModal';
+});
diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_confirm_validation_errors_modal/index.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_confirm_validation_errors_modal/index.tsx
new file mode 100644
index 0000000000000..505422ad807ae
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_confirm_validation_errors_modal/index.tsx
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export * from './use_confirm_validation_errors_modal';
diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_confirm_validation_errors_modal/translations.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_confirm_validation_errors_modal/translations.tsx
new file mode 100644
index 0000000000000..94d29b7d8fc43
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_confirm_validation_errors_modal/translations.tsx
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const SAVE_WITH_ERRORS_MODAL_TITLE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.upgradeRules.saveWithErrorsConfirmationModal.title',
+ {
+ defaultMessage: 'There are validation errors',
+ }
+);
+
+export const CANCEL = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.upgradeRules.saveWithErrorsConfirmationModal.cancel',
+ {
+ defaultMessage: 'Cancel',
+ }
+);
+
+export const CONFIRM = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.upgradeRules.saveWithErrorsConfirmationModal.confirm',
+ {
+ defaultMessage: 'Confirm',
+ }
+);
+
+export const SAVE_WITH_ERRORS_MESSAGE = (errorsCount: number) =>
+ i18n.translate('xpack.securitySolution.detectionEngine.createRule.saveWithErrorsModalMessage', {
+ defaultMessage:
+ 'There are {errorsCount} validation {errorsCount, plural, one {error} other {errors}} which can lead to failed rule executions, save anyway?',
+ values: { errorsCount },
+ });
diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_confirm_validation_errors_modal/use_confirm_validation_errors_modal.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_confirm_validation_errors_modal/use_confirm_validation_errors_modal.tsx
new file mode 100644
index 0000000000000..5dfdbfb969865
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_confirm_validation_errors_modal/use_confirm_validation_errors_modal.tsx
@@ -0,0 +1,56 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { ReactNode } from 'react';
+import React, { useCallback, useState, useMemo } from 'react';
+import { useBoolean } from '@kbn/react-hooks';
+import { useAsyncConfirmation } from '../../../detection_engine/rule_management_ui/components/rules_table/rules_table/use_async_confirmation';
+import { ConfirmValidationErrorsModal } from './confirm_validation_errors_modal';
+
+interface UseFieldConfirmValidationErrorsModalResult {
+ modal: ReactNode;
+ confirmValidationErrors: (errorMessages: string[]) => Promise;
+}
+
+export function useConfirmValidationErrorsModal(): UseFieldConfirmValidationErrorsModalResult {
+ const [visible, { on: showModal, off: hideModal }] = useBoolean(false);
+ const [initModal, confirm, cancel] = useAsyncConfirmation({
+ onInit: showModal,
+ onFinish: hideModal,
+ });
+ const [errorsToConfirm, setErrorsToConfirm] = useState([]);
+
+ const confirmValidationErrors = useCallback(
+ (errorMessages: string[]) => {
+ if (errorMessages.length === 0) {
+ return Promise.resolve(true);
+ }
+
+ setErrorsToConfirm(errorMessages);
+
+ return initModal();
+ },
+ [initModal, setErrorsToConfirm]
+ );
+
+ const modal = useMemo(
+ () =>
+ visible ? (
+
+ ) : null,
+ [visible, errorsToConfirm, confirm, cancel]
+ );
+
+ return {
+ modal,
+ confirmValidationErrors,
+ };
+}
diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_form_with_warn/extract_validation_results.ts b/x-pack/plugins/security_solution/public/common/hooks/use_form_with_warn/extract_validation_results.ts
new file mode 100644
index 0000000000000..1c9a67dd2e084
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_form_with_warn/extract_validation_results.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { FieldHook, ValidationError } from '../../../shared_imports';
+
+interface ExtractValidationResultsResult {
+ errors: ValidationError[];
+ warnings: ValidationError[];
+}
+
+export function extractValidationResults(
+ formFields: Readonly,
+ warningValidationCodes: Readonly
+): ExtractValidationResultsResult {
+ const warningValidationCodesSet = new Set(warningValidationCodes);
+ const errors: ValidationError[] = [];
+ const warnings: ValidationError[] = [];
+
+ for (const field of formFields) {
+ for (const error of field.errors) {
+ const path = error.path ?? field.path;
+
+ if (!error.code || !warningValidationCodesSet.has(error.code)) {
+ errors.push({ ...error, path });
+ } else {
+ warnings.push({ ...error, path });
+ }
+ }
+ }
+
+ return {
+ errors,
+ warnings,
+ };
+}
diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_form_with_warn/form_hook_with_warn.ts b/x-pack/plugins/security_solution/public/common/hooks/use_form_with_warn/form_hook_with_warn.ts
new file mode 100644
index 0000000000000..d1451f9de9148
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_form_with_warn/form_hook_with_warn.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { FormHook, FormData } from '../../../shared_imports';
+
+export interface FormHookWithWarn
+ extends FormHook {
+ getWarnings(): string[];
+}
diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_form_with_warn/index.ts b/x-pack/plugins/security_solution/public/common/hooks/use_form_with_warn/index.ts
new file mode 100644
index 0000000000000..5d6d54ed44462
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_form_with_warn/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export type * from './form_hook_with_warn';
+export * from './use_form_with_warn';
diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_form_with_warn/use_form_with_warn.ts b/x-pack/plugins/security_solution/public/common/hooks/use_form_with_warn/use_form_with_warn.ts
new file mode 100644
index 0000000000000..9d3a208264f56
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_form_with_warn/use_form_with_warn.ts
@@ -0,0 +1,125 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { isEmpty } from 'lodash';
+import type { FormHook, ValidationError } from '../../../shared_imports';
+import { useForm, type FormConfig, type FormData } from '../../../shared_imports';
+import type { FormHookWithWarn } from './form_hook_with_warn';
+import { extractValidationResults } from './extract_validation_results';
+
+interface SubmitHandlerWithWarnExtras {
+ errors: ValidationError[];
+ warnings: ValidationError[];
+}
+
+export type FormWithWarnSubmitHandler = (
+ formData: T,
+ isValid: boolean,
+ extras: SubmitHandlerWithWarnExtras
+) => Promise;
+
+interface FormWithWarnConfig
+ extends Omit, 'onSubmit'> {
+ onSubmit?: FormWithWarnSubmitHandler;
+ options: FormConfig['options'] & {
+ warningValidationCodes: Readonly;
+ };
+}
+
+interface UseFormWithWarnReturn {
+ form: FormHookWithWarn;
+}
+
+export function useFormWithWarn(
+ formConfig: FormWithWarnConfig
+): UseFormWithWarnReturn {
+ const {
+ onSubmit,
+ options: { warningValidationCodes },
+ } = formConfig;
+ const { form } = useForm(formConfig as FormConfig);
+ const { validate: originalValidate, getFormData, getFields } = form;
+
+ const errorsRef = useRef([]);
+ const warningsRef = useRef([]);
+ const [isSubmitted, setIsSubmitted] = useState(false);
+ const [isSubmitting, setSubmitting] = useState(false);
+ const [isValid, setIsValid] = useState();
+ const isMounted = useRef(false);
+
+ const validate: FormHook['validate'] = useCallback(async () => {
+ await originalValidate();
+
+ const validationResult = extractValidationResults(
+ Object.values(getFields()),
+ warningValidationCodes
+ );
+
+ errorsRef.current = validationResult.errors;
+ warningsRef.current = validationResult.warnings;
+
+ const isFormValid = isEmpty(errorsRef.current);
+
+ setIsValid(isFormValid);
+
+ return isFormValid;
+ }, [originalValidate, getFields, warningValidationCodes, errorsRef, warningsRef]);
+
+ const submit: FormHook['submit'] = useCallback(
+ async (e) => {
+ if (e) {
+ e.preventDefault();
+ }
+
+ setIsSubmitted(true);
+ setSubmitting(true);
+
+ const isFormValid = await validate();
+ const formData = isFormValid ? getFormData() : ({} as T);
+
+ if (onSubmit) {
+ await onSubmit(formData, isFormValid, {
+ errors: errorsRef.current,
+ warnings: warningsRef.current,
+ });
+ }
+
+ if (isMounted.current) {
+ setSubmitting(false);
+ }
+
+ return { data: formData, isValid: isFormValid };
+ },
+ [validate, getFormData, onSubmit, errorsRef, warningsRef]
+ );
+
+ // Track form's mounted state
+ useEffect(() => {
+ isMounted.current = true;
+
+ return () => {
+ isMounted.current = false;
+ };
+ }, []);
+
+ return useMemo(
+ () => ({
+ form: {
+ ...form,
+ isValid,
+ isSubmitted,
+ isSubmitting,
+ validate,
+ submit,
+ getErrors: () => errorsRef.current.map((x) => x.message),
+ getWarnings: () => warningsRef.current.map((x) => x.message),
+ },
+ }),
+ [form, validate, submit, isSubmitted, isSubmitting, isValid, errorsRef, warningsRef]
+ );
+}
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/eql_query_edit/eql_query_bar.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/eql_query_edit/eql_query_bar.tsx
index c7ef28f5ed909..6111d2e1a2d3d 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/eql_query_edit/eql_query_bar.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/eql_query_edit/eql_query_bar.tsx
@@ -6,7 +6,7 @@
*/
import type { FC, ChangeEvent } from 'react';
-import React, { useCallback, useEffect, useState, useRef } from 'react';
+import React, { useCallback, useEffect, useRef, useMemo } from 'react';
import { Subscription } from 'rxjs';
import styled from 'styled-components';
import deepEqual from 'fast-deep-equal';
@@ -20,9 +20,9 @@ import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import type { EqlOptions } from '../../../../../common/search_strategy';
import type { FieldValueQueryBar } from '../../../rule_creation_ui/components/query_bar_field';
import { useKibana } from '../../../../common/lib/kibana';
+import { EQL_ERROR_CODES } from '../../../../common/hooks/eql/api';
import type { EqlQueryBarFooterProps } from './footer';
import { EqlQueryBarFooter } from './footer';
-import { getValidationResults } from './validators';
import * as i18n from './translations';
const TextArea = styled(EuiTextArea)`
@@ -81,12 +81,10 @@ export const EqlQueryBar: FC = ({
onValidatingChange,
}) => {
const { addError } = useAppToasts();
- const [errorMessages, setErrorMessages] = useState([]);
- const { isValidating, value: fieldValue, setValue: setFieldValue } = field;
- const { isValid, message, messages, error } = getValidationResults(field);
-
const { uiSettings } = useKibana().services;
const filterManager = useRef(new FilterManager(uiSettings));
+ const { isValidating, value: fieldValue, setValue: setFieldValue, isValid, errors } = field;
+ const errorMessages = useMemo(() => errors.map((x) => x.message), [errors]);
// Bubbles up field validity to parent.
// Using something like form `getErrors` does
@@ -98,14 +96,12 @@ export const EqlQueryBar: FC = ({
}, [isValid, onValidityChange]);
useEffect(() => {
- setErrorMessages(messages ?? []);
- }, [messages]);
+ const requestError = errors.find((x) => x.code === EQL_ERROR_CODES.FAILED_REQUEST);
- useEffect(() => {
- if (error) {
- addError(error, { title: i18n.EQL_VALIDATION_REQUEST_ERROR });
+ if (requestError) {
+ addError(requestError.message, { title: i18n.EQL_VALIDATION_REQUEST_ERROR });
}
- }, [error, addError]);
+ }, [errors, addError]);
useEffect(() => {
if (onValidatingChange) {
@@ -152,7 +148,6 @@ export const EqlQueryBar: FC = ({
if (onValidatingChange) {
onValidatingChange(true);
}
- setErrorMessages([]);
setFieldValue({
filters: fieldValue.filters,
query: {
@@ -182,7 +177,7 @@ export const EqlQueryBar: FC = ({
label={field.label}
labelAppend={field.labelAppend}
helpText={field.helpText}
- error={message}
+ error={errorMessages[0]}
isInvalid={!isValid && !isValidating}
fullWidth
data-test-subj={dataTestSubj}
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/eql_query_edit/eql_query_edit.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/eql_query_edit/eql_query_edit.tsx
index 75d3412705fde..d4bb6f54a61e6 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/eql_query_edit/eql_query_edit.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/eql_query_edit/eql_query_edit.tsx
@@ -13,7 +13,7 @@ import { UseMultiFields } from '../../../../shared_imports';
import type { EqlFieldsComboBoxOptions, EqlOptions } from '../../../../../common/search_strategy';
import type { FieldValueQueryBar } from '../../../rule_creation_ui/components/query_bar_field';
import { queryRequiredValidatorFactory } from '../../../rule_creation_ui/validators/query_required_validator_factory';
-import { eqlQueryValidatorFactory } from './eql_query_validator_factory';
+import { eqlQueryValidatorFactory } from './validators/eql_query_validator_factory';
import { EqlQueryBar } from './eql_query_bar';
import * as i18n from './translations';
@@ -28,8 +28,6 @@ interface EqlQueryEditProps {
required?: boolean;
loading?: boolean;
disabled?: boolean;
- // This is a temporal solution for Prebuilt Customization workflow
- skipEqlValidation?: boolean;
onValidityChange?: (arg: boolean) => void;
}
@@ -43,7 +41,6 @@ export function EqlQueryEdit({
required,
loading,
disabled,
- skipEqlValidation,
onValidityChange,
}: EqlQueryEditProps): JSX.Element {
const componentProps = useMemo(
@@ -73,43 +70,28 @@ export function EqlQueryEdit({
},
]
: []),
- ...(!skipEqlValidation
- ? [
- {
- validator: debounceAsync(
- (data: ValidationFuncArg) => {
- const { formData } = data;
- const eqlOptions =
- eqlOptionsPath && formData[eqlOptionsPath] ? formData[eqlOptionsPath] : {};
+ {
+ validator: debounceAsync((data: ValidationFuncArg) => {
+ const { formData } = data;
+ const eqlOptions =
+ eqlOptionsPath && formData[eqlOptionsPath] ? formData[eqlOptionsPath] : {};
- return eqlQueryValidatorFactory(
- dataView.id
- ? {
- dataViewId: dataView.id,
- eqlOptions,
- }
- : {
- indexPatterns: dataView.title.split(','),
- eqlOptions,
- }
- )(data);
- },
- 300
- ),
- },
- ]
- : []),
+ return eqlQueryValidatorFactory(
+ dataView.id
+ ? {
+ dataViewId: dataView.id,
+ eqlOptions,
+ }
+ : {
+ indexPatterns: dataView.title.split(','),
+ eqlOptions,
+ }
+ )(data);
+ }, 300),
+ },
],
}),
- [
- skipEqlValidation,
- eqlOptionsPath,
- required,
- dataView.id,
- dataView.title,
- path,
- fieldsToValidateOnChange,
- ]
+ [eqlOptionsPath, required, dataView.id, dataView.title, path, fieldsToValidateOnChange]
);
return (
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/eql_query_edit/validators.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/eql_query_edit/validators.ts
deleted file mode 100644
index 676a780d9daf5..0000000000000
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/eql_query_edit/validators.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import type { FieldHook } from '../../../../shared_imports';
-import { EQL_ERROR_CODES } from '../../../../common/hooks/eql/api';
-
-export const getValidationResults = (
- field: FieldHook
-): { isValid: boolean; message: string; messages?: string[]; error?: Error } => {
- const hasErrors = field.errors.length > 0;
- const isValid = !field.isChangingValue && !hasErrors;
-
- if (hasErrors) {
- const [error] = field.errors;
- const message = error.message;
-
- if (error.code === EQL_ERROR_CODES.FAILED_REQUEST) {
- return { isValid, message, error: error.error };
- } else {
- return { isValid, message, messages: error.messages };
- }
- } else {
- return { isValid, message: '' };
- }
-};
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/eql_query_edit/eql_query_validator_factory.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/eql_query_edit/validators/eql_query_validator_factory.ts
similarity index 78%
rename from x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/eql_query_edit/eql_query_validator_factory.ts
rename to x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/eql_query_edit/validators/eql_query_validator_factory.ts
index 284d0670dfbc3..4de9e713f7f02 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/eql_query_edit/eql_query_validator_factory.ts
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/eql_query_edit/validators/eql_query_validator_factory.ts
@@ -6,13 +6,13 @@
*/
import { isEmpty } from 'lodash';
-import type { FormData, ValidationError, ValidationFunc } from '../../../../shared_imports';
-import { KibanaServices } from '../../../../common/lib/kibana';
-import type { FieldValueQueryBar } from '../../../rule_creation_ui/components/query_bar_field';
-import type { EqlOptions } from '../../../../../common/search_strategy';
-import type { EqlResponseError } from '../../../../common/hooks/eql/api';
-import { EQL_ERROR_CODES, validateEql } from '../../../../common/hooks/eql/api';
-import { EQL_VALIDATION_REQUEST_ERROR } from './translations';
+import type { FormData, ValidationError, ValidationFunc } from '../../../../../shared_imports';
+import { KibanaServices } from '../../../../../common/lib/kibana';
+import type { FieldValueQueryBar } from '../../../../rule_creation_ui/components/query_bar_field';
+import type { EqlOptions } from '../../../../../../common/search_strategy';
+import type { EqlResponseError } from '../../../../../common/hooks/eql/api';
+import { EQL_ERROR_CODES, validateEql } from '../../../../../common/hooks/eql/api';
+import { EQL_VALIDATION_REQUEST_ERROR } from '../translations';
type EqlQueryValidatorFactoryParams =
| {
@@ -71,7 +71,7 @@ export function eqlQueryValidatorFactory({
function transformEqlResponseErrorToValidationError(
responseError: EqlResponseError
): ValidationError {
- if (responseError.error) {
+ if (responseError.code === EQL_ERROR_CODES.FAILED_REQUEST) {
return {
code: EQL_ERROR_CODES.FAILED_REQUEST,
message: EQL_VALIDATION_REQUEST_ERROR,
@@ -81,8 +81,7 @@ function transformEqlResponseErrorToValidationError(
return {
code: responseError.code,
- message: '',
- messages: responseError.messages,
+ message: responseError.messages.join(', '),
};
}
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/esql_query_edit/esql_query_edit.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/esql_query_edit/esql_query_edit.tsx
index 695a3d121c9a6..9c7d9cc5784ba 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/esql_query_edit/esql_query_edit.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/esql_query_edit/esql_query_edit.tsx
@@ -25,7 +25,6 @@ interface EsqlQueryEditProps {
required?: boolean;
loading?: boolean;
disabled?: boolean;
- skipIdColumnCheck?: boolean;
onValidityChange?: (arg: boolean) => void;
}
@@ -36,7 +35,6 @@ export const EsqlQueryEdit = memo(function EsqlQueryEdit({
required = false,
loading = false,
disabled = false,
- skipIdColumnCheck,
onValidityChange,
}: EsqlQueryEditProps): JSX.Element {
const queryClient = useQueryClient();
@@ -67,14 +65,11 @@ export const EsqlQueryEdit = memo(function EsqlQueryEdit({
]
: []),
{
- validator: debounceAsync(
- esqlQueryValidatorFactory({ queryClient, skipIdColumnCheck }),
- 300
- ),
+ validator: debounceAsync(esqlQueryValidatorFactory({ queryClient }), 300),
},
],
}),
- [required, path, fieldsToValidateOnChange, queryClient, skipIdColumnCheck]
+ [required, path, fieldsToValidateOnChange, queryClient]
);
return (
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/esql_query_edit/validators/esql_query_validator_factory.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/esql_query_edit/validators/esql_query_validator_factory.ts
index 90cdaff14cc9b..267e000d21dc5 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/esql_query_edit/validators/esql_query_validator_factory.ts
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/esql_query_edit/validators/esql_query_validator_factory.ts
@@ -19,15 +19,10 @@ import * as i18n from './translations';
interface EsqlQueryValidatorFactoryParams {
queryClient: QueryClient;
- /**
- * This is a temporal fix to unlock prebuilt rule customization workflow
- */
- skipIdColumnCheck?: boolean;
}
export function esqlQueryValidatorFactory({
queryClient,
- skipIdColumnCheck,
}: EsqlQueryValidatorFactoryParams): ValidationFunc {
return async (...args) => {
const [{ value }] = args;
@@ -53,10 +48,6 @@ export function esqlQueryValidatorFactory({
};
}
- if (skipIdColumnCheck) {
- return;
- }
-
const columns = await fetchEsqlQueryColumns({
esqlQuery,
queryClient,
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/constants/non_blocking_error_codes.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/constants/non_blocking_error_codes.ts
new file mode 100644
index 0000000000000..b2b0e4f87f0fb
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/constants/non_blocking_error_codes.ts
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EQL_ERROR_CODES } from '../../../common/hooks/eql/api';
+import { ESQL_ERROR_CODES } from '../components/esql_query_edit';
+
+// const ESQL_FIELD_NAME = i18n.translate(
+// 'xpack.securitySolution.detectionEngine.createRule.nonBlockingErrorCodes.esqlFieldName',
+// {
+// defaultMessage: 'ES|QL Query',
+// }
+// );
+
+// const EQL_FIELD_NAME = i18n.translate(
+// 'xpack.securitySolution.detectionEngine.createRule.nonBlockingErrorCodes.eqlFieldName',
+// {
+// defaultMessage: 'EQL Query',
+// }
+// );
+
+export const NON_BLOCKING_ERROR_CODES = [
+ ESQL_ERROR_CODES.INVALID_ESQL,
+ EQL_ERROR_CODES.FAILED_REQUEST,
+ EQL_ERROR_CODES.INVALID_EQL,
+ EQL_ERROR_CODES.MISSING_DATA_SOURCE,
+] as const;
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/save_with_errors_confirmation/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/save_with_errors_confirmation/translations.ts
deleted file mode 100644
index e470b06c7e829..0000000000000
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/save_with_errors_confirmation/translations.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { i18n } from '@kbn/i18n';
-
-export const SAVE_WITH_ERRORS_MODAL_TITLE = i18n.translate(
- 'xpack.securitySolution.detectionEngine.createRule.saveWithErrorsModalTitle',
- {
- defaultMessage: 'This rule has validation errors',
- }
-);
-
-export const SAVE_WITH_ERRORS_CANCEL_BUTTON = i18n.translate(
- 'xpack.securitySolution.detectionEngine.createRule.saveWithErrorsCancelButton',
- {
- defaultMessage: 'Cancel',
- }
-);
-
-export const SAVE_WITH_ERRORS_CONFIRM_BUTTON = i18n.translate(
- 'xpack.securitySolution.detectionEngine.createRule.saveWithErrorsConfirmButton',
- {
- defaultMessage: 'Confirm',
- }
-);
-
-export const SAVE_WITH_ERRORS_MODAL_MESSAGE = (errorsCount: number) =>
- i18n.translate('xpack.securitySolution.detectionEngine.createRule.saveWithErrorsModalMessage', {
- defaultMessage:
- 'This rule has {errorsCount} validation {errorsCount, plural, one {error} other {errors}} which can lead to failed rule executions, save anyway?',
- values: { errorsCount },
- });
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/form.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/form.test.ts
deleted file mode 100644
index 3210ac84b159a..0000000000000
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/form.test.ts
+++ /dev/null
@@ -1,302 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-import { renderHook } from '@testing-library/react-hooks';
-
-import type { FormData, FormHook, ValidationError } from '../../../shared_imports';
-import { EQL_ERROR_CODES } from '../../../common/hooks/eql/api';
-import type {
- AboutStepRule,
- ActionsStepRule,
- DefineStepRule,
- ScheduleStepRule,
-} from '../../../detections/pages/detection_engine/rules/types';
-import { ALERT_SUPPRESSION_FIELDS_FIELD_NAME } from '../../rule_creation/components/alert_suppression_edit';
-import { ESQL_ERROR_CODES } from '../../rule_creation/components/esql_query_edit';
-import { useRuleFormsErrors } from './form';
-
-const getFormWithErrorsMock = (fields: {
- [key: string]: { errors: Array> };
-}) => {
- return {
- getFields: () => fields,
- } as unknown as FormHook;
-};
-
-describe('useRuleFormsErrors', () => {
- describe('EQL query validation errors', () => {
- it('should return blocking error in case of syntax validation error', async () => {
- const { result } = renderHook(() => useRuleFormsErrors());
-
- const defineStepForm = getFormWithErrorsMock({
- queryBar: {
- errors: [
- {
- code: EQL_ERROR_CODES.INVALID_SYNTAX,
- message: '',
- messages: ["line 1:5: missing 'where' at 'demo'"],
- },
- ],
- },
- });
-
- const { getRuleFormsErrors } = result.current;
- const { blockingErrors, nonBlockingErrors } = getRuleFormsErrors({ defineStepForm });
-
- expect(blockingErrors).toEqual(["line 1:5: missing 'where' at 'demo'"]);
- expect(nonBlockingErrors).toEqual([]);
- });
-
- it('should return non-blocking error in case of missing data source validation error', async () => {
- const { result } = renderHook(() => useRuleFormsErrors());
-
- const defineStepForm = getFormWithErrorsMock({
- queryBar: {
- errors: [
- {
- code: EQL_ERROR_CODES.MISSING_DATA_SOURCE,
- message: '',
- messages: [
- 'index_not_found_exception Found 1 problem line -1:-1: Unknown index [*,-*]',
- ],
- },
- ],
- },
- });
-
- const { getRuleFormsErrors } = result.current;
- const { blockingErrors, nonBlockingErrors } = getRuleFormsErrors({ defineStepForm });
-
- expect(blockingErrors).toEqual([]);
- expect(nonBlockingErrors).toEqual([
- 'Query bar: index_not_found_exception Found 1 problem line -1:-1: Unknown index [*,-*]',
- ]);
- });
-
- it('should return non-blocking error in case of missing data field validation error', async () => {
- const { result } = renderHook(() => useRuleFormsErrors());
-
- const defineStepForm = getFormWithErrorsMock({
- queryBar: {
- errors: [
- {
- code: EQL_ERROR_CODES.INVALID_EQL,
- message: '',
- messages: [
- 'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]',
- ],
- },
- ],
- },
- });
-
- const { getRuleFormsErrors } = result.current;
- const { blockingErrors, nonBlockingErrors } = getRuleFormsErrors({ defineStepForm });
-
- expect(blockingErrors).toEqual([]);
- expect(nonBlockingErrors).toEqual([
- 'Query bar: Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]',
- ]);
- });
-
- it('should return non-blocking error in case of failed request error', async () => {
- const { result } = renderHook(() => useRuleFormsErrors());
-
- const defineStepForm = getFormWithErrorsMock({
- queryBar: {
- errors: [
- {
- code: EQL_ERROR_CODES.FAILED_REQUEST,
- message: 'An error occurred while validating your EQL query',
- error: new Error('Some internal error'),
- },
- ],
- },
- });
-
- const { getRuleFormsErrors } = result.current;
- const { blockingErrors, nonBlockingErrors } = getRuleFormsErrors({ defineStepForm });
-
- expect(blockingErrors).toEqual([]);
- expect(nonBlockingErrors).toEqual([
- 'Query bar: An error occurred while validating your EQL query',
- ]);
- });
-
- it('should return blocking and non-blocking errors', async () => {
- const { result } = renderHook(() => useRuleFormsErrors());
-
- const defineStepForm = getFormWithErrorsMock({
- queryBar: {
- errors: [
- {
- code: EQL_ERROR_CODES.MISSING_DATA_SOURCE,
- message: '',
- messages: ['Missing data source'],
- },
- ],
- },
- });
- const aboutStepForm = getFormWithErrorsMock({
- name: {
- errors: [
- {
- message: 'Required field',
- },
- ],
- },
- });
-
- const { getRuleFormsErrors } = result.current;
- const { blockingErrors, nonBlockingErrors } = getRuleFormsErrors({
- defineStepForm,
- aboutStepForm,
- });
-
- expect(blockingErrors).toEqual(['Required field']);
- expect(nonBlockingErrors).toEqual(['Query bar: Missing data source']);
- });
- });
-
- describe('ES|QL query validation errors', () => {
- it('should return blocking error in case of syntax validation error', async () => {
- const { result } = renderHook(() => useRuleFormsErrors());
-
- const validationError = {
- code: ESQL_ERROR_CODES.INVALID_SYNTAX,
- message: 'Broken ES|QL syntax',
- };
- const defineStepForm = getFormWithErrorsMock({
- queryBar: {
- errors: [validationError],
- },
- });
-
- const { getRuleFormsErrors } = result.current;
- const { blockingErrors, nonBlockingErrors } = getRuleFormsErrors({ defineStepForm });
-
- expect(blockingErrors).toEqual(['Broken ES|QL syntax']);
- expect(nonBlockingErrors).toEqual([]);
- });
-
- it('should return blocking error in case of missed ES|QL metadata validation error', async () => {
- const { result } = renderHook(() => useRuleFormsErrors());
-
- const validationError = {
- code: ESQL_ERROR_CODES.ERR_MISSING_ID_FIELD_FROM_RESULT,
- message: 'Metadata is missing',
- };
- const defineStepForm = getFormWithErrorsMock({
- queryBar: {
- errors: [validationError],
- },
- });
-
- const { getRuleFormsErrors } = result.current;
- const { blockingErrors, nonBlockingErrors } = getRuleFormsErrors({ defineStepForm });
-
- expect(blockingErrors).toEqual(['Metadata is missing']);
- expect(nonBlockingErrors).toEqual([]);
- });
-
- it('should return non-blocking error in case of missing data field validation error', async () => {
- const { result } = renderHook(() => useRuleFormsErrors());
-
- const validationError = {
- code: ESQL_ERROR_CODES.INVALID_ESQL,
- message: 'Unknown column [hello.world]',
- };
- const defineStepForm = getFormWithErrorsMock({
- queryBar: {
- errors: [validationError],
- },
- });
-
- const { getRuleFormsErrors } = result.current;
- const { blockingErrors, nonBlockingErrors } = getRuleFormsErrors({ defineStepForm });
-
- expect(blockingErrors).toEqual([]);
- expect(nonBlockingErrors).toEqual(['Query bar: Unknown column [hello.world]']);
- });
- });
-
- describe('general cases', () => {
- it('should not return blocking and non-blocking errors in case there are none exist', async () => {
- const { result } = renderHook(() => useRuleFormsErrors());
-
- const defineStepForm = getFormWithErrorsMock({ queryBar: { errors: [] } });
- const aboutStepForm = getFormWithErrorsMock({ name: { errors: [] } });
- const scheduleStepForm = getFormWithErrorsMock({
- interval: { errors: [] },
- });
- const actionsStepForm = getFormWithErrorsMock({ actions: { errors: [] } });
-
- const { getRuleFormsErrors } = result.current;
- const { blockingErrors, nonBlockingErrors } = getRuleFormsErrors({
- defineStepForm,
- aboutStepForm,
- scheduleStepForm,
- actionsStepForm,
- });
-
- expect(blockingErrors).toEqual([]);
- expect(nonBlockingErrors).toEqual([]);
- });
-
- it('should not return all errors', async () => {
- const { result } = renderHook(() => useRuleFormsErrors());
-
- const esqlValidationError = {
- code: ESQL_ERROR_CODES.INVALID_ESQL,
- message: 'Missing index [logs*]',
- };
- const groupByValidationError = {
- message: 'Number of grouping fields must be at most 3',
- };
-
- const defineStepForm = getFormWithErrorsMock({
- queryBar: { errors: [esqlValidationError] },
- [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: { errors: [groupByValidationError] },
- });
- const aboutStepForm = getFormWithErrorsMock({
- name: {
- errors: [
- {
- message: 'Required field',
- },
- ],
- },
- });
- const scheduleStepForm = getFormWithErrorsMock({
- interval: { errors: [] },
- });
- const actionsStepForm = getFormWithErrorsMock({
- actions: {
- errors: [
- {
- message: 'Missing webhook connector',
- },
- ],
- },
- });
-
- const { getRuleFormsErrors } = result.current;
- const { blockingErrors, nonBlockingErrors } = getRuleFormsErrors({
- defineStepForm,
- aboutStepForm,
- scheduleStepForm,
- actionsStepForm,
- });
-
- expect(blockingErrors).toEqual([
- 'Number of grouping fields must be at most 3',
- 'Required field',
- 'Missing webhook connector',
- ]);
- expect(nonBlockingErrors).toEqual(['Query bar: Missing index [logs*]']);
- });
- });
-});
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/form.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/form.tsx
index f88d0c1449442..17211f447800f 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/form.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/form.tsx
@@ -5,8 +5,9 @@
* 2.0.
*/
-import { useState, useMemo, useEffect, useCallback } from 'react';
+import { useState, useMemo, useEffect } from 'react';
import type { DataViewBase } from '@kbn/es-query';
+import { useFormWithWarn } from '../../../common/hooks/use_form_with_warn';
import { isThreatMatchRule } from '../../../../common/detection_engine/utils';
import type {
AboutStepRule,
@@ -16,19 +17,17 @@ import type {
} from '../../../detections/pages/detection_engine/rules/types';
import { DataSourceType } from '../../../detections/pages/detection_engine/rules/types';
import { useKibana } from '../../../common/lib/kibana';
-import type { FormHook, ValidationError } from '../../../shared_imports';
-import { useForm, useFormData } from '../../../shared_imports';
+import type { FormHook } from '../../../shared_imports';
+import { useFormData } from '../../../shared_imports';
import { schema as defineRuleSchema } from '../components/step_define_rule/schema';
import {
schema as aboutRuleSchema,
threatMatchAboutSchema,
} from '../components/step_about_rule/schema';
-import { ESQL_ERROR_CODES } from '../../rule_creation/components/esql_query_edit';
import { schema as scheduleRuleSchema } from '../components/step_schedule_rule/schema';
import { getSchema as getActionsRuleSchema } from '../../rule_creation/components/step_rule_actions/get_schema';
import { useFetchIndex } from '../../../common/containers/source';
-import { EQL_ERROR_CODES } from '../../../common/hooks/eql/api';
-import * as i18n from './translations';
+import { NON_BLOCKING_ERROR_CODES } from '../../rule_creation/constants/non_blocking_error_codes';
export interface UseRuleFormsProps {
defineStepDefault: DefineStepRule;
@@ -47,9 +46,9 @@ export const useRuleForms = ({
triggersActionsUi: { actionTypeRegistry },
} = useKibana().services;
// DEFINE STEP FORM
- const { form: defineStepForm } = useForm({
+ const { form: defineStepForm } = useFormWithWarn({
defaultValue: defineStepDefault,
- options: { stripEmptyFields: false },
+ options: { stripEmptyFields: false, warningValidationCodes: NON_BLOCKING_ERROR_CODES },
schema: defineRuleSchema,
});
const [defineStepFormData] = useFormData({
@@ -67,9 +66,9 @@ export const useRuleForms = ({
() => (isThreatMatchRule(defineStepData.ruleType) ? threatMatchAboutSchema : aboutRuleSchema),
[defineStepData.ruleType]
);
- const { form: aboutStepForm } = useForm({
+ const { form: aboutStepForm } = useFormWithWarn({
defaultValue: aboutStepDefault,
- options: { stripEmptyFields: false },
+ options: { stripEmptyFields: false, warningValidationCodes: NON_BLOCKING_ERROR_CODES },
schema: typeDependentAboutRuleSchema,
});
const [aboutStepFormData] = useFormData({
@@ -78,9 +77,9 @@ export const useRuleForms = ({
const aboutStepData = 'name' in aboutStepFormData ? aboutStepFormData : aboutStepDefault;
// SCHEDULE STEP FORM
- const { form: scheduleStepForm } = useForm({
+ const { form: scheduleStepForm } = useFormWithWarn({
defaultValue: scheduleStepDefault,
- options: { stripEmptyFields: false },
+ options: { stripEmptyFields: false, warningValidationCodes: NON_BLOCKING_ERROR_CODES },
schema: scheduleRuleSchema,
});
const [scheduleStepFormData] = useFormData({
@@ -91,9 +90,9 @@ export const useRuleForms = ({
// ACTIONS STEP FORM
const schema = useMemo(() => getActionsRuleSchema({ actionTypeRegistry }), [actionTypeRegistry]);
- const { form: actionsStepForm } = useForm({
+ const { form: actionsStepForm } = useFormWithWarn({
defaultValue: actionsStepDefault,
- options: { stripEmptyFields: false },
+ options: { stripEmptyFields: false, warningValidationCodes: NON_BLOCKING_ERROR_CODES },
schema,
});
const [actionsStepFormData] = useFormData({
@@ -158,81 +157,3 @@ export interface UseRuleFormsErrors {
scheduleStepForm?: FormHook;
actionsStepForm?: FormHook;
}
-
-const getFieldErrorMessages = (fieldError: ValidationError) => {
- if (fieldError.message.length > 0) {
- return [fieldError.message];
- } else if (Array.isArray(fieldError.messages)) {
- // EQL validation can return multiple errors and thus we store them in a custom `messages` field on `ValidationError` object.
- // Here we double check that `messages` is in fact an array and the content is of type `string`, otherwise we stringify it.
- return fieldError.messages.map((message) =>
- typeof message === 'string' ? message : JSON.stringify(message)
- );
- }
- return [];
-};
-
-const NON_BLOCKING_QUERY_BAR_ERROR_CODES = [
- ESQL_ERROR_CODES.INVALID_ESQL,
- EQL_ERROR_CODES.FAILED_REQUEST,
- EQL_ERROR_CODES.INVALID_EQL,
- EQL_ERROR_CODES.MISSING_DATA_SOURCE,
-];
-
-const isNonBlockingQueryBarErrorCode = (errorCode?: string) => {
- return !!NON_BLOCKING_QUERY_BAR_ERROR_CODES.find((code) => code === errorCode);
-};
-
-const NON_BLOCKING_ERROR_CODES = [...NON_BLOCKING_QUERY_BAR_ERROR_CODES];
-
-const isNonBlockingErrorCode = (errorCode?: string) => {
- return !!NON_BLOCKING_ERROR_CODES.find((code) => code === errorCode);
-};
-
-const transformValidationError = ({
- errorCode,
- errorMessage,
-}: {
- errorCode?: string;
- errorMessage: string;
-}) => {
- if (isNonBlockingQueryBarErrorCode(errorCode)) {
- return i18n.QUERY_BAR_VALIDATION_ERROR(errorMessage);
- }
- return errorMessage;
-};
-
-export const useRuleFormsErrors = () => {
- const getRuleFormsErrors = useCallback(
- ({ defineStepForm, aboutStepForm, scheduleStepForm, actionsStepForm }: UseRuleFormsErrors) => {
- const blockingErrors: string[] = [];
- const nonBlockingErrors: string[] = [];
-
- for (const [_, fieldHook] of Object.entries(defineStepForm?.getFields() ?? {})) {
- fieldHook.errors.forEach((fieldError) => {
- const messages = getFieldErrorMessages(fieldError);
- if (isNonBlockingErrorCode(fieldError.code)) {
- nonBlockingErrors.push(
- ...messages.map((message) =>
- transformValidationError({ errorCode: fieldError.code, errorMessage: message })
- )
- );
- } else {
- blockingErrors.push(...messages);
- }
- });
- }
-
- const blockingForms = [aboutStepForm, scheduleStepForm, actionsStepForm];
- blockingForms.forEach((form) => {
- for (const [_, fieldHook] of Object.entries(form?.getFields() ?? {})) {
- blockingErrors.push(...fieldHook.errors.map((fieldError) => fieldError.message));
- }
- });
- return { blockingErrors, nonBlockingErrors };
- },
- []
- );
-
- return { getRuleFormsErrors };
-};
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx
index 6019b696a089c..9a598890e4cae 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx
@@ -56,6 +56,8 @@ import {
} from '../../../../detections/pages/detection_engine/rules/helpers';
import type { DefineStepRule } from '../../../../detections/pages/detection_engine/rules/types';
import { RuleStep } from '../../../../detections/pages/detection_engine/rules/types';
+import { ALERT_SUPPRESSION_FIELDS_FIELD_NAME } from '../../../rule_creation/components/alert_suppression_edit';
+import { useConfirmValidationErrorsModal } from '../../../../common/hooks/use_confirm_validation_errors_modal';
import { formatRule } from './helpers';
import { useEsqlIndex, useEsqlQueryForAboutStep } from '../../hooks';
import * as i18n from './translations';
@@ -78,10 +80,8 @@ import { RulePreview } from '../../components/rule_preview';
import { getIsRulePreviewDisabled } from '../../components/rule_preview/helpers';
import { useStartMlJobs } from '../../../rule_management/logic/use_start_ml_jobs';
import { NextStep } from '../../components/next_step';
-import { useRuleForms, useRuleFormsErrors, useRuleIndexPattern } from '../form';
+import { useRuleForms, useRuleIndexPattern } from '../form';
import { CustomHeaderPageMemo } from '..';
-import { SaveWithErrorsModal } from '../../components/save_with_errors_confirmation';
-import { ALERT_SUPPRESSION_FIELDS_FIELD_NAME } from '../../../rule_creation/components/alert_suppression_edit';
const MyEuiPanel = styled(EuiPanel)<{
zindex?: number;
@@ -178,6 +178,8 @@ const CreateRulePageComponent: React.FC = () => {
actionsStepDefault,
});
+ const { modal: saveWithErrorsModal, confirmValidationErrors } = useConfirmValidationErrorsModal();
+
const isThreatMatchRuleValue = useMemo(
() => isThreatMatchRule(defineStepData.ruleType),
[defineStepData.ruleType]
@@ -203,12 +205,6 @@ const CreateRulePageComponent: React.FC = () => {
const [isQueryBarValid, setIsQueryBarValid] = useState(false);
const [isThreatQueryBarValid, setIsThreatQueryBarValid] = useState(false);
- const [isSaveWithErrorsModalVisible, setIsSaveWithErrorsModalVisible] = useState(false);
- const [enableRuleAfterConfirmation, setEnableRuleAfterConfirmation] = useState(false);
- const [nonBlockingRuleErrors, setNonBlockingRuleErrors] = useState([]);
-
- const { getRuleFormsErrors } = useRuleFormsErrors();
-
const esqlQueryForAboutStep = useEsqlQueryForAboutStep({ defineStepData, activeStep });
const esqlIndex = useEsqlIndex(defineStepData.queryBar.query.query, ruleType);
@@ -315,73 +311,73 @@ const CreateRulePageComponent: React.FC = () => {
switch (step) {
case RuleStep.defineRule: {
const valid = await defineStepForm.validate();
- const { blockingErrors, nonBlockingErrors } = getRuleFormsErrors({ defineStepForm });
- return { valid, blockingErrors, nonBlockingErrors };
+
+ return {
+ valid,
+ warnings: defineStepForm.getWarnings(),
+ };
}
+
case RuleStep.aboutRule: {
const valid = await aboutStepForm.validate();
- const { blockingErrors, nonBlockingErrors } = getRuleFormsErrors({ aboutStepForm });
- return { valid, blockingErrors, nonBlockingErrors };
+
+ return {
+ valid,
+ warnings: aboutStepForm.getWarnings(),
+ };
}
case RuleStep.scheduleRule: {
const valid = await scheduleStepForm.validate();
- const { blockingErrors, nonBlockingErrors } = getRuleFormsErrors({ scheduleStepForm });
- return { valid, blockingErrors, nonBlockingErrors };
+
+ return {
+ valid,
+ warnings: scheduleStepForm.getWarnings(),
+ };
}
case RuleStep.ruleActions: {
const valid = await actionsStepForm.validate();
- const { blockingErrors, nonBlockingErrors } = getRuleFormsErrors({ actionsStepForm });
- return { valid, blockingErrors, nonBlockingErrors };
+
+ return {
+ valid,
+ warnings: actionsStepForm.getWarnings(),
+ };
}
}
},
- [aboutStepForm, actionsStepForm, defineStepForm, getRuleFormsErrors, scheduleStepForm]
- );
-
- const validateEachStep = useCallback(async () => {
- const {
- valid: defineStepFormValid,
- blockingErrors: defineStepBlockingErrors,
- nonBlockingErrors: defineStepNonBlockingErrors,
- } = await validateStep(RuleStep.defineRule);
- const {
- valid: aboutStepFormValid,
- blockingErrors: aboutStepBlockingErrors,
- nonBlockingErrors: aboutStepNonBlockingErrors,
- } = await validateStep(RuleStep.aboutRule);
- const {
- valid: scheduleStepFormValid,
- blockingErrors: scheduleStepBlockingErrors,
- nonBlockingErrors: scheduleStepNonBlockingErrors,
- } = await validateStep(RuleStep.scheduleRule);
- const {
- valid: actionsStepFormValid,
- blockingErrors: actionsStepBlockingErrors,
- nonBlockingErrors: actionsStepNonBlockingErrors,
- } = await validateStep(RuleStep.ruleActions);
+ [aboutStepForm, actionsStepForm, defineStepForm, scheduleStepForm]
+ );
+
+ const validateAllSteps = useCallback(async () => {
+ const { valid: defineStepFormValid, warnings: defineStepWarnings } = await validateStep(
+ RuleStep.defineRule
+ );
+ const { valid: aboutStepFormValid, warnings: aboutStepWarnings } = await validateStep(
+ RuleStep.aboutRule
+ );
+ const { valid: scheduleStepFormValid, warnings: scheduleStepWarnings } = await validateStep(
+ RuleStep.scheduleRule
+ );
+ const { valid: actionsStepFormValid, warnings: actionsStepWarnings } = await validateStep(
+ RuleStep.ruleActions
+ );
const valid =
defineStepFormValid && aboutStepFormValid && scheduleStepFormValid && actionsStepFormValid;
- const blockingErrors = [
- ...defineStepBlockingErrors,
- ...aboutStepBlockingErrors,
- ...scheduleStepBlockingErrors,
- ...actionsStepBlockingErrors,
- ];
- const nonBlockingErrors = [
- ...defineStepNonBlockingErrors,
- ...aboutStepNonBlockingErrors,
- ...scheduleStepNonBlockingErrors,
- ...actionsStepNonBlockingErrors,
+ const warnings = [
+ ...defineStepWarnings,
+ ...aboutStepWarnings,
+ ...scheduleStepWarnings,
+ ...actionsStepWarnings,
];
- return { valid, blockingErrors, nonBlockingErrors };
+ return { valid, warnings };
}, [validateStep]);
const editStep = useCallback(
async (step: RuleStep) => {
- const { valid, blockingErrors } = await validateStep(activeStep);
- if (valid || !blockingErrors.length) {
+ const { valid } = await validateStep(activeStep);
+
+ if (valid) {
goToStep(step);
}
},
@@ -440,34 +436,17 @@ const CreateRulePageComponent: React.FC = () => {
]
);
- const showSaveWithErrorsModal = useCallback(() => setIsSaveWithErrorsModalVisible(true), []);
- const closeSaveWithErrorsModal = useCallback(() => setIsSaveWithErrorsModalVisible(false), []);
- const onConfirmSaveWithErrors = useCallback(async () => {
- closeSaveWithErrorsModal();
- await createRuleFromFormData(enableRuleAfterConfirmation);
- }, [closeSaveWithErrorsModal, createRuleFromFormData, enableRuleAfterConfirmation]);
-
const submitRule = useCallback(
async (enabled: boolean) => {
- const { valid, blockingErrors, nonBlockingErrors } = await validateEachStep();
- if (valid) {
- // There are no validation errors, thus proceed to rule creation
- await createRuleFromFormData(enabled);
- return;
- }
+ const { valid, warnings } = await validateAllSteps();
- if (blockingErrors.length > 0) {
- // There are blocking validation errors, thus do not allow user to create a rule
+ if (!valid || !(await confirmValidationErrors(warnings))) {
return;
}
- if (nonBlockingErrors.length > 0) {
- // There are non-blocking validation errors, thus confirm that user understand that this can cause rule failures
- setEnableRuleAfterConfirmation(enabled);
- setNonBlockingRuleErrors(nonBlockingErrors);
- showSaveWithErrorsModal();
- }
+
+ await createRuleFromFormData(enabled);
},
- [createRuleFromFormData, showSaveWithErrorsModal, validateEachStep]
+ [createRuleFromFormData, validateAllSteps, confirmValidationErrors]
);
const defineRuleButtonType =
@@ -846,13 +825,7 @@ const CreateRulePageComponent: React.FC = () => {
return (
<>
- {isSaveWithErrorsModalVisible && (
-
- )}
+ {saveWithErrorsModal}
{(EuiResizablePanel, EuiResizableButton, { togglePanel }) => {
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx
index dbc753ca33d37..dff8d9e950e3f 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx
@@ -21,6 +21,7 @@ import type { FC } from 'react';
import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
+import { useConfirmValidationErrorsModal } from '../../../../common/hooks/use_confirm_validation_errors_modal';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { isEsqlRule } from '../../../../../common/detection_engine/utils';
import { RulePreview } from '../../components/rule_preview';
@@ -65,10 +66,9 @@ import {
import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction';
import { SINGLE_RULE_ACTIONS } from '../../../../common/lib/apm/user_actions';
import { useGetSavedQuery } from '../../../../detections/pages/detection_engine/rules/use_get_saved_query';
-import { useRuleForms, useRuleFormsErrors, useRuleIndexPattern } from '../form';
+import { useRuleForms, useRuleIndexPattern } from '../form';
import { useEsqlIndex, useEsqlQueryForAboutStep } from '../../hooks';
import { CustomHeaderPageMemo } from '..';
-import { SaveWithErrorsModal } from '../../components/save_with_errors_confirmation';
import { useIsPrebuiltRulesCustomizationEnabled } from '../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled';
import { ALERT_SUPPRESSION_FIELDS_FIELD_NAME } from '../../../rule_creation/components/alert_suppression_edit';
@@ -102,9 +102,6 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
const [isQueryBarValid, setIsQueryBarValid] = useState(false);
const [isThreatQueryBarValid, setIsThreatQueryBarValid] = useState(false);
- const [isSaveWithErrorsModalVisible, setIsSaveWithErrorsModalVisible] = useState(false);
- const [nonBlockingRuleErrors, setNonBlockingRuleErrors] = useState([]);
-
const backOptions = useMemo(
() => ({
path: getRuleDetailsUrl(ruleId ?? ''),
@@ -138,7 +135,7 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
actionsStepDefault: ruleActionsData,
});
- const { getRuleFormsErrors } = useRuleFormsErrors();
+ const { modal: saveWithErrorsModal, confirmValidationErrors } = useConfirmValidationErrorsModal();
const esqlQueryForAboutStep = useEsqlQueryForAboutStep({ defineStepData, activeStep });
@@ -401,16 +398,7 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
updateRule,
]);
- const showSaveWithErrorsModal = useCallback(() => setIsSaveWithErrorsModalVisible(true), []);
- const closeSaveWithErrorsModal = useCallback(() => setIsSaveWithErrorsModalVisible(false), []);
- const onConfirmSaveWithErrors = useCallback(async () => {
- closeSaveWithErrorsModal();
- await saveChanges();
- }, [closeSaveWithErrorsModal, saveChanges]);
-
const onSubmit = useCallback(async () => {
- setNonBlockingRuleErrors([]);
-
const actionsStepFormValid = await actionsStepForm.validate();
if (!isPrebuiltRulesCustomizationEnabled && rule.immutable) {
// Since users cannot edit Define, About and Schedule tabs of the rule, we skip validation of those to avoid
@@ -425,29 +413,33 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
const defineStepFormValid = await defineStepForm.validate();
const aboutStepFormValid = await aboutStepForm.validate();
const scheduleStepFormValid = await scheduleStepForm.validate();
+
if (
- defineStepFormValid &&
- aboutStepFormValid &&
- scheduleStepFormValid &&
- actionsStepFormValid
+ !defineStepFormValid ||
+ !aboutStepFormValid ||
+ !scheduleStepFormValid ||
+ !actionsStepFormValid
) {
- await saveChanges();
return;
}
- const { blockingErrors, nonBlockingErrors } = getRuleFormsErrors({
- defineStepForm,
- aboutStepForm,
- scheduleStepForm,
- actionsStepForm,
- });
- if (blockingErrors.length > 0) {
+ const defineRuleWarnings = defineStepForm.getWarnings();
+ const aboutRuleWarnings = aboutStepForm.getWarnings();
+ const scheduleRuleWarnings = scheduleStepForm.getWarnings();
+ const ruleActionsWarnings = actionsStepForm.getWarnings();
+
+ if (
+ !(await confirmValidationErrors([
+ ...defineRuleWarnings,
+ ...aboutRuleWarnings,
+ ...scheduleRuleWarnings,
+ ...ruleActionsWarnings,
+ ]))
+ ) {
return;
}
- if (nonBlockingErrors.length > 0) {
- setNonBlockingRuleErrors(nonBlockingErrors);
- showSaveWithErrorsModal();
- }
+
+ await saveChanges();
}, [
actionsStepForm,
isPrebuiltRulesCustomizationEnabled,
@@ -455,9 +447,8 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
defineStepForm,
aboutStepForm,
scheduleStepForm,
- getRuleFormsErrors,
+ confirmValidationErrors,
saveChanges,
- showSaveWithErrorsModal,
]);
const onTabClick = useCallback(async (tab: EuiTabbedContentTab) => {
@@ -513,13 +504,7 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
return (
<>
- {isSaveWithErrorsModalVisible && (
-
- )}
+ {saveWithErrorsModal}
{(EuiResizablePanel, EuiResizableButton, { togglePanel }) => {
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/translations.ts
index 77ea9438f66dc..e602b8be712c2 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/translations.ts
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/translations.ts
@@ -13,9 +13,3 @@ export const RULE_PREVIEW_TITLE = i18n.translate(
defaultMessage: 'Rule preview',
}
);
-
-export const QUERY_BAR_VALIDATION_ERROR = (validationError: string) =>
- i18n.translate('xpack.securitySolution.detectionEngine.createRule.validationError', {
- values: { validationError },
- defaultMessage: 'Query bar: {validationError}',
- });
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/eql_query/eql_query_edit_adapter.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/eql_query/eql_query_edit_adapter.tsx
index 787891452f1d7..cea9b9308c0df 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/eql_query/eql_query_edit_adapter.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/eql_query/eql_query_edit_adapter.tsx
@@ -29,7 +29,6 @@ export function EqlQueryEditAdapter({
dataView={dataView ?? DEFAULT_DATA_VIEW_BASE}
loading={isLoading}
disabled={isLoading}
- skipEqlValidation
/>
);
}
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/esql_query/esql_query_edit_adapter.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/esql_query/esql_query_edit_adapter.tsx
index a9375b7316bb3..faf43d5b88b22 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/esql_query/esql_query_edit_adapter.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/esql_query/esql_query_edit_adapter.tsx
@@ -23,7 +23,6 @@ export function EsqlQueryEditAdapter({
dataView={dataView ?? DEFAULT_DATA_VIEW_BASE}
loading={isLoading}
disabled={isLoading}
- skipIdColumnCheck
/>
);
}
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/rule_field_edit_form_wrapper.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/rule_field_edit_form_wrapper.tsx
index 1b45bea28880f..6adc8b83ea5a9 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/rule_field_edit_form_wrapper.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/rule_field_edit_form_wrapper.tsx
@@ -6,8 +6,11 @@
*/
import React, { useCallback, useEffect } from 'react';
+import { capitalize } from 'lodash';
import { EuiButtonEmpty, EuiFlexGroup } from '@elastic/eui';
-import { useForm, Form } from '../../../../../../../shared_imports';
+import type { FormWithWarnSubmitHandler } from '../../../../../../../common/hooks/use_form_with_warn';
+import { useFormWithWarn } from '../../../../../../../common/hooks/use_form_with_warn';
+import { Form } from '../../../../../../../shared_imports';
import type { FormSchema, FormData } from '../../../../../../../shared_imports';
import type {
DiffableAllFields,
@@ -17,6 +20,8 @@ import { useFinalSideContext } from '../../final_side/final_side_context';
import { useDiffableRuleContext } from '../../diffable_rule_context';
import * as i18n from '../../translations';
import type { RuleFieldEditComponentProps } from './rule_field_edit_component_props';
+import { useConfirmValidationErrorsModal } from '../../../../../../../common/hooks/use_confirm_validation_errors_modal';
+import { NON_BLOCKING_ERROR_CODES } from '../../../../../../rule_creation/constants/non_blocking_error_codes';
type RuleFieldEditComponent = React.ComponentType;
@@ -56,9 +61,13 @@ export function RuleFieldEditFormWrapper({
[deserializer, finalDiffableRule]
);
- const handleSubmit = useCallback(
- async (formData: FormData, isValid: boolean) => {
- if (!isValid) {
+ const { modal, confirmValidationErrors } = useConfirmValidationErrorsModal();
+
+ const handleSubmit = useCallback(
+ async (formData: FormData, isValid: boolean, { warnings }) => {
+ const warningMessages = warnings.map((x) => `${capitalize(x.path)}: ${x.message}`);
+
+ if (!isValid || !(await confirmValidationErrors(warningMessages))) {
return;
}
@@ -69,15 +78,24 @@ export function RuleFieldEditFormWrapper({
});
setReadOnlyMode();
},
- [fieldName, finalDiffableRule.rule_id, setReadOnlyMode, setRuleFieldResolvedValue]
+ [
+ confirmValidationErrors,
+ fieldName,
+ finalDiffableRule.rule_id,
+ setReadOnlyMode,
+ setRuleFieldResolvedValue,
+ ]
);
- const { form } = useForm({
+ const { form } = useFormWithWarn({
schema: ruleFieldFormSchema,
defaultValue: getDefaultValue(fieldName, finalDiffableRule),
deserializer: deserialize,
serializer,
onSubmit: handleSubmit,
+ options: {
+ warningValidationCodes: NON_BLOCKING_ERROR_CODES,
+ },
});
// form.isValid has `undefined` value until all fields are dirty.
@@ -96,6 +114,7 @@ export function RuleFieldEditFormWrapper({
{i18n.SAVE_BUTTON_LABEL}
+ {modal}