Skip to content

Commit 33d3784

Browse files
authored
[Security Solution] Changes rules table tag display (#77102) (#79380)
1 parent d7668a0 commit 33d3784

File tree

8 files changed

+248
-59
lines changed

8 files changed

+248
-59
lines changed

x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
/* eslint-disable react/display-name */
88

99
import {
10-
EuiBadge,
1110
EuiBasicTableColumn,
1211
EuiTableActionsColumnType,
1312
EuiText,
@@ -24,7 +23,6 @@ import { getEmptyTagValue } from '../../../../../common/components/empty_value';
2423
import { FormattedDate } from '../../../../../common/components/formatted_date';
2524
import { getRuleDetailsUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine';
2625
import { ActionToaster } from '../../../../../common/components/toasters';
27-
import { TruncatableText } from '../../../../../common/components/truncatable_text';
2826
import { getStatusColor } from '../../../../components/rules/rule_status/helpers';
2927
import { RuleSwitch } from '../../../../components/rules/rule_switch';
3028
import { SeverityBadge } from '../../../../components/rules/severity_badge';
@@ -39,6 +37,7 @@ import { Action } from './reducer';
3937
import { LocalizedDateTooltip } from '../../../../../common/components/localized_date_tooltip';
4038
import * as detectionI18n from '../../translations';
4139
import { LinkAnchor } from '../../../../../common/components/links';
40+
import { TagsDisplay } from './tag_display';
4241

4342
export const getActions = (
4443
dispatch: React.Dispatch<Action>,
@@ -207,22 +206,19 @@ export const getColumns = ({
207206
);
208207
},
209208
truncateText: true,
210-
width: '10%',
209+
width: '8%',
211210
},
212211
{
213212
field: 'tags',
214213
name: i18n.COLUMN_TAGS,
215-
render: (value: Rule['tags']) => (
216-
<TruncatableText data-test-subj="tags">
217-
{value.map((tag, i) => (
218-
<EuiBadge color="hollow" key={`${tag}-${i}`}>
219-
{tag}
220-
</EuiBadge>
221-
))}
222-
</TruncatableText>
223-
),
214+
render: (value: Rule['tags']) => {
215+
if (value.length > 0) {
216+
return <TagsDisplay tags={value} />;
217+
}
218+
return getEmptyTagValue();
219+
},
224220
truncateText: true,
225-
width: '14%',
221+
width: '20%',
226222
},
227223
{
228224
align: 'center',

x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.test.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { bucketRulesResponse, showRulesTable } from './helpers';
7+
import { bucketRulesResponse, caseInsensitiveSort, showRulesTable } from './helpers';
88
import { mockRule, mockRuleError } from './__mocks__/mock';
99
import uuid from 'uuid';
1010
import { Rule, RuleError } from '../../../../containers/detection_engine/rules';
@@ -86,4 +86,15 @@ describe('AllRulesTable Helpers', () => {
8686
expect(result).toBeTruthy();
8787
});
8888
});
89+
90+
describe('caseInsensitiveSort', () => {
91+
describe('when an array of differently cased tags is passed', () => {
92+
const unsortedTags = ['atest', 'Ctest', 'Btest', 'ctest', 'btest', 'Atest'];
93+
const result = caseInsensitiveSort(unsortedTags);
94+
it('returns an alphabetically sorted array with no regard for casing', () => {
95+
const expected = ['atest', 'Atest', 'Btest', 'btest', 'Ctest', 'ctest'];
96+
expect(result).toEqual(expected);
97+
});
98+
});
99+
});
89100
});

x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,7 @@ export const showRulesTable = ({
3333
}) =>
3434
(rulesCustomInstalled != null && rulesCustomInstalled > 0) ||
3535
(rulesInstalled != null && rulesInstalled > 0);
36+
37+
export const caseInsensitiveSort = (tags: string[]): string[] => {
38+
return tags.sort((a: string, b: string) => a.toLowerCase().localeCompare(b.toLowerCase())); // Case insensitive
39+
};

x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
*/
66

