Skip to content

Commit

Permalink
[Security Solution] Updates rules table tooling (#76719) (#77238)
Browse files Browse the repository at this point in the history
  • Loading branch information
dplumlee authored Sep 11, 2020
1 parent c9a5082 commit d683d01
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ export const selectNumberOfRules = (numberOfRules: number) => {
};

export const sortByActivatedRules = () => {
cy.get(SORT_RULES_BTN).click({ force: true });
cy.get(SORT_RULES_BTN).contains('Activated').click({ force: true });
waitForRulesToBeLoaded();
cy.get(SORT_RULES_BTN).click({ force: true });
cy.get(SORT_RULES_BTN).contains('Activated').click({ force: true });
waitForRulesToBeLoaded();
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
RulesColumns,
RuleStatusRowItemType,
} from '../../../pages/detection_engine/rules/all/columns';
import { Rule, Rules } from '../../../containers/detection_engine/rules/types';
import { Rule, Rules, RulesSortingFields } from '../../../containers/detection_engine/rules/types';
import { AllRulesTabs } from '../../../pages/detection_engine/rules/all';

// EuiBasicTable give me a hardtime with adding the ref attributes so I went the easy way
Expand All @@ -30,7 +30,7 @@ const MyEuiBasicTable = styled(EuiBasicTable as any)`` as any;

export interface SortingType {
sort: {
field: 'enabled';
field: RulesSortingFields;
direction: Direction;
};
}
Expand All @@ -48,12 +48,7 @@ interface AllRulesTablesProps {
rules: Rules;
rulesColumns: RulesColumns[];
rulesStatuses: RuleStatusRowItemType[];
sorting: {
sort: {
field: 'enabled';
direction: Direction;
};
};
sorting: SortingType;
tableOnChangeCallback: ({ page, sort }: EuiBasicTableOnChange) => void;
tableRef?: React.MutableRefObject<EuiBasicTable | undefined>;
selectedTab: AllRulesTabs;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ describe('Detections Rules API', () => {
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', {
method: 'GET',
query: {
filter: 'alert.attributes.tags: "hello" AND alert.attributes.tags: "world"',
filter: 'alert.attributes.tags: "hello" OR alert.attributes.tags: "world"',
page: 1,
per_page: 20,
sort_field: 'enabled',
Expand Down Expand Up @@ -297,7 +297,7 @@ describe('Detections Rules API', () => {
method: 'GET',
query: {
filter:
'alert.attributes.name: ruleName AND alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags: "hello" AND alert.attributes.tags: "world"',
'alert.attributes.name: ruleName AND alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND (alert.attributes.tags: "hello" OR alert.attributes.tags: "world")',
page: 1,
per_page: 20,
sort_field: 'enabled',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,23 +107,35 @@ export const fetchRules = async ({
},
signal,
}: FetchRulesProps): Promise<FetchRulesResponse> => {
const filters = [
const filtersWithoutTags = [
...(filterOptions.filter.length ? [`alert.attributes.name: ${filterOptions.filter}`] : []),
...(filterOptions.showCustomRules
? [`alert.attributes.tags: "__internal_immutable:false"`]
: []),
...(filterOptions.showElasticRules
? [`alert.attributes.tags: "__internal_immutable:true"`]
: []),
].join(' AND ');

const tags = [
...(filterOptions.tags?.map((t) => `alert.attributes.tags: "${t.replace(/"/g, '\\"')}"`) ?? []),
];
].join(' OR ');

const filterString =
filtersWithoutTags !== '' && tags !== ''
? `${filtersWithoutTags} AND (${tags})`
: filtersWithoutTags + tags;

const getFieldNameForSortField = (field: string) => {
return field === 'name' ? `${field}.keyword` : field;
};

const query = {
page: pagination.page,
per_page: pagination.perPage,
sort_field: filterOptions.sortField,
sort_field: getFieldNameForSortField(filterOptions.sortField),
sort_order: filterOptions.sortOrder,
...(filters.length ? { filter: filters.join(' AND ') } : {}),
...(filterString !== '' ? { filter: filterString } : {}),
};

return KibanaServices.get().http.fetch<FetchRulesResponse>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,10 @@ export interface FetchRulesProps {
signal: AbortSignal;
}

export type RulesSortingFields = 'enabled' | 'updated_at' | 'name' | 'created_at';
export interface FilterOptions {
filter: string;
sortField: string;
sortField: RulesSortingFields;
sortOrder: SortOrder;
showCustomRules?: boolean;
showElasticRules?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ interface GetColumns {
reFetchRules: (refreshPrePackagedRule?: boolean) => void;
}

// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes?
export const getColumns = ({
dispatch,
dispatchToaster,
Expand Down Expand Up @@ -127,7 +126,8 @@ export const getColumns = ({
</LinkAnchor>
),
truncateText: true,
width: '24%',
width: '20%',
sortable: true,
},
{
field: 'risk_score',
Expand All @@ -138,14 +138,14 @@ export const getColumns = ({
</EuiText>
),
truncateText: true,
width: '14%',
width: '10%',
},
{
field: 'severity',
name: i18n.COLUMN_SEVERITY,
render: (value: Rule['severity']) => <SeverityBadge value={value} />,
truncateText: true,
width: '16%',
width: '12%',
},
{
field: 'status_date',
Expand All @@ -160,7 +160,7 @@ export const getColumns = ({
);
},
truncateText: true,
width: '20%',
width: '14%',
},
{
field: 'status',
Expand All @@ -174,9 +174,40 @@ export const getColumns = ({
</>
);
},
width: '16%',
width: '12%',
truncateText: true,
},
{
field: 'updated_at',
name: i18n.COLUMN_LAST_UPDATE,
render: (value: Rule['updated_at']) => {
return value == null ? (
getEmptyTagValue()
) : (
<LocalizedDateTooltip fieldName={i18n.COLUMN_LAST_UPDATE} date={new Date(value)}>
<FormattedRelative value={value} />
</LocalizedDateTooltip>
);
},
sortable: true,
truncateText: true,
width: '14%',
},
{
field: 'version',
name: i18n.COLUMN_VERSION,
render: (value: Rule['version']) => {
return value == null ? (
getEmptyTagValue()
) : (
<EuiText data-test-subj="version" size="s">
{value}
</EuiText>
);
},
truncateText: true,
width: '10%',
},
{
field: 'tags',
name: i18n.COLUMN_TAGS,
Expand All @@ -190,7 +221,7 @@ export const getColumns = ({
</TruncatableText>
),
truncateText: true,
width: '20%',
width: '14%',
},
{
align: 'center',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
Rule,
PaginationOptions,
exportRules,
RulesSortingFields,
} from '../../../../containers/detection_engine/rules';
import { HeaderSection } from '../../../../../common/components/header_section';
import {
Expand Down Expand Up @@ -53,12 +54,12 @@ import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_l
import { SecurityPageName } from '../../../../../app/types';
import { useFormatUrl } from '../../../../../common/components/link_to';

const SORT_FIELD = 'enabled';
const INITIAL_SORT_FIELD = 'enabled';
const initialState: State = {
exportRuleIds: [],
filterOptions: {
filter: '',
sortField: SORT_FIELD,
sortField: INITIAL_SORT_FIELD,
sortOrder: 'desc',
},
loadingRuleIds: [],
Expand Down Expand Up @@ -164,8 +165,13 @@ export const AllRules = React.memo<AllRulesProps>(
});

const sorting = useMemo(
(): SortingType => ({ sort: { field: 'enabled', direction: filterOptions.sortOrder } }),
[filterOptions.sortOrder]
(): SortingType => ({
sort: {
field: filterOptions.sortField,
direction: filterOptions.sortOrder,
},
}),
[filterOptions]
);

const prePackagedRuleStatus = getPrePackagedRuleStatus(
Expand Down Expand Up @@ -215,7 +221,7 @@ export const AllRules = React.memo<AllRulesProps>(
dispatch({
type: 'updateFilterOptions',
filterOptions: {
sortField: SORT_FIELD, // Only enabled is supported for sorting currently
sortField: (sort?.field as RulesSortingFields) ?? INITIAL_SORT_FIELD, // Narrowing EuiBasicTable sorting types
sortOrder: sort?.direction ?? 'desc',
},
pagination: { page: page.index + 1, perPage: page.size },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { Dispatch, SetStateAction, useState } from 'react';
import React, {
ChangeEvent,
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import {
EuiFilterButton,
EuiFilterSelectItem,
Expand All @@ -13,6 +21,8 @@ import {
EuiPanel,
EuiPopover,
EuiText,
EuiFieldSearch,
EuiPopoverTitle,
} from '@elastic/eui';
import styled from 'styled-components';
import * as i18n from '../../translations';
Expand All @@ -37,12 +47,39 @@ const ScrollableDiv = styled.div`
* @param tags to display for filtering
* @param onSelectedTagsChanged change listener to be notified when tag selection changes
*/
export const TagsFilterPopoverComponent = ({
const TagsFilterPopoverComponent = ({
tags,
selectedTags,
onSelectedTagsChanged,
}: TagsFilterPopoverProps) => {
const sortedTags = useMemo(() => {
return tags.sort((a: string, b: string) => a.toLowerCase().localeCompare(b.toLowerCase())); // Case insensitive
}, [tags]);
const [isTagPopoverOpen, setIsTagPopoverOpen] = useState(false);
const [searchInput, setSearchInput] = useState('');
const [filterTags, setFilterTags] = useState(sortedTags);

const tagsComponent = useMemo(() => {
return filterTags.map((tag, index) => (
<EuiFilterSelectItem
checked={selectedTags.includes(tag) ? 'on' : undefined}
key={`${index}-${tag}`}
onClick={() => toggleSelectedGroup(tag, selectedTags, onSelectedTagsChanged)}
>
{`${tag}`}
</EuiFilterSelectItem>
));
}, [onSelectedTagsChanged, selectedTags, filterTags]);

const onSearchInputChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setSearchInput(event.target.value);
}, []);

useEffect(() => {
setFilterTags(
sortedTags.filter((tag) => tag.toLowerCase().includes(searchInput.toLowerCase()))
);
}, [sortedTags, searchInput]);

return (
<EuiPopover
Expand All @@ -64,18 +101,17 @@ export const TagsFilterPopoverComponent = ({
panelPaddingSize="none"
repositionOnScroll
>
<ScrollableDiv>
{tags.map((tag, index) => (
<EuiFilterSelectItem
checked={selectedTags.includes(tag) ? 'on' : undefined}
key={`${index}-${tag}`}
onClick={() => toggleSelectedGroup(tag, selectedTags, onSelectedTagsChanged)}
>
{`${tag}`}
</EuiFilterSelectItem>
))}
</ScrollableDiv>
{tags.length === 0 && (
<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>
Expand Down
Loading

0 comments on commit d683d01

Please sign in to comment.