Skip to content

Commit c2af32c

Browse files
[Security Solution] [Endpoint] Event filters uses the new card design (#114126) (#114772)
* 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> Co-authored-by: David Sánchez <davidsansol92@gmail.com>
1 parent cdb1817 commit c2af32c

13 files changed

Lines changed: 329 additions & 77 deletions

File tree

x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe.each([
2525
) => ReturnType<AppContextTestRender['render']>;
2626

2727
beforeEach(() => {
28-
item = generateItem();
28+
item = generateItem() as AnyArtifact;
2929
appTestContext = createAppRootMockRenderer();
3030
render = (props = {}) => {
3131
renderResult = appTestContext.render(
@@ -77,13 +77,31 @@ describe.each([
7777
expect(renderResult.getByTestId('testCard-description').textContent).toEqual(item.description);
7878
});
7979

80+
it("shouldn't display description", async () => {
81+
render({ hideDescription: true });
82+
expect(renderResult.queryByTestId('testCard-description')).toBeNull();
83+
});
84+
8085
it('should display default empty value if description does not exist', async () => {
8186
item.description = undefined;
8287
render();
83-
8488
expect(renderResult.getByTestId('testCard-description').textContent).toEqual('—');
8589
});
8690

91+
it('should display comments if one exists', async () => {
92+
render();
93+
if (isTrustedApp(item)) {
94+
expect(renderResult.queryByTestId('testCard-comments')).toBeNull();
95+
} else {
96+
expect(renderResult.queryByTestId('testCard-comments')).not.toBeNull();
97+
}
98+
});
99+
100+
it("shouldn't display comments", async () => {
101+
render({ hideComments: true });
102+
expect(renderResult.queryByTestId('testCard-comments')).toBeNull();
103+
});
104+
87105
it('should display OS and criteria conditions', () => {
88106
render();
89107

x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.tsx

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ import { useNormalizedArtifact } from './hooks/use_normalized_artifact';
1616
import { useTestIdGenerator } from '../hooks/use_test_id_generator';
1717
import { CardContainerPanel } from './components/card_container_panel';
1818
import { CardSectionPanel } from './components/card_section_panel';
19+
import { CardComments } from './components/card_comments';
1920
import { usePolicyNavLinks } from './hooks/use_policy_nav_links';
2021
import { MaybeImmutable } from '../../../../common/endpoint/types';
2122

22-
export interface ArtifactEntryCardProps extends CommonProps {
23+
export interface CommonArtifactEntryCardProps extends CommonProps {
2324
item: MaybeImmutable<AnyArtifact>;
2425
/**
2526
* 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 {
3435
policies?: MenuItemPropsByPolicyId;
3536
}
3637

38+
export interface ArtifactEntryCardProps extends CommonArtifactEntryCardProps {
39+
// A flag to hide description section, false by default
40+
hideDescription?: boolean;
41+
// A flag to hide comments section, false by default
42+
hideComments?: boolean;
43+
}
44+
3745
/**
3846
* Display Artifact Items (ex. Trusted App, Event Filter, etc) as a card.
3947
* This component is a TS Generic that allows you to set what the Item type is
4048
*/
4149
export const ArtifactEntryCard = memo<ArtifactEntryCardProps>(
42-
({ item, policies, actions, 'data-test-subj': dataTestSubj, ...commonProps }) => {
50+
({
51+
item,
52+
policies,
53+
actions,
54+
hideDescription = false,
55+
hideComments = false,
56+
'data-test-subj': dataTestSubj,
57+
...commonProps
58+
}) => {
4359
const artifact = useNormalizedArtifact(item as AnyArtifact);
4460
const getTestId = useTestIdGenerator(dataTestSubj);
4561
const policyNavLinks = usePolicyNavLinks(artifact, policies);
@@ -63,11 +79,16 @@ export const ArtifactEntryCard = memo<ArtifactEntryCardProps>(
6379

6480
<EuiSpacer size="m" />
6581

66-
<EuiText>
67-
<p data-test-subj={getTestId('description')}>
68-
{artifact.description || getEmptyValue()}
69-
</p>
70-
</EuiText>
82+
{!hideDescription ? (
83+
<EuiText>
84+
<p data-test-subj={getTestId('description')}>
85+
{artifact.description || getEmptyValue()}
86+
</p>
87+
</EuiText>
88+
) : null}
89+
{!hideComments ? (
90+
<CardComments comments={artifact.comments} data-test-subj={getTestId('comments')} />
91+
) : null}
7192
</CardSectionPanel>
7293

7394
<EuiHorizontalRule margin="none" />

x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_collapsible_card.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77

88
import React, { memo } from 'react';
99
import { EuiHorizontalRule } from '@elastic/eui';
10-
import { ArtifactEntryCardProps } from './artifact_entry_card';
10+
import { CommonArtifactEntryCardProps } from './artifact_entry_card';
1111
import { CardContainerPanel } from './components/card_container_panel';
1212
import { useNormalizedArtifact } from './hooks/use_normalized_artifact';
1313
import { useTestIdGenerator } from '../hooks/use_test_id_generator';
1414
import { CardSectionPanel } from './components/card_section_panel';
1515
import { CriteriaConditions, CriteriaConditionsProps } from './components/criteria_conditions';
1616
import { CardCompressedHeader } from './components/card_compressed_header';
1717

18-
export interface ArtifactEntryCollapsibleCardProps extends ArtifactEntryCardProps {
18+
export interface ArtifactEntryCollapsibleCardProps extends CommonArtifactEntryCardProps {
1919
onExpandCollapse: () => void;
2020
expanded?: boolean;
2121
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React, { memo, useMemo, useCallback, useState } from 'react';
9+
import {
10+
CommonProps,
11+
EuiAccordion,
12+
EuiCommentList,
13+
EuiCommentProps,
14+
EuiButtonEmpty,
15+
EuiSpacer,
16+
} from '@elastic/eui';
17+
import { isEmpty } from 'lodash/fp';
18+
import { useTestIdGenerator } from '../../hooks/use_test_id_generator';
19+
import { CardActionsFlexItemProps } from './card_actions_flex_item';
20+
import { ArtifactInfo } from '../types';
21+
import { getFormattedComments } from '../utils/get_formatted_comments';
22+
import { SHOW_COMMENTS_LABEL, HIDE_COMMENTS_LABEL } from './translations';
23+
24+
export interface CardCommentsProps
25+
extends CardActionsFlexItemProps,
26+
Pick<CommonProps, 'data-test-subj'> {
27+
comments: ArtifactInfo['comments'];
28+
}
29+
30+
export const CardComments = memo<CardCommentsProps>(
31+
({ comments, 'data-test-subj': dataTestSubj }) => {
32+
const getTestId = useTestIdGenerator(dataTestSubj);
33+
34+
const [showComments, setShowComments] = useState(false);
35+
const onCommentsClick = useCallback((): void => {
36+
setShowComments(!showComments);
37+
}, [setShowComments, showComments]);
38+
const formattedComments = useMemo((): EuiCommentProps[] => {
39+
return getFormattedComments(comments);
40+
}, [comments]);
41+
42+
const buttonText = useMemo(
43+
() =>
44+
showComments ? HIDE_COMMENTS_LABEL(comments.length) : SHOW_COMMENTS_LABEL(comments.length),
45+
[comments.length, showComments]
46+
);
47+
48+
return !isEmpty(comments) ? (
49+
<div data-test-subj={dataTestSubj}>
50+
<EuiSpacer size="s" />
51+
<EuiButtonEmpty
52+
onClick={onCommentsClick}
53+
flush="left"
54+
size="xs"
55+
data-test-subj={getTestId('label')}
56+
>
57+
{buttonText}
58+
</EuiButtonEmpty>
59+
<EuiAccordion id={'1'} arrowDisplay="none" forceState={showComments ? 'open' : 'closed'}>
60+
<EuiSpacer size="m" />
61+
<EuiCommentList comments={formattedComments} data-test-subj={getTestId('list')} />
62+
</EuiAccordion>
63+
</div>
64+
) : null;
65+
}
66+
);
67+
68+
CardComments.displayName = 'CardComments';

x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/criteria_conditions.tsx

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
* 2.0.
66
*/
77

8-
import React, { memo } from 'react';
9-
import { CommonProps, EuiExpression } from '@elastic/eui';
8+
import React, { memo, useCallback } from 'react';
9+
import { CommonProps, EuiExpression, EuiToken, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
10+
import styled from 'styled-components';
1011
import { ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
1112
import {
1213
CONDITION_OS,
@@ -21,7 +22,7 @@ import {
2122
CONDITION_OPERATOR_TYPE_EXISTS,
2223
CONDITION_OPERATOR_TYPE_LIST,
2324
} from './translations';
24-
import { ArtifactInfo } from '../types';
25+
import { ArtifactInfo, ArtifactInfoEntry } from '../types';
2526
import { useTestIdGenerator } from '../../hooks/use_test_id_generator';
2627

2728
const OS_LABELS = Object.freeze({
@@ -39,13 +40,60 @@ const OPERATOR_TYPE_LABELS = Object.freeze({
3940
[ListOperatorTypeEnum.LIST]: CONDITION_OPERATOR_TYPE_LIST,
4041
});
4142

43+
const EuiFlexGroupNested = styled(EuiFlexGroup)`
44+
margin-left: ${({ theme }) => theme.eui.spacerSizes.l};
45+
`;
46+
47+
const EuiFlexItemNested = styled(EuiFlexItem)`
48+
margin-bottom: 6px !important;
49+
margin-top: 6px !important;
50+
`;
51+
4252
export type CriteriaConditionsProps = Pick<ArtifactInfo, 'os' | 'entries'> &
4353
Pick<CommonProps, 'data-test-subj'>;
4454

4555
export const CriteriaConditions = memo<CriteriaConditionsProps>(
4656
({ os, entries, 'data-test-subj': dataTestSubj }) => {
4757
const getTestId = useTestIdGenerator(dataTestSubj);
4858

59+
const getNestedEntriesContent = useCallback(
60+
(type: string, nestedEntries: ArtifactInfoEntry[]) => {
61+
if (type === 'nested' && nestedEntries.length) {
62+
return nestedEntries.map(
63+
({ field: nestedField, type: nestedType, value: nestedValue }) => {
64+
return (
65+
<EuiFlexGroupNested
66+
data-test-subj={getTestId('nestedCondition')}
67+
key={nestedField + nestedType + nestedValue}
68+
direction="row"
69+
alignItems="center"
70+
gutterSize="m"
71+
responsive={false}
72+
>
73+
<EuiFlexItemNested grow={false}>
74+
<EuiToken iconType="tokenNested" size="s" />
75+
</EuiFlexItemNested>
76+
<EuiFlexItemNested grow={false}>
77+
<EuiExpression description={''} value={nestedField} color="subdued" />
78+
</EuiFlexItemNested>
79+
<EuiFlexItemNested grow={false}>
80+
<EuiExpression
81+
description={
82+
OPERATOR_TYPE_LABELS[nestedType as keyof typeof OPERATOR_TYPE_LABELS] ??
83+
nestedType
84+
}
85+
value={nestedValue}
86+
/>
87+
</EuiFlexItemNested>
88+
</EuiFlexGroupNested>
89+
);
90+
}
91+
);
92+
}
93+
},
94+
[getTestId]
95+
);
96+
4997
return (
5098
<div data-test-subj={dataTestSubj}>
5199
<div data-test-subj={getTestId('os')}>
@@ -57,7 +105,7 @@ export const CriteriaConditions = memo<CriteriaConditionsProps>(
57105
/>
58106
</strong>
59107
</div>
60-
{entries.map(({ field, type, value }) => {
108+
{entries.map(({ field, type, value, entries: nestedEntries = [] }) => {
61109
return (
62110
<div data-test-subj={getTestId('condition')} key={field + type + value}>
63111
<EuiExpression description={CONDITION_AND} value={field} color="subdued" />
@@ -67,6 +115,7 @@ export const CriteriaConditions = memo<CriteriaConditionsProps>(
67115
}
68116
value={value}
69117
/>
118+
{getNestedEntriesContent(type, nestedEntries)}
70119
</div>
71120
);
72121
})}

x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/translations.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,15 @@ export const COLLAPSE_ACTION = i18n.translate(
114114
defaultMessage: 'Collapse',
115115
}
116116
);
117+
118+
export const SHOW_COMMENTS_LABEL = (count: number = 0) =>
119+
i18n.translate('xpack.securitySolution.artifactCard.comments.label.show', {
120+
defaultMessage: 'Show comments ({count})',
121+
values: { count },
122+
});
123+
124+
export const HIDE_COMMENTS_LABEL = (count: number = 0) =>
125+
i18n.translate('xpack.securitySolution.artifactCard.comments.label.hide', {
126+
defaultMessage: 'Hide comments ({count})',
127+
values: { count },
128+
});

x-pack/plugins/security_solution/public/management/components/artifact_entry_card/test_utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import { cloneDeep } from 'lodash';
9+
import uuid from 'uuid';
910
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
1011
import { TrustedAppGenerator } from '../../../../common/endpoint/data_generators/trusted_app_generator';
1112
import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
@@ -54,6 +55,14 @@ export const getExceptionProviderMock = (): ExceptionListItemSchema => {
5455
},
5556
],
5657
tags: ['policy:all'],
58+
comments: [
59+
{
60+
id: uuid.v4(),
61+
comment: 'test',
62+
created_at: new Date().toISOString(),
63+
created_by: 'Justa',
64+
},
65+
],
5766
})
5867
);
5968
};

x-pack/plugins/security_solution/public/management/components/artifact_entry_card/types.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,25 @@ import { EffectScope, TrustedApp } from '../../../../common/endpoint/types';
1010
import { ContextMenuItemNavByRouterProps } from '../context_menu_with_router_support/context_menu_item_nav_by_router';
1111

1212
export type AnyArtifact = ExceptionListItemSchema | TrustedApp;
13+
export interface ArtifactInfoEntry {
14+
field: string;
15+
type: string;
16+
operator: string;
17+
value: string;
18+
}
19+
type ArtifactInfoEntries = ArtifactInfoEntry & { entries?: ArtifactInfoEntry[] };
1320

1421
/**
1522
* A normalized structured that is used internally through out the card's components.
1623
*/
1724
export interface ArtifactInfo
1825
extends Pick<
1926
ExceptionListItemSchema,
20-
'name' | 'created_at' | 'updated_at' | 'created_by' | 'updated_by' | 'description'
27+
'name' | 'created_at' | 'updated_at' | 'created_by' | 'updated_by' | 'description' | 'comments'
2128
> {
2229
effectScope: EffectScope;
2330
os: string;
24-
entries: Array<{
25-
field: string;
26-
type: string;
27-
operator: string;
28-
value: string;
29-
}>;
31+
entries: ArtifactInfoEntries[];
3032
}
3133

3234
export interface MenuItemPropsByPolicyId {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React from 'react';
9+
import { EuiAvatar, EuiText, EuiCommentProps } from '@elastic/eui';
10+
import styled from 'styled-components';
11+
import { CommentsArray } from '@kbn/securitysolution-io-ts-list-types';
12+
import { COMMENT_EVENT } from '../../../../common/components/exceptions/translations';
13+
import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date';
14+
15+
const CustomEuiAvatar = styled(EuiAvatar)`
16+
background-color: ${({ theme }) => theme.eui.euiColorLightShade} !important;
17+
`;
18+
19+
/**
20+
* Formats ExceptionItem.comments into EuiCommentList format
21+
*
22+
* @param comments ExceptionItem.comments
23+
*/
24+
export const getFormattedComments = (comments: CommentsArray): EuiCommentProps[] => {
25+
return comments.map((commentItem) => ({
26+
username: commentItem.created_by,
27+
timestamp: (
28+
<FormattedRelativePreferenceDate value={commentItem.created_at} dateFormat="MMM D, YYYY" />
29+
),
30+
event: COMMENT_EVENT,
31+
timelineIcon: <CustomEuiAvatar size="s" name={commentItem.created_by} />,
32+
children: <EuiText size="s">{commentItem.comment}</EuiText>,
33+
}));
34+
};

0 commit comments

Comments
 (0)