Skip to content

Commit

Permalink
[Security Solution] Changes rules table tag display (#77102) (#79380)
Browse files Browse the repository at this point in the history
  • Loading branch information
dplumlee authored Oct 3, 2020
1 parent d7668a0 commit 33d3784
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
/* eslint-disable react/display-name */

import {
EuiBadge,
EuiBasicTableColumn,
EuiTableActionsColumnType,
EuiText,
Expand All @@ -24,7 +23,6 @@ import { getEmptyTagValue } from '../../../../../common/components/empty_value';
import { FormattedDate } from '../../../../../common/components/formatted_date';
import { getRuleDetailsUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine';
import { ActionToaster } from '../../../../../common/components/toasters';
import { TruncatableText } from '../../../../../common/components/truncatable_text';
import { getStatusColor } from '../../../../components/rules/rule_status/helpers';
import { RuleSwitch } from '../../../../components/rules/rule_switch';
import { SeverityBadge } from '../../../../components/rules/severity_badge';
Expand All @@ -39,6 +37,7 @@ import { Action } from './reducer';
import { LocalizedDateTooltip } from '../../../../../common/components/localized_date_tooltip';
import * as detectionI18n from '../../translations';
import { LinkAnchor } from '../../../../../common/components/links';
import { TagsDisplay } from './tag_display';

export const getActions = (
dispatch: React.Dispatch<Action>,
Expand Down Expand Up @@ -207,22 +206,19 @@ export const getColumns = ({
);
},
truncateText: true,
width: '10%',
width: '8%',
},
{
field: 'tags',
name: i18n.COLUMN_TAGS,
render: (value: Rule['tags']) => (
<TruncatableText data-test-subj="tags">
{value.map((tag, i) => (
<EuiBadge color="hollow" key={`${tag}-${i}`}>
{tag}
</EuiBadge>
))}
</TruncatableText>
),
render: (value: Rule['tags']) => {
if (value.length > 0) {
return <TagsDisplay tags={value} />;
}
return getEmptyTagValue();
},
truncateText: true,
width: '14%',
width: '20%',
},
{
align: 'center',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { bucketRulesResponse, showRulesTable } from './helpers';
import { bucketRulesResponse, caseInsensitiveSort, showRulesTable } from './helpers';
import { mockRule, mockRuleError } from './__mocks__/mock';
import uuid from 'uuid';
import { Rule, RuleError } from '../../../../containers/detection_engine/rules';
Expand Down Expand Up @@ -86,4 +86,15 @@ describe('AllRulesTable Helpers', () => {
expect(result).toBeTruthy();
});
});

describe('caseInsensitiveSort', () => {
describe('when an array of differently cased tags is passed', () => {
const unsortedTags = ['atest', 'Ctest', 'Btest', 'ctest', 'btest', 'Atest'];
const result = caseInsensitiveSort(unsortedTags);
it('returns an alphabetically sorted array with no regard for casing', () => {
const expected = ['atest', 'Atest', 'Btest', 'btest', 'Ctest', 'ctest'];
expect(result).toEqual(expected);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ export const showRulesTable = ({
}) =>
(rulesCustomInstalled != null && rulesCustomInstalled > 0) ||
(rulesInstalled != null && rulesInstalled > 0);

export const caseInsensitiveSort = (tags: string[]): string[] => {
return tags.sort((a: string, b: string) => a.toLowerCase().localeCompare(b.toLowerCase())); // Case insensitive
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
*/

import React from 'react';
import { shallow, mount } from 'enzyme';
import { shallow, mount, ReactWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';

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

it('renders rules tab', async () => {
const wrapper = mount(
<TestProviders>
<AllRules
createPrePackagedRules={jest.fn()}
hasNoPermissions={false}
loading={false}
loadingCreatePrePackagedRules={false}
refetchPrePackagedRulesStatus={jest.fn()}
rulesCustomInstalled={1}
rulesInstalled={0}
rulesNotInstalled={0}
rulesNotUpdated={0}
setRefreshRulesData={jest.fn()}
/>
</TestProviders>
);
describe('rules tab', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
wrapper = mount(
<TestProviders>
<AllRules
createPrePackagedRules={jest.fn()}
hasNoPermissions={false}
loading={false}
loadingCreatePrePackagedRules={false}
refetchPrePackagedRulesStatus={jest.fn()}
rulesCustomInstalled={1}
rulesInstalled={0}
rulesNotInstalled={0}
rulesNotUpdated={0}
setRefreshRulesData={jest.fn()}
/>
</TestProviders>
);
});

await waitFor(() => {
expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeFalsy();
expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeTruthy();
it('renders correctly', async () => {
await act(async () => {
await waitFor(() => {
expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeFalsy();
expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeTruthy();
});
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import styled from 'styled-components';
import * as i18n from '../../translations';
import { toggleSelectedGroup } from '../../../../../../common/components/ml_popover/jobs_table/filters/toggle_selected_group';
import { caseInsensitiveSort } from '../helpers';

interface TagsFilterPopoverProps {
selectedTags: string[];
Expand All @@ -36,9 +37,19 @@ interface TagsFilterPopoverProps {
isLoading: boolean; // TO DO reimplement?
}

const PopoverContentWrapper = styled.div`
width: 275px;
`;

const ScrollableDiv = styled.div`
max-height: 250px;
overflow: auto;
overflow-y: auto;
`;

const TagOverflowContainer = styled.span`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;

/**
Expand All @@ -52,9 +63,7 @@ const TagsFilterPopoverComponent = ({
selectedTags,
onSelectedTagsChanged,
}: TagsFilterPopoverProps) => {
const sortedTags = useMemo(() => {
return tags.sort((a: string, b: string) => a.toLowerCase().localeCompare(b.toLowerCase())); // Case insensitive
}, [tags]);
const sortedTags = useMemo(() => caseInsensitiveSort(tags), [tags]);
const [isTagPopoverOpen, setIsTagPopoverOpen] = useState(false);
const [searchInput, setSearchInput] = useState('');
const [filterTags, setFilterTags] = useState(sortedTags);
Expand All @@ -65,8 +74,9 @@ const TagsFilterPopoverComponent = ({
checked={selectedTags.includes(tag) ? 'on' : undefined}
key={`${index}-${tag}`}
onClick={() => toggleSelectedGroup(tag, selectedTags, onSelectedTagsChanged)}
title={tag}
>
{`${tag}`}
<TagOverflowContainer>{tag}</TagOverflowContainer>
</EuiFilterSelectItem>
));
}, [onSelectedTagsChanged, selectedTags, filterTags]);
Expand Down Expand Up @@ -101,25 +111,27 @@ const TagsFilterPopoverComponent = ({
panelPaddingSize="none"
repositionOnScroll
>
<EuiPopoverTitle>
<EuiFieldSearch
placeholder="Search tags"
value={searchInput}
onChange={onSearchInputChange}
isClearable
aria-label="Rules tag search"
/>
</EuiPopoverTitle>
<ScrollableDiv>{tagsComponent}</ScrollableDiv>
{filterTags.length === 0 && (
<EuiFlexGroup gutterSize="m" justifyContent="spaceAround">
<EuiFlexItem grow={true}>
<EuiPanel>
<EuiText>{i18n.NO_TAGS_AVAILABLE}</EuiText>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
)}
<PopoverContentWrapper>
<EuiPopoverTitle>
<EuiFieldSearch
placeholder="Search tags"
value={searchInput}
onChange={onSearchInputChange}
isClearable
aria-label="Rules tag search"
/>
</EuiPopoverTitle>
<ScrollableDiv>{tagsComponent}</ScrollableDiv>
{filterTags.length === 0 && (
<EuiFlexGroup gutterSize="m" justifyContent="spaceAround">
<EuiFlexItem grow={true}>
<EuiPanel>
<EuiText>{i18n.NO_TAGS_AVAILABLE}</EuiText>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
)}
</PopoverContentWrapper>
</EuiPopover>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import { mount, ReactWrapper } from 'enzyme';

import { TagsDisplay } from './tag_display';
import { TestProviders } from '../../../../../common/mock';
import { waitFor } from '@testing-library/react';

const mockTags = ['Elastic', 'Endpoint', 'Data Protection', 'ML', 'Continuous Monitoring'];

describe('When tag display loads', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
wrapper = mount(
<TestProviders>
<TagsDisplay tags={mockTags} />
</TestProviders>
);
});
it('visibly renders 3 initial tags', () => {
for (let i = 0; i < 3; i++) {
expect(wrapper.exists(`[data-test-subj="rules-table-column-tags-${i}"]`)).toBeTruthy();
}
});
describe("when the 'see all' button is clicked", () => {
beforeEach(() => {
const seeAllButton = wrapper.find('[data-test-subj="tags-display-popover-button"] button');
seeAllButton.simulate('click');
});
it('renders all the tags in the popover', async () => {
await waitFor(() => {
wrapper.update();
expect(wrapper.exists('[data-test-subj="tags-display-popover"]')).toBeTruthy();
for (let i = 0; i < mockTags.length; i++) {
expect(
wrapper.exists(`[data-test-subj="rules-table-column-popover-tags-${i}"]`)
).toBeTruthy();
}
});
});
});
});
Loading

0 comments on commit 33d3784

Please sign in to comment.