mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[8.x] [Security Solution] Allow users to save rule query with non critical validation errors (#202544) (#203233)
# Backport This will backport the following commits from `main` to `8.x`: - [[Security Solution] Allow users to save rule query with non critical validation errors (#202544)](https://github.com/elastic/kibana/pull/202544) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Maxim Palenov","email":"maxim.palenov@elastic.co"},"sourceCommit":{"committedDate":"2024-12-06T12:06:39Z","message":"[Security Solution] Allow users to save rule query with non critical validation errors (#202544)\n\n**Addresses:** https://github.com/elastic/kibana/issues/171520\r\n\r\n## Summary\r\n\r\nThis PR adds functionality to allow users save EQL and ES|QL queries in Prebuilt Rule Customization workflow by displaying a confirmation modal with non critical validation errors (a.k.a warnings). It also refactors confirmation modal usage in rule creation/editing forms for better reusability.\r\n\r\n## Screenshots\r\n\r\n<img width=\"1909\" alt=\"Screenshot 2024-12-03 at 14 01 36\" src=\"50b5cf5a
-ea3f-4c22-a443-b5d4056a92c8\r\n\r\n<img width=\"2541\" alt=\"Screenshot 2024-12-03 at 14 06 29\" src=\"https://github.com/user-attachments/assets/dde3fd60-6c69-4f8e-a65a-837b2319e4ac\">\r\n\r\n<img width=\"2552\" alt=\"Screenshot 2024-12-03 at 14 06 51\" src=\"https://github.com/user-attachments/assets/220817a6-991f-4361-88d2-ee3a47a36ad6\">\r\n\r\n<img width=\"2555\" alt=\"Screenshot 2024-12-03 at 14 07 52\" src=\"https://github.com/user-attachments/assets/c46fc49c-9ce1-4472-bdea-f9507aa62ece\">\r\n\r\n<img width=\"2553\" alt=\"Screenshot 2024-12-03 at 14 08 18\" src=\"https://github.com/user-attachments/assets/92388c56-8644-4c54-8727-b9a73b3497d1\">","sha":"b0c7a8ce4f0ea528a7f96246e7f2a46d17f61d3f","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:Detections and Resp","Team: SecuritySolution","Team:Detection Rule Management","Feature:Prebuilt Detection Rules","backport:version","v8.18.0"],"title":"[Security Solution] Allow users to save rule query with non critical validation errors","number":202544,"url":"https://github.com/elastic/kibana/pull/202544","mergeCommit":{"message":"[Security Solution] Allow users to save rule query with non critical validation errors (#202544)\n\n**Addresses:** https://github.com/elastic/kibana/issues/171520\r\n\r\n## Summary\r\n\r\nThis PR adds functionality to allow users save EQL and ES|QL queries in Prebuilt Rule Customization workflow by displaying a confirmation modal with non critical validation errors (a.k.a warnings). It also refactors confirmation modal usage in rule creation/editing forms for better reusability.\r\n\r\n## Screenshots\r\n\r\n<img width=\"1909\" alt=\"Screenshot 2024-12-03 at 14 01 36\" src=\"50b5cf5a
-ea3f-4c22-a443-b5d4056a92c8\r\n\r\n<img width=\"2541\" alt=\"Screenshot 2024-12-03 at 14 06 29\" src=\"https://github.com/user-attachments/assets/dde3fd60-6c69-4f8e-a65a-837b2319e4ac\">\r\n\r\n<img width=\"2552\" alt=\"Screenshot 2024-12-03 at 14 06 51\" src=\"https://github.com/user-attachments/assets/220817a6-991f-4361-88d2-ee3a47a36ad6\">\r\n\r\n<img width=\"2555\" alt=\"Screenshot 2024-12-03 at 14 07 52\" src=\"https://github.com/user-attachments/assets/c46fc49c-9ce1-4472-bdea-f9507aa62ece\">\r\n\r\n<img width=\"2553\" alt=\"Screenshot 2024-12-03 at 14 08 18\" src=\"https://github.com/user-attachments/assets/92388c56-8644-4c54-8727-b9a73b3497d1\">","sha":"b0c7a8ce4f0ea528a7f96246e7f2a46d17f61d3f"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/202544","number":202544,"mergeCommit":{"message":"[Security Solution] Allow users to save rule query with non critical validation errors (#202544)\n\n**Addresses:** https://github.com/elastic/kibana/issues/171520\r\n\r\n## Summary\r\n\r\nThis PR adds functionality to allow users save EQL and ES|QL queries in Prebuilt Rule Customization workflow by displaying a confirmation modal with non critical validation errors (a.k.a warnings). It also refactors confirmation modal usage in rule creation/editing forms for better reusability.\r\n\r\n## Screenshots\r\n\r\n<img width=\"1909\" alt=\"Screenshot 2024-12-03 at 14 01 36\" src=\"50b5cf5a
-ea3f-4c22-a443-b5d4056a92c8\r\n\r\n<img width=\"2541\" alt=\"Screenshot 2024-12-03 at 14 06 29\" src=\"https://github.com/user-attachments/assets/dde3fd60-6c69-4f8e-a65a-837b2319e4ac\">\r\n\r\n<img width=\"2552\" alt=\"Screenshot 2024-12-03 at 14 06 51\" src=\"https://github.com/user-attachments/assets/220817a6-991f-4361-88d2-ee3a47a36ad6\">\r\n\r\n<img width=\"2555\" alt=\"Screenshot 2024-12-03 at 14 07 52\" src=\"https://github.com/user-attachments/assets/c46fc49c-9ce1-4472-bdea-f9507aa62ece\">\r\n\r\n<img width=\"2553\" alt=\"Screenshot 2024-12-03 at 14 08 18\" src=\"https://github.com/user-attachments/assets/92388c56-8644-4c54-8727-b9a73b3497d1\">","sha":"b0c7a8ce4f0ea528a7f96246e7f2a46d17f61d3f"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Maxim Palenov <maxim.palenov@elastic.co>
This commit is contained in:
parent
9cdf4d21a2
commit
ba9ba62dbb
33 changed files with 825 additions and 706 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -1688,6 +1688,7 @@ x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/
|
|||
/x-pack/plugins/security_solution/public/common/components/with_hover_actions @elastic/security-threat-hunting-explore
|
||||
/x-pack/plugins/security_solution/public/common/containers/matrix_histogram @elastic/security-threat-hunting-explore
|
||||
/x-pack/plugins/security_solution/public/common/lib/cell_actions @elastic/security-threat-hunting-explore
|
||||
/x-pack/plugins/security_solution/public/common/hooks/use_form_with_warn @elastic/security-detection-rule-management
|
||||
/x-pack/plugins/security_solution/public/cases @elastic/security-threat-hunting-explore
|
||||
/x-pack/plugins/security_solution/public/explore @elastic/security-threat-hunting-explore
|
||||
/x-pack/plugins/security_solution/public/overview @elastic/security-threat-hunting-explore
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 (
|
||||
<EuiConfirmModal
|
||||
data-test-subj="save-with-errors-confirmation-modal"
|
||||
title={i18n.SAVE_WITH_ERRORS_MODAL_TITLE}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
cancelButtonText={i18n.SAVE_WITH_ERRORS_CANCEL_BUTTON}
|
||||
confirmButtonText={i18n.SAVE_WITH_ERRORS_CONFIRM_BUTTON}
|
||||
cancelButtonText={i18n.CANCEL}
|
||||
confirmButtonText={i18n.CONFIRM}
|
||||
defaultFocusedButton="confirm"
|
||||
>
|
||||
<>
|
||||
{i18n.SAVE_WITH_ERRORS_MODAL_MESSAGE(errors.length)}
|
||||
{i18n.SAVE_WITH_ERRORS_MESSAGE(errors.length)}
|
||||
<EuiSpacer size="s" />
|
||||
<ul>
|
||||
{errors.map((validationError, idx) => {
|
||||
{errors.map((error) => {
|
||||
return (
|
||||
<li key={idx}>
|
||||
<EuiText>{validationError}</EuiText>
|
||||
<li key={error}>
|
||||
<EuiText>{error}</EuiText>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
@ -47,7 +45,4 @@ const SaveWithErrorsModalComponent = ({
|
|||
</>
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
};
|
||||
|
||||
export const SaveWithErrorsModal = React.memo(SaveWithErrorsModalComponent);
|
||||
SaveWithErrorsModal.displayName = 'SaveWithErrorsModal';
|
||||
});
|
|
@ -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';
|
|
@ -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 {errorsCount, plural, one {is} other {are}} {errorsCount} validation {errorsCount, plural, one {error} other {errors}} which can lead to failed rule executions, save anyway?',
|
||||
values: { errorsCount },
|
||||
});
|
|
@ -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<boolean>;
|
||||
}
|
||||
|
||||
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<string[]>([]);
|
||||
|
||||
const confirmValidationErrors = useCallback(
|
||||
(errorMessages: string[]) => {
|
||||
if (errorMessages.length === 0) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
setErrorsToConfirm(errorMessages);
|
||||
|
||||
return initModal();
|
||||
},
|
||||
[initModal, setErrorsToConfirm]
|
||||
);
|
||||
|
||||
const modal = useMemo(
|
||||
() =>
|
||||
visible ? (
|
||||
<ConfirmValidationErrorsModal
|
||||
errors={errorsToConfirm}
|
||||
onConfirm={confirm}
|
||||
onCancel={cancel}
|
||||
/>
|
||||
) : null,
|
||||
[visible, errorsToConfirm, confirm, cancel]
|
||||
);
|
||||
|
||||
return {
|
||||
modal,
|
||||
confirmValidationErrors,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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';
|
||||
import type { ValidationResults } from './validation_results';
|
||||
|
||||
export function extractValidationResults(
|
||||
formFields: Readonly<FieldHook[]>,
|
||||
warningValidationCodes: Readonly<string[]>
|
||||
): ValidationResults {
|
||||
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,
|
||||
};
|
||||
}
|
|
@ -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, ValidationError } from '../../../shared_imports';
|
||||
|
||||
export interface FormHookWithWarnings<T extends FormData = FormData, I extends FormData = T>
|
||||
extends FormHook<T, I> {
|
||||
getValidationWarnings(): ValidationError[];
|
||||
}
|
|
@ -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_warnings';
|
||||
export * from './use_form_with_warnings';
|
|
@ -0,0 +1,241 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { TextField } from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
import type { FieldConfig } from '../../../shared_imports';
|
||||
import { Form, UseField } from '../../../shared_imports';
|
||||
import type { FormWithWarningsSubmitHandler } from './use_form_with_warnings';
|
||||
import { useFormWithWarnings } from './use_form_with_warnings';
|
||||
|
||||
describe('useFormWithWarn', () => {
|
||||
describe('isValid', () => {
|
||||
it('is `undefined` initially', async () => {
|
||||
render(<TestForm warningValidationCodes={['warning']} />);
|
||||
|
||||
expect(screen.getByText('isValid: "undefined"')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('is `true` when input is valid', async () => {
|
||||
render(<TestForm warningValidationCodes={['warning']} />);
|
||||
|
||||
typeText('someValue');
|
||||
await submitForm();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('isValid: true')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('is `true` when input has warnings', async () => {
|
||||
render(<TestForm warningValidationCodes={['warning']} />);
|
||||
|
||||
typeText('warning');
|
||||
await submitForm();
|
||||
|
||||
expect(screen.getByText('isValid: true')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('is `false` when input has error', async () => {
|
||||
render(<TestForm warningValidationCodes={['warning']} />);
|
||||
|
||||
typeText('error');
|
||||
await submitForm();
|
||||
|
||||
expect(screen.getByText('isValid: false')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSubmitting', () => {
|
||||
it('toggles upon form submission', async () => {
|
||||
render(<TestForm warningValidationCodes={['warning']} />);
|
||||
|
||||
expect(screen.getByText('isSubmitting: false')).toBeInTheDocument();
|
||||
|
||||
const finishAct = submitForm();
|
||||
|
||||
expect(screen.getByText('isSubmitting: true')).toBeInTheDocument();
|
||||
|
||||
await finishAct;
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('isSubmitting: false')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSubmitted', () => {
|
||||
it('switched to true after form submission', async () => {
|
||||
render(<TestForm warningValidationCodes={['warning']} />);
|
||||
|
||||
expect(screen.getByText('isSubmitted: false')).toBeInTheDocument();
|
||||
|
||||
await submitForm();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('isSubmitted: true')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('input w/o warnings', () => {
|
||||
it('submits form successfully', async () => {
|
||||
const handleSubmit = jest.fn();
|
||||
|
||||
render(<TestForm warningValidationCodes={['warning']} onSubmit={handleSubmit} />);
|
||||
typeText('someValue');
|
||||
|
||||
await submitForm();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleSubmit).toHaveBeenCalledWith({ testField: 'someValue' }, true, {
|
||||
errors: [],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('w/ warnings', () => {
|
||||
it('submits form successfully', async () => {
|
||||
const handleSubmit = jest.fn();
|
||||
|
||||
render(<TestForm warningValidationCodes={['warning']} onSubmit={handleSubmit} />);
|
||||
typeText('warning');
|
||||
|
||||
await submitForm();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleSubmit).toHaveBeenCalledWith({ testField: 'warning' }, true, {
|
||||
errors: [],
|
||||
warnings: [
|
||||
expect.objectContaining({
|
||||
code: 'warning',
|
||||
message: 'Validation warning',
|
||||
path: 'testField',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('w/ errors', () => {
|
||||
it('passes validation errors to submit handler', async () => {
|
||||
const handleSubmit = jest.fn();
|
||||
|
||||
render(<TestForm warningValidationCodes={['warning']} onSubmit={handleSubmit} />);
|
||||
typeText('error');
|
||||
|
||||
await submitForm();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleSubmit).toHaveBeenCalledWith({}, false, {
|
||||
errors: [
|
||||
expect.objectContaining({
|
||||
code: 'error',
|
||||
message: 'Validation error',
|
||||
path: 'testField',
|
||||
}),
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('w/ errors and warnings', () => {
|
||||
it('passes validation errors and warnings to submit handler', async () => {
|
||||
const handleSubmit = jest.fn();
|
||||
|
||||
render(<TestForm warningValidationCodes={['warning']} onSubmit={handleSubmit} />);
|
||||
typeText('error warning');
|
||||
|
||||
await submitForm();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleSubmit).toHaveBeenCalledWith({}, false, {
|
||||
errors: [
|
||||
expect.objectContaining({
|
||||
code: 'error',
|
||||
message: 'Validation error',
|
||||
path: 'testField',
|
||||
}),
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
interface TestFormProps {
|
||||
onSubmit?: FormWithWarningsSubmitHandler;
|
||||
warningValidationCodes: string[];
|
||||
}
|
||||
|
||||
function TestForm({ onSubmit, warningValidationCodes }: TestFormProps): JSX.Element {
|
||||
const { form } = useFormWithWarnings({
|
||||
onSubmit,
|
||||
options: {
|
||||
warningValidationCodes,
|
||||
},
|
||||
});
|
||||
const textFieldConfig: FieldConfig<string> = {
|
||||
validations: [
|
||||
{
|
||||
validator: (data) => {
|
||||
if (data.value.includes('error')) {
|
||||
return {
|
||||
code: 'error',
|
||||
message: 'Validation error',
|
||||
};
|
||||
}
|
||||
|
||||
if (data.value.includes('warning')) {
|
||||
return {
|
||||
code: 'warning',
|
||||
message: 'Validation warning',
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<div>
|
||||
{'isValid:'} {JSON.stringify(form.isValid ?? 'undefined')}
|
||||
</div>
|
||||
<div>
|
||||
{'isSubmitting:'} {JSON.stringify(form.isSubmitting)}
|
||||
</div>
|
||||
<div>
|
||||
{'isSubmitted:'} {JSON.stringify(form.isSubmitted)}
|
||||
</div>
|
||||
<UseField path="testField" component={TextField} config={textFieldConfig} />
|
||||
<button type="button" onClick={form.submit}>
|
||||
{'Submit'}
|
||||
</button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
function submitForm(): Promise<void> {
|
||||
return act(async () => {
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
});
|
||||
}
|
||||
|
||||
function typeText(value: string): void {
|
||||
act(() => {
|
||||
fireEvent.input(screen.getByRole('textbox'), {
|
||||
target: { value },
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* 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 } from '../../../shared_imports';
|
||||
import { useForm, type FormConfig, type FormData } from '../../../shared_imports';
|
||||
import type { FormHookWithWarnings } from './form_hook_with_warnings';
|
||||
import { extractValidationResults } from './extract_validation_results';
|
||||
import type { ValidationResults } from './validation_results';
|
||||
|
||||
export type FormWithWarningsSubmitHandler<T extends FormData = FormData> = (
|
||||
formData: T,
|
||||
isValid: boolean,
|
||||
validationResults: ValidationResults
|
||||
) => Promise<void>;
|
||||
|
||||
interface FormWithWarningsConfig<T extends FormData = FormData, I extends FormData = T>
|
||||
extends Omit<FormConfig<T, I>, 'onSubmit'> {
|
||||
onSubmit?: FormWithWarningsSubmitHandler<T>;
|
||||
options: FormConfig['options'] & {
|
||||
warningValidationCodes: Readonly<string[]>;
|
||||
};
|
||||
}
|
||||
|
||||
interface UseFormWithWarningsReturn<T extends FormData = FormData, I extends FormData = T> {
|
||||
form: FormHookWithWarnings<T, I>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form lib implements warning functionality via non blocking validators. `validations` allows to
|
||||
* specify validation configuration with validator functions and extra parameters including
|
||||
* `isBlocking`. Validators marked as `isBlocking` will produce non blocking validation errors
|
||||
* a.k.a. warnings.
|
||||
*
|
||||
* The problem with the supported approach is lack of flexibility and necessary API like one for getting
|
||||
* only blocking or non blocking errors. Flexibility requirement comes from complex async validators
|
||||
* producing blocking and non blocking validation errors. There is no way to use `isBlocking` configuration
|
||||
* option to separate errors. Separating such validating functions in two would lead to sending two
|
||||
* HTTP requests and performing another async operations twice.
|
||||
*
|
||||
* On top of just having an ability to mark validation errors as non blocking via `isBlocking: false`
|
||||
* configuration we require a way to return blocking and non blocking errors from a single validation
|
||||
* function. It'd be possible by returning an error with `isBlocking` (or `isWarning`) flag along with
|
||||
* `message` and `code` fields from a validator function. Attempts to reuse `__isBlocking__` internal
|
||||
* field lead to inconsistent behavior.
|
||||
*
|
||||
* `useFormWithWarnings` implements warnings (non blocking errors) on top of `FormHook` using validation
|
||||
* error codes as a flexible way to determine whether an error is a blocking error or it's a warning.
|
||||
* It provides little interface extension to simplify errors and warnings consumption
|
||||
*
|
||||
* In some cases business logic requires implementing functionality to allow users perform an action
|
||||
* despite non-critical validation errors a.k.a. warnings. Usually it's also required to inform users
|
||||
* about warnings they got before proceeding for example via a modal.
|
||||
*
|
||||
* Since `FormHook` returned by `useForm` lacks of such functionality `useFormWithWarnings` is here to
|
||||
* provide warnings functionality. It could be used and passed as `FormHook` when warnings functionality
|
||||
* isn't required making absolutely transparent.
|
||||
*
|
||||
* **Important:** Validators use short circuiting by default. It means that any failed validation in
|
||||
* `validations` configuration array will prevent the rest validators from running. When used with warnings
|
||||
* it may lead to bugs when validator checks first for warnings. You have to make sure a value is validated
|
||||
* for errors first and then for warnings.
|
||||
*
|
||||
* There is a ticket to move this functionality to Form lib https://github.com/elastic/kibana/issues/203097.
|
||||
*/
|
||||
export function useFormWithWarnings<T extends FormData = FormData, I extends FormData = T>(
|
||||
formConfig: FormWithWarningsConfig<T, I>
|
||||
): UseFormWithWarningsReturn<T, I> {
|
||||
const {
|
||||
onSubmit,
|
||||
options: { warningValidationCodes },
|
||||
} = formConfig;
|
||||
const { form } = useForm(formConfig as FormConfig<T, I>);
|
||||
const { validate: originalValidate, getFormData, getFields } = form;
|
||||
|
||||
const validationResultsRef = useRef<ValidationResults>({
|
||||
errors: [],
|
||||
warnings: [],
|
||||
});
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const [isSubmitting, setSubmitting] = useState(false);
|
||||
const [isValid, setIsValid] = useState<boolean>();
|
||||
const isMounted = useRef(false);
|
||||
|
||||
const validate: FormHook<T, I>['validate'] = useCallback(async () => {
|
||||
await originalValidate();
|
||||
|
||||
validationResultsRef.current = extractValidationResults(
|
||||
Object.values(getFields()),
|
||||
warningValidationCodes
|
||||
);
|
||||
|
||||
const isFormValid = isEmpty(validationResultsRef.current.errors);
|
||||
|
||||
setIsValid(isFormValid);
|
||||
|
||||
return isFormValid;
|
||||
}, [originalValidate, getFields, warningValidationCodes, validationResultsRef]);
|
||||
|
||||
const submit: FormHook<T, I>['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, validationResultsRef.current);
|
||||
}
|
||||
|
||||
if (isMounted.current) {
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
return { data: formData, isValid: isFormValid };
|
||||
},
|
||||
[validate, getFormData, onSubmit, validationResultsRef]
|
||||
);
|
||||
|
||||
// Track form's mounted state
|
||||
useEffect(() => {
|
||||
isMounted.current = true;
|
||||
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
form: {
|
||||
...form,
|
||||
isValid,
|
||||
isSubmitted,
|
||||
isSubmitting,
|
||||
validate,
|
||||
submit,
|
||||
getErrors: () => validationResultsRef.current.errors.map((x) => x.message),
|
||||
getValidationWarnings: () => validationResultsRef.current.warnings,
|
||||
},
|
||||
}),
|
||||
[form, validate, submit, isSubmitted, isSubmitting, isValid, validationResultsRef]
|
||||
);
|
||||
}
|
|
@ -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 { ValidationError } from '../../../shared_imports';
|
||||
|
||||
export interface ValidationResults {
|
||||
errors: ValidationError[];
|
||||
warnings: ValidationError[];
|
||||
}
|
|
@ -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<EqlQueryBarProps> = ({
|
|||
onValidatingChange,
|
||||
}) => {
|
||||
const { addError } = useAppToasts();
|
||||
const [errorMessages, setErrorMessages] = useState<string[]>([]);
|
||||
const { isValidating, value: fieldValue, setValue: setFieldValue } = field;
|
||||
const { isValid, message, messages, error } = getValidationResults(field);
|
||||
|
||||
const { uiSettings } = useKibana().services;
|
||||
const filterManager = useRef<FilterManager>(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<EqlQueryBarProps> = ({
|
|||
}, [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<EqlQueryBarProps> = ({
|
|||
if (onValidatingChange) {
|
||||
onValidatingChange(true);
|
||||
}
|
||||
setErrorMessages([]);
|
||||
setFieldValue({
|
||||
filters: fieldValue.filters,
|
||||
query: {
|
||||
|
@ -182,7 +177,7 @@ export const EqlQueryBar: FC<EqlQueryBarProps> = ({
|
|||
label={field.label}
|
||||
labelAppend={field.labelAppend}
|
||||
helpText={field.helpText}
|
||||
error={message}
|
||||
error={errorMessages[0]}
|
||||
isInvalid={!isValid && !isValidating}
|
||||
fullWidth
|
||||
data-test-subj={dataTestSubj}
|
||||
|
|
|
@ -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,29 @@ export function EqlQueryEdit({
|
|||
},
|
||||
]
|
||||
: []),
|
||||
...(!skipEqlValidation
|
||||
? [
|
||||
{
|
||||
validator: debounceAsync(
|
||||
(data: ValidationFuncArg<FormData, FieldValueQueryBar>) => {
|
||||
const { formData } = data;
|
||||
const eqlOptions =
|
||||
eqlOptionsPath && formData[eqlOptionsPath] ? formData[eqlOptionsPath] : {};
|
||||
{
|
||||
validator: debounceAsync((data: ValidationFuncArg<FormData, FieldValueQueryBar>) => {
|
||||
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),
|
||||
isAsync: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
[
|
||||
skipEqlValidation,
|
||||
eqlOptionsPath,
|
||||
required,
|
||||
dataView.id,
|
||||
dataView.title,
|
||||
path,
|
||||
fieldsToValidateOnChange,
|
||||
]
|
||||
[eqlOptionsPath, required, dataView.id, dataView.title, path, fieldsToValidateOnChange]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -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 = <T = unknown>(
|
||||
field: FieldHook<T>
|
||||
): { 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: '' };
|
||||
}
|
||||
};
|
|
@ -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<EQL_ERROR_CODES> {
|
||||
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(', '),
|
||||
};
|
||||
}
|
||||
|
|
@ -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,12 @@ export const EsqlQueryEdit = memo(function EsqlQueryEdit({
|
|||
]
|
||||
: []),
|
||||
{
|
||||
validator: debounceAsync(
|
||||
esqlQueryValidatorFactory({ queryClient, skipIdColumnCheck }),
|
||||
300
|
||||
),
|
||||
validator: debounceAsync(esqlQueryValidatorFactory({ queryClient }), 300),
|
||||
isAsync: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
[required, path, fieldsToValidateOnChange, queryClient, skipIdColumnCheck]
|
||||
[required, path, fieldsToValidateOnChange, queryClient]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -16,15 +16,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<FormData, string, FieldValueQueryBar> {
|
||||
return async (...args) => {
|
||||
const [{ value }] = args;
|
||||
|
@ -50,10 +45,6 @@ export function esqlQueryValidatorFactory({
|
|||
};
|
||||
}
|
||||
|
||||
if (skipIdColumnCheck) {
|
||||
return;
|
||||
}
|
||||
|
||||
const columns = await fetchEsqlQueryColumns({
|
||||
esqlQuery,
|
||||
queryClient,
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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';
|
||||
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 VALIDATION_WARNING_CODES = [
|
||||
ESQL_ERROR_CODES.INVALID_ESQL,
|
||||
EQL_ERROR_CODES.FAILED_REQUEST,
|
||||
EQL_ERROR_CODES.INVALID_EQL,
|
||||
EQL_ERROR_CODES.MISSING_DATA_SOURCE,
|
||||
] as const;
|
||||
|
||||
export const VALIDATION_WARNING_CODE_FIELD_NAME_MAP: Readonly<Record<string, string>> = {
|
||||
[ESQL_ERROR_CODES.INVALID_ESQL]: ESQL_FIELD_NAME,
|
||||
[EQL_ERROR_CODES.FAILED_REQUEST]: EQL_FIELD_NAME,
|
||||
[EQL_ERROR_CODES.INVALID_EQL]: EQL_FIELD_NAME,
|
||||
[EQL_ERROR_CODES.MISSING_DATA_SOURCE]: EQL_FIELD_NAME,
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { capitalize } from 'lodash';
|
||||
import type { ValidationError } from '../../../shared_imports';
|
||||
|
||||
export function extractValidationMessages(
|
||||
validationErrors: ValidationError[],
|
||||
errorCodeFieldNameMap: Readonly<Record<string, string>>
|
||||
): string[] {
|
||||
return validationErrors.map(
|
||||
(x) => `${errorCodeFieldNameMap[x.code ?? ''] ?? capitalize(x.path)}: ${x.message}`
|
||||
);
|
||||
}
|
|
@ -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 },
|
||||
});
|
|
@ -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 = <T extends FormData = FormData>(fields: {
|
||||
[key: string]: { errors: Array<ValidationError<EQL_ERROR_CODES | ESQL_ERROR_CODES>> };
|
||||
}) => {
|
||||
return {
|
||||
getFields: () => fields,
|
||||
} as unknown as FormHook<T, T>;
|
||||
};
|
||||
|
||||
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<DefineStepRule>({
|
||||
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<DefineStepRule>({
|
||||
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<DefineStepRule>({
|
||||
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<DefineStepRule>({
|
||||
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<DefineStepRule>({
|
||||
queryBar: {
|
||||
errors: [
|
||||
{
|
||||
code: EQL_ERROR_CODES.MISSING_DATA_SOURCE,
|
||||
message: '',
|
||||
messages: ['Missing data source'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const aboutStepForm = getFormWithErrorsMock<AboutStepRule>({
|
||||
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<DefineStepRule>({
|
||||
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<DefineStepRule>({
|
||||
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<DefineStepRule>({
|
||||
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<DefineStepRule>({ queryBar: { errors: [] } });
|
||||
const aboutStepForm = getFormWithErrorsMock<AboutStepRule>({ name: { errors: [] } });
|
||||
const scheduleStepForm = getFormWithErrorsMock<ScheduleStepRule>({
|
||||
interval: { errors: [] },
|
||||
});
|
||||
const actionsStepForm = getFormWithErrorsMock<ActionsStepRule>({ 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<DefineStepRule>({
|
||||
queryBar: { errors: [esqlValidationError] },
|
||||
[ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: { errors: [groupByValidationError] },
|
||||
});
|
||||
const aboutStepForm = getFormWithErrorsMock<AboutStepRule>({
|
||||
name: {
|
||||
errors: [
|
||||
{
|
||||
message: 'Required field',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const scheduleStepForm = getFormWithErrorsMock<ScheduleStepRule>({
|
||||
interval: { errors: [] },
|
||||
});
|
||||
const actionsStepForm = getFormWithErrorsMock<ActionsStepRule>({
|
||||
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*]']);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 { useFormWithWarnings } from '../../../common/hooks/use_form_with_warnings';
|
||||
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 { VALIDATION_WARNING_CODES } from '../../rule_creation/constants/validation_warning_codes';
|
||||
|
||||
export interface UseRuleFormsProps {
|
||||
defineStepDefault: DefineStepRule;
|
||||
|
@ -47,9 +46,9 @@ export const useRuleForms = ({
|
|||
triggersActionsUi: { actionTypeRegistry },
|
||||
} = useKibana().services;
|
||||
// DEFINE STEP FORM
|
||||
const { form: defineStepForm } = useForm<DefineStepRule>({
|
||||
const { form: defineStepForm } = useFormWithWarnings<DefineStepRule>({
|
||||
defaultValue: defineStepDefault,
|
||||
options: { stripEmptyFields: false },
|
||||
options: { stripEmptyFields: false, warningValidationCodes: VALIDATION_WARNING_CODES },
|
||||
schema: defineRuleSchema,
|
||||
});
|
||||
const [defineStepFormData] = useFormData<DefineStepRule | {}>({
|
||||
|
@ -67,9 +66,9 @@ export const useRuleForms = ({
|
|||
() => (isThreatMatchRule(defineStepData.ruleType) ? threatMatchAboutSchema : aboutRuleSchema),
|
||||
[defineStepData.ruleType]
|
||||
);
|
||||
const { form: aboutStepForm } = useForm<AboutStepRule>({
|
||||
const { form: aboutStepForm } = useFormWithWarnings<AboutStepRule>({
|
||||
defaultValue: aboutStepDefault,
|
||||
options: { stripEmptyFields: false },
|
||||
options: { stripEmptyFields: false, warningValidationCodes: VALIDATION_WARNING_CODES },
|
||||
schema: typeDependentAboutRuleSchema,
|
||||
});
|
||||
const [aboutStepFormData] = useFormData<AboutStepRule | {}>({
|
||||
|
@ -78,9 +77,9 @@ export const useRuleForms = ({
|
|||
const aboutStepData = 'name' in aboutStepFormData ? aboutStepFormData : aboutStepDefault;
|
||||
|
||||
// SCHEDULE STEP FORM
|
||||
const { form: scheduleStepForm } = useForm<ScheduleStepRule>({
|
||||
const { form: scheduleStepForm } = useFormWithWarnings<ScheduleStepRule>({
|
||||
defaultValue: scheduleStepDefault,
|
||||
options: { stripEmptyFields: false },
|
||||
options: { stripEmptyFields: false, warningValidationCodes: VALIDATION_WARNING_CODES },
|
||||
schema: scheduleRuleSchema,
|
||||
});
|
||||
const [scheduleStepFormData] = useFormData<ScheduleStepRule | {}>({
|
||||
|
@ -91,9 +90,9 @@ export const useRuleForms = ({
|
|||
|
||||
// ACTIONS STEP FORM
|
||||
const schema = useMemo(() => getActionsRuleSchema({ actionTypeRegistry }), [actionTypeRegistry]);
|
||||
const { form: actionsStepForm } = useForm<ActionsStepRule>({
|
||||
const { form: actionsStepForm } = useFormWithWarnings<ActionsStepRule>({
|
||||
defaultValue: actionsStepDefault,
|
||||
options: { stripEmptyFields: false },
|
||||
options: { stripEmptyFields: false, warningValidationCodes: VALIDATION_WARNING_CODES },
|
||||
schema,
|
||||
});
|
||||
const [actionsStepFormData] = useFormData<ActionsStepRule | {}>({
|
||||
|
@ -158,81 +157,3 @@ export interface UseRuleFormsErrors {
|
|||
scheduleStepForm?: FormHook<ScheduleStepRule, ScheduleStepRule>;
|
||||
actionsStepForm?: FormHook<ActionsStepRule, ActionsStepRule>;
|
||||
}
|
||||
|
||||
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 };
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
@ -77,11 +79,11 @@ import { useKibana, useUiSetting$ } from '../../../../common/lib/kibana';
|
|||
import { RulePreview } from '../../components/rule_preview';
|
||||
import { getIsRulePreviewDisabled } from '../../components/rule_preview/helpers';
|
||||
import { useStartMlJobs } from '../../../rule_management/logic/use_start_ml_jobs';
|
||||
import { VALIDATION_WARNING_CODE_FIELD_NAME_MAP } from '../../../rule_creation/constants/validation_warning_codes';
|
||||
import { extractValidationMessages } from '../../../rule_creation/logic/extract_validation_messages';
|
||||
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 +180,9 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
actionsStepDefault,
|
||||
});
|
||||
|
||||
const { modal: confirmSavingWithWarningModal, confirmValidationErrors } =
|
||||
useConfirmValidationErrorsModal();
|
||||
|
||||
const isThreatMatchRuleValue = useMemo(
|
||||
() => isThreatMatchRule(defineStepData.ruleType),
|
||||
[defineStepData.ruleType]
|
||||
|
@ -203,12 +208,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<string[]>([]);
|
||||
|
||||
const { getRuleFormsErrors } = useRuleFormsErrors();
|
||||
|
||||
const esqlQueryForAboutStep = useEsqlQueryForAboutStep({ defineStepData, activeStep });
|
||||
|
||||
const esqlIndex = useEsqlIndex(defineStepData.queryBar.query.query, ruleType);
|
||||
|
@ -315,73 +314,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.getValidationWarnings(),
|
||||
};
|
||||
}
|
||||
|
||||
case RuleStep.aboutRule: {
|
||||
const valid = await aboutStepForm.validate();
|
||||
const { blockingErrors, nonBlockingErrors } = getRuleFormsErrors({ aboutStepForm });
|
||||
return { valid, blockingErrors, nonBlockingErrors };
|
||||
|
||||
return {
|
||||
valid,
|
||||
warnings: aboutStepForm.getValidationWarnings(),
|
||||
};
|
||||
}
|
||||
case RuleStep.scheduleRule: {
|
||||
const valid = await scheduleStepForm.validate();
|
||||
const { blockingErrors, nonBlockingErrors } = getRuleFormsErrors({ scheduleStepForm });
|
||||
return { valid, blockingErrors, nonBlockingErrors };
|
||||
|
||||
return {
|
||||
valid,
|
||||
warnings: scheduleStepForm.getValidationWarnings(),
|
||||
};
|
||||
}
|
||||
case RuleStep.ruleActions: {
|
||||
const valid = await actionsStepForm.validate();
|
||||
const { blockingErrors, nonBlockingErrors } = getRuleFormsErrors({ actionsStepForm });
|
||||
return { valid, blockingErrors, nonBlockingErrors };
|
||||
|
||||
return {
|
||||
valid,
|
||||
warnings: actionsStepForm.getValidationWarnings(),
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
[aboutStepForm, actionsStepForm, defineStepForm, getRuleFormsErrors, scheduleStepForm]
|
||||
[aboutStepForm, actionsStepForm, defineStepForm, 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);
|
||||
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 +439,21 @@ 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);
|
||||
const { valid, warnings } = await validateAllSteps();
|
||||
const warningMessages = extractValidationMessages(
|
||||
warnings,
|
||||
VALIDATION_WARNING_CODE_FIELD_NAME_MAP
|
||||
);
|
||||
|
||||
if (!valid || !(await confirmValidationErrors(warningMessages))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (blockingErrors.length > 0) {
|
||||
// There are blocking validation errors, thus do not allow user to create a rule
|
||||
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 +832,7 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
{isSaveWithErrorsModalVisible && (
|
||||
<SaveWithErrorsModal
|
||||
errors={nonBlockingRuleErrors}
|
||||
onCancel={closeSaveWithErrorsModal}
|
||||
onConfirm={onConfirmSaveWithErrors}
|
||||
/>
|
||||
)}
|
||||
{confirmSavingWithWarningModal}
|
||||
<SecuritySolutionPageWrapper>
|
||||
<EuiResizableContainer>
|
||||
{(EuiResizablePanel, EuiResizableButton, { togglePanel }) => {
|
||||
|
|
|
@ -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';
|
||||
|
@ -67,10 +68,11 @@ 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 { extractValidationMessages } from '../../../rule_creation/logic/extract_validation_messages';
|
||||
import { VALIDATION_WARNING_CODE_FIELD_NAME_MAP } from '../../../rule_creation/constants/validation_warning_codes';
|
||||
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';
|
||||
|
||||
|
@ -104,9 +106,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<string[]>([]);
|
||||
|
||||
const backOptions = useMemo(
|
||||
() => ({
|
||||
path: getRuleDetailsUrl(ruleId ?? ''),
|
||||
|
@ -140,7 +139,8 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
|
|||
actionsStepDefault: ruleActionsData,
|
||||
});
|
||||
|
||||
const { getRuleFormsErrors } = useRuleFormsErrors();
|
||||
const { modal: confirmSavingWithWarningModal, confirmValidationErrors } =
|
||||
useConfirmValidationErrorsModal();
|
||||
|
||||
const esqlQueryForAboutStep = useEsqlQueryForAboutStep({ defineStepData, activeStep });
|
||||
|
||||
|
@ -411,16 +411,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
|
||||
|
@ -435,29 +426,36 @@ 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.getValidationWarnings();
|
||||
const aboutRuleWarnings = aboutStepForm.getValidationWarnings();
|
||||
const scheduleRuleWarnings = scheduleStepForm.getValidationWarnings();
|
||||
const ruleActionsWarnings = actionsStepForm.getValidationWarnings();
|
||||
|
||||
const warnings = extractValidationMessages(
|
||||
[
|
||||
...defineRuleWarnings,
|
||||
...aboutRuleWarnings,
|
||||
...scheduleRuleWarnings,
|
||||
...ruleActionsWarnings,
|
||||
],
|
||||
VALIDATION_WARNING_CODE_FIELD_NAME_MAP
|
||||
);
|
||||
|
||||
if (!(await confirmValidationErrors(warnings))) {
|
||||
return;
|
||||
}
|
||||
if (nonBlockingErrors.length > 0) {
|
||||
setNonBlockingRuleErrors(nonBlockingErrors);
|
||||
showSaveWithErrorsModal();
|
||||
}
|
||||
|
||||
await saveChanges();
|
||||
}, [
|
||||
actionsStepForm,
|
||||
isPrebuiltRulesCustomizationEnabled,
|
||||
|
@ -465,9 +463,8 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
|
|||
defineStepForm,
|
||||
aboutStepForm,
|
||||
scheduleStepForm,
|
||||
getRuleFormsErrors,
|
||||
confirmValidationErrors,
|
||||
saveChanges,
|
||||
showSaveWithErrorsModal,
|
||||
]);
|
||||
|
||||
const onTabClick = useCallback(async (tab: EuiTabbedContentTab) => {
|
||||
|
@ -523,13 +520,7 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
{isSaveWithErrorsModalVisible && (
|
||||
<SaveWithErrorsModal
|
||||
errors={nonBlockingRuleErrors}
|
||||
onCancel={closeSaveWithErrorsModal}
|
||||
onConfirm={onConfirmSaveWithErrors}
|
||||
/>
|
||||
)}
|
||||
{confirmSavingWithWarningModal}
|
||||
<SecuritySolutionPageWrapper>
|
||||
<EuiResizableContainer>
|
||||
{(EuiResizablePanel, EuiResizableButton, { togglePanel }) => {
|
||||
|
|
|
@ -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}',
|
||||
});
|
||||
|
|
|
@ -29,7 +29,6 @@ export function EqlQueryEditAdapter({
|
|||
dataView={dataView ?? DEFAULT_DATA_VIEW_BASE}
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
skipEqlValidation
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ export function EsqlQueryEditAdapter({
|
|||
dataView={dataView ?? DEFAULT_DATA_VIEW_BASE}
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
skipIdColumnCheck
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,10 @@
|
|||
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { EuiButtonEmpty, EuiFlexGroup } from '@elastic/eui';
|
||||
import { useForm, Form } from '../../../../../../../shared_imports';
|
||||
import { extractValidationMessages } from '../../../../../../rule_creation/logic/extract_validation_messages';
|
||||
import type { FormWithWarningsSubmitHandler } from '../../../../../../../common/hooks/use_form_with_warnings';
|
||||
import { useFormWithWarnings } from '../../../../../../../common/hooks/use_form_with_warnings';
|
||||
import { Form } from '../../../../../../../shared_imports';
|
||||
import type { FormSchema, FormData } from '../../../../../../../shared_imports';
|
||||
import type {
|
||||
DiffableAllFields,
|
||||
|
@ -17,6 +20,11 @@ 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 {
|
||||
VALIDATION_WARNING_CODE_FIELD_NAME_MAP,
|
||||
VALIDATION_WARNING_CODES,
|
||||
} from '../../../../../../rule_creation/constants/validation_warning_codes';
|
||||
|
||||
type RuleFieldEditComponent = React.ComponentType<RuleFieldEditComponentProps>;
|
||||
|
||||
|
@ -56,9 +64,16 @@ export function RuleFieldEditFormWrapper({
|
|||
[deserializer, finalDiffableRule]
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (formData: FormData, isValid: boolean) => {
|
||||
if (!isValid) {
|
||||
const { modal, confirmValidationErrors } = useConfirmValidationErrorsModal();
|
||||
|
||||
const handleSubmit = useCallback<FormWithWarningsSubmitHandler>(
|
||||
async (formData: FormData, isValid: boolean, { warnings }) => {
|
||||
const warningMessages = extractValidationMessages(
|
||||
warnings,
|
||||
VALIDATION_WARNING_CODE_FIELD_NAME_MAP
|
||||
);
|
||||
|
||||
if (!isValid || !(await confirmValidationErrors(warningMessages))) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -69,15 +84,24 @@ export function RuleFieldEditFormWrapper({
|
|||
});
|
||||
setReadOnlyMode();
|
||||
},
|
||||
[fieldName, finalDiffableRule.rule_id, setReadOnlyMode, setRuleFieldResolvedValue]
|
||||
[
|
||||
confirmValidationErrors,
|
||||
fieldName,
|
||||
finalDiffableRule.rule_id,
|
||||
setReadOnlyMode,
|
||||
setRuleFieldResolvedValue,
|
||||
]
|
||||
);
|
||||
|
||||
const { form } = useForm({
|
||||
const { form } = useFormWithWarnings({
|
||||
schema: ruleFieldFormSchema,
|
||||
defaultValue: getDefaultValue(fieldName, finalDiffableRule),
|
||||
deserializer: deserialize,
|
||||
serializer,
|
||||
onSubmit: handleSubmit,
|
||||
options: {
|
||||
warningValidationCodes: VALIDATION_WARNING_CODES,
|
||||
},
|
||||
});
|
||||
|
||||
// form.isValid has `undefined` value until all fields are dirty.
|
||||
|
@ -96,6 +120,7 @@ export function RuleFieldEditFormWrapper({
|
|||
{i18n.SAVE_BUTTON_LABEL}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexGroup>
|
||||
{modal}
|
||||
<Form form={form}>
|
||||
<FieldComponent
|
||||
finalDiffableRule={finalDiffableRule}
|
||||
|
|
|
@ -10,6 +10,7 @@ export type {
|
|||
FieldValidateResponse,
|
||||
FormData,
|
||||
FormHook,
|
||||
FormConfig,
|
||||
FormSchema,
|
||||
FormSubmitHandler,
|
||||
ValidationError,
|
||||
|
|
|
@ -37876,10 +37876,7 @@
|
|||
"xpack.securitySolution.detectionEngine.createRule.savedIdLabel": "Nom de requête enregistré",
|
||||
"xpack.securitySolution.detectionEngine.createRule.savedQueryFiltersLabel": "Filtres de requête enregistrés",
|
||||
"xpack.securitySolution.detectionEngine.createRule.savedQueryLabel": "Requête enregistrée",
|
||||
"xpack.securitySolution.detectionEngine.createRule.saveWithErrorsCancelButton": "Annuler",
|
||||
"xpack.securitySolution.detectionEngine.createRule.saveWithErrorsConfirmButton": "Confirmer",
|
||||
"xpack.securitySolution.detectionEngine.createRule.saveWithErrorsModalMessage": "Cette règle contient {errorsCount} {errorsCount, plural, one {erreur} other {erreurs}} de validation ce qui peut conduire à l'échec de l'exécution des règles. Enregistrer malgré tout ?",
|
||||
"xpack.securitySolution.detectionEngine.createRule.saveWithErrorsModalTitle": "Cette règle comporte des erreurs de validation",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.authorFieldEmptyError": "L'auteur doit être indiqué",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.dataViewSelector": "Vue de données",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.dataViewTitleSelector": "Vue de données du modèle d'indexation",
|
||||
|
@ -38030,7 +38027,6 @@
|
|||
"xpack.securitySolution.detectionEngine.createRule.threatMatchRuleTypeDescription": "Correspondance d'indicateur",
|
||||
"xpack.securitySolution.detectionEngine.createRule.threatQueryLabel": "Requête d'index d'indicateur",
|
||||
"xpack.securitySolution.detectionEngine.createRule.thresholdRuleTypeDescription": "Seuil",
|
||||
"xpack.securitySolution.detectionEngine.createRule.validationError": "Barre de requêtes : {validationError}",
|
||||
"xpack.securitySolution.detectionEngine.details.stepAboutRule.aboutText": "À propos",
|
||||
"xpack.securitySolution.detectionEngine.details.stepAboutRule.controlLegend": "Affichage",
|
||||
"xpack.securitySolution.detectionEngine.details.stepAboutRule.detailsLabel": "Détails",
|
||||
|
|
|
@ -37732,10 +37732,7 @@
|
|||
"xpack.securitySolution.detectionEngine.createRule.savedIdLabel": "保存されたクエリ名",
|
||||
"xpack.securitySolution.detectionEngine.createRule.savedQueryFiltersLabel": "保存されたクエリフィルター",
|
||||
"xpack.securitySolution.detectionEngine.createRule.savedQueryLabel": "保存されたクエリ",
|
||||
"xpack.securitySolution.detectionEngine.createRule.saveWithErrorsCancelButton": "キャンセル",
|
||||
"xpack.securitySolution.detectionEngine.createRule.saveWithErrorsConfirmButton": "確認",
|
||||
"xpack.securitySolution.detectionEngine.createRule.saveWithErrorsModalMessage": "このルールには{errorsCount}件の検証{errorsCount, plural, other {エラー}}があり、ルール実行が失敗する可能性があります。保存しますか?",
|
||||
"xpack.securitySolution.detectionEngine.createRule.saveWithErrorsModalTitle": "このルールには検証エラーがあります",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.authorFieldEmptyError": "作成者は空にする必要があります",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.dataViewSelector": "データビュー",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.dataViewTitleSelector": "データビューインデックスパターン",
|
||||
|
@ -37886,7 +37883,6 @@
|
|||
"xpack.securitySolution.detectionEngine.createRule.threatMatchRuleTypeDescription": "インジケーター一致",
|
||||
"xpack.securitySolution.detectionEngine.createRule.threatQueryLabel": "インジケーターインデックスクエリ",
|
||||
"xpack.securitySolution.detectionEngine.createRule.thresholdRuleTypeDescription": "しきい値",
|
||||
"xpack.securitySolution.detectionEngine.createRule.validationError": "クエリバー:{validationError}",
|
||||
"xpack.securitySolution.detectionEngine.details.stepAboutRule.aboutText": "概要",
|
||||
"xpack.securitySolution.detectionEngine.details.stepAboutRule.controlLegend": "表示",
|
||||
"xpack.securitySolution.detectionEngine.details.stepAboutRule.detailsLabel": "詳細",
|
||||
|
|
|
@ -37823,10 +37823,7 @@
|
|||
"xpack.securitySolution.detectionEngine.createRule.savedIdLabel": "已保存查询名称",
|
||||
"xpack.securitySolution.detectionEngine.createRule.savedQueryFiltersLabel": "已保存查询筛选",
|
||||
"xpack.securitySolution.detectionEngine.createRule.savedQueryLabel": "已保存查询",
|
||||
"xpack.securitySolution.detectionEngine.createRule.saveWithErrorsCancelButton": "取消",
|
||||
"xpack.securitySolution.detectionEngine.createRule.saveWithErrorsConfirmButton": "确认",
|
||||
"xpack.securitySolution.detectionEngine.createRule.saveWithErrorsModalMessage": "此规则具有 {errorsCount} 个验证{errorsCount, plural, other {错误}},这可能导致无法执行规则,确定要保存?",
|
||||
"xpack.securitySolution.detectionEngine.createRule.saveWithErrorsModalTitle": "此规则包含验证错误",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.authorFieldEmptyError": "作者不得为空",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.dataViewSelector": "数据视图",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.dataViewTitleSelector": "数据视图索引模式",
|
||||
|
@ -37977,7 +37974,6 @@
|
|||
"xpack.securitySolution.detectionEngine.createRule.threatMatchRuleTypeDescription": "指标匹配",
|
||||
"xpack.securitySolution.detectionEngine.createRule.threatQueryLabel": "指标索引查询",
|
||||
"xpack.securitySolution.detectionEngine.createRule.thresholdRuleTypeDescription": "阈值",
|
||||
"xpack.securitySolution.detectionEngine.createRule.validationError": "查询栏:{validationError}",
|
||||
"xpack.securitySolution.detectionEngine.details.stepAboutRule.aboutText": "关于",
|
||||
"xpack.securitySolution.detectionEngine.details.stepAboutRule.controlLegend": "正在查看",
|
||||
"xpack.securitySolution.detectionEngine.details.stepAboutRule.detailsLabel": "详情",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue