mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Security Solution] [Exceptions] Add ListExceptionItem Component and its components with implementing the logic + restructuring exceptions under security (#144622)
## Summary **Shared List Collapsed** <img width="1226" alt="image" src="https://user-images.githubusercontent.com/12671903/200085472-ba638911-af4e-4e6d-85a3-7692b174fad5.png"> **One Shared list expanded** <img width="1442" alt="image" src="https://user-images.githubusercontent.com/12671903/200085548-2b6ef100-8587-47c5-b08d-9727bd2c25c0.png"> **Shared List with no Exceptions** <img width="1180" alt="image" src="https://user-images.githubusercontent.com/12671903/200531307-4b3d0d02-a7c7-4232-98cc-7d0f1b4e48c5.png"> **Add Exceptions from Shared List Card** <img width="1461" alt="image" src="https://user-images.githubusercontent.com/12671903/200531393-833173ea-c5a8-4ab3-b947-257154f6aa90.png"> **Exit Exception from Shared List Card** <img width="1463" alt="image" src="https://user-images.githubusercontent.com/12671903/200531750-e888a0fa-d95c-4994-8ead-f119611fc561.png"> **Delete Endpoint is disabled** <img width="1186" alt="image" src="https://user-images.githubusercontent.com/12671903/200531542-773348c2-c2e2-4062-94e1-12340756ebc3.png"> 1. **New components** a. `list_details_link_anchor` => This component should be removed and moved to @kbn/securitysolution-exception-list-components once all the building components get moved b. `exceptions_utility` => This component should be removed and moved to @kbn/securitysolution-exception-list-components once all the building components get moved c. `list_exception_items ` a wrapper over the ExceptionItem from the `@kbn/securitysolution-exception-list-components` added to pass the missing above components, should be removed soon once everything gets moved to the kbn package 2. **New Hooks** a. `use_list_exception_items` => holds all the Exceptions' items' logic b. `use_exceptions_list.card` => hold all the exception card logic 4. Apply Designs to the Shared Lists 5. **Restructure folders under the `x-pack=>security_solution=>exceptions`** a. components b. hooks c. pages d. translations e. utils 6. Added excluded files in `jest.config` 7. Renamed the `shared_list` components in `routes` ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
f7758e0ada
commit
47f38bc3df
43 changed files with 1734 additions and 442 deletions
|
@ -5,7 +5,7 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { createEvent, fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { HeaderMenu } from '.';
|
||||
import { actions, actionsWithDisabledDelete } from '../mocks/header.mock';
|
||||
|
@ -90,7 +90,6 @@ describe('HeaderMenu', () => {
|
|||
expect(wrapper.queryByTestId('ActionItemedit')).not.toBeInTheDocument();
|
||||
expect(wrapper.queryByTestId('MenuPanel')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onEdit if action has onClick', () => {
|
||||
const onEdit = jest.fn();
|
||||
const customAction = [...actions];
|
||||
|
@ -113,4 +112,16 @@ describe('HeaderMenu', () => {
|
|||
fireEvent.click(wrapper.getByTestId('EmptyButton'));
|
||||
expect(wrapper.queryByTestId('MenuPanel')).toBeInTheDocument();
|
||||
});
|
||||
it('should stop propagation when clicking on the menu', () => {
|
||||
const onEdit = jest.fn();
|
||||
const customAction = [...actions];
|
||||
customAction[0].onClick = onEdit;
|
||||
const wrapper = render(
|
||||
<HeaderMenu dataTestSubj="headerMenu" disableActions={false} actions={actions} />
|
||||
);
|
||||
const headerMenu = wrapper.getByTestId('headerMenuItems');
|
||||
const click = createEvent.click(headerMenu);
|
||||
const result = fireEvent(headerMenu, click);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,12 +20,12 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { ButtonContentIconSide } from '@elastic/eui/src/components/button/_button_content_deprecated';
|
||||
|
||||
interface Action {
|
||||
export interface Action {
|
||||
key: string;
|
||||
icon: string;
|
||||
label: string | boolean;
|
||||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
onClick: (e: React.MouseEvent<Element, MouseEvent>) => void;
|
||||
}
|
||||
interface HeaderMenuComponentProps {
|
||||
disableActions: boolean;
|
||||
|
@ -66,9 +66,9 @@ const HeaderMenuComponent: FC<HeaderMenuComponentProps> = ({
|
|||
icon={action.icon}
|
||||
disabled={action.disabled}
|
||||
layoutAlign="center"
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
onClosePopover();
|
||||
if (typeof action.onClick === 'function') action.onClick();
|
||||
if (typeof action.onClick === 'function') action.onClick(e);
|
||||
}}
|
||||
>
|
||||
{action.label}
|
||||
|
@ -103,6 +103,7 @@ const HeaderMenuComponent: FC<HeaderMenuComponentProps> = ({
|
|||
</EuiButtonIcon>
|
||||
)
|
||||
}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
panelPaddingSize={panelPaddingSize}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={onClosePopover}
|
||||
|
|
|
@ -20,10 +20,9 @@ import {
|
|||
searchForExceptionList,
|
||||
waitForExceptionsTableToBeLoaded,
|
||||
clearSearchSelection,
|
||||
expandExceptionActions,
|
||||
} from '../../../tasks/exceptions_table';
|
||||
import {
|
||||
EXCEPTIONS_TABLE_DELETE_BTN,
|
||||
EXCEPTIONS_OVERFLOW_ACTIONS_BTN,
|
||||
EXCEPTIONS_TABLE_LIST_NAME,
|
||||
EXCEPTIONS_TABLE_SHOWING_LISTS,
|
||||
} from '../../../screens/exceptions';
|
||||
|
@ -182,8 +181,7 @@ describe('Exceptions Table - read only', () => {
|
|||
cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 1 list`);
|
||||
});
|
||||
|
||||
it('Delete icon is not shown', () => {
|
||||
expandExceptionActions();
|
||||
cy.get(EXCEPTIONS_TABLE_DELETE_BTN).should('be.disabled');
|
||||
it('Card menu actions should be disabled', () => {
|
||||
cy.get(EXCEPTIONS_OVERFLOW_ACTIONS_BTN).first().should('be.disabled');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -35,7 +35,7 @@ export const ENTRY_DELETE_BTN = '[data-test-subj="builderItemEntryDeleteButton"]
|
|||
export const CANCEL_BTN = '[data-test-subj="cancelExceptionAddButton"]';
|
||||
|
||||
export const EXCEPTIONS_OVERFLOW_ACTIONS_BTN =
|
||||
'[data-test-subj="exceptionsListCardOverflowActions"]';
|
||||
'[data-test-subj="sharedListOverflowCardButtonIcon"]';
|
||||
|
||||
export const EXCEPTIONS_TABLE = '[data-test-subj="pageContainer"]';
|
||||
|
||||
|
@ -43,9 +43,11 @@ export const EXCEPTIONS_TABLE_SEARCH = '[data-test-subj="exceptionsHeaderSearchI
|
|||
|
||||
export const EXCEPTIONS_TABLE_SHOWING_LISTS = '[data-test-subj="showingExceptionLists"]';
|
||||
|
||||
export const EXCEPTIONS_TABLE_DELETE_BTN = '[data-test-subj="exceptionsTableDeleteButton"]';
|
||||
export const EXCEPTIONS_TABLE_DELETE_BTN =
|
||||
'[data-test-subj="sharedListOverflowCardActionItemDelete"]';
|
||||
|
||||
export const EXCEPTIONS_TABLE_EXPORT_BTN = '[data-test-subj="exceptionsTableExportButton"]';
|
||||
export const EXCEPTIONS_TABLE_EXPORT_BTN =
|
||||
'[data-test-subj="sharedListOverflowCardActionItemExport"]';
|
||||
|
||||
export const EXCEPTIONS_TABLE_SEARCH_CLEAR =
|
||||
'[data-test-subj="allExceptionListsPanel"] button.euiFormControlLayoutClearButton';
|
||||
|
|
|
@ -50,7 +50,7 @@ import type { Rule } from '../../../rule_management/logic/types';
|
|||
import { ExceptionItemsFlyoutAlertsActions } from '../flyout_components/alerts_actions';
|
||||
import { ExceptionsAddToRulesOrLists } from '../flyout_components/add_exception_to_rule_or_list';
|
||||
import { useAddNewExceptionItems } from './use_add_new_exceptions';
|
||||
import { entrichNewExceptionItems } from '../flyout_components/utils';
|
||||
import { enrichNewExceptionItems } from '../flyout_components/utils';
|
||||
import { useCloseAlertsFromExceptions } from '../../logic/use_close_alerts';
|
||||
import { ruleTypesThatAllowLargeValueLists } from '../../utils/constants';
|
||||
|
||||
|
@ -74,6 +74,7 @@ export interface AddExceptionFlyoutProps {
|
|||
*/
|
||||
isAlertDataLoading?: boolean;
|
||||
alertStatus?: Status;
|
||||
sharedListToAddTo?: ExceptionListSchema[];
|
||||
onCancel: (didRuleChange: boolean) => void;
|
||||
onConfirm: (didRuleChange: boolean, didCloseAlert: boolean, didBulkCloseAlert: boolean) => void;
|
||||
}
|
||||
|
@ -106,6 +107,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
|
|||
showAlertCloseOptions,
|
||||
isAlertDataLoading,
|
||||
alertStatus,
|
||||
sharedListToAddTo,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: AddExceptionFlyoutProps) {
|
||||
|
@ -125,6 +127,12 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
|
|||
}
|
||||
}, [rules]);
|
||||
|
||||
const getListType = useMemo(() => {
|
||||
if (isEndpointItem) return ExceptionListTypeEnum.ENDPOINT;
|
||||
if (sharedListToAddTo) return ExceptionListTypeEnum.DETECTION;
|
||||
|
||||
return ExceptionListTypeEnum.RULE_DEFAULT;
|
||||
}, [isEndpointItem, sharedListToAddTo]);
|
||||
const [
|
||||
{
|
||||
exceptionItemMeta: { name: exceptionItemName },
|
||||
|
@ -151,10 +159,9 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
|
|||
: rules != null && rules.length === 1
|
||||
? 'add_to_rule'
|
||||
: 'select_rules_to_add_to',
|
||||
listType: isEndpointItem ? ExceptionListTypeEnum.ENDPOINT : ExceptionListTypeEnum.RULE_DEFAULT,
|
||||
listType: getListType,
|
||||
selectedRulesToAddTo: rules != null ? rules : [],
|
||||
});
|
||||
|
||||
const hasAlertData = useMemo((): boolean => {
|
||||
return alertData != null;
|
||||
}, [alertData]);
|
||||
|
@ -320,14 +327,17 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
|
|||
try {
|
||||
const ruleDefaultOptions = ['add_to_rule', 'add_to_rules', 'select_rules_to_add_to'];
|
||||
const addToRules = ruleDefaultOptions.includes(addExceptionToRadioSelection);
|
||||
const addToSharedLists = addExceptionToRadioSelection === 'add_to_lists';
|
||||
const addToSharedLists =
|
||||
!!sharedListToAddTo?.length ||
|
||||
(addExceptionToRadioSelection === 'add_to_lists' && !isEmpty(exceptionListsToAddTo));
|
||||
const sharedLists = sharedListToAddTo?.length ? sharedListToAddTo : exceptionListsToAddTo;
|
||||
|
||||
const items = entrichNewExceptionItems({
|
||||
const items = enrichNewExceptionItems({
|
||||
itemName: exceptionItemName,
|
||||
commentToAdd: newComment,
|
||||
addToRules,
|
||||
addToSharedLists,
|
||||
sharedLists: exceptionListsToAddTo,
|
||||
sharedLists,
|
||||
listType,
|
||||
selectedOs: osTypesSelection,
|
||||
items: exceptionItems,
|
||||
|
@ -338,8 +348,8 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
|
|||
selectedRulesToAddTo,
|
||||
listType,
|
||||
addToRules: addToRules && !isEmpty(selectedRulesToAddTo),
|
||||
addToSharedLists: addToSharedLists && !isEmpty(exceptionListsToAddTo),
|
||||
sharedLists: exceptionListsToAddTo,
|
||||
addToSharedLists,
|
||||
sharedLists,
|
||||
});
|
||||
|
||||
const alertIdToClose = closeSingleAlert && alertData ? alertData._id : undefined;
|
||||
|
@ -358,6 +368,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
|
|||
setErrorSubmitting(e);
|
||||
}
|
||||
}, [
|
||||
sharedListToAddTo,
|
||||
submitNewExceptionItems,
|
||||
addExceptionToRadioSelection,
|
||||
exceptionItemName,
|
||||
|
@ -463,7 +474,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
|
|||
onFilterIndexPatterns={filterIndexPatterns}
|
||||
/>
|
||||
|
||||
{listType !== ExceptionListTypeEnum.ENDPOINT && (
|
||||
{listType !== ExceptionListTypeEnum.ENDPOINT && !sharedListToAddTo?.length && (
|
||||
<>
|
||||
<EuiHorizontalRule />
|
||||
<ExceptionsAddToRulesOrLists
|
||||
|
|
|
@ -50,7 +50,7 @@ import { filterIndexPatterns } from '../../utils/helpers';
|
|||
import { useFetchIndexPatterns } from '../../logic/use_exception_flyout_data';
|
||||
import { useCloseAlertsFromExceptions } from '../../logic/use_close_alerts';
|
||||
import { useFindExceptionListReferences } from '../../logic/use_find_references';
|
||||
import { entrichExceptionItemsForUpdate } from '../flyout_components/utils';
|
||||
import { enrichExceptionItemsForUpdate } from '../flyout_components/utils';
|
||||
import { ExceptionItemComments } from '../item_comments';
|
||||
import { createExceptionItemsReducer } from './reducer';
|
||||
import { useEditExceptionItems } from './use_edit_exception';
|
||||
|
@ -62,6 +62,7 @@ interface EditExceptionFlyoutProps {
|
|||
itemToEdit: ExceptionListItemSchema;
|
||||
showAlertCloseOptions: boolean;
|
||||
rule?: Rule;
|
||||
openedFromListDetailPage?: boolean;
|
||||
onCancel: (arg: boolean) => void;
|
||||
onConfirm: (arg: boolean) => void;
|
||||
}
|
||||
|
@ -97,6 +98,7 @@ const EditExceptionFlyoutComponent: React.FC<EditExceptionFlyoutProps> = ({
|
|||
itemToEdit,
|
||||
rule,
|
||||
showAlertCloseOptions,
|
||||
openedFromListDetailPage,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}): JSX.Element => {
|
||||
|
@ -244,7 +246,7 @@ const EditExceptionFlyoutComponent: React.FC<EditExceptionFlyoutProps> = ({
|
|||
if (submitEditExceptionItems == null) return;
|
||||
|
||||
try {
|
||||
const items = entrichExceptionItemsForUpdate({
|
||||
const items = enrichExceptionItemsForUpdate({
|
||||
itemName: exceptionItemName,
|
||||
commentToAdd: newComment,
|
||||
listType,
|
||||
|
@ -339,7 +341,7 @@ const EditExceptionFlyoutComponent: React.FC<EditExceptionFlyoutProps> = ({
|
|||
onSetErrorExists={setConditionsValidationError}
|
||||
onFilterIndexPatterns={filterIndexPatterns}
|
||||
/>
|
||||
{listType === ExceptionListTypeEnum.DETECTION && (
|
||||
{!openedFromListDetailPage && listType === ExceptionListTypeEnum.DETECTION && (
|
||||
<>
|
||||
<EuiHorizontalRule />
|
||||
<ExceptionsLinkedToLists
|
||||
|
@ -349,12 +351,14 @@ const EditExceptionFlyoutComponent: React.FC<EditExceptionFlyoutProps> = ({
|
|||
/>
|
||||
</>
|
||||
)}
|
||||
{listType === ExceptionListTypeEnum.RULE_DEFAULT && rule != null && (
|
||||
<>
|
||||
<EuiHorizontalRule />
|
||||
<ExceptionsLinkedToRule rule={rule} />
|
||||
</>
|
||||
)}
|
||||
{!openedFromListDetailPage &&
|
||||
listType === ExceptionListTypeEnum.RULE_DEFAULT &&
|
||||
rule != null && (
|
||||
<>
|
||||
<EuiHorizontalRule />
|
||||
<ExceptionsLinkedToRule rule={rule} />
|
||||
</>
|
||||
)}
|
||||
<EuiHorizontalRule />
|
||||
<ExceptionItemComments
|
||||
accordionTitle={
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
enrichEndpointItems,
|
||||
enrichItemsForDefaultRuleList,
|
||||
enrichItemsForSharedLists,
|
||||
entrichNewExceptionItems,
|
||||
enrichNewExceptionItems,
|
||||
} from './utils';
|
||||
|
||||
const getExceptionItems = (): ExceptionsBuilderReturnExceptionItem[] => [
|
||||
|
@ -30,7 +30,7 @@ describe('add_exception_flyout#utils', () => {
|
|||
const items = getExceptionItems();
|
||||
|
||||
expect(
|
||||
entrichNewExceptionItems({
|
||||
enrichNewExceptionItems({
|
||||
itemName: 'My item',
|
||||
commentToAdd: 'New comment',
|
||||
addToRules: true,
|
||||
|
@ -66,7 +66,7 @@ describe('add_exception_flyout#utils', () => {
|
|||
];
|
||||
|
||||
expect(
|
||||
entrichNewExceptionItems({
|
||||
enrichNewExceptionItems({
|
||||
itemName: 'My item',
|
||||
commentToAdd: 'New comment',
|
||||
addToRules: false,
|
||||
|
@ -105,7 +105,7 @@ describe('add_exception_flyout#utils', () => {
|
|||
];
|
||||
|
||||
expect(
|
||||
entrichNewExceptionItems({
|
||||
enrichNewExceptionItems({
|
||||
itemName: 'My item',
|
||||
commentToAdd: 'New comment',
|
||||
addToRules: false,
|
||||
|
|
|
@ -111,7 +111,7 @@ export const enrichItemsForSharedLists =
|
|||
* @param listType exception list type
|
||||
* @param items exception items to be modified
|
||||
*/
|
||||
export const entrichNewExceptionItems = ({
|
||||
export const enrichNewExceptionItems = ({
|
||||
itemName,
|
||||
commentToAdd,
|
||||
addToRules,
|
||||
|
@ -152,7 +152,7 @@ export const entrichNewExceptionItems = ({
|
|||
* @param listType exception list type
|
||||
* @param items exception items to be modified
|
||||
*/
|
||||
export const entrichExceptionItemsForUpdate = ({
|
||||
export const enrichExceptionItemsForUpdate = ({
|
||||
itemName,
|
||||
commentToAdd,
|
||||
selectedOs,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { Query } from '@elastic/eui';
|
||||
import { EXCEPTIONS_SEARCH_SCHEMA } from '../../../../exceptions/manage_exceptions/exceptions_search_bar';
|
||||
import { EXCEPTIONS_SEARCH_SCHEMA } from '../../../../exceptions/components/list_search_bar';
|
||||
import { caseInsensitiveSort, getSearchFilters } from './helpers';
|
||||
|
||||
describe('AllRulesTable Helpers', () => {
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* 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 {
|
||||
Pagination as ServerPagination,
|
||||
ExceptionListSchema,
|
||||
ListArray,
|
||||
NamespaceType,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import {
|
||||
deleteExceptionListItemById,
|
||||
fetchExceptionListsItemsByListIds,
|
||||
updateExceptionListItem,
|
||||
addExceptionListItem,
|
||||
} from '@kbn/securitysolution-list-api';
|
||||
import { transformInput } from '@kbn/securitysolution-list-hooks';
|
||||
import type {
|
||||
GetExceptionItemProps,
|
||||
RuleReferences,
|
||||
} from '@kbn/securitysolution-exception-list-components';
|
||||
import type { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { findRuleExceptionReferences } from '../../detection_engine/rule_management/api/api';
|
||||
import type { AddExceptionItem, DeleteExceptionItem, EditExceptionItem, FetchItems } from './types';
|
||||
|
||||
// Some of the APIs here are already defined in Kbn packages, need to be refactored
|
||||
|
||||
export const prepareFetchExceptionItemsParams = (
|
||||
exceptions: ListArray | null,
|
||||
list: ExceptionListSchema | null,
|
||||
options?: GetExceptionItemProps | null
|
||||
) => {
|
||||
const { pagination, search, filters } = options || {};
|
||||
let listIds: string[] = [];
|
||||
let namespaceTypes: NamespaceType[] = [];
|
||||
|
||||
if (Array.isArray(exceptions) && exceptions.length) {
|
||||
listIds = exceptions.map((excList) => excList.list_id);
|
||||
namespaceTypes = exceptions.map((excList) => excList.namespace_type);
|
||||
} else if (list) {
|
||||
listIds = [list.list_id];
|
||||
namespaceTypes = [list.namespace_type];
|
||||
}
|
||||
|
||||
return {
|
||||
listIds,
|
||||
namespaceTypes,
|
||||
pagination,
|
||||
search,
|
||||
filters,
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchListExceptionItems = async ({
|
||||
namespaceTypes,
|
||||
listIds,
|
||||
http,
|
||||
pagination,
|
||||
search,
|
||||
}: FetchItems) => {
|
||||
try {
|
||||
const abortCtrl = new AbortController();
|
||||
const {
|
||||
pageIndex: inputPageIndex,
|
||||
pageSize: inputPageSize,
|
||||
totalItemCount: inputTotalItemCount,
|
||||
} = pagination || {};
|
||||
|
||||
// TODO transform Pagination object from Frontend=>Backend & <=
|
||||
const {
|
||||
page: pageIndex,
|
||||
per_page: pageSize,
|
||||
total: totalItemCount,
|
||||
data,
|
||||
} = await fetchExceptionListsItemsByListIds({
|
||||
filter: undefined,
|
||||
http: http as HttpSetup,
|
||||
listIds: listIds ?? [],
|
||||
namespaceTypes: namespaceTypes ?? [],
|
||||
search,
|
||||
pagination: {
|
||||
perPage: inputPageSize,
|
||||
page: (inputPageIndex || 0) + 1,
|
||||
total: inputTotalItemCount,
|
||||
} as ServerPagination,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
const transformedData = data.map((item) => transformInput(item));
|
||||
|
||||
return {
|
||||
data: transformedData,
|
||||
pagination: { pageIndex: pageIndex - 1, pageSize, totalItemCount },
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const getExceptionItemsReferences = async (list: ExceptionListSchema) => {
|
||||
try {
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
const { references } = await findRuleExceptionReferences({
|
||||
lists: [list].map((listInput) => ({
|
||||
id: listInput.id,
|
||||
listId: listInput.list_id,
|
||||
namespaceType: listInput.namespace_type,
|
||||
})),
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
return references.reduce<RuleReferences>((acc, reference) => {
|
||||
return { ...acc, ...reference } as RuleReferences;
|
||||
}, {});
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteException = async ({ id, namespaceType, http }: DeleteExceptionItem) => {
|
||||
try {
|
||||
const abortCtrl = new AbortController();
|
||||
await deleteExceptionListItemById({
|
||||
http: http as HttpSetup,
|
||||
id,
|
||||
namespaceType,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const editException = async ({ http, exception }: EditExceptionItem) => {
|
||||
try {
|
||||
const abortCtrl = new AbortController();
|
||||
await updateExceptionListItem({
|
||||
http: http as HttpSetup,
|
||||
listItem: exception,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
};
|
||||
export const addException = async ({ http, exception }: AddExceptionItem) => {
|
||||
try {
|
||||
const abortCtrl = new AbortController();
|
||||
await addExceptionListItem({
|
||||
http: http as HttpSetup,
|
||||
listItem: exception,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
};
|
|
@ -4,12 +4,4 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const READ_ONLY_BADGE_TOOLTIP = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.badge.readOnly.tooltip',
|
||||
{
|
||||
defaultMessage: 'Unable to create, edit or delete exceptions',
|
||||
}
|
||||
);
|
||||
export * from './exception_api';
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 {
|
||||
CreateExceptionListItemSchema,
|
||||
NamespaceType,
|
||||
UpdateExceptionListItemSchema,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import type { Pagination } from '@elastic/eui';
|
||||
import type { HttpSetup } from '@kbn/core-http-browser';
|
||||
|
||||
export interface FetchItems {
|
||||
http: HttpSetup | undefined;
|
||||
listIds: string[];
|
||||
namespaceTypes: NamespaceType[];
|
||||
pagination: Pagination | undefined;
|
||||
search?: string;
|
||||
filter?: string;
|
||||
}
|
||||
export interface DeleteExceptionItem {
|
||||
id: string;
|
||||
namespaceType: NamespaceType;
|
||||
http: HttpSetup | undefined;
|
||||
}
|
||||
export interface EditExceptionItem {
|
||||
http: HttpSetup | undefined;
|
||||
exception: UpdateExceptionListItemSchema;
|
||||
}
|
||||
|
||||
export interface AddExceptionItem {
|
||||
http: HttpSetup | undefined;
|
||||
exception: CreateExceptionListItemSchema;
|
||||
}
|
|
@ -27,7 +27,7 @@ import type { ErrorToastOptions, Toast, ToastInput } from '@kbn/core-notificatio
|
|||
import { i18n as translate } from '@kbn/i18n';
|
||||
import type { ListDetails } from '@kbn/securitysolution-exception-list-components';
|
||||
|
||||
import { useCreateSharedExceptionListWithOptionalSignal } from './use_create_shared_list';
|
||||
import { useCreateSharedExceptionListWithOptionalSignal } from '../../hooks/use_create_shared_list';
|
||||
import {
|
||||
CREATE_SHARED_LIST_TITLE,
|
||||
CREATE_SHARED_LIST_NAME_FIELD,
|
||||
|
@ -38,7 +38,7 @@ import {
|
|||
CREATE_SHARED_LIST_NAME_FIELD_PLACEHOLDER,
|
||||
SUCCESS_TITLE,
|
||||
getSuccessText,
|
||||
} from './translations';
|
||||
} from '../../translations';
|
||||
|
||||
export const CreateSharedListFlyout = memo(
|
||||
({
|
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
* 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, { memo } from 'react';
|
||||
|
||||
import {
|
||||
EuiLink,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTextColor,
|
||||
EuiPanel,
|
||||
EuiText,
|
||||
EuiAccordion,
|
||||
EuiButtonIcon,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import type { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import type { NamespaceType } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { HeaderMenu } from '@kbn/securitysolution-exception-list-components';
|
||||
import styled from 'styled-components';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { EditExceptionFlyout } from '../../../detection_engine/rule_exceptions/components/edit_exception_flyout';
|
||||
import { AddExceptionFlyout } from '../../../detection_engine/rule_exceptions/components/add_exception_flyout';
|
||||
import type { ExceptionListInfo } from '../../hooks/use_all_exception_lists';
|
||||
import { TitleBadge } from '../title_badge';
|
||||
import * as i18n from '../../translations';
|
||||
import { ListExceptionItems } from '../list_exception_items';
|
||||
import { useExceptionsListCard } from '../../hooks/use_exceptions_list.card';
|
||||
|
||||
interface ExceptionsListCardProps {
|
||||
exceptionsList: ExceptionListInfo;
|
||||
http: HttpSetup;
|
||||
handleDelete: ({
|
||||
id,
|
||||
listId,
|
||||
namespaceType,
|
||||
}: {
|
||||
id: string;
|
||||
listId: string;
|
||||
namespaceType: NamespaceType;
|
||||
}) => () => Promise<void>;
|
||||
handleExport: ({
|
||||
id,
|
||||
listId,
|
||||
namespaceType,
|
||||
}: {
|
||||
id: string;
|
||||
listId: string;
|
||||
namespaceType: NamespaceType;
|
||||
}) => () => Promise<void>;
|
||||
readOnly: boolean;
|
||||
}
|
||||
const buttonCss = css`
|
||||
// Ask KIBANA Team why Emotion is not working fully under xpack
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
span {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
const ExceptionPanel = styled(EuiPanel)`
|
||||
margin: -${euiThemeVars.euiSizeS} ${euiThemeVars.euiSizeM} 0 ${euiThemeVars.euiSizeM};
|
||||
`;
|
||||
const ListHeaderContainer = styled(EuiFlexGroup)`
|
||||
padding: ${euiThemeVars.euiSizeS};
|
||||
`;
|
||||
export const ExceptionsListCard = memo<ExceptionsListCardProps>(
|
||||
({ exceptionsList, handleDelete, handleExport, readOnly }) => {
|
||||
const {
|
||||
listId,
|
||||
listName,
|
||||
listType,
|
||||
createdAt,
|
||||
createdBy,
|
||||
exceptions,
|
||||
pagination,
|
||||
ruleReferences,
|
||||
toggleAccordion,
|
||||
openAccordionId,
|
||||
menuActionItems,
|
||||
listRulesCount,
|
||||
listDescription,
|
||||
exceptionItemsCount,
|
||||
onEditExceptionItem,
|
||||
onDeleteException,
|
||||
onPaginationChange,
|
||||
setToggleAccordion,
|
||||
exceptionViewerStatus,
|
||||
showAddExceptionFlyout,
|
||||
showEditExceptionFlyout,
|
||||
exceptionToEdit,
|
||||
onAddExceptionClick,
|
||||
handleConfirmExceptionFlyout,
|
||||
handleCancelExceptionItemFlyout,
|
||||
} = useExceptionsListCard({
|
||||
exceptionsList,
|
||||
handleExport,
|
||||
handleDelete,
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasShadow={false}>
|
||||
<EuiAccordion
|
||||
buttonProps={{ css: buttonCss }}
|
||||
id={openAccordionId}
|
||||
arrowDisplay="none"
|
||||
onToggle={() => setToggleAccordion(!toggleAccordion)}
|
||||
buttonContent={
|
||||
<EuiPanel>
|
||||
<ListHeaderContainer gutterSize="m" alignItems="flexStart">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType={toggleAccordion ? 'arrowDown' : 'arrowRight'}
|
||||
aria-label="Next"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
key={listId}
|
||||
alignItems="flexStart"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem grow>
|
||||
<EuiText size="m">
|
||||
<EuiLink data-test-subj="exception-list-name">{listName}</EuiLink>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow>
|
||||
<EuiText size="xs">
|
||||
<EuiTextColor color="subdued">{listDescription}</EuiTextColor>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<TitleBadge title={i18n.DATE_CREATED} badgeString={createdAt} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<TitleBadge title={i18n.CREATED_BY} badgeString={createdBy} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<TitleBadge title={i18n.EXCEPTIONS} badgeString={exceptionItemsCount} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<TitleBadge title={i18n.RULES} badgeString={listRulesCount} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<HeaderMenu
|
||||
disableActions={readOnly}
|
||||
dataTestSubj="sharedListOverflowCard"
|
||||
actions={menuActionItems}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</ListHeaderContainer>
|
||||
</EuiPanel>
|
||||
}
|
||||
>
|
||||
<ExceptionPanel hasBorder>
|
||||
<ListExceptionItems
|
||||
isReadOnly={readOnly}
|
||||
exceptions={exceptions}
|
||||
listType={exceptionsList.type as ExceptionListTypeEnum}
|
||||
pagination={pagination}
|
||||
hideUtility
|
||||
viewerStatus={exceptionViewerStatus}
|
||||
ruleReferences={ruleReferences}
|
||||
onDeleteException={onDeleteException}
|
||||
onEditExceptionItem={onEditExceptionItem}
|
||||
onPaginationChange={onPaginationChange}
|
||||
onCreateExceptionListItem={onAddExceptionClick}
|
||||
lastUpdated={null}
|
||||
/>
|
||||
</ExceptionPanel>
|
||||
</EuiAccordion>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
{showAddExceptionFlyout ? (
|
||||
<AddExceptionFlyout
|
||||
rules={null}
|
||||
isBulkAction={false}
|
||||
isEndpointItem={listType === ExceptionListTypeEnum.ENDPOINT}
|
||||
sharedListToAddTo={[exceptionsList]}
|
||||
onCancel={handleCancelExceptionItemFlyout}
|
||||
onConfirm={handleConfirmExceptionFlyout}
|
||||
data-test-subj="addExceptionItemFlyoutInSharedLists"
|
||||
showAlertCloseOptions={false}
|
||||
/>
|
||||
) : null}
|
||||
{showEditExceptionFlyout && exceptionToEdit ? (
|
||||
<EditExceptionFlyout
|
||||
list={exceptionsList}
|
||||
itemToEdit={exceptionToEdit}
|
||||
showAlertCloseOptions
|
||||
openedFromListDetailPage
|
||||
onCancel={handleCancelExceptionItemFlyout}
|
||||
onConfirm={handleConfirmExceptionFlyout}
|
||||
data-test-subj="editExceptionItemFlyoutInSharedLists"
|
||||
/>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ExceptionsListCard.displayName = 'ExceptionsListCard';
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
|
||||
import { ExceptionsUtility } from '.';
|
||||
|
||||
describe('ExceptionsUtility', () => {
|
||||
it('it renders correct item counts', () => {
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<ExceptionsUtility
|
||||
pagination={{
|
||||
pageIndex: 0,
|
||||
pageSize: 50,
|
||||
totalItemCount: 105,
|
||||
pageSizeOptions: [5, 10, 20, 50, 100],
|
||||
}}
|
||||
lastUpdated={1660534202}
|
||||
dataTestSubj="exceptionUtility"
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.getByTestId('exceptionUtilityShowingText')).toHaveTextContent(
|
||||
'Showing 1-50 of 105'
|
||||
);
|
||||
});
|
||||
it('it renders last updated message', () => {
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<ExceptionsUtility
|
||||
pagination={{
|
||||
pageIndex: 0,
|
||||
pageSize: 50,
|
||||
totalItemCount: 1,
|
||||
pageSizeOptions: [5, 10, 20, 50, 100],
|
||||
}}
|
||||
lastUpdated={Date.now()}
|
||||
dataTestSubj="exceptionUtility"
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.getByTestId('exceptionUtilityLastUpdated')).toHaveTextContent('Updated now');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 type { FC } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { Pagination } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
|
||||
import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date';
|
||||
import {
|
||||
UtilityBar,
|
||||
UtilityBarSection,
|
||||
UtilityBarGroup,
|
||||
UtilityBarText,
|
||||
} from '../../../common/components/utility_bar';
|
||||
|
||||
const StyledText = styled.span`
|
||||
font-weight: bold;
|
||||
color: ${({ theme }) => theme.eui.euiColorDarkestShade};
|
||||
`;
|
||||
|
||||
const MyUtilities = styled(EuiFlexGroup)`
|
||||
height: 50px;
|
||||
`;
|
||||
|
||||
const StyledCondition = styled.span`
|
||||
display: inline-block !important;
|
||||
vertical-align: middle !important;
|
||||
`;
|
||||
interface ExceptionsUtilityComponentProps {
|
||||
dataTestSubj?: string;
|
||||
exceptionsTitle?: string;
|
||||
pagination: Pagination;
|
||||
// Corresponds to last time exception items were fetched
|
||||
lastUpdated: string | number | null;
|
||||
}
|
||||
// This component should be removed and moved to @kbn/securitysolution-exception-list-components
|
||||
// once all the building components get moved
|
||||
|
||||
const ExceptionsUtilityComponent: FC<ExceptionsUtilityComponentProps> = ({
|
||||
dataTestSubj,
|
||||
pagination,
|
||||
lastUpdated,
|
||||
exceptionsTitle,
|
||||
}) => {
|
||||
const { pageSize, totalItemCount } = pagination;
|
||||
return (
|
||||
<MyUtilities alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<UtilityBar>
|
||||
<UtilityBarSection>
|
||||
<UtilityBarGroup>
|
||||
<UtilityBarText dataTestSubj={`${dataTestSubj}ShowingText`}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.exceptions.viewer.paginationDetails"
|
||||
defaultMessage="Showing {partOne} of {partTwo}"
|
||||
values={{
|
||||
partOne: <StyledText>{`1-${Math.min(pageSize, totalItemCount)}`}</StyledText>,
|
||||
partTwo: <StyledText>{`${totalItemCount}`}</StyledText>,
|
||||
}}
|
||||
/>
|
||||
</UtilityBarText>
|
||||
{exceptionsTitle && (
|
||||
<StyledText data-test-subj={`${dataTestSubj}exceptionsTitle`}>
|
||||
{exceptionsTitle}
|
||||
</StyledText>
|
||||
)}
|
||||
</UtilityBarGroup>
|
||||
</UtilityBarSection>
|
||||
</UtilityBar>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s" data-test-subj={`${dataTestSubj}LastUpdated`}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.exceptions.viewer.lastUpdated"
|
||||
defaultMessage="Updated {updated}"
|
||||
values={{
|
||||
updated: (
|
||||
<StyledCondition>
|
||||
<FormattedRelativePreferenceDate
|
||||
value={lastUpdated}
|
||||
tooltipAnchorClassName="eui-textTruncate"
|
||||
/>
|
||||
</StyledCondition>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</MyUtilities>
|
||||
);
|
||||
};
|
||||
|
||||
ExceptionsUtilityComponent.displayName = 'ExceptionsUtilityComponent';
|
||||
|
||||
export const ExceptionsUtility = React.memo(ExceptionsUtilityComponent);
|
||||
|
||||
ExceptionsUtility.displayName = 'ExceptionsUtility';
|
|
@ -31,9 +31,9 @@ import type {
|
|||
import type { HttpSetup } from '@kbn/core-http-browser';
|
||||
import type { ToastInput, Toast, ErrorToastOptions } from '@kbn/core-notifications-browser';
|
||||
|
||||
import { useImportExceptionList } from './use_import_exception_list';
|
||||
import { useImportExceptionList } from '../../hooks/use_import_exception_list';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import * as i18n from '../../translations';
|
||||
|
||||
export const ImportExceptionListFlyout = React.memo(
|
||||
({
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 type { FC } from 'react';
|
||||
import { SecurityPageName } from '../../../../common/constants';
|
||||
import { getRuleDetailsTabUrl } from '../../../common/components/link_to/redirect_to_detection_engine';
|
||||
import { RuleDetailTabs } from '../../../detection_engine/rule_details_ui/pages/rule_details';
|
||||
import { SecuritySolutionLinkAnchor } from '../../../common/components/links';
|
||||
|
||||
interface LinkAnchorProps {
|
||||
referenceName: string;
|
||||
referenceId: string;
|
||||
external?: boolean;
|
||||
}
|
||||
// This component should be removed and moved to @kbn/securitysolution-exception-list-components
|
||||
// once all the building components get moved
|
||||
|
||||
const LinkAnchor: FC<LinkAnchorProps> = ({ referenceName, referenceId, external }) => {
|
||||
return (
|
||||
<SecuritySolutionLinkAnchor
|
||||
data-test-subj="SecuritySolutionLinkAnchor"
|
||||
deepLinkId={SecurityPageName.rules}
|
||||
path={getRuleDetailsTabUrl(referenceId, RuleDetailTabs.alerts)}
|
||||
external={external}
|
||||
>
|
||||
{referenceName}
|
||||
</SecuritySolutionLinkAnchor>
|
||||
);
|
||||
};
|
||||
|
||||
LinkAnchor.displayName = 'LinkAnchor';
|
||||
|
||||
export const ListDetailsLinkAnchor = React.memo(LinkAnchor);
|
||||
|
||||
ListDetailsLinkAnchor.displayName = 'ListDetailsLinkAnchor';
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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 type { FC } from 'react';
|
||||
import type {
|
||||
ExceptionListItemIdentifiers,
|
||||
GetExceptionItemProps,
|
||||
RuleReferences,
|
||||
ViewerStatus,
|
||||
} from '@kbn/securitysolution-exception-list-components';
|
||||
import { ExceptionItems } from '@kbn/securitysolution-exception-list-components';
|
||||
import type {
|
||||
ExceptionListItemSchema,
|
||||
ExceptionListTypeEnum,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import type { Pagination } from '@elastic/eui';
|
||||
import { FormattedDate } from '../../../common/components/formatted_date';
|
||||
import { getFormattedComments } from '../../utils/ui.helpers';
|
||||
import { ListDetailsLinkAnchor } from '../list_details_link_anchor';
|
||||
import { ExceptionsUtility } from '../exceptions_utility';
|
||||
import * as i18n from '../../translations/list_exception_items';
|
||||
|
||||
interface ListExceptionItemsProps {
|
||||
isReadOnly: boolean;
|
||||
exceptions: ExceptionListItemSchema[];
|
||||
listType: ExceptionListTypeEnum;
|
||||
lastUpdated: string | number | null;
|
||||
pagination: Pagination;
|
||||
emptyViewerTitle?: string;
|
||||
emptyViewerBody?: string;
|
||||
viewerStatus: ViewerStatus | '';
|
||||
ruleReferences: RuleReferences;
|
||||
hideUtility?: boolean;
|
||||
onDeleteException: (arg: ExceptionListItemIdentifiers) => void;
|
||||
onEditExceptionItem: (item: ExceptionListItemSchema) => void;
|
||||
onPaginationChange: (arg: GetExceptionItemProps) => void;
|
||||
onCreateExceptionListItem: () => void;
|
||||
}
|
||||
|
||||
const ListExceptionItemsComponent: FC<ListExceptionItemsProps> = ({
|
||||
isReadOnly,
|
||||
exceptions,
|
||||
listType,
|
||||
lastUpdated,
|
||||
pagination,
|
||||
emptyViewerTitle,
|
||||
emptyViewerBody,
|
||||
viewerStatus,
|
||||
ruleReferences,
|
||||
hideUtility = false,
|
||||
onDeleteException,
|
||||
onEditExceptionItem,
|
||||
onPaginationChange,
|
||||
onCreateExceptionListItem,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<ExceptionItems
|
||||
viewerStatus={viewerStatus as ViewerStatus}
|
||||
listType={listType as ExceptionListTypeEnum}
|
||||
ruleReferences={ruleReferences}
|
||||
isReadOnly={isReadOnly}
|
||||
exceptions={exceptions}
|
||||
emptyViewerTitle={emptyViewerTitle}
|
||||
emptyViewerBody={emptyViewerBody}
|
||||
pagination={pagination}
|
||||
lastUpdated={lastUpdated}
|
||||
editActionLabel={i18n.EXCEPTION_ITEM_CARD_EDIT_LABEL}
|
||||
deleteActionLabel={i18n.EXCEPTION_ITEM_CARD_DELETE_LABEL}
|
||||
onPaginationChange={onPaginationChange}
|
||||
onEditExceptionItem={onEditExceptionItem}
|
||||
onDeleteException={onDeleteException}
|
||||
getFormattedComments={getFormattedComments}
|
||||
securityLinkAnchorComponent={ListDetailsLinkAnchor}
|
||||
formattedDateComponent={FormattedDate}
|
||||
onCreateExceptionListItem={onCreateExceptionListItem}
|
||||
exceptionsUtilityComponent={() =>
|
||||
hideUtility ? null : (
|
||||
<ExceptionsUtility
|
||||
exceptionsTitle={i18n.EXCEPTION_UTILITY_TITLE}
|
||||
pagination={pagination}
|
||||
lastUpdated={lastUpdated}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ListExceptionItemsComponent.displayName = 'ListExceptionItemsComponent';
|
||||
|
||||
export const ListExceptionItems = React.memo(ListExceptionItemsComponent);
|
||||
|
||||
ListExceptionItems.displayName = 'ListExceptionItems';
|
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
import type { EuiSearchBarProps } from '@elastic/eui';
|
||||
import { EuiSearchBar } from '@elastic/eui';
|
||||
|
||||
import * as i18n from './translations_exceptions_table';
|
||||
import * as i18n from '../../translations';
|
||||
|
||||
interface ExceptionListsTableSearchProps {
|
||||
onSearch: (args: Parameters<NonNullable<EuiSearchBarProps['onChange']>>[0]) => void;
|
|
@ -13,8 +13,8 @@ import {
|
|||
UtilityBarGroup,
|
||||
UtilityBarSection,
|
||||
UtilityBarText,
|
||||
} from '../../common/components/utility_bar';
|
||||
import * as i18n from './translations_exceptions_table';
|
||||
} from '../../../common/components/utility_bar';
|
||||
import * as i18n from '../../translations';
|
||||
|
||||
interface ExceptionsTableUtilityBarProps {
|
||||
onRefresh?: () => void;
|
|
@ -6,9 +6,9 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import { ExceptionsTableUtilityBar } from './exceptions_table_utility_bar';
|
||||
import { ExceptionsTableUtilityBar } from '.';
|
||||
|
||||
describe('ExceptionsTableUtilityBar', () => {
|
||||
it('displays correct exception lists label and refresh rules action button', () => {
|
|
@ -9,6 +9,7 @@ import React, { memo } from 'react';
|
|||
import styled from 'styled-components';
|
||||
|
||||
import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
|
||||
interface TitleBadgeProps {
|
||||
title: string;
|
||||
|
@ -17,20 +18,25 @@ interface TitleBadgeProps {
|
|||
|
||||
const StyledFlexItem = styled(EuiFlexItem)`
|
||||
border-right: 1px solid #d3dae6;
|
||||
padding: 4px 12px 4px 0;
|
||||
padding: ${euiThemeVars.euiSizeXS} ${euiThemeVars.euiSizeS} ${euiThemeVars.euiSizeXS} 0;
|
||||
`;
|
||||
|
||||
const TextContainer = styled(EuiText)`
|
||||
width: max-content;
|
||||
`;
|
||||
|
||||
export const TitleBadge = memo<TitleBadgeProps>(({ title, badgeString }) => {
|
||||
return (
|
||||
<EuiFlexItem style={{ flex: '1 1 auto' }}>
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>{title}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<StyledFlexItem grow={false}>
|
||||
<EuiBadge>{badgeString}</EuiBadge>{' '}
|
||||
</StyledFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<TextContainer grow size="xs">
|
||||
{`${title}:`}
|
||||
</TextContainer>
|
||||
</EuiFlexItem>
|
||||
<StyledFlexItem>
|
||||
<EuiBadge>{badgeString}</EuiBadge>
|
||||
</StyledFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 listIDsCannotBeEdited = ['endpoint_list'];
|
|
@ -8,8 +8,8 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import type { Rule } from '../../detection_engine/rule_management/logic';
|
||||
import { fetchRules } from '../../detection_engine/rule_management/api/api';
|
||||
import type { Rule } from '../../../detection_engine/rule_management/logic';
|
||||
import { fetchRules } from '../../../detection_engine/rule_management/api/api';
|
||||
export interface ExceptionListInfo extends ExceptionListSchema {
|
||||
rules: Rule[];
|
||||
}
|
|
@ -10,7 +10,7 @@ import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types
|
|||
import type { HttpStart } from '@kbn/core/public';
|
||||
import { useAsync, withOptionalSignal } from '@kbn/securitysolution-hook-utils';
|
||||
|
||||
import { SHARED_EXCEPTION_LIST_URL } from '../../../common/constants';
|
||||
import { SHARED_EXCEPTION_LIST_URL } from '../../../../common/constants';
|
||||
|
||||
export const createSharedExceptionList = async ({
|
||||
name,
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* 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, useState } from 'react';
|
||||
import type {
|
||||
ExceptionListItemSchema,
|
||||
NamespaceType,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { ViewerStatus } from '@kbn/securitysolution-exception-list-components';
|
||||
import { useGeneratedHtmlId } from '@elastic/eui';
|
||||
import type { ExceptionListInfo } from '../use_all_exception_lists';
|
||||
import { useListExceptionItems } from '../use_list_exception_items';
|
||||
import * as i18n from '../../translations/list_details';
|
||||
import { checkIfListCannotBeEdited } from '../../utils/list.utils';
|
||||
|
||||
interface ListAction {
|
||||
id: string;
|
||||
listId: string;
|
||||
namespaceType: NamespaceType;
|
||||
}
|
||||
export const useExceptionsListCard = ({
|
||||
exceptionsList,
|
||||
handleExport,
|
||||
handleDelete,
|
||||
}: {
|
||||
exceptionsList: ExceptionListInfo;
|
||||
handleExport: ({ id, listId, namespaceType }: ListAction) => () => Promise<void>;
|
||||
handleDelete: ({ id, listId, namespaceType }: ListAction) => () => Promise<void>;
|
||||
}) => {
|
||||
const [viewerStatus, setViewerStatus] = useState<ViewerStatus | string>(ViewerStatus.LOADING);
|
||||
const [exceptionToEdit, setExceptionToEdit] = useState<ExceptionListItemSchema>();
|
||||
const [showAddExceptionFlyout, setShowAddExceptionFlyout] = useState(false);
|
||||
const [showEditExceptionFlyout, setShowEditExceptionFlyout] = useState(false);
|
||||
|
||||
const {
|
||||
name: listName,
|
||||
list_id: listId,
|
||||
rules: listRules,
|
||||
type: listType,
|
||||
created_by: createdBy,
|
||||
created_at: createdAt,
|
||||
description: listDescription,
|
||||
} = exceptionsList;
|
||||
|
||||
const onFinishFetchingExceptions = useCallback(() => {
|
||||
setViewerStatus('');
|
||||
}, [setViewerStatus]);
|
||||
|
||||
const onEditExceptionItem = (exception: ExceptionListItemSchema) => {
|
||||
setExceptionToEdit(exception);
|
||||
setShowEditExceptionFlyout(true);
|
||||
};
|
||||
const {
|
||||
lastUpdated,
|
||||
exceptionViewerStatus,
|
||||
exceptions,
|
||||
pagination,
|
||||
ruleReferences,
|
||||
fetchItems,
|
||||
onDeleteException,
|
||||
onPaginationChange,
|
||||
} = useListExceptionItems({
|
||||
list: exceptionsList,
|
||||
deleteToastTitle: i18n.EXCEPTION_ITEM_DELETE_TITLE,
|
||||
deleteToastBody: (name) => i18n.EXCEPTION_ITEM_DELETE_TEXT(name),
|
||||
errorToastBody: i18n.EXCEPTION_ERROR_DESCRIPTION,
|
||||
errorToastTitle: i18n.EXCEPTION_ERROR_TITLE,
|
||||
onEditListExceptionItem: onEditExceptionItem,
|
||||
onFinishFetchingExceptions,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchItems(null, ViewerStatus.LOADING);
|
||||
}, [fetchItems]);
|
||||
|
||||
const [toggleAccordion, setToggleAccordion] = useState(false);
|
||||
const openAccordionId = useGeneratedHtmlId({ prefix: 'openAccordion' });
|
||||
|
||||
const listCannotBeEdited = checkIfListCannotBeEdited(exceptionsList);
|
||||
|
||||
const menuActionItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'Export',
|
||||
icon: 'exportAction',
|
||||
label: i18n.EXPORT_EXCEPTION_LIST,
|
||||
onClick: (e: React.MouseEvent<Element, MouseEvent>) => {
|
||||
handleExport({
|
||||
id: exceptionsList.id,
|
||||
listId: exceptionsList.list_id,
|
||||
namespaceType: exceptionsList.namespace_type,
|
||||
})();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'Delete',
|
||||
icon: 'trash',
|
||||
disabled: listCannotBeEdited,
|
||||
label: i18n.DELETE_EXCEPTION_LIST,
|
||||
onClick: (e: React.MouseEvent<Element, MouseEvent>) => {
|
||||
handleDelete({
|
||||
id: exceptionsList.id,
|
||||
listId: exceptionsList.list_id,
|
||||
namespaceType: exceptionsList.namespace_type,
|
||||
})();
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
exceptionsList.id,
|
||||
exceptionsList.list_id,
|
||||
exceptionsList.namespace_type,
|
||||
handleDelete,
|
||||
handleExport,
|
||||
listCannotBeEdited,
|
||||
]
|
||||
);
|
||||
|
||||
// Once details Page is added all of these methods will be used from it as well
|
||||
// as their own states
|
||||
const onAddExceptionClick = useCallback(() => {
|
||||
setShowAddExceptionFlyout(true);
|
||||
}, [setShowAddExceptionFlyout]);
|
||||
|
||||
const handleCancelExceptionItemFlyout = () => {
|
||||
setShowAddExceptionFlyout(false);
|
||||
setShowEditExceptionFlyout(false);
|
||||
};
|
||||
const handleConfirmExceptionFlyout = useCallback(
|
||||
(didExceptionChange: boolean): void => {
|
||||
setShowAddExceptionFlyout(false);
|
||||
setShowEditExceptionFlyout(false);
|
||||
if (!didExceptionChange) return;
|
||||
fetchItems();
|
||||
},
|
||||
[fetchItems, setShowAddExceptionFlyout, setShowEditExceptionFlyout]
|
||||
);
|
||||
|
||||
return {
|
||||
listId,
|
||||
listName,
|
||||
listDescription,
|
||||
createdAt: new Date(createdAt).toDateString(),
|
||||
createdBy,
|
||||
listRulesCount: listRules.length.toString(),
|
||||
exceptionItemsCount: pagination.totalItemCount.toString(),
|
||||
listType,
|
||||
menuActionItems,
|
||||
showAddExceptionFlyout,
|
||||
toggleAccordion,
|
||||
openAccordionId,
|
||||
viewerStatus,
|
||||
exceptionToEdit,
|
||||
showEditExceptionFlyout,
|
||||
lastUpdated,
|
||||
exceptions,
|
||||
ruleReferences,
|
||||
pagination,
|
||||
exceptionViewerStatus,
|
||||
onEditExceptionItem,
|
||||
onDeleteException,
|
||||
onPaginationChange,
|
||||
setToggleAccordion,
|
||||
onAddExceptionClick,
|
||||
handleConfirmExceptionFlyout,
|
||||
handleCancelExceptionItemFlyout,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* 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, useState } from 'react';
|
||||
import type { Pagination } from '@elastic/eui';
|
||||
import { ViewerStatus } from '@kbn/securitysolution-exception-list-components';
|
||||
import type { RuleReferences } from '@kbn/securitysolution-exception-list-components';
|
||||
import type {
|
||||
ExceptionListItemSchema,
|
||||
ExceptionListSchema,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { useKibana, useToasts } from '../../../common/lib/kibana';
|
||||
import {
|
||||
deleteException,
|
||||
fetchListExceptionItems,
|
||||
getExceptionItemsReferences,
|
||||
prepareFetchExceptionItemsParams,
|
||||
} from '../../api';
|
||||
|
||||
export interface UseListExceptionItemsProps {
|
||||
list: ExceptionListSchema;
|
||||
deleteToastTitle: string;
|
||||
deleteToastBody: (exceptionName: string) => string;
|
||||
errorToastTitle: string;
|
||||
errorToastBody: string;
|
||||
onEditListExceptionItem: (exceptionItem: ExceptionListItemSchema) => void;
|
||||
onFinishFetchingExceptions?: () => void;
|
||||
}
|
||||
|
||||
export const useListExceptionItems = ({
|
||||
list,
|
||||
deleteToastTitle,
|
||||
deleteToastBody,
|
||||
errorToastTitle,
|
||||
errorToastBody,
|
||||
onEditListExceptionItem,
|
||||
onFinishFetchingExceptions,
|
||||
}: UseListExceptionItemsProps) => {
|
||||
const { services } = useKibana();
|
||||
const { http } = services;
|
||||
const toasts = useToasts();
|
||||
|
||||
const [exceptions, setExceptions] = useState<ExceptionListItemSchema[]>([]);
|
||||
const [exceptionListReferences, setExceptionListReferences] = useState<RuleReferences>({});
|
||||
const [pagination, setPagination] = useState<Pagination>({
|
||||
pageIndex: 0,
|
||||
pageSize: 0,
|
||||
totalItemCount: 0,
|
||||
});
|
||||
const [lastUpdated, setLastUpdated] = useState<null | string | number>(null);
|
||||
const [viewerStatus, setViewerStatus] = useState<ViewerStatus | ''>('');
|
||||
|
||||
const handleErrorStatus = useCallback(
|
||||
(error: Error, errorTitle?: string, errorDescription?: string) => {
|
||||
toasts?.addError(error, {
|
||||
title: errorTitle || errorToastTitle,
|
||||
toastMessage: errorDescription || errorToastBody,
|
||||
});
|
||||
setViewerStatus(ViewerStatus.ERROR);
|
||||
},
|
||||
[errorToastBody, errorToastTitle, toasts]
|
||||
);
|
||||
|
||||
const getReferences = useCallback(async () => {
|
||||
try {
|
||||
const result: RuleReferences = await getExceptionItemsReferences(list);
|
||||
setExceptionListReferences(result);
|
||||
} catch (error) {
|
||||
handleErrorStatus(error);
|
||||
}
|
||||
}, [handleErrorStatus, list, setExceptionListReferences]);
|
||||
|
||||
const updateViewer = useCallback((paginationResult, dataLength, viewStatus) => {
|
||||
setPagination(paginationResult);
|
||||
setLastUpdated(Date.now());
|
||||
setTimeout(() => {
|
||||
if (viewStatus === ViewerStatus.EMPTY_SEARCH)
|
||||
return setViewerStatus(!dataLength ? viewStatus : '');
|
||||
|
||||
setViewerStatus(!dataLength ? ViewerStatus.EMPTY : '');
|
||||
}, 200);
|
||||
}, []);
|
||||
|
||||
const fetchItems = useCallback(
|
||||
async (options?, viewStatus?) => {
|
||||
try {
|
||||
setViewerStatus(ViewerStatus.LOADING);
|
||||
const { data, pagination: paginationResult } = await fetchListExceptionItems({
|
||||
http,
|
||||
...prepareFetchExceptionItemsParams(null, list, options),
|
||||
});
|
||||
setExceptions(data);
|
||||
getReferences();
|
||||
updateViewer(paginationResult, data.length, viewStatus);
|
||||
if (typeof onFinishFetchingExceptions === 'function') onFinishFetchingExceptions();
|
||||
} catch (error) {
|
||||
handleErrorStatus(error);
|
||||
}
|
||||
},
|
||||
[http, list, getReferences, updateViewer, onFinishFetchingExceptions, handleErrorStatus]
|
||||
);
|
||||
|
||||
const onDeleteException = useCallback(
|
||||
async ({ id, name, namespaceType }) => {
|
||||
try {
|
||||
setViewerStatus(ViewerStatus.LOADING);
|
||||
await deleteException({ id, http, namespaceType });
|
||||
toasts?.addSuccess({
|
||||
title: deleteToastTitle,
|
||||
text: typeof deleteToastBody === 'function' ? deleteToastBody(name) : '',
|
||||
});
|
||||
fetchItems();
|
||||
} catch (error) {
|
||||
handleErrorStatus(error);
|
||||
}
|
||||
},
|
||||
[http, toasts, deleteToastTitle, deleteToastBody, fetchItems, handleErrorStatus]
|
||||
);
|
||||
const onEditExceptionItem = (exception: ExceptionListItemSchema) => {
|
||||
if (typeof onEditListExceptionItem === 'function') onEditListExceptionItem(exception);
|
||||
};
|
||||
const onPaginationChange = useCallback(
|
||||
async (options) => {
|
||||
fetchItems(options);
|
||||
},
|
||||
[fetchItems]
|
||||
);
|
||||
return {
|
||||
exceptions,
|
||||
lastUpdated,
|
||||
pagination,
|
||||
exceptionViewerStatus: viewerStatus,
|
||||
|
||||
ruleReferences: exceptionListReferences,
|
||||
fetchItems,
|
||||
onDeleteException,
|
||||
onEditExceptionItem,
|
||||
onPaginationChange,
|
||||
};
|
||||
};
|
|
@ -14,6 +14,16 @@ module.exports = {
|
|||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/plugins/security_solution/public/exceptions/**/*.{ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/security_solution/public/exceptions/*.test.{ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/security_solution/public/exceptions/*.constants.{ts}',
|
||||
'!<rootDir>/x-pack/plugins/security_solution/public/exceptions/{__test__,__snapshots__,__examples__,*mock*,tests,test_helpers,integration_tests,types}/**/*',
|
||||
'!<rootDir>/x-pack/plugins/security_solution/public/exceptions/*mock*.{ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/security_solution/public/exceptions/*.test.{ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/security_solution/public/exceptions/*.d.ts',
|
||||
'!<rootDir>/x-pack/plugins/security_solution/public/exceptions/*.config.ts',
|
||||
'!<rootDir>/x-pack/plugins/security_solution/public/exceptions/index.{js,ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/security_solution/public/exceptions/translations/*',
|
||||
'!<rootDir>/x-pack/plugins/security_solution/public/exceptions/*.translations',
|
||||
],
|
||||
// See: https://github.com/elastic/kibana/issues/117255, the moduleNameMapper creates mocks to avoid memory leaks from kibana core.
|
||||
moduleNameMapper: {
|
||||
|
|
|
@ -1,150 +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 React, { memo, useState } from 'react';
|
||||
|
||||
import {
|
||||
EuiLink,
|
||||
EuiButtonIcon,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTextColor,
|
||||
EuiPanel,
|
||||
EuiPopover,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import type { HttpSetup } from '@kbn/core-http-browser';
|
||||
import type { NamespaceType } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import type { ExceptionListInfo } from './use_all_exception_lists';
|
||||
import { TitleBadge } from './title_badge';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface ExceptionsListCardProps {
|
||||
exceptionsList: ExceptionListInfo;
|
||||
http: HttpSetup;
|
||||
handleDelete: ({
|
||||
id,
|
||||
listId,
|
||||
namespaceType,
|
||||
}: {
|
||||
id: string;
|
||||
listId: string;
|
||||
namespaceType: NamespaceType;
|
||||
}) => () => Promise<void>;
|
||||
handleExport: ({
|
||||
id,
|
||||
listId,
|
||||
namespaceType,
|
||||
}: {
|
||||
id: string;
|
||||
listId: string;
|
||||
namespaceType: NamespaceType;
|
||||
}) => () => Promise<void>;
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
export const ExceptionsListCard = memo<ExceptionsListCardProps>(
|
||||
({ exceptionsList, http, handleDelete, handleExport, readOnly }) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const onItemActionsClick = () => setIsPopoverOpen((isOpen) => !isOpen);
|
||||
const onClosePopover = () => setIsPopoverOpen(false);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel>
|
||||
<EuiFlexGroup key={exceptionsList.list_id} alignItems="center" gutterSize="l">
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiFlexGroup direction="column" alignItems="flexStart">
|
||||
<EuiFlexItem grow={false} component={'span'}>
|
||||
<EuiLink data-test-subj="exception-list-name">
|
||||
{exceptionsList.name.toString()}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
<EuiTextColor color="subdued">{exceptionsList.description}</EuiTextColor>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<TitleBadge title={i18n.CREATED_BY} badgeString={exceptionsList.created_by} />
|
||||
<TitleBadge title={i18n.CREATED_AT} badgeString={exceptionsList.created_at} />
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
data-test-subj="exceptionsListCardOverflowActions"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
isDisabled={false}
|
||||
data-test-subj="exceptionsListCardOverflowActions"
|
||||
aria-label="Exception item actions menu"
|
||||
iconType="boxesHorizontal"
|
||||
onClick={onItemActionsClick}
|
||||
/>
|
||||
}
|
||||
panelPaddingSize="none"
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={onClosePopover}
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
size="s"
|
||||
items={[
|
||||
<EuiContextMenuItem
|
||||
key={'delete'}
|
||||
disabled={exceptionsList.list_id === 'endpoint_list' || readOnly}
|
||||
data-test-subj="exceptionsTableDeleteButton"
|
||||
icon={'trash'}
|
||||
onClick={() => {
|
||||
onClosePopover();
|
||||
handleDelete({
|
||||
id: exceptionsList.id,
|
||||
listId: exceptionsList.list_id,
|
||||
namespaceType: exceptionsList.namespace_type,
|
||||
})();
|
||||
}}
|
||||
>
|
||||
{i18n.DELETE_EXCEPTION_LIST}
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
key={'export'}
|
||||
icon={'exportAction'}
|
||||
data-test-subj="exceptionsTableExportButton"
|
||||
onClick={() => {
|
||||
onClosePopover();
|
||||
handleExport({
|
||||
id: exceptionsList.id,
|
||||
listId: exceptionsList.list_id,
|
||||
namespaceType: exceptionsList.namespace_type,
|
||||
})();
|
||||
}}
|
||||
>
|
||||
{i18n.EXPORT_EXCEPTION_LIST}
|
||||
</EuiContextMenuItem>,
|
||||
]}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ExceptionsListCard.displayName = 'ExceptionsListCard';
|
|
@ -1,161 +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 uploadSuccessMessage = (fileName: string) =>
|
||||
i18n.translate('xpack.securitySolution.lists.exceptionListImportSuccess', {
|
||||
defaultMessage: "Exception list '{fileName}' was imported",
|
||||
values: { fileName },
|
||||
});
|
||||
|
||||
export const CREATED_BY = i18n.translate('xpack.securitySolution.exceptionsTable.createdBy', {
|
||||
defaultMessage: 'Created By',
|
||||
});
|
||||
|
||||
export const CREATED_AT = i18n.translate('xpack.securitySolution.exceptionsTable.createdAt', {
|
||||
defaultMessage: 'Created At',
|
||||
});
|
||||
|
||||
export const DELETE_EXCEPTION_LIST = i18n.translate(
|
||||
'xpack.securitySolution.exceptionsTable.deleteExceptionList',
|
||||
{
|
||||
defaultMessage: 'Delete Exception List',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXPORT_EXCEPTION_LIST = i18n.translate(
|
||||
'xpack.securitySolution.exceptionsTable.exportExceptionList',
|
||||
{
|
||||
defaultMessage: 'Export Exception List',
|
||||
}
|
||||
);
|
||||
|
||||
export const IMPORT_EXCEPTION_LIST_HEADER = i18n.translate(
|
||||
'xpack.securitySolution.exceptionsTable.importExceptionListHeader',
|
||||
{
|
||||
defaultMessage: 'Import shared exception list',
|
||||
}
|
||||
);
|
||||
|
||||
export const IMPORT_EXCEPTION_LIST_BODY = i18n.translate(
|
||||
'xpack.securitySolution.exceptionsTable.importExceptionListFlyoutBody',
|
||||
{
|
||||
defaultMessage: 'Select shared exception lists to import',
|
||||
}
|
||||
);
|
||||
|
||||
export const IMPORT_EXCEPTION_LIST_WARNING = i18n.translate(
|
||||
'xpack.securitySolution.exceptionsTable.importExceptionListWarning',
|
||||
{
|
||||
defaultMessage: 'We found a pre-existing list with that id',
|
||||
}
|
||||
);
|
||||
|
||||
export const IMPORT_EXCEPTION_LIST_OVERWRITE = i18n.translate(
|
||||
'xpack.securitySolution.exceptionsTable.importExceptionListOverwrite',
|
||||
{
|
||||
defaultMessage: 'Overwrite the existing list',
|
||||
}
|
||||
);
|
||||
|
||||
export const IMPORT_EXCEPTION_LIST_AS_NEW_LIST = i18n.translate(
|
||||
'xpack.securitySolution.exceptionsTable.importExceptionListAsNewList',
|
||||
{
|
||||
defaultMessage: 'Create new list',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPLOAD_SUCCESS_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.lists.exceptionListImportSuccessTitle',
|
||||
{
|
||||
defaultMessage: 'Exception list imported',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPLOAD_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.lists.exceptionListUploadError',
|
||||
{
|
||||
defaultMessage: 'There was an error uploading the exception list.',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPLOAD_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionListsImportButton',
|
||||
{
|
||||
defaultMessage: 'Import list',
|
||||
}
|
||||
);
|
||||
|
||||
export const CLOSE_FLYOUT = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionListsCloseImportFlyout',
|
||||
{
|
||||
defaultMessage: 'Close',
|
||||
}
|
||||
);
|
||||
|
||||
export const IMPORT_PROMPT = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionListsFilePickerPrompt',
|
||||
{
|
||||
defaultMessage: 'Select or drag and drop multiple files',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_SHARED_LIST_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.createSharedExceptionListTitle',
|
||||
{
|
||||
defaultMessage: 'Create shared exception list',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_SHARED_LIST_NAME_FIELD = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.createSharedExceptionListFlyoutNameField',
|
||||
{
|
||||
defaultMessage: 'Shared exception list name',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_SHARED_LIST_NAME_FIELD_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.createSharedExceptionListFlyoutNameFieldPlaceholder',
|
||||
{
|
||||
defaultMessage: 'New exception list',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_SHARED_LIST_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.createSharedExceptionListFlyoutDescription',
|
||||
{
|
||||
defaultMessage: 'Description (optional)',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_SHARED_LIST_DESCRIPTION_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.createSharedExceptionListFlyoutDescriptionPlaceholder',
|
||||
{
|
||||
defaultMessage: 'New exception list',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.createSharedExceptionListFlyoutCreateButton',
|
||||
{
|
||||
defaultMessage: 'Create shared exception list',
|
||||
}
|
||||
);
|
||||
|
||||
export const getSuccessText = (listName: string) =>
|
||||
i18n.translate('xpack.securitySolution.exceptions.createSharedExceptionListSuccessDescription', {
|
||||
defaultMessage: 'list with name ${listName} was created!',
|
||||
values: { listName },
|
||||
});
|
||||
|
||||
export const SUCCESS_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.createSharedExceptionListSuccessTitle',
|
||||
{
|
||||
defaultMessage: 'created list',
|
||||
}
|
||||
);
|
|
@ -27,28 +27,28 @@ import {
|
|||
import type { NamespaceType, ExceptionListFilter } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { useApi, useExceptionLists } from '@kbn/securitysolution-list-hooks';
|
||||
|
||||
import { AutoDownload } from '../../common/components/auto_download/auto_download';
|
||||
import { Loader } from '../../common/components/loader';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { useAppToasts } from '../../common/hooks/use_app_toasts';
|
||||
import { AutoDownload } from '../../../common/components/auto_download/auto_download';
|
||||
import { Loader } from '../../../common/components/loader';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
|
||||
|
||||
import * as i18n from './translations_exceptions_table';
|
||||
import { ExceptionsTableUtilityBar } from './exceptions_table_utility_bar';
|
||||
import { useAllExceptionLists } from './use_all_exception_lists';
|
||||
import { ReferenceErrorModal } from '../../detections/components/value_lists_management_flyout/reference_error_modal';
|
||||
import { patchRule } from '../../detection_engine/rule_management/api/api';
|
||||
import { ExceptionsSearchBar } from './exceptions_search_bar';
|
||||
import { getSearchFilters } from '../../detection_engine/rule_management_ui/components/rules_table/helpers';
|
||||
import { useUserData } from '../../detections/components/user_info';
|
||||
import { useListsConfig } from '../../detections/containers/detection_engine/lists/use_lists_config';
|
||||
import { MissingPrivilegesCallOut } from '../../detections/components/callouts/missing_privileges_callout';
|
||||
import { ALL_ENDPOINT_ARTIFACT_LIST_IDS } from '../../../common/endpoint/service/artifacts/constants';
|
||||
import { ExceptionsListCard } from './exceptions_list_card';
|
||||
import * as i18n from '../../translations/shared_list';
|
||||
import { ExceptionsTableUtilityBar } from '../../components/shared_list_utilty_bar';
|
||||
import { useAllExceptionLists } from '../../hooks/use_all_exception_lists';
|
||||
import { ReferenceErrorModal } from '../../../detections/components/value_lists_management_flyout/reference_error_modal';
|
||||
import { patchRule } from '../../../detection_engine/rule_management/api/api';
|
||||
import { ExceptionsSearchBar } from '../../components/list_search_bar';
|
||||
import { getSearchFilters } from '../../../detection_engine/rule_management_ui/components/rules_table/helpers';
|
||||
import { useUserData } from '../../../detections/components/user_info';
|
||||
import { useListsConfig } from '../../../detections/containers/detection_engine/lists/use_lists_config';
|
||||
import { MissingPrivilegesCallOut } from '../../../detections/components/callouts/missing_privileges_callout';
|
||||
import { ALL_ENDPOINT_ARTIFACT_LIST_IDS } from '../../../../common/endpoint/service/artifacts/constants';
|
||||
import { ExceptionsListCard } from '../../components/exceptions_list_card';
|
||||
|
||||
import { ImportExceptionListFlyout } from './import_exceptions_list_flyout';
|
||||
import { CreateSharedListFlyout } from './create_shared_exception_list';
|
||||
import { ImportExceptionListFlyout } from '../../components/import_exceptions_list_flyout';
|
||||
import { CreateSharedListFlyout } from '../../components/create_shared_exception_list';
|
||||
|
||||
import { AddExceptionFlyout } from '../../detection_engine/rule_exceptions/components/add_exception_flyout';
|
||||
import { AddExceptionFlyout } from '../../../detection_engine/rule_exceptions/components/add_exception_flyout';
|
||||
|
||||
export type Func = () => Promise<void>;
|
||||
|
||||
|
@ -68,7 +68,7 @@ const exceptionReferenceModalInitialState: ReferenceModalState = {
|
|||
listNamespaceType: 'single',
|
||||
};
|
||||
|
||||
export const ExceptionListsTable = React.memo(() => {
|
||||
export const SharedLists = React.memo(() => {
|
||||
const [{ loading: userInfoLoading, canUserCRUD, canUserREAD }] = useUserData();
|
||||
|
||||
const { loading: listsConfigLoading } = useListsConfig();
|
||||
|
@ -304,6 +304,7 @@ export const ExceptionListsTable = React.memo(() => {
|
|||
iconSide="right"
|
||||
onClick={onRowSizeButtonClick}
|
||||
>
|
||||
{/* TODO move to translations */}
|
||||
{`Rows per page: ${rowSize}`}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
@ -479,9 +480,8 @@ export const ExceptionListsTable = React.memo(() => {
|
|||
totalExceptionLists={exceptionListsWithRuleRefs.length}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
{exceptionListsWithRuleRefs.length > 0 && canUserCRUD !== null && canUserREAD !== null && (
|
||||
<React.Fragment data-test-subj="exceptionsTable">
|
||||
<div data-test-subj="exceptionsTable">
|
||||
{exceptionListsWithRuleRefs.map((excList) => (
|
||||
<ExceptionsListCard
|
||||
key={excList.list_id}
|
||||
|
@ -493,7 +493,7 @@ export const ExceptionListsTable = React.memo(() => {
|
|||
handleExport={handleExport}
|
||||
/>
|
||||
))}
|
||||
</React.Fragment>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
@ -546,4 +546,4 @@ export const ExceptionListsTable = React.memo(() => {
|
|||
);
|
||||
});
|
||||
|
||||
ExceptionListsTable.displayName = 'ExceptionListsTable';
|
||||
SharedLists.displayName = 'SharedLists';
|
|
@ -6,21 +6,21 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { getExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock';
|
||||
import { useUserData } from '../../detections/components/user_info';
|
||||
import { useUserData } from '../../../detections/components/user_info';
|
||||
|
||||
import { ExceptionListsTable } from './exceptions_table';
|
||||
import { SharedLists } from '.';
|
||||
import { useApi, useExceptionLists } from '@kbn/securitysolution-list-hooks';
|
||||
import { useAllExceptionLists } from './use_all_exception_lists';
|
||||
import { useAllExceptionLists } from '../../hooks/use_all_exception_lists';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { generateHistoryMock } from '../../common/utils/route/mocks';
|
||||
import { generateHistoryMock } from '../../../common/utils/route/mocks';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
|
||||
jest.mock('../../detections/components/user_info');
|
||||
jest.mock('../../common/lib/kibana');
|
||||
jest.mock('./use_all_exception_lists');
|
||||
jest.mock('../../../detections/components/user_info');
|
||||
jest.mock('../../../common/utils/route/mocks');
|
||||
jest.mock('../../hooks/use_all_exception_lists');
|
||||
jest.mock('@kbn/securitysolution-list-hooks');
|
||||
jest.mock('react-router-dom', () => {
|
||||
const originalModule = jest.requireActual('react-router-dom');
|
||||
|
@ -39,11 +39,11 @@ jest.mock('@kbn/i18n-react', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('../../detections/containers/detection_engine/lists/use_lists_config', () => ({
|
||||
jest.mock('../../../detections/containers/detection_engine/lists/use_lists_config', () => ({
|
||||
useListsConfig: jest.fn().mockReturnValue({ loading: false }),
|
||||
}));
|
||||
|
||||
describe('ExceptionListsTable', () => {
|
||||
describe('SharedLists', () => {
|
||||
const mockHistory = generateHistoryMock();
|
||||
const exceptionList1 = getExceptionListSchemaMock();
|
||||
const exceptionList2 = { ...getExceptionListSchemaMock(), list_id: 'not_endpoint_list', id: '2' };
|
||||
|
@ -91,21 +91,18 @@ describe('ExceptionListsTable', () => {
|
|||
});
|
||||
|
||||
it('renders delete option as disabled if list is "endpoint_list"', async () => {
|
||||
const wrapper = mount(
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<ExceptionListsTable />
|
||||
<SharedLists />
|
||||
</TestProviders>
|
||||
);
|
||||
const allMenuActions = wrapper.getAllByTestId('sharedListOverflowCardButtonIcon');
|
||||
fireEvent.click(allMenuActions[0]);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="exceptionsListCardOverflowActions"] button')
|
||||
.at(0)
|
||||
.simulate('click');
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionsTableDeleteButton"] button')).toHaveLength(1);
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="exceptionsTableDeleteButton"] button').at(0).prop('disabled')
|
||||
).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
const allDeleteActions = wrapper.getAllByTestId('sharedListOverflowCardActionItemDelete');
|
||||
expect(allDeleteActions[0]).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders delete option as disabled if user is read only', async () => {
|
||||
|
@ -117,17 +114,19 @@ describe('ExceptionListsTable', () => {
|
|||
},
|
||||
]);
|
||||
|
||||
const wrapper = mount(
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<ExceptionListsTable />
|
||||
<SharedLists />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper
|
||||
.find('[data-test-subj="exceptionsListCardOverflowActions"] button')
|
||||
.at(0)
|
||||
.simulate('click');
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="exceptionsTableDeleteButton"] button').at(0).prop('disabled')
|
||||
).toBeTruthy();
|
||||
const allMenuActions = wrapper.getAllByTestId('sharedListOverflowCardButtonIcon');
|
||||
fireEvent.click(allMenuActions[1]);
|
||||
|
||||
await waitFor(() => {
|
||||
const allDeleteActions = wrapper.queryAllByTestId('sharedListOverflowCardActionItemDelete');
|
||||
expect(allDeleteActions).toEqual([]);
|
||||
const allExportActions = wrapper.queryAllByTestId('sharedListOverflowCardActionItemExport');
|
||||
expect(allExportActions).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -11,7 +11,7 @@ import { Route } from '@kbn/kibana-react-plugin/public';
|
|||
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
|
||||
import * as i18n from './translations';
|
||||
import { EXCEPTIONS_PATH, SecurityPageName } from '../../common/constants';
|
||||
import { ExceptionListsTable } from './manage_exceptions/exceptions_table';
|
||||
import { SharedLists } from './pages/shared_lists';
|
||||
import { SpyRoute } from '../common/utils/route/spy_routes';
|
||||
import { NotFoundPage } from '../app/404';
|
||||
import { useReadonlyHeader } from '../use_readonly_header';
|
||||
|
@ -20,7 +20,7 @@ import { PluginTemplateWrapper } from '../common/components/plugin_template_wrap
|
|||
const ExceptionsRoutes = () => (
|
||||
<PluginTemplateWrapper>
|
||||
<TrackApplicationView viewId={SecurityPageName.exceptions}>
|
||||
<ExceptionListsTable />
|
||||
<SharedLists />
|
||||
<SpyRoute pageName={SecurityPageName.exceptions} />
|
||||
</TrackApplicationView>
|
||||
</PluginTemplateWrapper>
|
||||
|
|
|
@ -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 * from './list_details';
|
||||
export * from './list_exception_items';
|
||||
export * from './shared_list';
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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 EXCEPTION_LIST_EMPTY_VIEWER_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.exception.list.empty.viewer_title',
|
||||
{
|
||||
defaultMessage: 'Create exceptions to this list',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_LIST_EMPTY_VIEWER_BODY = (listName: string) =>
|
||||
i18n.translate('xpack.securitySolution.exception.list.empty.viewer_body', {
|
||||
values: { listName },
|
||||
defaultMessage:
|
||||
'There is no exception in your [{listName}]. Create rule exceptions to this list.',
|
||||
});
|
||||
|
||||
export const EXCEPTION_LIST_EMPTY_VIEWER_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.exception.list.empty.viewer_button',
|
||||
{
|
||||
defaultMessage: 'Create rule exception',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_LIST_EMPTY_SEARCH_BAR_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.exception.list.search_bar_button',
|
||||
{
|
||||
defaultMessage: 'Add rule exception to list',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_LIST_SEARCH_ERROR_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.list.exceptionItemSearchErrorTitle',
|
||||
{
|
||||
defaultMessage: 'Error searching',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_LIST_SEARCH_ERROR_BODY = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.list.exceptionItemSearchErrorBody',
|
||||
{
|
||||
defaultMessage: 'An error occurred searching for exception items. Please try again.',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_ERROR_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.list.exceptionItemsFetchError',
|
||||
{
|
||||
defaultMessage: 'Unable to load exception items',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_ERROR_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.list.exceptionItemsFetchErrorDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'There was an error loading the exception items. Contact your administrator for help.',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_ITEM_DELETE_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.list.exception.item.card.exceptionItemDeleteSuccessTitle',
|
||||
{
|
||||
defaultMessage: 'Exception deleted',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_ITEM_DELETE_TEXT = (itemName: string) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.exceptions.list.exception.item.card.exceptionItemDeleteSuccessText',
|
||||
{
|
||||
values: { itemName },
|
||||
defaultMessage: '"{itemName}" deleted successfully.',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_LIST_EXPORTED_SUCCESSFULLY = (listName: string) =>
|
||||
i18n.translate('xpack.securitySolution.exceptions.list.exported_successfully', {
|
||||
values: { listName },
|
||||
defaultMessage: '{listName} exported successfully',
|
||||
});
|
||||
|
||||
export const EXCEPTION_LIST_DELETED_SUCCESSFULLY = (listName: string) =>
|
||||
i18n.translate('xpack.securitySolution.exceptions.list.deleted_successfully', {
|
||||
values: { listName },
|
||||
defaultMessage: '{listName} deleted successfully',
|
||||
});
|
||||
export const MANAGE_RULES_CANCEL = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.list.manage_rules_cancel',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
);
|
||||
|
||||
export const MANAGE_RULES_SAVE = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.list.manage_rules_save',
|
||||
{
|
||||
defaultMessage: 'Save',
|
||||
}
|
||||
);
|
||||
export const MANAGE_RULES_HEADER = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.list.manage_rules_header',
|
||||
{
|
||||
defaultMessage: 'Manege rules',
|
||||
}
|
||||
);
|
||||
|
||||
export const MANAGE_RULES_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.list.manage_rules_description',
|
||||
{
|
||||
defaultMessage: 'Link or unlink rules to this exception list.',
|
||||
}
|
||||
);
|
||||
|
||||
export const DELETE_EXCEPTION_LIST = i18n.translate(
|
||||
'xpack.securitySolution.exceptionsTable.deleteExceptionList',
|
||||
{
|
||||
defaultMessage: 'Delete Exception List',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXPORT_EXCEPTION_LIST = i18n.translate(
|
||||
'xpack.securitySolution.exceptionsTable.exportExceptionList',
|
||||
{
|
||||
defaultMessage: 'Export Exception List',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 EXCEPTION_ITEM_CARD_EDIT_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.list.exception.item.card.edit.label',
|
||||
{
|
||||
defaultMessage: 'Edit rule exception',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_ITEM_CARD_DELETE_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.list.exception.item.card.delete.label',
|
||||
{
|
||||
defaultMessage: 'Delete rule exception',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_UTILITY_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.list.utility.title',
|
||||
{
|
||||
defaultMessage: 'rule exceptions',
|
||||
}
|
||||
);
|
|
@ -202,10 +202,74 @@ export const IMPORT_EXCEPTION_LIST_BUTTON = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const CREATE_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.manageExceptions.create',
|
||||
export const IMPORT_EXCEPTION_LIST_HEADER = i18n.translate(
|
||||
'xpack.securitySolution.exceptionsTable.importExceptionListFlyoutHeader',
|
||||
{
|
||||
defaultMessage: 'Create',
|
||||
defaultMessage: 'Import shared exception list',
|
||||
}
|
||||
);
|
||||
|
||||
export const IMPORT_EXCEPTION_LIST_BODY = i18n.translate(
|
||||
'xpack.securitySolution.exceptionsTable.importExceptionListFlyoutBody',
|
||||
{
|
||||
defaultMessage: 'Select shared exception lists to import',
|
||||
}
|
||||
);
|
||||
export const IMPORT_EXCEPTION_LIST_WARNING = i18n.translate(
|
||||
'xpack.securitySolution.exceptionsTable.importExceptionListWarning',
|
||||
{
|
||||
defaultMessage: 'We found a pre-existing list with that id',
|
||||
}
|
||||
);
|
||||
|
||||
export const IMPORT_EXCEPTION_LIST_OVERWRITE = i18n.translate(
|
||||
'xpack.securitySolution.exceptionsTable.importExceptionListOverwrite',
|
||||
{
|
||||
defaultMessage: 'Overwrite the existing list',
|
||||
}
|
||||
);
|
||||
|
||||
export const IMPORT_EXCEPTION_LIST_AS_NEW_LIST = i18n.translate(
|
||||
'xpack.securitySolution.exceptionsTable.importExceptionListAsNewList',
|
||||
{
|
||||
defaultMessage: 'Create new list',
|
||||
}
|
||||
);
|
||||
|
||||
export const READ_ONLY_BADGE_TOOLTIP = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.badge.readOnly.tooltip',
|
||||
{
|
||||
defaultMessage: 'Unable to create, edit or delete exceptions',
|
||||
}
|
||||
);
|
||||
|
||||
export const CLOSE_FLYOUT = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionListsCloseImportFlyout',
|
||||
{
|
||||
defaultMessage: 'Close',
|
||||
}
|
||||
);
|
||||
export const IMPORT_PROMPT = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionListsFilePickerPrompt',
|
||||
{
|
||||
defaultMessage: 'Select or drag and drop multiple files',
|
||||
}
|
||||
);
|
||||
|
||||
export const RULES = i18n.translate('xpack.securitySolution.exceptionsTable.rulesCountLabel', {
|
||||
defaultMessage: 'Rules',
|
||||
});
|
||||
export const CREATED_BY = i18n.translate('xpack.securitySolution.exceptionsTable.createdBy', {
|
||||
defaultMessage: 'Created By',
|
||||
});
|
||||
|
||||
export const DATE_CREATED = i18n.translate('xpack.securitySolution.exceptionsTable.createdAt', {
|
||||
defaultMessage: 'Date created',
|
||||
});
|
||||
export const EXCEPTIONS = i18n.translate(
|
||||
'xpack.securitySolution.exceptionsTable.exceptionsCountLabel',
|
||||
{
|
||||
defaultMessage: 'Exceptions',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -222,3 +286,58 @@ export const CREATE_BUTTON_ITEM_BUTTON = i18n.translate(
|
|||
defaultMessage: 'create exception item',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_SHARED_LIST_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.createSharedExceptionListTitle',
|
||||
{
|
||||
defaultMessage: 'Create shared exception list',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_SHARED_LIST_NAME_FIELD = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.createSharedExceptionListFlyoutNameField',
|
||||
{
|
||||
defaultMessage: 'Shared exception list name',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_SHARED_LIST_NAME_FIELD_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.createSharedExceptionListFlyoutNameFieldPlaceholder',
|
||||
{
|
||||
defaultMessage: 'New exception list',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_SHARED_LIST_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.createSharedExceptionListFlyoutDescription',
|
||||
{
|
||||
defaultMessage: 'Description (optional)',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_SHARED_LIST_DESCRIPTION_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.createSharedExceptionListFlyoutDescriptionPlaceholder',
|
||||
{
|
||||
defaultMessage: 'New exception list',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.createSharedExceptionListFlyoutCreateButton',
|
||||
{
|
||||
defaultMessage: 'Create shared exception list',
|
||||
}
|
||||
);
|
||||
|
||||
export const getSuccessText = (listName: string) =>
|
||||
i18n.translate('xpack.securitySolution.exceptions.createSharedExceptionListSuccessDescription', {
|
||||
defaultMessage: 'list with name ${listName} was created!',
|
||||
values: { listName },
|
||||
});
|
||||
|
||||
export const SUCCESS_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.createSharedExceptionListSuccessTitle',
|
||||
{
|
||||
defaultMessage: 'created list',
|
||||
}
|
||||
);
|
|
@ -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 { listIDsCannotBeEdited } from '../config';
|
||||
import type { ExceptionListInfo } from '../hooks/use_all_exception_lists';
|
||||
|
||||
export const checkIfListCannotBeEdited = (list: ExceptionListInfo) => {
|
||||
return !!listIDsCannotBeEdited.find((id) => id === list.list_id);
|
||||
};
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* 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 COMMENTS_SHOW = (comments: number) =>
|
||||
i18n.translate('xpack.securitySolution.exceptions.showCommentsLabel', {
|
||||
values: { comments },
|
||||
defaultMessage: 'Show ({comments}) {comments, plural, =1 {Comment} other {Comments}}',
|
||||
});
|
||||
|
||||
export const COMMENTS_HIDE = (comments: number) =>
|
||||
i18n.translate('xpack.securitySolution.exceptions.hideCommentsLabel', {
|
||||
values: { comments },
|
||||
defaultMessage: 'Hide ({comments}) {comments, plural, =1 {Comment} other {Comments}}',
|
||||
});
|
||||
|
||||
export const COMMENT_EVENT = i18n.translate('xpack.securitySolution.exceptions.commentEventLabel', {
|
||||
defaultMessage: 'added a comment',
|
||||
});
|
||||
|
||||
export const OPERATING_SYSTEM_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.operatingSystemFullLabel',
|
||||
{
|
||||
defaultMessage: 'Operating System',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_TO_ENDPOINT_LIST = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.viewer.addToEndpointListLabel',
|
||||
{
|
||||
defaultMessage: 'Add endpoint exception',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_TO_DETECTIONS_LIST = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel',
|
||||
{
|
||||
defaultMessage: 'Add rule exception',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_COMMENT_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.viewer.addCommentPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Add a new comment...',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_TO_CLIPBOARD = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.viewer.addToClipboard',
|
||||
{
|
||||
defaultMessage: 'Comment',
|
||||
}
|
||||
);
|
||||
|
||||
export const CLEAR_EXCEPTIONS_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.clearExceptionsLabel',
|
||||
{
|
||||
defaultMessage: 'Remove Exception List',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_EXCEPTION_FETCH_404_ERROR = (listId: string) =>
|
||||
i18n.translate('xpack.securitySolution.exceptions.fetch404Error', {
|
||||
values: { listId },
|
||||
defaultMessage:
|
||||
'The associated exception list ({listId}) no longer exists. Please remove the missing exception list to add additional exceptions to the detection rule.',
|
||||
});
|
||||
|
||||
export const ADD_EXCEPTION_FETCH_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.fetchError',
|
||||
{
|
||||
defaultMessage: 'Error fetching exception list',
|
||||
}
|
||||
);
|
||||
|
||||
export const ERROR = i18n.translate('xpack.securitySolution.exceptions.errorLabel', {
|
||||
defaultMessage: 'Error',
|
||||
});
|
||||
|
||||
export const CANCEL = i18n.translate('xpack.securitySolution.exceptions.cancelLabel', {
|
||||
defaultMessage: 'Cancel',
|
||||
});
|
||||
|
||||
export const MODAL_ERROR_ACCORDION_TEXT = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.modalErrorAccordionText',
|
||||
{
|
||||
defaultMessage: 'Show rule reference information:',
|
||||
}
|
||||
);
|
||||
|
||||
export const DISASSOCIATE_LIST_SUCCESS = (id: string) =>
|
||||
i18n.translate('xpack.securitySolution.exceptions.disassociateListSuccessText', {
|
||||
values: { id },
|
||||
defaultMessage: 'Exception list ({id}) has successfully been removed',
|
||||
});
|
||||
|
||||
export const DISASSOCIATE_EXCEPTION_LIST_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.disassociateExceptionListError',
|
||||
{
|
||||
defaultMessage: 'Failed to remove exception list',
|
||||
}
|
||||
);
|
||||
|
||||
export const OPERATING_SYSTEM_WINDOWS = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.operatingSystemWindows',
|
||||
{
|
||||
defaultMessage: 'Windows',
|
||||
}
|
||||
);
|
||||
|
||||
export const OPERATING_SYSTEM_MAC = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.operatingSystemMac',
|
||||
{
|
||||
defaultMessage: 'macOS',
|
||||
}
|
||||
);
|
||||
|
||||
export const OPERATING_SYSTEM_WINDOWS_AND_MAC = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.operatingSystemWindowsAndMac',
|
||||
{
|
||||
defaultMessage: 'Windows and macOS',
|
||||
}
|
||||
);
|
||||
|
||||
export const OPERATING_SYSTEM_LINUX = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.operatingSystemLinux',
|
||||
{
|
||||
defaultMessage: 'Linux',
|
||||
}
|
||||
);
|
||||
|
||||
export const ERROR_FETCHING_REFERENCES_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.fetchingReferencesErrorToastTitle',
|
||||
{
|
||||
defaultMessage: 'Error fetching exception references',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import { getFormattedComments } from './ui.helpers';
|
||||
import { getCommentsArrayMock } from '@kbn/lists-plugin/common/schemas/types/comment.mock';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn().mockReturnValue('123'),
|
||||
}));
|
||||
|
||||
describe('Exception helpers', () => {
|
||||
beforeEach(() => {
|
||||
moment.tz.setDefault('UTC');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
moment.tz.setDefault('Browser');
|
||||
});
|
||||
|
||||
describe('#getFormattedComments', () => {
|
||||
test('it returns formatted comment object with username and timestamp', () => {
|
||||
const payload = getCommentsArrayMock();
|
||||
const result = getFormattedComments(payload);
|
||||
|
||||
expect(result[0].username).toEqual('some user');
|
||||
expect(result[0].timestamp).toEqual('on Apr 20th 2020 @ 15:25:31');
|
||||
});
|
||||
|
||||
test('it returns formatted timeline icon with comment users initial', () => {
|
||||
const payload = getCommentsArrayMock();
|
||||
const result = getFormattedComments(payload);
|
||||
|
||||
const wrapper = mount<React.ReactElement>(result[0].timelineAvatar as React.ReactElement);
|
||||
|
||||
expect(wrapper.text()).toEqual('SU');
|
||||
});
|
||||
|
||||
test('it returns comment text', () => {
|
||||
const payload = getCommentsArrayMock();
|
||||
const result = getFormattedComments(payload);
|
||||
|
||||
const wrapper = mount<React.ReactElement>(result[0].children as React.ReactElement);
|
||||
|
||||
expect(wrapper.text()).toEqual('some old comment');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 type { EuiCommentProps } from '@elastic/eui';
|
||||
import { EuiText, EuiAvatar } from '@elastic/eui';
|
||||
|
||||
import type { CommentsArray } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import moment from 'moment';
|
||||
import * as i18n from './translations';
|
||||
import { WithCopyToClipboard } from '../../common/lib/clipboard/with_copy_to_clipboard';
|
||||
|
||||
/**
|
||||
* Formats ExceptionItem.comments into EuiCommentList format
|
||||
*
|
||||
* @param comments ExceptionItem.comments
|
||||
*/
|
||||
export const getFormattedComments = (comments: CommentsArray): EuiCommentProps[] =>
|
||||
comments.map((commentItem) => ({
|
||||
username: commentItem.created_by,
|
||||
timestamp: moment(commentItem.created_at).format('on MMM Do YYYY @ HH:mm:ss'),
|
||||
event: i18n.COMMENT_EVENT,
|
||||
timelineAvatar: <EuiAvatar size="l" name={commentItem.created_by.toUpperCase()} />,
|
||||
children: <EuiText size="s">{commentItem.comment}</EuiText>,
|
||||
actions: (
|
||||
<WithCopyToClipboard
|
||||
data-test-subj="copy-to-clipboard"
|
||||
text={commentItem.comment}
|
||||
titleSummary={i18n.ADD_TO_CLIPBOARD}
|
||||
/>
|
||||
),
|
||||
}));
|
Loading…
Add table
Add a link
Reference in a new issue