[Security Solution] [Endpoint] Event filters uses the new card design (#114126)
* Adds new card design to event filters and also adds comments list * Adds nested comments * Hides comments if there are no commentes * Fixes i18n check error because duplicated key * Fix wrong type and unit test * Fixes ts error * Address pr comments and fix unit tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
db4bcdee2c
commit
0bf0b94ee0
13 changed files with 329 additions and 77 deletions
|
@ -25,7 +25,7 @@ describe.each([
|
||||||
) => ReturnType<AppContextTestRender['render']>;
|
) => ReturnType<AppContextTestRender['render']>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
item = generateItem();
|
item = generateItem() as AnyArtifact;
|
||||||
appTestContext = createAppRootMockRenderer();
|
appTestContext = createAppRootMockRenderer();
|
||||||
render = (props = {}) => {
|
render = (props = {}) => {
|
||||||
renderResult = appTestContext.render(
|
renderResult = appTestContext.render(
|
||||||
|
@ -77,13 +77,31 @@ describe.each([
|
||||||
expect(renderResult.getByTestId('testCard-description').textContent).toEqual(item.description);
|
expect(renderResult.getByTestId('testCard-description').textContent).toEqual(item.description);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shouldn't display description", async () => {
|
||||||
|
render({ hideDescription: true });
|
||||||
|
expect(renderResult.queryByTestId('testCard-description')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('should display default empty value if description does not exist', async () => {
|
it('should display default empty value if description does not exist', async () => {
|
||||||
item.description = undefined;
|
item.description = undefined;
|
||||||
render();
|
render();
|
||||||
|
|
||||||
expect(renderResult.getByTestId('testCard-description').textContent).toEqual('—');
|
expect(renderResult.getByTestId('testCard-description').textContent).toEqual('—');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should display comments if one exists', async () => {
|
||||||
|
render();
|
||||||
|
if (isTrustedApp(item)) {
|
||||||
|
expect(renderResult.queryByTestId('testCard-comments')).toBeNull();
|
||||||
|
} else {
|
||||||
|
expect(renderResult.queryByTestId('testCard-comments')).not.toBeNull();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldn't display comments", async () => {
|
||||||
|
render({ hideComments: true });
|
||||||
|
expect(renderResult.queryByTestId('testCard-comments')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('should display OS and criteria conditions', () => {
|
it('should display OS and criteria conditions', () => {
|
||||||
render();
|
render();
|
||||||
|
|
||||||
|
|
|
@ -16,10 +16,11 @@ import { useNormalizedArtifact } from './hooks/use_normalized_artifact';
|
||||||
import { useTestIdGenerator } from '../hooks/use_test_id_generator';
|
import { useTestIdGenerator } from '../hooks/use_test_id_generator';
|
||||||
import { CardContainerPanel } from './components/card_container_panel';
|
import { CardContainerPanel } from './components/card_container_panel';
|
||||||
import { CardSectionPanel } from './components/card_section_panel';
|
import { CardSectionPanel } from './components/card_section_panel';
|
||||||
|
import { CardComments } from './components/card_comments';
|
||||||
import { usePolicyNavLinks } from './hooks/use_policy_nav_links';
|
import { usePolicyNavLinks } from './hooks/use_policy_nav_links';
|
||||||
import { MaybeImmutable } from '../../../../common/endpoint/types';
|
import { MaybeImmutable } from '../../../../common/endpoint/types';
|
||||||
|
|
||||||
export interface ArtifactEntryCardProps extends CommonProps {
|
export interface CommonArtifactEntryCardProps extends CommonProps {
|
||||||
item: MaybeImmutable<AnyArtifact>;
|
item: MaybeImmutable<AnyArtifact>;
|
||||||
/**
|
/**
|
||||||
* The list of actions for the card. Will display an icon with the actions in a menu if defined.
|
* The list of actions for the card. Will display an icon with the actions in a menu if defined.
|
||||||
|
@ -34,12 +35,27 @@ export interface ArtifactEntryCardProps extends CommonProps {
|
||||||
policies?: MenuItemPropsByPolicyId;
|
policies?: MenuItemPropsByPolicyId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ArtifactEntryCardProps extends CommonArtifactEntryCardProps {
|
||||||
|
// A flag to hide description section, false by default
|
||||||
|
hideDescription?: boolean;
|
||||||
|
// A flag to hide comments section, false by default
|
||||||
|
hideComments?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display Artifact Items (ex. Trusted App, Event Filter, etc) as a card.
|
* Display Artifact Items (ex. Trusted App, Event Filter, etc) as a card.
|
||||||
* This component is a TS Generic that allows you to set what the Item type is
|
* This component is a TS Generic that allows you to set what the Item type is
|
||||||
*/
|
*/
|
||||||
export const ArtifactEntryCard = memo<ArtifactEntryCardProps>(
|
export const ArtifactEntryCard = memo<ArtifactEntryCardProps>(
|
||||||
({ item, policies, actions, 'data-test-subj': dataTestSubj, ...commonProps }) => {
|
({
|
||||||
|
item,
|
||||||
|
policies,
|
||||||
|
actions,
|
||||||
|
hideDescription = false,
|
||||||
|
hideComments = false,
|
||||||
|
'data-test-subj': dataTestSubj,
|
||||||
|
...commonProps
|
||||||
|
}) => {
|
||||||
const artifact = useNormalizedArtifact(item as AnyArtifact);
|
const artifact = useNormalizedArtifact(item as AnyArtifact);
|
||||||
const getTestId = useTestIdGenerator(dataTestSubj);
|
const getTestId = useTestIdGenerator(dataTestSubj);
|
||||||
const policyNavLinks = usePolicyNavLinks(artifact, policies);
|
const policyNavLinks = usePolicyNavLinks(artifact, policies);
|
||||||
|
@ -63,11 +79,16 @@ export const ArtifactEntryCard = memo<ArtifactEntryCardProps>(
|
||||||
|
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
|
|
||||||
<EuiText>
|
{!hideDescription ? (
|
||||||
<p data-test-subj={getTestId('description')}>
|
<EuiText>
|
||||||
{artifact.description || getEmptyValue()}
|
<p data-test-subj={getTestId('description')}>
|
||||||
</p>
|
{artifact.description || getEmptyValue()}
|
||||||
</EuiText>
|
</p>
|
||||||
|
</EuiText>
|
||||||
|
) : null}
|
||||||
|
{!hideComments ? (
|
||||||
|
<CardComments comments={artifact.comments} data-test-subj={getTestId('comments')} />
|
||||||
|
) : null}
|
||||||
</CardSectionPanel>
|
</CardSectionPanel>
|
||||||
|
|
||||||
<EuiHorizontalRule margin="none" />
|
<EuiHorizontalRule margin="none" />
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
import { EuiHorizontalRule } from '@elastic/eui';
|
import { EuiHorizontalRule } from '@elastic/eui';
|
||||||
import { ArtifactEntryCardProps } from './artifact_entry_card';
|
import { CommonArtifactEntryCardProps } from './artifact_entry_card';
|
||||||
import { CardContainerPanel } from './components/card_container_panel';
|
import { CardContainerPanel } from './components/card_container_panel';
|
||||||
import { useNormalizedArtifact } from './hooks/use_normalized_artifact';
|
import { useNormalizedArtifact } from './hooks/use_normalized_artifact';
|
||||||
import { useTestIdGenerator } from '../hooks/use_test_id_generator';
|
import { useTestIdGenerator } from '../hooks/use_test_id_generator';
|
||||||
|
@ -15,7 +15,7 @@ import { CardSectionPanel } from './components/card_section_panel';
|
||||||
import { CriteriaConditions, CriteriaConditionsProps } from './components/criteria_conditions';
|
import { CriteriaConditions, CriteriaConditionsProps } from './components/criteria_conditions';
|
||||||
import { CardCompressedHeader } from './components/card_compressed_header';
|
import { CardCompressedHeader } from './components/card_compressed_header';
|
||||||
|
|
||||||
export interface ArtifactEntryCollapsibleCardProps extends ArtifactEntryCardProps {
|
export interface ArtifactEntryCollapsibleCardProps extends CommonArtifactEntryCardProps {
|
||||||
onExpandCollapse: () => void;
|
onExpandCollapse: () => void;
|
||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* 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, useMemo, useCallback, useState } from 'react';
|
||||||
|
import {
|
||||||
|
CommonProps,
|
||||||
|
EuiAccordion,
|
||||||
|
EuiCommentList,
|
||||||
|
EuiCommentProps,
|
||||||
|
EuiButtonEmpty,
|
||||||
|
EuiSpacer,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import { isEmpty } from 'lodash/fp';
|
||||||
|
import { useTestIdGenerator } from '../../hooks/use_test_id_generator';
|
||||||
|
import { CardActionsFlexItemProps } from './card_actions_flex_item';
|
||||||
|
import { ArtifactInfo } from '../types';
|
||||||
|
import { getFormattedComments } from '../utils/get_formatted_comments';
|
||||||
|
import { SHOW_COMMENTS_LABEL, HIDE_COMMENTS_LABEL } from './translations';
|
||||||
|
|
||||||
|
export interface CardCommentsProps
|
||||||
|
extends CardActionsFlexItemProps,
|
||||||
|
Pick<CommonProps, 'data-test-subj'> {
|
||||||
|
comments: ArtifactInfo['comments'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CardComments = memo<CardCommentsProps>(
|
||||||
|
({ comments, 'data-test-subj': dataTestSubj }) => {
|
||||||
|
const getTestId = useTestIdGenerator(dataTestSubj);
|
||||||
|
|
||||||
|
const [showComments, setShowComments] = useState(false);
|
||||||
|
const onCommentsClick = useCallback((): void => {
|
||||||
|
setShowComments(!showComments);
|
||||||
|
}, [setShowComments, showComments]);
|
||||||
|
const formattedComments = useMemo((): EuiCommentProps[] => {
|
||||||
|
return getFormattedComments(comments);
|
||||||
|
}, [comments]);
|
||||||
|
|
||||||
|
const buttonText = useMemo(
|
||||||
|
() =>
|
||||||
|
showComments ? HIDE_COMMENTS_LABEL(comments.length) : SHOW_COMMENTS_LABEL(comments.length),
|
||||||
|
[comments.length, showComments]
|
||||||
|
);
|
||||||
|
|
||||||
|
return !isEmpty(comments) ? (
|
||||||
|
<div data-test-subj={dataTestSubj}>
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
<EuiButtonEmpty
|
||||||
|
onClick={onCommentsClick}
|
||||||
|
flush="left"
|
||||||
|
size="xs"
|
||||||
|
data-test-subj={getTestId('label')}
|
||||||
|
>
|
||||||
|
{buttonText}
|
||||||
|
</EuiButtonEmpty>
|
||||||
|
<EuiAccordion id={'1'} arrowDisplay="none" forceState={showComments ? 'open' : 'closed'}>
|
||||||
|
<EuiSpacer size="m" />
|
||||||
|
<EuiCommentList comments={formattedComments} data-test-subj={getTestId('list')} />
|
||||||
|
</EuiAccordion>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
CardComments.displayName = 'CardComments';
|
|
@ -5,8 +5,9 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { memo } from 'react';
|
import React, { memo, useCallback } from 'react';
|
||||||
import { CommonProps, EuiExpression } from '@elastic/eui';
|
import { CommonProps, EuiExpression, EuiToken, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||||
|
import styled from 'styled-components';
|
||||||
import { ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
import { ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||||
import {
|
import {
|
||||||
CONDITION_OS,
|
CONDITION_OS,
|
||||||
|
@ -21,7 +22,7 @@ import {
|
||||||
CONDITION_OPERATOR_TYPE_EXISTS,
|
CONDITION_OPERATOR_TYPE_EXISTS,
|
||||||
CONDITION_OPERATOR_TYPE_LIST,
|
CONDITION_OPERATOR_TYPE_LIST,
|
||||||
} from './translations';
|
} from './translations';
|
||||||
import { ArtifactInfo } from '../types';
|
import { ArtifactInfo, ArtifactInfoEntry } from '../types';
|
||||||
import { useTestIdGenerator } from '../../hooks/use_test_id_generator';
|
import { useTestIdGenerator } from '../../hooks/use_test_id_generator';
|
||||||
|
|
||||||
const OS_LABELS = Object.freeze({
|
const OS_LABELS = Object.freeze({
|
||||||
|
@ -39,6 +40,15 @@ const OPERATOR_TYPE_LABELS = Object.freeze({
|
||||||
[ListOperatorTypeEnum.LIST]: CONDITION_OPERATOR_TYPE_LIST,
|
[ListOperatorTypeEnum.LIST]: CONDITION_OPERATOR_TYPE_LIST,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const EuiFlexGroupNested = styled(EuiFlexGroup)`
|
||||||
|
margin-left: ${({ theme }) => theme.eui.spacerSizes.l};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const EuiFlexItemNested = styled(EuiFlexItem)`
|
||||||
|
margin-bottom: 6px !important;
|
||||||
|
margin-top: 6px !important;
|
||||||
|
`;
|
||||||
|
|
||||||
export type CriteriaConditionsProps = Pick<ArtifactInfo, 'os' | 'entries'> &
|
export type CriteriaConditionsProps = Pick<ArtifactInfo, 'os' | 'entries'> &
|
||||||
Pick<CommonProps, 'data-test-subj'>;
|
Pick<CommonProps, 'data-test-subj'>;
|
||||||
|
|
||||||
|
@ -46,6 +56,44 @@ export const CriteriaConditions = memo<CriteriaConditionsProps>(
|
||||||
({ os, entries, 'data-test-subj': dataTestSubj }) => {
|
({ os, entries, 'data-test-subj': dataTestSubj }) => {
|
||||||
const getTestId = useTestIdGenerator(dataTestSubj);
|
const getTestId = useTestIdGenerator(dataTestSubj);
|
||||||
|
|
||||||
|
const getNestedEntriesContent = useCallback(
|
||||||
|
(type: string, nestedEntries: ArtifactInfoEntry[]) => {
|
||||||
|
if (type === 'nested' && nestedEntries.length) {
|
||||||
|
return nestedEntries.map(
|
||||||
|
({ field: nestedField, type: nestedType, value: nestedValue }) => {
|
||||||
|
return (
|
||||||
|
<EuiFlexGroupNested
|
||||||
|
data-test-subj={getTestId('nestedCondition')}
|
||||||
|
key={nestedField + nestedType + nestedValue}
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
gutterSize="m"
|
||||||
|
responsive={false}
|
||||||
|
>
|
||||||
|
<EuiFlexItemNested grow={false}>
|
||||||
|
<EuiToken iconType="tokenNested" size="s" />
|
||||||
|
</EuiFlexItemNested>
|
||||||
|
<EuiFlexItemNested grow={false}>
|
||||||
|
<EuiExpression description={''} value={nestedField} color="subdued" />
|
||||||
|
</EuiFlexItemNested>
|
||||||
|
<EuiFlexItemNested grow={false}>
|
||||||
|
<EuiExpression
|
||||||
|
description={
|
||||||
|
OPERATOR_TYPE_LABELS[nestedType as keyof typeof OPERATOR_TYPE_LABELS] ??
|
||||||
|
nestedType
|
||||||
|
}
|
||||||
|
value={nestedValue}
|
||||||
|
/>
|
||||||
|
</EuiFlexItemNested>
|
||||||
|
</EuiFlexGroupNested>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[getTestId]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-test-subj={dataTestSubj}>
|
<div data-test-subj={dataTestSubj}>
|
||||||
<div data-test-subj={getTestId('os')}>
|
<div data-test-subj={getTestId('os')}>
|
||||||
|
@ -57,7 +105,7 @@ export const CriteriaConditions = memo<CriteriaConditionsProps>(
|
||||||
/>
|
/>
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
{entries.map(({ field, type, value }) => {
|
{entries.map(({ field, type, value, entries: nestedEntries = [] }) => {
|
||||||
return (
|
return (
|
||||||
<div data-test-subj={getTestId('condition')} key={field + type + value}>
|
<div data-test-subj={getTestId('condition')} key={field + type + value}>
|
||||||
<EuiExpression description={CONDITION_AND} value={field} color="subdued" />
|
<EuiExpression description={CONDITION_AND} value={field} color="subdued" />
|
||||||
|
@ -67,6 +115,7 @@ export const CriteriaConditions = memo<CriteriaConditionsProps>(
|
||||||
}
|
}
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
|
{getNestedEntriesContent(type, nestedEntries)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -114,3 +114,15 @@ export const COLLAPSE_ACTION = i18n.translate(
|
||||||
defaultMessage: 'Collapse',
|
defaultMessage: 'Collapse',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const SHOW_COMMENTS_LABEL = (count: number = 0) =>
|
||||||
|
i18n.translate('xpack.securitySolution.artifactCard.comments.label.show', {
|
||||||
|
defaultMessage: 'Show comments ({count})',
|
||||||
|
values: { count },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const HIDE_COMMENTS_LABEL = (count: number = 0) =>
|
||||||
|
i18n.translate('xpack.securitySolution.artifactCard.comments.label.hide', {
|
||||||
|
defaultMessage: 'Hide comments ({count})',
|
||||||
|
values: { count },
|
||||||
|
});
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
|
import uuid from 'uuid';
|
||||||
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||||
import { TrustedAppGenerator } from '../../../../common/endpoint/data_generators/trusted_app_generator';
|
import { TrustedAppGenerator } from '../../../../common/endpoint/data_generators/trusted_app_generator';
|
||||||
import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
|
import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
|
||||||
|
@ -54,6 +55,14 @@ export const getExceptionProviderMock = (): ExceptionListItemSchema => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
tags: ['policy:all'],
|
tags: ['policy:all'],
|
||||||
|
comments: [
|
||||||
|
{
|
||||||
|
id: uuid.v4(),
|
||||||
|
comment: 'test',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
created_by: 'Justa',
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,6 +10,13 @@ import { EffectScope, TrustedApp } from '../../../../common/endpoint/types';
|
||||||
import { ContextMenuItemNavByRouterProps } from '../context_menu_with_router_support/context_menu_item_nav_by_router';
|
import { ContextMenuItemNavByRouterProps } from '../context_menu_with_router_support/context_menu_item_nav_by_router';
|
||||||
|
|
||||||
export type AnyArtifact = ExceptionListItemSchema | TrustedApp;
|
export type AnyArtifact = ExceptionListItemSchema | TrustedApp;
|
||||||
|
export interface ArtifactInfoEntry {
|
||||||
|
field: string;
|
||||||
|
type: string;
|
||||||
|
operator: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
type ArtifactInfoEntries = ArtifactInfoEntry & { entries?: ArtifactInfoEntry[] };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A normalized structured that is used internally through out the card's components.
|
* A normalized structured that is used internally through out the card's components.
|
||||||
|
@ -17,16 +24,11 @@ export type AnyArtifact = ExceptionListItemSchema | TrustedApp;
|
||||||
export interface ArtifactInfo
|
export interface ArtifactInfo
|
||||||
extends Pick<
|
extends Pick<
|
||||||
ExceptionListItemSchema,
|
ExceptionListItemSchema,
|
||||||
'name' | 'created_at' | 'updated_at' | 'created_by' | 'updated_by' | 'description'
|
'name' | 'created_at' | 'updated_at' | 'created_by' | 'updated_by' | 'description' | 'comments'
|
||||||
> {
|
> {
|
||||||
effectScope: EffectScope;
|
effectScope: EffectScope;
|
||||||
os: string;
|
os: string;
|
||||||
entries: Array<{
|
entries: ArtifactInfoEntries[];
|
||||||
field: string;
|
|
||||||
type: string;
|
|
||||||
operator: string;
|
|
||||||
value: string;
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuItemPropsByPolicyId {
|
export interface MenuItemPropsByPolicyId {
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* 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 { EuiAvatar, EuiText, EuiCommentProps } from '@elastic/eui';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { CommentsArray } from '@kbn/securitysolution-io-ts-list-types';
|
||||||
|
import { COMMENT_EVENT } from '../../../../common/components/exceptions/translations';
|
||||||
|
import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date';
|
||||||
|
|
||||||
|
const CustomEuiAvatar = styled(EuiAvatar)`
|
||||||
|
background-color: ${({ theme }) => theme.eui.euiColorLightShade} !important;
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats ExceptionItem.comments into EuiCommentList format
|
||||||
|
*
|
||||||
|
* @param comments ExceptionItem.comments
|
||||||
|
*/
|
||||||
|
export const getFormattedComments = (comments: CommentsArray): EuiCommentProps[] => {
|
||||||
|
return comments.map((commentItem) => ({
|
||||||
|
username: commentItem.created_by,
|
||||||
|
timestamp: (
|
||||||
|
<FormattedRelativePreferenceDate value={commentItem.created_at} dateFormat="MMM D, YYYY" />
|
||||||
|
),
|
||||||
|
event: COMMENT_EVENT,
|
||||||
|
timelineIcon: <CustomEuiAvatar size="s" name={commentItem.created_by} />,
|
||||||
|
children: <EuiText size="s">{commentItem.comment}</EuiText>,
|
||||||
|
}));
|
||||||
|
};
|
|
@ -24,6 +24,7 @@ export const mapToArtifactInfo = (_item: MaybeImmutable<AnyArtifact>): ArtifactI
|
||||||
updated_at,
|
updated_at,
|
||||||
updated_by,
|
updated_by,
|
||||||
description,
|
description,
|
||||||
|
comments: isTrustedApp(item) ? [] : item.comments,
|
||||||
entries: entries as unknown as ArtifactInfo['entries'],
|
entries: entries as unknown as ArtifactInfo['entries'],
|
||||||
os: isTrustedApp(item) ? item.os : getOsFromExceptionItem(item),
|
os: isTrustedApp(item) ? item.os : getOsFromExceptionItem(item),
|
||||||
effectScope: isTrustedApp(item) ? item.effectScope : getEffectScopeFromExceptionItem(item),
|
effectScope: isTrustedApp(item) ? item.effectScope : getEffectScopeFromExceptionItem(item),
|
||||||
|
|
|
@ -109,19 +109,15 @@ describe('When on the Event Filters List Page', () => {
|
||||||
it('should render expected fields on card', async () => {
|
it('should render expected fields on card', async () => {
|
||||||
render();
|
render();
|
||||||
await dataReceived();
|
await dataReceived();
|
||||||
const eventMeta = ([] as HTMLElement[]).map.call(
|
|
||||||
renderResult.getByTestId('exceptionsViewerItemDetails').querySelectorAll('dd'),
|
|
||||||
(ele) => ele.textContent
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(eventMeta).toEqual([
|
[
|
||||||
'some name',
|
['subHeader-touchedBy-createdBy-value', 'some user'],
|
||||||
'April 20th 2020 @ 11:25:31',
|
['subHeader-touchedBy-updatedBy-value', 'some user'],
|
||||||
'some user',
|
['header-created-value', '4/20/2020'],
|
||||||
'April 20th 2020 @ 11:25:31',
|
['header-updated-value', '4/20/2020'],
|
||||||
'some user',
|
].forEach(([suffix, value]) =>
|
||||||
'some description',
|
expect(renderResult.getByTestId(`eventFilterCard-${suffix}`).textContent).toEqual(value)
|
||||||
]);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show API error if one is encountered', async () => {
|
it('should show API error if one is encountered', async () => {
|
||||||
|
@ -143,8 +139,13 @@ describe('When on the Event Filters List Page', () => {
|
||||||
it('should show modal when delete is clicked on a card', async () => {
|
it('should show modal when delete is clicked on a card', async () => {
|
||||||
render();
|
render();
|
||||||
await dataReceived();
|
await dataReceived();
|
||||||
act(() => {
|
|
||||||
fireEvent.click(renderResult.getByTestId('exceptionsViewerDeleteBtn'));
|
await act(async () => {
|
||||||
|
(await renderResult.findAllByTestId('eventFilterCard-header-actions-button'))[0].click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
(await renderResult.findByTestId('deleteEventFilterAction')).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
|
|
@ -36,16 +36,20 @@ import {
|
||||||
} from '../store/selector';
|
} from '../store/selector';
|
||||||
import { PaginatedContent, PaginatedContentProps } from '../../../components/paginated_content';
|
import { PaginatedContent, PaginatedContentProps } from '../../../components/paginated_content';
|
||||||
import { Immutable, ListPageRouteState } from '../../../../../common/endpoint/types';
|
import { Immutable, ListPageRouteState } from '../../../../../common/endpoint/types';
|
||||||
|
import { ExceptionItem } from '../../../../common/components/exceptions/viewer/exception_item';
|
||||||
import {
|
import {
|
||||||
ExceptionItem,
|
AnyArtifact,
|
||||||
ExceptionItemProps,
|
ArtifactEntryCard,
|
||||||
} from '../../../../common/components/exceptions/viewer/exception_item';
|
ArtifactEntryCardProps,
|
||||||
|
} from '../../../components/artifact_entry_card';
|
||||||
import { EventFilterDeleteModal } from './components/event_filter_delete_modal';
|
import { EventFilterDeleteModal } from './components/event_filter_delete_modal';
|
||||||
|
|
||||||
import { SearchExceptions } from '../../../components/search_exceptions';
|
import { SearchExceptions } from '../../../components/search_exceptions';
|
||||||
import { BackToExternalAppButton } from '../../../components/back_to_external_app_button';
|
import { BackToExternalAppButton } from '../../../components/back_to_external_app_button';
|
||||||
import { ABOUT_EVENT_FILTERS } from './translations';
|
import { ABOUT_EVENT_FILTERS } from './translations';
|
||||||
|
|
||||||
|
type ArtifactEntryCardType = typeof ArtifactEntryCard;
|
||||||
|
|
||||||
type EventListPaginatedContent = PaginatedContentProps<
|
type EventListPaginatedContent = PaginatedContentProps<
|
||||||
Immutable<ExceptionListItemSchema>,
|
Immutable<ExceptionListItemSchema>,
|
||||||
typeof ExceptionItem
|
typeof ExceptionItem
|
||||||
|
@ -61,6 +65,20 @@ const AdministrationListPage = styled(_AdministrationListPage)`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const EDIT_EVENT_FILTER_ACTION_LABEL = i18n.translate(
|
||||||
|
'xpack.securitySolution.eventFilters.list.cardAction.edit',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Edit event filter',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const DELETE_EVENT_FILTER_ACTION_LABEL = i18n.translate(
|
||||||
|
'xpack.securitySolution.eventFilters.list.cardAction.delete',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Delete event filter',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const EventFiltersListPage = memo(() => {
|
export const EventFiltersListPage = memo(() => {
|
||||||
const { state: routeState } = useLocation<ListPageRouteState | undefined>();
|
const { state: routeState } = useLocation<ListPageRouteState | undefined>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
@ -133,41 +151,6 @@ export const EventFiltersListPage = memo(() => {
|
||||||
[navigateCallback]
|
[navigateCallback]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleItemEdit: ExceptionItemProps['onEditException'] = useCallback(
|
|
||||||
(item: ExceptionListItemSchema) => {
|
|
||||||
navigateCallback({
|
|
||||||
show: 'edit',
|
|
||||||
id: item.id,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[navigateCallback]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleItemDelete: ExceptionItemProps['onDeleteException'] = useCallback(
|
|
||||||
({ id }) => {
|
|
||||||
dispatch({
|
|
||||||
type: 'eventFilterForDeletion',
|
|
||||||
// Casting below needed due to error around the comments array needing to be mutable
|
|
||||||
payload: listItems.find((item) => item.id === id)! as ExceptionListItemSchema,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[dispatch, listItems]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleItemComponentProps: EventListPaginatedContent['itemComponentProps'] = useCallback(
|
|
||||||
(exceptionItem) => ({
|
|
||||||
exceptionItem: exceptionItem as ExceptionListItemSchema,
|
|
||||||
loadingItemIds: [],
|
|
||||||
commentsAccordionId: '',
|
|
||||||
onEditException: handleItemEdit,
|
|
||||||
onDeleteException: handleItemDelete,
|
|
||||||
showModified: true,
|
|
||||||
showName: true,
|
|
||||||
'data-test-subj': `eventFilterCard`,
|
|
||||||
}),
|
|
||||||
[handleItemDelete, handleItemEdit]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlePaginatedContentChange: EventListPaginatedContent['onChange'] = useCallback(
|
const handlePaginatedContentChange: EventListPaginatedContent['onChange'] = useCallback(
|
||||||
({ pageIndex, pageSize }) => {
|
({ pageIndex, pageSize }) => {
|
||||||
navigateCallback({
|
navigateCallback({
|
||||||
|
@ -186,6 +169,59 @@ export const EventFiltersListPage = memo(() => {
|
||||||
[navigateCallback, dispatch]
|
[navigateCallback, dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const artifactCardPropsPerItem = useMemo(() => {
|
||||||
|
const cachedCardProps: Record<string, ArtifactEntryCardProps> = {};
|
||||||
|
|
||||||
|
// Casting `listItems` below to remove the `Immutable<>` from it in order to prevent errors
|
||||||
|
// with common component's props
|
||||||
|
for (const eventFilter of listItems as ExceptionListItemSchema[]) {
|
||||||
|
let policies: ArtifactEntryCardProps['policies'];
|
||||||
|
|
||||||
|
cachedCardProps[eventFilter.id] = {
|
||||||
|
item: eventFilter as AnyArtifact,
|
||||||
|
policies,
|
||||||
|
hideDescription: true,
|
||||||
|
'data-test-subj': 'eventFilterCard',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'controlsHorizontal',
|
||||||
|
onClick: () => {
|
||||||
|
history.push(
|
||||||
|
getEventFiltersListPath({
|
||||||
|
...location,
|
||||||
|
show: 'edit',
|
||||||
|
id: eventFilter.id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
'data-test-subj': 'editEventFilterAction',
|
||||||
|
children: EDIT_EVENT_FILTER_ACTION_LABEL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'trash',
|
||||||
|
onClick: () => {
|
||||||
|
dispatch({
|
||||||
|
type: 'eventFilterForDeletion',
|
||||||
|
payload: eventFilter,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
'data-test-subj': 'deleteEventFilterAction',
|
||||||
|
children: DELETE_EVENT_FILTER_ACTION_LABEL,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedCardProps;
|
||||||
|
}, [dispatch, history, listItems, location]);
|
||||||
|
|
||||||
|
const handleArtifactCardProps = useCallback(
|
||||||
|
(eventFilter: ExceptionListItemSchema) => {
|
||||||
|
return artifactCardPropsPerItem[eventFilter.id];
|
||||||
|
},
|
||||||
|
[artifactCardPropsPerItem]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdministrationListPage
|
<AdministrationListPage
|
||||||
headerBackComponent={backButton}
|
headerBackComponent={backButton}
|
||||||
|
@ -244,10 +280,10 @@ export const EventFiltersListPage = memo(() => {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PaginatedContent<Immutable<ExceptionListItemSchema>, typeof ExceptionItem>
|
<PaginatedContent<ExceptionListItemSchema, ArtifactEntryCardType>
|
||||||
items={listItems}
|
items={listItems}
|
||||||
ItemComponent={ExceptionItem}
|
ItemComponent={ArtifactEntryCard}
|
||||||
itemComponentProps={handleItemComponentProps}
|
itemComponentProps={handleArtifactCardProps}
|
||||||
onChange={handlePaginatedContentChange}
|
onChange={handlePaginatedContentChange}
|
||||||
error={fetchError?.message}
|
error={fetchError?.message}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
|
|
|
@ -138,6 +138,7 @@ export const TrustedAppsGrid = memo(() => {
|
||||||
cachedCardProps[trustedApp.id] = {
|
cachedCardProps[trustedApp.id] = {
|
||||||
item: trustedApp,
|
item: trustedApp,
|
||||||
policies,
|
policies,
|
||||||
|
hideComments: true,
|
||||||
'data-test-subj': 'trustedAppCard',
|
'data-test-subj': 'trustedAppCard',
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue