-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Security Solution][Detections] Improves UX for Rules and Exceptions …
…tables (#118940) [Security Solution][Detections] Improves UX for Rules and Exceptions tables
- Loading branch information
1 parent
87d4486
commit 2c0fe1e
Showing
15 changed files
with
536 additions
and
433 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
19 changes: 19 additions & 0 deletions
19
...ck/plugins/security_solution/public/common/components/health_truncate_text/index.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
/* | ||
* 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, screen } from '@testing-library/react'; | ||
|
||
import { HealthTruncateText } from '.'; | ||
|
||
describe('Component HealthTruncateText', () => { | ||
it('should render component without errors', () => { | ||
render(<HealthTruncateText dataTestSubj="testItem">{'Test'}</HealthTruncateText>); | ||
|
||
expect(screen.getByTestId('testItem')).toHaveTextContent('Test'); | ||
}); | ||
}); |
43 changes: 43 additions & 0 deletions
43
x-pack/plugins/security_solution/public/common/components/health_truncate_text/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
/* | ||
* 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 { EuiHealth, EuiToolTip, EuiHealthProps } from '@elastic/eui'; | ||
import styled from 'styled-components'; | ||
|
||
const StatusTextWrapper = styled.div` | ||
width: 100%; | ||
display: inline-grid; | ||
`; | ||
|
||
interface HealthTruncateTextProps { | ||
healthColor?: EuiHealthProps['color']; | ||
tooltipContent?: React.ReactNode; | ||
dataTestSubj?: string; | ||
} | ||
|
||
/** | ||
* Allows text in EuiHealth to be properly truncated with tooltip | ||
* @param healthColor - color for EuiHealth component | ||
* @param tooltipContent - tooltip content | ||
*/ | ||
export const HealthTruncateText: React.FC<HealthTruncateTextProps> = ({ | ||
tooltipContent, | ||
children, | ||
healthColor, | ||
dataTestSubj, | ||
}) => ( | ||
<EuiToolTip content={tooltipContent}> | ||
<EuiHealth color={healthColor} data-test-subj={dataTestSubj}> | ||
<StatusTextWrapper> | ||
<span className="eui-textTruncate">{children}</span> | ||
</StatusTextWrapper> | ||
</EuiHealth> | ||
</EuiToolTip> | ||
); | ||
|
||
HealthTruncateText.displayName = 'HealthTruncateText'; |
76 changes: 76 additions & 0 deletions
76
x-pack/plugins/security_solution/public/common/components/popover_items/index.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
/* | ||
* 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 { PopoverItems, PopoverItemsProps } from '.'; | ||
import { TestProviders } from '../../mock'; | ||
import { render, screen } from '@testing-library/react'; | ||
import { within } from '@testing-library/dom'; | ||
|
||
const mockTags = ['Elastic', 'Endpoint', 'Data Protection', 'ML', 'Continuous Monitoring']; | ||
|
||
const renderHelper = (props: Partial<PopoverItemsProps<string>> = {}) => | ||
render( | ||
<TestProviders> | ||
<PopoverItems | ||
dataTestPrefix="tags" | ||
items={mockTags} | ||
popoverButtonTitle="show mocks" | ||
renderItem={(item: string, index: number) => <span key={`${item}-${index}`}>{item}</span>} | ||
{...props} | ||
/> | ||
</TestProviders> | ||
); | ||
|
||
const getButton = () => screen.getByRole('button', { name: 'show mocks' }); | ||
const withinPopover = () => within(screen.getByTestId('tagsDisplayPopoverWrapper')); | ||
|
||
describe('Component PopoverItems', () => { | ||
it('shoud render only 2 first items in display and rest in popup', async () => { | ||
renderHelper({ numberOfItemsToDisplay: 2 }); | ||
mockTags.slice(0, 2).forEach((tag) => { | ||
expect(screen.getByText(tag)).toBeInTheDocument(); | ||
}); | ||
|
||
// items not rendered yet | ||
mockTags.slice(2).forEach((tag) => { | ||
expect(screen.queryByText(tag)).toBeNull(); | ||
}); | ||
|
||
getButton().click(); | ||
expect(await screen.findByTestId('tagsDisplayPopoverWrapper')).toBeInTheDocument(); | ||
|
||
// items rendered in popup | ||
mockTags.slice(2).forEach((tag) => { | ||
expect(withinPopover().getByText(tag)).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
it('shoud render popover button and items in popover without popover title', () => { | ||
renderHelper(); | ||
mockTags.forEach((tag) => { | ||
expect(screen.queryByText(tag)).toBeNull(); | ||
}); | ||
getButton().click(); | ||
|
||
mockTags.forEach((tag) => { | ||
expect(withinPopover().queryByText(tag)).toBeInTheDocument(); | ||
}); | ||
|
||
expect(screen.queryByTestId('tagsDisplayPopoverTitle')).toBeNull(); | ||
}); | ||
|
||
it('shoud render popover title', async () => { | ||
renderHelper({ popoverTitle: 'Tags popover title' }); | ||
|
||
getButton().click(); | ||
|
||
expect(await screen.findByTestId('tagsDisplayPopoverWrapper')).toBeInTheDocument(); | ||
expect(screen.getByTestId('tagsDisplayPopoverTitle')).toHaveTextContent('Tags popover title'); | ||
}); | ||
}); |
116 changes: 116 additions & 0 deletions
116
x-pack/plugins/security_solution/public/common/components/popover_items/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
/* | ||
* 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, { useState } from 'react'; | ||
import { | ||
EuiPopover, | ||
EuiBadgeGroup, | ||
EuiBadge, | ||
EuiPopoverTitle, | ||
EuiFlexGroup, | ||
EuiFlexItem, | ||
} from '@elastic/eui'; | ||
import styled from 'styled-components'; | ||
|
||
export interface PopoverItemsProps<T> { | ||
renderItem: (item: T, index: number, items: T[]) => React.ReactNode; | ||
items: T[]; | ||
popoverButtonTitle: string; | ||
popoverButtonIcon?: string; | ||
popoverTitle?: string; | ||
numberOfItemsToDisplay?: number; | ||
dataTestPrefix?: string; | ||
} | ||
|
||
interface OverflowListProps<T> { | ||
readonly items: T[]; | ||
} | ||
|
||
const PopoverItemsWrapper = styled(EuiFlexGroup)` | ||
width: 100%; | ||
`; | ||
|
||
const PopoverWrapper = styled(EuiBadgeGroup)` | ||
max-height: 200px; | ||
max-width: 600px; | ||
overflow: auto; | ||
line-height: ${({ theme }) => theme.eui.euiLineHeight}; | ||
`; | ||
|
||
/** | ||
* Component to render list of items in popover, wicth configurabe number of display items by default | ||
* @param items - array of items to render | ||
* @param renderItem - render function that render item, arguments: item, index, items[] | ||
* @param popoverTitle - title of popover | ||
* @param popoverButtonTitle - title of popover button that triggers popover | ||
* @param popoverButtonIcon - icon of popover button that triggers popover | ||
* @param numberOfItemsToDisplay - number of items to render that are no in popover, defaults to 0 | ||
* @param dataTestPrefix - data-test-subj prefix to apply to elements | ||
*/ | ||
const PopoverItemsComponent = <T extends unknown>({ | ||
items, | ||
renderItem, | ||
popoverTitle, | ||
popoverButtonTitle, | ||
popoverButtonIcon, | ||
numberOfItemsToDisplay = 0, | ||
dataTestPrefix = 'items', | ||
}: PopoverItemsProps<T>) => { | ||
const [isExceptionOverflowPopoverOpen, setIsExceptionOverflowPopoverOpen] = useState(false); | ||
|
||
const OverflowList = ({ items: itemsToRender }: OverflowListProps<T>) => ( | ||
<>{itemsToRender.map(renderItem)}</> | ||
); | ||
|
||
if (items.length <= numberOfItemsToDisplay) { | ||
return ( | ||
<PopoverItemsWrapper data-test-subj={dataTestPrefix} alignItems="center" gutterSize="s"> | ||
<OverflowList items={items} /> | ||
</PopoverItemsWrapper> | ||
); | ||
} | ||
|
||
return ( | ||
<PopoverItemsWrapper alignItems="center" gutterSize="s" data-test-subj={dataTestPrefix}> | ||
<EuiFlexItem grow={1} className="eui-textTruncate"> | ||
<OverflowList items={items.slice(0, numberOfItemsToDisplay)} /> | ||
</EuiFlexItem> | ||
<EuiPopover | ||
ownFocus | ||
data-test-subj={`${dataTestPrefix}DisplayPopover`} | ||
button={ | ||
<EuiBadge | ||
iconType={popoverButtonIcon} | ||
color="hollow" | ||
data-test-subj={`${dataTestPrefix}DisplayPopoverButton`} | ||
onClick={() => setIsExceptionOverflowPopoverOpen(!isExceptionOverflowPopoverOpen)} | ||
onClickAriaLabel={popoverButtonTitle} | ||
> | ||
{popoverButtonTitle} | ||
</EuiBadge> | ||
} | ||
isOpen={isExceptionOverflowPopoverOpen} | ||
closePopover={() => setIsExceptionOverflowPopoverOpen(!isExceptionOverflowPopoverOpen)} | ||
repositionOnScroll | ||
> | ||
{popoverTitle ? ( | ||
<EuiPopoverTitle data-test-subj={`${dataTestPrefix}DisplayPopoverTitle`}> | ||
{popoverTitle} | ||
</EuiPopoverTitle> | ||
) : null} | ||
<PopoverWrapper data-test-subj={`${dataTestPrefix}DisplayPopoverWrapper`}> | ||
<OverflowList items={items.slice(numberOfItemsToDisplay)} /> | ||
</PopoverWrapper> | ||
</EuiPopover> | ||
</PopoverItemsWrapper> | ||
); | ||
}; | ||
|
||
const MemoizedPopoverItems = React.memo(PopoverItemsComponent); | ||
MemoizedPopoverItems.displayName = 'PopoverItems'; | ||
|
||
export const PopoverItems = MemoizedPopoverItems as typeof PopoverItemsComponent; |
21 changes: 21 additions & 0 deletions
21
...ty_solution/public/detections/components/rules/rule_execution_status_badge/index.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/* | ||
* 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, screen } from '@testing-library/react'; | ||
|
||
import { RuleExecutionStatusBadge } from '.'; | ||
|
||
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; | ||
|
||
describe('Component RuleExecutionStatus', () => { | ||
it('should render component correctly with capitalized status text', () => { | ||
render(<RuleExecutionStatusBadge status={RuleExecutionStatus.succeeded} />); | ||
|
||
expect(screen.getByText('Succeeded')).toBeInTheDocument(); | ||
}); | ||
}); |
40 changes: 40 additions & 0 deletions
40
...ecurity_solution/public/detections/components/rules/rule_execution_status_badge/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { getEmptyTagValue } from '../../../../common/components/empty_value'; | ||
import { HealthTruncateText } from '../../../../common/components/health_truncate_text'; | ||
import { getStatusColor } from '../rule_status/helpers'; | ||
|
||
import { getCapitalizedRuleStatusText } from '../../../../../common/detection_engine/utils'; | ||
import type { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; | ||
|
||
interface RuleExecutionStatusBadgeProps { | ||
status: RuleExecutionStatus | null | undefined; | ||
} | ||
|
||
/** | ||
* Shows rule execution status | ||
* @param status - rule execution status | ||
*/ | ||
const RuleExecutionStatusBadgeComponent = ({ status }: RuleExecutionStatusBadgeProps) => { | ||
const displayStatus = getCapitalizedRuleStatusText(status); | ||
return ( | ||
<HealthTruncateText | ||
tooltipContent={displayStatus} | ||
healthColor={getStatusColor(status ?? null)} | ||
dataTestSubj="ruleExecutionStatus" | ||
> | ||
{displayStatus ?? getEmptyTagValue()} | ||
</HealthTruncateText> | ||
); | ||
}; | ||
|
||
export const RuleExecutionStatusBadge = React.memo(RuleExecutionStatusBadgeComponent); | ||
|
||
RuleExecutionStatusBadge.displayName = 'RuleExecutionStatusBadge'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.