77
import React from 'react';
8-
import { shallow, mount } from 'enzyme';
8+
import { shallow, mount, ReactWrapper } from 'enzyme';
9+
import { act } from 'react-dom/test-utils';
910

1011
import '../../../../../common/mock/match_media';
1112
import '../../../../../common/mock/formatted_relative';
@@ -179,27 +180,34 @@ describe('AllRules', () => {
179180
expect(wrapper.find('[title="All rules"]')).toHaveLength(1);
180181
});
181182

182-
it('renders rules tab', async () => {
183-
const wrapper = mount(
184-
<TestProviders>
185-
<AllRules
186-
createPrePackagedRules={jest.fn()}
187-
hasNoPermissions={false}
188-
loading={false}
189-
loadingCreatePrePackagedRules={false}
190-
refetchPrePackagedRulesStatus={jest.fn()}
191-
rulesCustomInstalled={1}
192-
rulesInstalled={0}
193-
rulesNotInstalled={0}
194-
rulesNotUpdated={0}
195-
setRefreshRulesData={jest.fn()}
196-
/>
197-
</TestProviders>
198-
);
183+
describe('rules tab', () => {
184+
let wrapper: ReactWrapper;
185+
beforeEach(() => {
186+
wrapper = mount(
187+
<TestProviders>
188+
<AllRules
189+
createPrePackagedRules={jest.fn()}
190+
hasNoPermissions={false}
191+
loading={false}
192+
loadingCreatePrePackagedRules={false}
193+
refetchPrePackagedRulesStatus={jest.fn()}
194+
rulesCustomInstalled={1}
195+
rulesInstalled={0}
196+
rulesNotInstalled={0}
197+
rulesNotUpdated={0}
198+
setRefreshRulesData={jest.fn()}
199+
/>
200+
</TestProviders>
201+
);
202+
});
199203

200-
await waitFor(() => {
201-
expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeFalsy();
202-
expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeTruthy();
204+
it('renders correctly', async () => {
205+
await act(async () => {
206+
await waitFor(() => {
207+
expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeFalsy();
208+
expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeTruthy();
209+
});
210+
});
203211
});
204212
});
205213

x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
import styled from 'styled-components';
2828
import * as i18n from '../../translations';
2929
import { toggleSelectedGroup } from '../../../../../../common/components/ml_popover/jobs_table/filters/toggle_selected_group';
30+
import { caseInsensitiveSort } from '../helpers';
3031

3132
interface TagsFilterPopoverProps {
3233
selectedTags: string[];
@@ -36,9 +37,19 @@ interface TagsFilterPopoverProps {
3637
isLoading: boolean; // TO DO reimplement?
3738
}
3839

40+
const PopoverContentWrapper = styled.div`
41+
width: 275px;
42+
`;
43+
3944
const ScrollableDiv = styled.div`
4045
max-height: 250px;
41-
overflow: auto;
46+
overflow-y: auto;
47+
`;
48+
49+
const TagOverflowContainer = styled.span`
50+
overflow: hidden;
51+
text-overflow: ellipsis;
52+
white-space: nowrap;
4253
`;
4354

4455
/**
@@ -52,9 +63,7 @@ const TagsFilterPopoverComponent = ({
5263
selectedTags,
5364
onSelectedTagsChanged,
5465
}: TagsFilterPopoverProps) => {
55-
const sortedTags = useMemo(() => {
56-
return tags.sort((a: string, b: string) => a.toLowerCase().localeCompare(b.toLowerCase())); // Case insensitive
57-
}, [tags]);
66+
const sortedTags = useMemo(() => caseInsensitiveSort(tags), [tags]);
5867
const [isTagPopoverOpen, setIsTagPopoverOpen] = useState(false);
5968
const [searchInput, setSearchInput] = useState('');
6069
const [filterTags, setFilterTags] = useState(sortedTags);
@@ -65,8 +74,9 @@ const TagsFilterPopoverComponent = ({
6574
checked={selectedTags.includes(tag) ? 'on' : undefined}
6675
key={`${index}-${tag}`}
6776
onClick={() => toggleSelectedGroup(tag, selectedTags, onSelectedTagsChanged)}
77+
title={tag}
6878
>
69-
{`${tag}`}
79+
<TagOverflowContainer>{tag}</TagOverflowContainer>
7080
</EuiFilterSelectItem>
7181
));
7282
}, [onSelectedTagsChanged, selectedTags, filterTags]);
@@ -101,25 +111,27 @@ const TagsFilterPopoverComponent = ({
101111
panelPaddingSize="none"
102112
repositionOnScroll
103113
>
104-
<EuiPopoverTitle>
105-
<EuiFieldSearch
106-
placeholder="Search tags"
107-
value={searchInput}
108-
onChange={onSearchInputChange}
109-
isClearable
110-
aria-label="Rules tag search"
111-
/>
112-
</EuiPopoverTitle>
113-
<ScrollableDiv>{tagsComponent}</ScrollableDiv>
114-
{filterTags.length === 0 && (
115-
<EuiFlexGroup gutterSize="m" justifyContent="spaceAround">
116-
<EuiFlexItem grow={true}>
117-
<EuiPanel>
118-
<EuiText>{i18n.NO_TAGS_AVAILABLE}</EuiText>
119-
</EuiPanel>
120-
</EuiFlexItem>
121-
</EuiFlexGroup>
122-
)}
114+
<PopoverContentWrapper>
115+
<EuiPopoverTitle>
116+
<EuiFieldSearch
117+
placeholder="Search tags"
118+
value={searchInput}
119+
onChange={onSearchInputChange}
120+
isClearable
121+
aria-label="Rules tag search"
122+
/>
123+
</EuiPopoverTitle>
124+
<ScrollableDiv>{tagsComponent}</ScrollableDiv>
125+
{filterTags.length === 0 && (
126+
<EuiFlexGroup gutterSize="m" justifyContent="spaceAround">
127+
<EuiFlexItem grow={true}>
128+
<EuiPanel>
129+
<EuiText>{i18n.NO_TAGS_AVAILABLE}</EuiText>
130+
</EuiPanel>
131+
</EuiFlexItem>
132+
</EuiFlexGroup>
133+
)}
134+
</PopoverContentWrapper>
123135
</EuiPopover>
124136
);
125137
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React from 'react';
8+
import { mount, ReactWrapper } from 'enzyme';
9+
10+
import { TagsDisplay } from './tag_display';
11+
import { TestProviders } from '../../../../../common/mock';
12+
import { waitFor } from '@testing-library/react';
13+
14+
const mockTags = ['Elastic', 'Endpoint', 'Data Protection', 'ML', 'Continuous Monitoring'];
15+
16+
describe('When tag display loads', () => {
17+
let wrapper: ReactWrapper;
18+
beforeEach(() => {
19+
wrapper = mount(
20+
<TestProviders>
21+
<TagsDisplay tags={mockTags} />
22+
</TestProviders>
23+
);
24+
});
25+
it('visibly renders 3 initial tags', () => {
26+
for (let i = 0; i < 3; i++) {
27+
expect(wrapper.exists(`[data-test-subj="rules-table-column-tags-${i}"]`)).toBeTruthy();
28+
}
29+
});
30+
describe("when the 'see all' button is clicked", () => {
31+
beforeEach(() => {
32+
const seeAllButton = wrapper.find('[data-test-subj="tags-display-popover-button"] button');
33+
seeAllButton.simulate('click');
34+
});
35+
it('renders all the tags in the popover', async () => {
36+
await waitFor(() => {
37+
wrapper.update();
38+
expect(wrapper.exists('[data-test-subj="tags-display-popover"]')).toBeTruthy();
39+
for (let i = 0; i < mockTags.length; i++) {
40+
expect(
41+
wrapper.exists(`[data-test-subj="rules-table-column-popover-tags-${i}"]`)
42+
).toBeTruthy();
43+
}
44+
});
45+
});
46+
});
47+
});

0 commit comments

Comments
 (0)