diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx
index 573419676a086..0449147b93b40 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import type { ReactWrapper } from 'enzyme';
import { mount, shallow } from 'enzyme';
-import { waitFor, render } from '@testing-library/react';
+import { act, fireEvent, render, waitFor } from '@testing-library/react';
import { getExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock';
import { getExceptionBuilderComponentLazy } from '@kbn/lists-plugin/public';
@@ -35,6 +35,7 @@ import {
import type { AlertData } from '../../utils/types';
import { useFindRules } from '../../../rule_management/logic/use_find_rules';
import { useFindExceptionListReferences } from '../../logic/use_find_references';
+import { MAX_COMMENT_LENGTH } from '../constants';
jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index');
jest.mock('../../../../common/lib/kibana');
@@ -1305,5 +1306,46 @@ describe('When the add exception modal is opened', () => {
wrapper.find('button[data-test-subj="addExceptionConfirmButton"]').getDOMNode()
).toBeDisabled();
});
+
+ test('when there is a comment error has submit button disabled', async () => {
+ const { getByLabelText, queryByText, getByTestId } = render(
+
+
+
+ );
+
+ const commentInput = getByLabelText('Comment Input');
+
+ const commentErrorMessage = `The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH} characters.`;
+ expect(queryByText(commentErrorMessage)).toBeNull();
+
+ // Put comment with the length above maximum allowed
+ act(() => {
+ fireEvent.change(commentInput, {
+ target: {
+ value: [...new Array(MAX_COMMENT_LENGTH + 1).keys()].map((_) => 'a').join(''),
+ },
+ });
+ fireEvent.blur(commentInput);
+ });
+ expect(queryByText(commentErrorMessage)).not.toBeNull();
+ expect(getByTestId('addExceptionConfirmButton')).toBeDisabled();
+ });
});
});
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx
index d4108c3eddede..9eefb96be62c9 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx
@@ -157,6 +157,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
selectedRulesToAddTo,
exceptionListsToAddTo,
newComment,
+ commentErrorExists,
itemConditionValidationErrorExists,
errorSubmitting,
expireTime,
@@ -267,6 +268,16 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
[dispatch]
);
+ const setCommentError = useCallback(
+ (errorExists: boolean): void => {
+ dispatch({
+ type: 'setCommentError',
+ errorExists,
+ });
+ },
+ [dispatch]
+ );
+
const setBulkCloseIndex = useCallback(
(index: string[] | undefined): void => {
dispatch({
@@ -445,6 +456,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
exceptionItemName.trim() === '' ||
exceptionItems.every((item) => item.entries.length === 0) ||
itemConditionValidationErrorExists ||
+ commentErrorExists ||
expireErrorExists ||
(addExceptionToRadioSelection === 'add_to_lists' && isEmpty(exceptionListsToAddTo)) ||
(addExceptionToRadioSelection === 'select_rules_to_add_to' &&
@@ -462,6 +474,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
expireErrorExists,
selectedRulesToAddTo,
listType,
+ commentErrorExists,
]
);
@@ -555,6 +568,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
initialIsOpen={!!newComment}
newCommentValue={newComment}
newCommentOnChange={setComment}
+ setCommentError={setCommentError}
/>
{listType !== ExceptionListTypeEnum.ENDPOINT && (
<>
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/reducer.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/reducer.ts
index 04d13c3a1b4e9..ec8040d1fe7cc 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/reducer.ts
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/reducer.ts
@@ -21,6 +21,7 @@ export interface State {
initialItems: ExceptionsBuilderExceptionItem[];
exceptionItems: ExceptionsBuilderReturnExceptionItem[];
newComment: string;
+ commentErrorExists: boolean;
addExceptionToRadioSelection: string;
itemConditionValidationErrorExists: boolean;
closeSingleAlert: boolean;
@@ -40,6 +41,7 @@ export const initialState: State = {
exceptionItems: [],
exceptionItemMeta: { name: '' },
newComment: '',
+ commentErrorExists: false,
itemConditionValidationErrorExists: false,
closeSingleAlert: false,
bulkCloseAlerts: false,
@@ -76,6 +78,10 @@ export type Action =
type: 'setComment';
comment: string;
}
+ | {
+ type: 'setCommentError';
+ errorExists: boolean;
+ }
| {
type: 'setCloseSingleAlert';
close: boolean;
@@ -127,6 +133,7 @@ export type Action =
export const createExceptionItemsReducer =
() =>
+ /* eslint complexity: ["error", 21]*/
(state: State, action: Action): State => {
switch (action.type) {
case 'setExceptionItemMeta': {
@@ -172,6 +179,14 @@ export const createExceptionItemsReducer =
newComment: comment,
};
}
+ case 'setCommentError': {
+ const { errorExists } = action;
+
+ return {
+ ...state,
+ commentErrorExists: errorExists,
+ };
+ }
case 'setCloseSingleAlert': {
const { close } = action;
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/constants.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/constants.ts
new file mode 100644
index 0000000000000..4fea306862d34
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/constants.ts
@@ -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 const MAX_COMMENT_LENGTH = 30000 as const;
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.test.tsx
index faa7c1385142c..ec50eda940cae 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.test.tsx
@@ -6,7 +6,7 @@
*/
import React from 'react';
-import { waitFor } from '@testing-library/react';
+import { act, fireEvent, render, waitFor } from '@testing-library/react';
import { ThemeProvider } from 'styled-components';
import type { ReactWrapper } from 'enzyme';
import { mount } from 'enzyme';
@@ -34,6 +34,7 @@ import { useFetchIndexPatterns } from '../../logic/use_exception_flyout_data';
import { useCreateOrUpdateException } from '../../logic/use_create_update_exception';
import { useFindExceptionListReferences } from '../../logic/use_find_references';
import * as i18n from './translations';
+import { MAX_COMMENT_LENGTH } from '../constants';
const mockTheme = getMockTheme({
eui: {
@@ -693,5 +694,60 @@ describe('When the edit exception modal is opened', () => {
wrapper.find('button[data-test-subj="editExceptionConfirmButton"]').getDOMNode()
).toBeDisabled();
});
+
+ test('when there is a comment error has submit button disabled', async () => {
+ const { getByLabelText, queryByText, getByTestId } = render(
+
+
+
+ );
+
+ const commentInput = getByLabelText('Comment Input');
+
+ const commentErrorMessage = `The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH} characters.`;
+ expect(queryByText(commentErrorMessage)).toBeNull();
+
+ // Put comment with the length above maximum allowed
+ act(() => {
+ fireEvent.change(commentInput, {
+ target: {
+ value: [...new Array(MAX_COMMENT_LENGTH + 1).keys()].map((_) => 'a').join(''),
+ },
+ });
+ fireEvent.blur(commentInput);
+ });
+ expect(queryByText(commentErrorMessage)).not.toBeNull();
+ expect(getByTestId('editExceptionConfirmButton')).toBeDisabled();
+ });
});
});
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.tsx
index 4d9e7c3bbc4ef..6d2526cdbf239 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.tsx
@@ -117,6 +117,7 @@ const EditExceptionFlyoutComponent: React.FC = ({
exceptionItems,
exceptionItemMeta: { name: exceptionItemName },
newComment,
+ commentErrorExists,
bulkCloseAlerts,
disableBulkClose,
bulkCloseIndex,
@@ -129,6 +130,7 @@ const EditExceptionFlyoutComponent: React.FC = ({
exceptionItems: [itemToEdit],
exceptionItemMeta: { name: itemToEdit.name },
newComment: '',
+ commentErrorExists: false,
bulkCloseAlerts: false,
disableBulkClose: true,
bulkCloseIndex: undefined,
@@ -197,6 +199,16 @@ const EditExceptionFlyoutComponent: React.FC = ({
[dispatch]
);
+ const setCommentError = useCallback(
+ (errorExists: boolean): void => {
+ dispatch({
+ type: 'setCommentError',
+ errorExists,
+ });
+ },
+ [dispatch]
+ );
+
const setBulkCloseAlerts = useCallback(
(bulkClose: boolean): void => {
dispatch({
@@ -337,8 +349,17 @@ const EditExceptionFlyoutComponent: React.FC = ({
exceptionItems.every((item) => item.entries.length === 0) ||
isLoading ||
entryErrorExists ||
+ expireErrorExists ||
+ commentErrorExists,
+ [
+ isLoading,
+ entryErrorExists,
+ exceptionItems,
+ isSubmitting,
+ isClosingAlerts,
expireErrorExists,
- [isLoading, entryErrorExists, exceptionItems, isSubmitting, isClosingAlerts, expireErrorExists]
+ commentErrorExists,
+ ]
);
return (
@@ -398,6 +419,7 @@ const EditExceptionFlyoutComponent: React.FC = ({
exceptionItemComments={itemToEdit.comments}
newCommentValue={newComment}
newCommentOnChange={setComment}
+ setCommentError={setCommentError}
/>
{listType !== ExceptionListTypeEnum.ENDPOINT && (
<>
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/reducer.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/reducer.ts
index e08b3c8d135c0..e6dee3af16572 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/reducer.ts
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/reducer.ts
@@ -12,6 +12,7 @@ export interface State {
exceptionItems: ExceptionsBuilderReturnExceptionItem[];
exceptionItemMeta: { name: string };
newComment: string;
+ commentErrorExists: boolean;
bulkCloseAlerts: boolean;
disableBulkClose: boolean;
bulkCloseIndex: string[] | undefined;
@@ -29,6 +30,10 @@ export type Action =
type: 'setComment';
comment: string;
}
+ | {
+ type: 'setCommentError';
+ errorExists: boolean;
+ }
| {
type: 'setBulkCloseAlerts';
bulkClose: boolean;
@@ -81,6 +86,14 @@ export const createExceptionItemsReducer =
newComment: comment,
};
}
+ case 'setCommentError': {
+ const { errorExists } = action;
+
+ return {
+ ...state,
+ commentErrorExists: errorExists,
+ };
+ }
case 'setBulkCloseAlerts': {
const { bulkClose } = action;
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.test.tsx
index 47933db0b3522..60c0d3f244217 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.test.tsx
@@ -6,6 +6,7 @@
*/
import React from 'react';
+import { act, fireEvent, render } from '@testing-library/react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { EuiTextArea } from '@elastic/eui';
@@ -13,6 +14,7 @@ import { ExceptionItemComments } from '.';
import { TestProviders } from '../../../../common/mock';
import { useCurrentUser } from '../../../../common/lib/kibana';
import { shallow } from 'enzyme';
+import { MAX_COMMENT_LENGTH } from '../constants';
jest.mock('../../../../common/lib/kibana');
@@ -38,6 +40,7 @@ describe('ExceptionItemComments', () => {
);
@@ -65,6 +68,7 @@ describe('ExceptionItemComments', () => {
);
@@ -92,6 +96,7 @@ describe('ExceptionItemComments', () => {
);
@@ -106,6 +111,7 @@ describe('ExceptionItemComments', () => {
);
@@ -122,6 +128,7 @@ describe('ExceptionItemComments', () => {
);
@@ -152,10 +159,53 @@ describe('ExceptionItemComments', () => {
]}
newCommentValue={''}
newCommentOnChange={mockOnCommentChange}
+ setCommentError={jest.fn()}
/>
);
expect(wrapper.find('[data-test-subj="exceptionItemCommentsAccordion"]').exists()).toBeTruthy();
});
+
+ it('it calls setCommentError on comment error update change', async () => {
+ const mockSetCommentError = jest.fn();
+ const { getByLabelText, queryByText } = render(
+
+
+
+ );
+
+ const commentInput = getByLabelText('Comment Input');
+
+ const commentErrorMessage = `The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH} characters.`;
+ expect(queryByText(commentErrorMessage)).toBeNull();
+
+ // Put comment with the length above maximum allowed
+ act(() => {
+ fireEvent.change(commentInput, {
+ target: {
+ value: [...new Array(MAX_COMMENT_LENGTH + 1).keys()].map((_) => 'a').join(''),
+ },
+ });
+ fireEvent.blur(commentInput);
+ });
+ expect(queryByText(commentErrorMessage)).not.toBeNull();
+ expect(mockSetCommentError).toHaveBeenCalledWith(true);
+
+ // Put comment with the allowed length
+ act(() => {
+ fireEvent.change(commentInput, {
+ target: {
+ value: 'Updating my new comment',
+ },
+ });
+ fireEvent.blur(commentInput);
+ });
+ expect(queryByText(commentErrorMessage)).toBeNull();
+ expect(mockSetCommentError).toHaveBeenCalledWith(false);
+ });
});
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx
index 0f32e2b4d1ab8..6291097d06941 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx
@@ -5,13 +5,14 @@
* 2.0.
*/
-import React, { memo, useState, useCallback, useMemo } from 'react';
+import React, { memo, useState, useCallback, useMemo, useEffect } from 'react';
import styled, { css } from 'styled-components';
import type { EuiCommentProps } from '@elastic/eui';
import {
EuiTextArea,
EuiFlexGroup,
EuiFlexItem,
+ EuiFormRow,
EuiAvatar,
EuiAccordion,
EuiCommentList,
@@ -21,6 +22,7 @@ import type { Comment } from '@kbn/securitysolution-io-ts-list-types';
import * as i18n from './translations';
import { useCurrentUser } from '../../../../common/lib/kibana';
import { getFormattedComments } from '../../utils/helpers';
+import { MAX_COMMENT_LENGTH } from '../constants';
interface ExceptionItemCommentsProps {
exceptionItemComments?: Comment[];
@@ -28,6 +30,7 @@ interface ExceptionItemCommentsProps {
accordionTitle?: JSX.Element;
initialIsOpen?: boolean;
newCommentOnChange: (value: string) => void;
+ setCommentError: (errorExists: boolean) => void;
}
const COMMENT_ACCORDION_BUTTON_CLASS_NAME = 'exceptionCommentAccordionButton';
@@ -53,8 +56,11 @@ export const ExceptionItemComments = memo(function ExceptionItemComments({
accordionTitle,
initialIsOpen = false,
newCommentOnChange,
+ setCommentError,
}: ExceptionItemCommentsProps) {
+ const [errorExists, setErrorExists] = useState(false);
const [shouldShowComments, setShouldShowComments] = useState(false);
+
const currentUser = useCurrentUser();
const fullName = currentUser?.fullName;
const userName = currentUser?.username;
@@ -73,9 +79,14 @@ export const ExceptionItemComments = memo(function ExceptionItemComments({
return userName && userName.length > 0 ? userName : i18n.UNKNOWN_AVATAR_NAME;
}, [fullName, userEmail, userName]);
+ useEffect(() => {
+ setCommentError(errorExists);
+ }, [errorExists, setCommentError]);
+
const handleOnChange = useCallback(
(event: React.ChangeEvent) => {
newCommentOnChange(event.target.value);
+ setErrorExists(event.target.value.length > MAX_COMMENT_LENGTH);
},
[newCommentOnChange]
);
@@ -121,14 +132,20 @@ export const ExceptionItemComments = memo(function ExceptionItemComments({
-
+
+
+
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/translations.ts
index afe20c6aada98..90c3d9bd0bc48 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/translations.ts
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/translations.ts
@@ -32,3 +32,10 @@ export const COMMENTS_HIDE = (comments: number) =>
values: { comments },
defaultMessage: 'Hide ({comments}) {comments, plural, =1 {Comment} other {Comments}}',
});
+
+export const COMMENT_MAX_LENGTH_ERROR = (length: number) =>
+ i18n.translate('xpack.securitySolution.rule_exceptions.itemComments.maxLengthError', {
+ values: { length },
+ defaultMessage:
+ 'The length of the comment is too long. The maximum length is {length} characters.',
+ });
diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.test.tsx
index be028512cb0a8..380f709ed25f2 100644
--- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.test.tsx
@@ -25,6 +25,7 @@ import { OperatingSystem } from '@kbn/securitysolution-utils';
import { EventFiltersForm } from './form';
import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data';
import type { PolicyData } from '../../../../../../common/endpoint/types';
+import { MAX_COMMENT_LENGTH } from '../../../../../detection_engine/rule_exceptions/components/constants';
jest.mock('../../../../../common/lib/kibana');
jest.mock('../../../../../common/containers/source');
@@ -466,4 +467,35 @@ describe('Event filter form', () => {
expect(renderResult.findByTestId('duplicate-fields-warning-message')).not.toBeNull();
});
});
+
+ describe('Errors', () => {
+ beforeEach(() => {
+ render();
+ });
+
+ it('should not show warning text when unique fields are added', async () => {
+ rerender();
+
+ const commentInput = renderResult.getByLabelText('Comment Input');
+
+ expect(
+ renderResult.queryByText(
+ `The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH} characters.`
+ )
+ ).toBeNull();
+ act(() => {
+ fireEvent.change(commentInput, {
+ target: {
+ value: [...new Array(MAX_COMMENT_LENGTH + 1).keys()].map((_) => 'a').join(''),
+ },
+ });
+ fireEvent.blur(commentInput);
+ });
+ expect(
+ renderResult.queryByText(
+ `The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH} characters.`
+ )
+ ).not.toBeNull();
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx
index 3b4ff4e394a82..e4e1fa7e14638 100644
--- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx
@@ -133,6 +133,7 @@ export const EventFiltersForm: React.FC(!exception.name);
const [newComment, setNewComment] = useState('');
+ const [hasCommentError, setHasCommentError] = useState(false);
const [hasBeenInputNameVisited, setHasBeenInputNameVisited] = useState(false);
const [selectedPolicies, setSelectedPolicies] = useState([]);
const isPlatinumPlus = useLicense().isPlatinumPlus();
@@ -173,10 +174,11 @@ export const EventFiltersForm: React.FC e.value !== '' || e.value.length)
);
- }, [hasNameError, exception.entries]);
+ }, [hasCommentError, hasNameError, exception.entries]);
const processChanged = useCallback(
(updatedItem?: Partial) => {
@@ -340,6 +342,7 @@ export const EventFiltersForm: React.FC
),
[existingComments, handleOnChangeComment, newComment]