Skip to content

Commit

Permalink
[Security Solution] [Endpoint] Event filters uses the new card design (
Browse files Browse the repository at this point in the history
…#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>
  • Loading branch information
dasansol92 and kibanamachine authored Oct 13, 2021
1 parent db4bcde commit 0bf0b94
Show file tree
Hide file tree
Showing 13 changed files with 329 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe.each([
) => ReturnType<AppContextTestRender['render']>;

beforeEach(() => {
item = generateItem();
item = generateItem() as AnyArtifact;
appTestContext = createAppRootMockRenderer();
render = (props = {}) => {
renderResult = appTestContext.render(
Expand Down Expand Up @@ -77,13 +77,31 @@ describe.each([
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 () => {
item.description = undefined;
render();

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', () => {
render();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ import { useNormalizedArtifact } from './hooks/use_normalized_artifact';
import { useTestIdGenerator } from '../hooks/use_test_id_generator';
import { CardContainerPanel } from './components/card_container_panel';
import { CardSectionPanel } from './components/card_section_panel';
import { CardComments } from './components/card_comments';
import { usePolicyNavLinks } from './hooks/use_policy_nav_links';
import { MaybeImmutable } from '../../../../common/endpoint/types';

export interface ArtifactEntryCardProps extends CommonProps {
export interface CommonArtifactEntryCardProps extends CommonProps {
item: MaybeImmutable<AnyArtifact>;
/**
* The list of actions for the card. Will display an icon with the actions in a menu if defined.
Expand All @@ -34,12 +35,27 @@ export interface ArtifactEntryCardProps extends CommonProps {
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.
* This component is a TS Generic that allows you to set what the Item type is
*/
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 getTestId = useTestIdGenerator(dataTestSubj);
const policyNavLinks = usePolicyNavLinks(artifact, policies);
Expand All @@ -63,11 +79,16 @@ export const ArtifactEntryCard = memo<ArtifactEntryCardProps>(

<EuiSpacer size="m" />

<EuiText>
<p data-test-subj={getTestId('description')}>
{artifact.description || getEmptyValue()}
</p>
</EuiText>
{!hideDescription ? (
<EuiText>
<p data-test-subj={getTestId('description')}>
{artifact.description || getEmptyValue()}
</p>
</EuiText>
) : null}
{!hideComments ? (
<CardComments comments={artifact.comments} data-test-subj={getTestId('comments')} />
) : null}
</CardSectionPanel>

<EuiHorizontalRule margin="none" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@

import React, { memo } from 'react';
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 { useNormalizedArtifact } from './hooks/use_normalized_artifact';
import { useTestIdGenerator } from '../hooks/use_test_id_generator';
import { CardSectionPanel } from './components/card_section_panel';
import { CriteriaConditions, CriteriaConditionsProps } from './components/criteria_conditions';
import { CardCompressedHeader } from './components/card_compressed_header';

export interface ArtifactEntryCollapsibleCardProps extends ArtifactEntryCardProps {
export interface ArtifactEntryCollapsibleCardProps extends CommonArtifactEntryCardProps {
onExpandCollapse: () => void;
expanded?: boolean;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
* 2.0.
*/

import React, { memo } from 'react';
import { CommonProps, EuiExpression } from '@elastic/eui';
import React, { memo, useCallback } from 'react';
import { CommonProps, EuiExpression, EuiToken, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import { ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import {
CONDITION_OS,
Expand All @@ -21,7 +22,7 @@ import {
CONDITION_OPERATOR_TYPE_EXISTS,
CONDITION_OPERATOR_TYPE_LIST,
} from './translations';
import { ArtifactInfo } from '../types';
import { ArtifactInfo, ArtifactInfoEntry } from '../types';
import { useTestIdGenerator } from '../../hooks/use_test_id_generator';

const OS_LABELS = Object.freeze({
Expand All @@ -39,13 +40,60 @@ const OPERATOR_TYPE_LABELS = Object.freeze({
[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'> &
Pick<CommonProps, 'data-test-subj'>;

export const CriteriaConditions = memo<CriteriaConditionsProps>(
({ os, entries, 'data-test-subj': 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 (
<div data-test-subj={dataTestSubj}>
<div data-test-subj={getTestId('os')}>
Expand All @@ -57,7 +105,7 @@ export const CriteriaConditions = memo<CriteriaConditionsProps>(
/>
</strong>
</div>
{entries.map(({ field, type, value }) => {
{entries.map(({ field, type, value, entries: nestedEntries = [] }) => {
return (
<div data-test-subj={getTestId('condition')} key={field + type + value}>
<EuiExpression description={CONDITION_AND} value={field} color="subdued" />
Expand All @@ -67,6 +115,7 @@ export const CriteriaConditions = memo<CriteriaConditionsProps>(
}
value={value}
/>
{getNestedEntriesContent(type, nestedEntries)}
</div>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,15 @@ export const COLLAPSE_ACTION = i18n.translate(
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 },
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { cloneDeep } from 'lodash';
import uuid from 'uuid';
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { TrustedAppGenerator } from '../../../../common/endpoint/data_generators/trusted_app_generator';
import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
Expand Down Expand Up @@ -54,6 +55,14 @@ export const getExceptionProviderMock = (): ExceptionListItemSchema => {
},
],
tags: ['policy:all'],
comments: [
{
id: uuid.v4(),
comment: 'test',
created_at: new Date().toISOString(),
created_by: 'Justa',
},
],
})
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,25 @@ import { EffectScope, TrustedApp } from '../../../../common/endpoint/types';
import { ContextMenuItemNavByRouterProps } from '../context_menu_with_router_support/context_menu_item_nav_by_router';

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.
*/
export interface ArtifactInfo
extends Pick<
ExceptionListItemSchema,
'name' | 'created_at' | 'updated_at' | 'created_by' | 'updated_by' | 'description'
'name' | 'created_at' | 'updated_at' | 'created_by' | 'updated_by' | 'description' | 'comments'
> {
effectScope: EffectScope;
os: string;
entries: Array<{
field: string;
type: string;
operator: string;
value: string;
}>;
entries: ArtifactInfoEntries[];
}

export interface MenuItemPropsByPolicyId {
Expand Down
Original file line number Diff line number Diff line change
@@ -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>,
}));
};
Loading

0 comments on commit 0bf0b94

Please sign in to comment.