Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Security Solution] Allow users to save rule query with non critical validation errors #202544

Merged
merged 12 commits into from
Dec 6, 2024
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -2283,6 +2283,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
Expand Down
17 changes: 12 additions & 5 deletions x-pack/plugins/security_solution/public/common/hooks/eql/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,49 +5,44 @@
* 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>
);
})}
</ul>
</>
</EuiConfirmModal>
);
};

export const SaveWithErrorsModal = React.memo(SaveWithErrorsModalComponent);
SaveWithErrorsModal.displayName = 'SaveWithErrorsModal';
});
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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 },
});
Original file line number Diff line number Diff line change
@@ -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,
};
}
Original file line number Diff line number Diff line change
@@ -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,
};
}
Original file line number Diff line number Diff line change
@@ -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[];
}
Original file line number Diff line number Diff line change
@@ -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';
Loading
Loading