[SIEM] [Detection Engine] All Rules fixes (#55641)
## Summary This PR addresses bugs outlined in https://github.com/elastic/kibana/issues/54935 Including: * Add Risk Score column * Remove Method column * Fixes `Showing Events` on Alerts table * Fixes Tag overflow * Shows Tags/Filters ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. - [ ] ~This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~ - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [ ] ~[Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~ - [ ] ~[Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios~ - [ ] ~This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~ ### For maintainers - [ ] ~This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~ - [ ] ~This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~
This commit is contained in:
parent
da5710dac6
commit
90e3bb0a62
|
@ -64,6 +64,7 @@ const AlertsTableComponent: React.FC<Props> = ({ endDate, startDate, pageFilters
|
|||
documentType: i18n.ALERTS_DOCUMENT_TYPE,
|
||||
footerText: i18n.TOTAL_COUNT_OF_ALERTS,
|
||||
title: i18n.ALERTS_TABLE_TITLE,
|
||||
unit: i18n.UNIT,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
|
|
@ -154,20 +154,18 @@ const EventsViewerComponent: React.FC<Props> = ({
|
|||
const totalCountMinusDeleted =
|
||||
totalCount > 0 ? totalCount - deletedEventIds.length : 0;
|
||||
|
||||
const subtitle = `${
|
||||
i18n.SHOWING
|
||||
}: ${totalCountMinusDeleted.toLocaleString()} ${timelineTypeContext.unit?.(
|
||||
totalCountMinusDeleted
|
||||
) ?? i18n.UNIT(totalCountMinusDeleted)}`;
|
||||
|
||||
// TODO: Reset eventDeletedIds/eventLoadingIds on refresh/loadmore (getUpdatedAt)
|
||||
return (
|
||||
<>
|
||||
<HeaderSection
|
||||
id={id}
|
||||
subtitle={
|
||||
utilityBar
|
||||
? undefined
|
||||
: `${
|
||||
i18n.SHOWING
|
||||
}: ${totalCountMinusDeleted.toLocaleString()} ${i18n.UNIT(
|
||||
totalCountMinusDeleted
|
||||
)}`
|
||||
}
|
||||
subtitle={utilityBar ? undefined : subtitle}
|
||||
title={timelineTypeContext?.title ?? i18n.EVENTS}
|
||||
>
|
||||
{headerFilterGroup}
|
||||
|
|
|
@ -23,6 +23,7 @@ export interface TimelineTypeContextProps {
|
|||
selectAll?: boolean;
|
||||
timelineActions?: TimelineAction[];
|
||||
title?: string;
|
||||
unit?: (totalCount: number) => string;
|
||||
}
|
||||
const initTimelineType: TimelineTypeContextProps = {
|
||||
documentType: undefined,
|
||||
|
@ -32,6 +33,7 @@ const initTimelineType: TimelineTypeContextProps = {
|
|||
selectAll: false,
|
||||
timelineActions: [],
|
||||
title: undefined,
|
||||
unit: undefined,
|
||||
};
|
||||
export const TimelineTypeContext = createContext<TimelineTypeContextProps>(initTimelineType);
|
||||
export const useTimelineTypeContext = () => useContext(TimelineTypeContext);
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
DETECTION_ENGINE_PREPACKAGED_URL,
|
||||
DETECTION_ENGINE_RULES_STATUS_URL,
|
||||
DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL,
|
||||
DETECTION_ENGINE_TAGS_URL,
|
||||
} from '../../../../common/constants';
|
||||
import * as i18n from '../../../pages/detection_engine/rules/translations';
|
||||
|
||||
|
@ -54,61 +55,72 @@ export const addRule = async ({ rule, signal }: AddRulesProps): Promise<NewRule>
|
|||
};
|
||||
|
||||
/**
|
||||
* Fetches all rules or single specified rule from the Detection Engine API
|
||||
* Fetches all rules from the Detection Engine API
|
||||
*
|
||||
* @param filterOptions desired filters (e.g. filter/sortField/sortOrder)
|
||||
* @param pagination desired pagination options (e.g. page/perPage)
|
||||
* @param id if specified, will return specific rule if exists
|
||||
* @param signal to cancel request
|
||||
*
|
||||
*/
|
||||
export const fetchRules = async ({
|
||||
filterOptions = {
|
||||
filter: '',
|
||||
sortField: 'name',
|
||||
sortField: 'enabled',
|
||||
sortOrder: 'desc',
|
||||
showCustomRules: false,
|
||||
showElasticRules: false,
|
||||
tags: [],
|
||||
},
|
||||
pagination = {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
},
|
||||
id,
|
||||
signal,
|
||||
}: FetchRulesProps): Promise<FetchRulesResponse> => {
|
||||
const filters = [
|
||||
...(filterOptions.filter.length !== 0
|
||||
? [`alert.attributes.name:%20${encodeURIComponent(filterOptions.filter)}`]
|
||||
: []),
|
||||
...(filterOptions.showCustomRules
|
||||
? ['alert.attributes.tags:%20%22__internal_immutable:false%22']
|
||||
: []),
|
||||
...(filterOptions.showElasticRules
|
||||
? ['alert.attributes.tags:%20%22__internal_immutable:true%22']
|
||||
: []),
|
||||
...(filterOptions.tags?.map(t => `alert.attributes.tags:${encodeURIComponent(t)}`) ?? []),
|
||||
];
|
||||
|
||||
const queryParams = [
|
||||
`page=${pagination.page}`,
|
||||
`per_page=${pagination.perPage}`,
|
||||
`sort_field=${filterOptions.sortField}`,
|
||||
`sort_order=${filterOptions.sortOrder}`,
|
||||
...(filterOptions.filter.length !== 0
|
||||
? [`filter=alert.attributes.name:%20${encodeURIComponent(filterOptions.filter)}`]
|
||||
: []),
|
||||
...(filters.length > 0 ? [`filter=${filters.join('%20AND%20')}`] : []),
|
||||
];
|
||||
|
||||
const endpoint =
|
||||
id != null
|
||||
? `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}?id="${id}"`
|
||||
: `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_find?${queryParams.join('&')}`;
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
const response = await fetch(
|
||||
`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_find?${queryParams.join('&')}`,
|
||||
{
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'kbn-xsrf': 'true',
|
||||
},
|
||||
signal,
|
||||
}
|
||||
);
|
||||
await throwIfNotOk(response);
|
||||
return id != null
|
||||
? {
|
||||
page: 0,
|
||||
perPage: 1,
|
||||
total: 1,
|
||||
data: response.json(),
|
||||
}
|
||||
: response.json();
|
||||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch a Rule by providing a Rule ID
|
||||
*
|
||||
* @param id Rule ID's (not rule_id)
|
||||
* @param signal to cancel request
|
||||
*
|
||||
*/
|
||||
export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise<Rule> => {
|
||||
const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}?id=${id}`, {
|
||||
|
@ -344,6 +356,27 @@ export const getRuleStatusById = async ({
|
|||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch all unique Tags used by Rules
|
||||
*
|
||||
* @param signal to cancel request
|
||||
*
|
||||
*/
|
||||
export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise<string[]> => {
|
||||
const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_TAGS_URL}`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'kbn-xsrf': 'true',
|
||||
},
|
||||
signal,
|
||||
});
|
||||
|
||||
await throwIfNotOk(response);
|
||||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get pre packaged rules Status
|
||||
*
|
||||
|
|
|
@ -30,3 +30,10 @@ export const RULE_PREPACKAGED_SUCCESS = i18n.translate(
|
|||
defaultMessage: 'Installed pre-packaged rules from elastic',
|
||||
}
|
||||
);
|
||||
|
||||
export const TAG_FETCH_FAILURE = i18n.translate(
|
||||
'xpack.siem.containers.detectionEngine.tagFetchFailDescription',
|
||||
{
|
||||
defaultMessage: 'Failed to fetch Tags',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -114,7 +114,6 @@ export interface PaginationOptions {
|
|||
export interface FetchRulesProps {
|
||||
pagination?: PaginationOptions;
|
||||
filterOptions?: FilterOptions;
|
||||
id?: string;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
|
@ -122,6 +121,9 @@ export interface FilterOptions {
|
|||
filter: string;
|
||||
sortField: string;
|
||||
sortOrder: 'asc' | 'desc';
|
||||
showCustomRules?: boolean;
|
||||
showElasticRules?: boolean;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface FetchRulesResponse {
|
||||
|
|
|
@ -70,6 +70,9 @@ export const useRules = (pagination: PaginationOptions, filterOptions: FilterOpt
|
|||
filterOptions.filter,
|
||||
filterOptions.sortField,
|
||||
filterOptions.sortOrder,
|
||||
filterOptions.tags?.sort().join(),
|
||||
filterOptions.showCustomRules,
|
||||
filterOptions.showElasticRules,
|
||||
]);
|
||||
|
||||
return [loading, rules, reFetchRules.current];
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
import { useStateToaster } from '../../../components/toasters';
|
||||
import { fetchTags } from './api';
|
||||
import { errorToToaster } from '../../../components/ml/api/error_to_toaster';
|
||||
import * as i18n from './translations';
|
||||
|
||||
type Return = [boolean, string[]];
|
||||
|
||||
/**
|
||||
* Hook for using the list of Tags from the Detection Engine API
|
||||
*
|
||||
*/
|
||||
export const useTags = (): Return => {
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
async function fetchData() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const fetchTagsResult = await fetchTags({
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
if (isSubscribed) {
|
||||
setTags(fetchTagsResult);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isSubscribed) {
|
||||
errorToToaster({ title: i18n.TAG_FETCH_FAILURE, error, dispatchToaster });
|
||||
}
|
||||
}
|
||||
if (isSubscribed) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return [loading, tags];
|
||||
};
|
|
@ -56,13 +56,10 @@ export const mockTableData: TableData[] = [
|
|||
id: 'abe6c564-050d-45a5-aaf0-386c37dd1f61',
|
||||
immutable: false,
|
||||
isLoading: false,
|
||||
lastCompletedRun: undefined,
|
||||
lastResponse: { type: '—' },
|
||||
method: 'saved_query',
|
||||
risk_score: 21,
|
||||
rule: {
|
||||
href: '#/detections/rules/id/abe6c564-050d-45a5-aaf0-386c37dd1f61',
|
||||
name: 'Home Grown!',
|
||||
status: 'Status Placeholder',
|
||||
},
|
||||
rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea',
|
||||
severity: 'low',
|
||||
|
@ -108,13 +105,10 @@ export const mockTableData: TableData[] = [
|
|||
id: '63f06f34-c181-4b2d-af35-f2ace572a1ee',
|
||||
immutable: false,
|
||||
isLoading: false,
|
||||
lastCompletedRun: undefined,
|
||||
lastResponse: { type: '—' },
|
||||
method: 'saved_query',
|
||||
risk_score: 21,
|
||||
rule: {
|
||||
href: '#/detections/rules/id/63f06f34-c181-4b2d-af35-f2ace572a1ee',
|
||||
name: 'Home Grown!',
|
||||
status: 'Status Placeholder',
|
||||
},
|
||||
rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea',
|
||||
severity: 'low',
|
||||
|
|
|
@ -31,6 +31,7 @@ import { RuleSwitch } from '../components/rule_switch';
|
|||
import { SeverityBadge } from '../components/severity_badge';
|
||||
import { ActionToaster } from '../../../../components/toasters';
|
||||
import { getStatusColor } from '../components/rule_status/helpers';
|
||||
import { TruncatableText } from '../../../../components/truncatable_text';
|
||||
|
||||
const getActions = (
|
||||
dispatch: React.Dispatch<Action>,
|
||||
|
@ -84,8 +85,8 @@ export const getColumns = (
|
|||
width: '24%',
|
||||
},
|
||||
{
|
||||
field: 'method',
|
||||
name: i18n.COLUMN_METHOD,
|
||||
field: 'risk_score',
|
||||
name: i18n.COLUMN_RISK_SCORE,
|
||||
truncateText: true,
|
||||
width: '14%',
|
||||
},
|
||||
|
@ -129,13 +130,13 @@ export const getColumns = (
|
|||
field: 'tags',
|
||||
name: i18n.COLUMN_TAGS,
|
||||
render: (value: TableData['tags']) => (
|
||||
<>
|
||||
<TruncatableText>
|
||||
{value.map((tag, i) => (
|
||||
<EuiBadge color="hollow" key={`${tag}-${i}`}>
|
||||
{tag}
|
||||
</EuiBadge>
|
||||
))}
|
||||
</>
|
||||
</TruncatableText>
|
||||
),
|
||||
truncateText: true,
|
||||
width: '20%',
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
RuleResponseBuckets,
|
||||
} from '../../../../containers/detection_engine/rules';
|
||||
import { TableData } from '../types';
|
||||
import { getEmptyValue } from '../../../../components/empty_value';
|
||||
|
||||
/**
|
||||
* Formats rules into the correct format for the AllRulesTable
|
||||
|
@ -26,14 +25,9 @@ export const formatRules = (rules: Rule[], selectedIds?: string[]): TableData[]
|
|||
rule: {
|
||||
href: `#/detections/rules/id/${encodeURIComponent(rule.id)}`,
|
||||
name: rule.name,
|
||||
status: 'Status Placeholder',
|
||||
},
|
||||
method: rule.type, // TODO: Map to i18n?
|
||||
risk_score: rule.risk_score,
|
||||
severity: rule.severity,
|
||||
lastCompletedRun: undefined, // TODO: Not available yet
|
||||
lastResponse: {
|
||||
type: getEmptyValue(), // TODO: Not available yet
|
||||
},
|
||||
tags: rule.tags ?? [],
|
||||
activate: rule.enabled,
|
||||
status: rule.status ?? null,
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButton,
|
||||
EuiContextMenuPanel,
|
||||
EuiFieldSearch,
|
||||
EuiEmptyPrompt,
|
||||
EuiLoadingContent,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
@ -16,7 +17,11 @@ import React, { useCallback, useEffect, useMemo, useReducer, useState } from 're
|
|||
import { useHistory } from 'react-router-dom';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { useRules, CreatePreBuiltRules } from '../../../../containers/detection_engine/rules';
|
||||
import {
|
||||
useRules,
|
||||
CreatePreBuiltRules,
|
||||
FilterOptions,
|
||||
} from '../../../../containers/detection_engine/rules';
|
||||
import { HeaderSection } from '../../../../components/header_section';
|
||||
import {
|
||||
UtilityBar,
|
||||
|
@ -36,6 +41,8 @@ import { EuiBasicTableOnChange, TableData } from '../types';
|
|||
import { getBatchItems } from './batch_actions';
|
||||
import { getColumns } from './columns';
|
||||
import { allRulesReducer, State } from './reducer';
|
||||
import { RulesTableFilters } from './rules_table_filters/rules_table_filters';
|
||||
import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine';
|
||||
|
||||
const initialState: State = {
|
||||
isLoading: true,
|
||||
|
@ -209,6 +216,20 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
[]
|
||||
);
|
||||
|
||||
const onFilterChangedCallback = useCallback((newFilterOptions: Partial<FilterOptions>) => {
|
||||
dispatch({
|
||||
type: 'updateFilterOptions',
|
||||
filterOptions: {
|
||||
...filterOptions,
|
||||
...newFilterOptions,
|
||||
},
|
||||
});
|
||||
dispatch({
|
||||
type: 'updatePagination',
|
||||
pagination: { ...pagination, page: 1 },
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<RuleDownloader
|
||||
|
@ -231,26 +252,8 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
<Panel loading={isGlobalLoading}>
|
||||
<>
|
||||
{rulesInstalled != null && rulesInstalled > 0 && (
|
||||
<HeaderSection split title={i18n.ALL_RULES} border={true}>
|
||||
<EuiFieldSearch
|
||||
aria-label={i18n.SEARCH_RULES}
|
||||
fullWidth
|
||||
incremental={false}
|
||||
placeholder={i18n.SEARCH_PLACEHOLDER}
|
||||
onSearch={filterString => {
|
||||
dispatch({
|
||||
type: 'updateFilterOptions',
|
||||
filterOptions: {
|
||||
...filterOptions,
|
||||
filter: filterString,
|
||||
},
|
||||
});
|
||||
dispatch({
|
||||
type: 'updatePagination',
|
||||
pagination: { ...pagination, page: 1 },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<HeaderSection split title={i18n.ALL_RULES}>
|
||||
<RulesTableFilters onFilterChanged={onFilterChangedCallback} />
|
||||
</HeaderSection>
|
||||
)}
|
||||
{isInitialLoad && isEmpty(tableData) && (
|
||||
|
@ -301,6 +304,24 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
isSelectable={!hasNoPermissions ?? false}
|
||||
itemId="id"
|
||||
items={tableData}
|
||||
noItemsMessage={
|
||||
<EuiEmptyPrompt
|
||||
title={<h3>{i18n.NO_RULES}</h3>}
|
||||
titleSize="xs"
|
||||
body={i18n.NO_RULES_BODY}
|
||||
actions={
|
||||
<EuiButton
|
||||
fill
|
||||
size="s"
|
||||
href={`#${DETECTION_ENGINE_PAGE_NAME}/rules/create`}
|
||||
iconType="plusInCircle"
|
||||
isDisabled={hasNoPermissions}
|
||||
>
|
||||
{i18n.ADD_NEW_RULE}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
}
|
||||
onChange={tableOnChangeCallback}
|
||||
pagination={{
|
||||
pageIndex: pagination.page - 1,
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
EuiFieldSearch,
|
||||
EuiFilterButton,
|
||||
EuiFilterGroup,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import * as i18n from '../../translations';
|
||||
|
||||
import { FilterOptions } from '../../../../../containers/detection_engine/rules';
|
||||
import { useTags } from '../../../../../containers/detection_engine/rules/use_tags';
|
||||
import { TagsFilterPopover } from './tags_filter_popover';
|
||||
|
||||
interface RulesTableFiltersProps {
|
||||
onFilterChanged: (filterOptions: Partial<FilterOptions>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection of filters for filtering data within the RulesTable. Contains search bar, Elastic/Custom
|
||||
* Rules filter button toggle, and tag selection
|
||||
*
|
||||
* @param onFilterChanged change listener to be notified on filter changes
|
||||
*/
|
||||
const RulesTableFiltersComponent = ({ onFilterChanged }: RulesTableFiltersProps) => {
|
||||
const [filter, setFilter] = useState<string>('');
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [showCustomRules, setShowCustomRules] = useState<boolean>(false);
|
||||
const [showElasticRules, setShowElasticRules] = useState<boolean>(false);
|
||||
const [isLoadingTags, tags] = useTags();
|
||||
|
||||
// Propagate filter changes to parent
|
||||
useEffect(() => {
|
||||
onFilterChanged({ filter, showCustomRules, showElasticRules, tags: selectedTags });
|
||||
}, [filter, selectedTags, showCustomRules, showElasticRules, onFilterChanged]);
|
||||
|
||||
const handleOnSearch = useCallback(filterString => setFilter(filterString.trim()), [setFilter]);
|
||||
|
||||
const handleElasticRulesClick = useCallback(() => {
|
||||
setShowElasticRules(!showElasticRules);
|
||||
setShowCustomRules(false);
|
||||
}, [setShowElasticRules, showElasticRules, setShowCustomRules]);
|
||||
|
||||
const handleCustomRulesClick = useCallback(() => {
|
||||
setShowCustomRules(!showCustomRules);
|
||||
setShowElasticRules(false);
|
||||
}, [setShowElasticRules, showCustomRules, setShowCustomRules]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="m" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiFieldSearch
|
||||
aria-label={i18n.SEARCH_RULES}
|
||||
fullWidth
|
||||
incremental={false}
|
||||
placeholder={i18n.SEARCH_PLACEHOLDER}
|
||||
onSearch={handleOnSearch}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFilterGroup>
|
||||
<TagsFilterPopover
|
||||
tags={tags}
|
||||
onSelectedTagsChanged={setSelectedTags}
|
||||
isLoading={isLoadingTags}
|
||||
/>
|
||||
</EuiFilterGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFilterGroup>
|
||||
<EuiFilterButton
|
||||
hasActiveFilters={showElasticRules}
|
||||
onClick={handleElasticRulesClick}
|
||||
data-test-subj="show-elastic-rules-filter-button"
|
||||
withNext
|
||||
>
|
||||
{i18n.ELASTIC_RULES}
|
||||
</EuiFilterButton>
|
||||
<EuiFilterButton
|
||||
hasActiveFilters={showCustomRules}
|
||||
onClick={handleCustomRulesClick}
|
||||
data-test-subj="show-custom-rules-filter-button"
|
||||
>
|
||||
{i18n.CUSTOM_RULES}
|
||||
</EuiFilterButton>
|
||||
</EuiFilterGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
RulesTableFiltersComponent.displayName = 'RulesTableFiltersComponent';
|
||||
|
||||
export const RulesTableFilters = React.memo(RulesTableFiltersComponent);
|
||||
|
||||
RulesTableFilters.displayName = 'RulesTableFilters';
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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, { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import {
|
||||
EuiFilterButton,
|
||||
EuiFilterSelectItem,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiPopover,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import * as i18n from '../../translations';
|
||||
import { toggleSelectedGroup } from '../../../../../components/ml_popover/jobs_table/filters/toggle_selected_group';
|
||||
|
||||
interface TagsFilterPopoverProps {
|
||||
tags: string[];
|
||||
onSelectedTagsChanged: Dispatch<SetStateAction<string[]>>;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const ScrollableDiv = styled.div`
|
||||
max-height: 250px;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
/**
|
||||
* Popover for selecting tags to filter on
|
||||
*
|
||||
* @param tags to display for filtering
|
||||
* @param onSelectedTagsChanged change listener to be notified when tag selection changes
|
||||
*/
|
||||
export const TagsFilterPopoverComponent = ({
|
||||
tags,
|
||||
onSelectedTagsChanged,
|
||||
}: TagsFilterPopoverProps) => {
|
||||
const [isTagPopoverOpen, setIsTagPopoverOpen] = useState(false);
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
onSelectedTagsChanged(selectedTags);
|
||||
}, [selectedTags.sort().join()]);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
ownFocus
|
||||
button={
|
||||
<EuiFilterButton
|
||||
data-test-subj={'tags-filter-popover-button'}
|
||||
iconType="arrowDown"
|
||||
onClick={() => setIsTagPopoverOpen(!isTagPopoverOpen)}
|
||||
isSelected={isTagPopoverOpen}
|
||||
hasActiveFilters={selectedTags.length > 0}
|
||||
numActiveFilters={selectedTags.length}
|
||||
>
|
||||
{i18n.TAGS}
|
||||
</EuiFilterButton>
|
||||
}
|
||||
isOpen={isTagPopoverOpen}
|
||||
closePopover={() => setIsTagPopoverOpen(!isTagPopoverOpen)}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<ScrollableDiv>
|
||||
{tags.map((tag, index) => (
|
||||
<EuiFilterSelectItem
|
||||
checked={selectedTags.includes(tag) ? 'on' : undefined}
|
||||
key={`${index}-${tag}`}
|
||||
onClick={() => toggleSelectedGroup(tag, selectedTags, setSelectedTags)}
|
||||
>
|
||||
{`${tag}`}
|
||||
</EuiFilterSelectItem>
|
||||
))}
|
||||
</ScrollableDiv>
|
||||
{tags.length === 0 && (
|
||||
<EuiFlexGroup gutterSize="m" justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiPanel>
|
||||
<EuiText>{i18n.NO_TAGS_AVAILABLE}</EuiText>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
||||
|
||||
TagsFilterPopoverComponent.displayName = 'TagsFilterPopoverComponent';
|
||||
|
||||
export const TagsFilterPopover = React.memo(TagsFilterPopoverComponent);
|
||||
|
||||
TagsFilterPopover.displayName = 'TagsFilterPopover';
|
|
@ -213,10 +213,10 @@ export const COLUMN_RULE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const COLUMN_METHOD = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.allRules.columns.methodTitle',
|
||||
export const COLUMN_RISK_SCORE = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.allRules.columns.riskScoreTitle',
|
||||
{
|
||||
defaultMessage: 'Method',
|
||||
defaultMessage: 'Risk score',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -255,16 +255,42 @@ export const COLUMN_ACTIVATE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const COLUMN_STATUS = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.allRules.columns.currentStatusTitle',
|
||||
export const CUSTOM_RULES = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.allRules.filters.customRulesTitle',
|
||||
{
|
||||
defaultMessage: 'Current status',
|
||||
defaultMessage: 'Custom rules',
|
||||
}
|
||||
);
|
||||
export const NO_STATUS = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.allRules.columns.unknownStatusDescription',
|
||||
|
||||
export const ELASTIC_RULES = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.allRules.filters.elasticRulesTitle',
|
||||
{
|
||||
defaultMessage: 'Unknown',
|
||||
defaultMessage: 'Elastic rules',
|
||||
}
|
||||
);
|
||||
|
||||
export const TAGS = i18n.translate('xpack.siem.detectionEngine.rules.allRules.filters.tagsLabel', {
|
||||
defaultMessage: 'Tags',
|
||||
});
|
||||
|
||||
export const NO_TAGS_AVAILABLE = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.allRules.filters.noTagsAvailableDescription',
|
||||
{
|
||||
defaultMessage: 'No tags available',
|
||||
}
|
||||
);
|
||||
|
||||
export const NO_RULES = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.allRules.filters.noRulesTitle',
|
||||
{
|
||||
defaultMessage: 'No rules found',
|
||||
}
|
||||
);
|
||||
|
||||
export const NO_RULES_BODY = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.allRules.filters.noRulesBodyTitle',
|
||||
{
|
||||
defaultMessage: "We weren't able to find any rules with the above filters.",
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -30,15 +30,9 @@ export interface TableData {
|
|||
rule: {
|
||||
href: string;
|
||||
name: string;
|
||||
status: string;
|
||||
};
|
||||
method: string;
|
||||
risk_score: number;
|
||||
severity: string;
|
||||
lastCompletedRun: string | undefined;
|
||||
lastResponse: {
|
||||
type: string;
|
||||
message?: string;
|
||||
};
|
||||
tags: string[];
|
||||
activate: boolean;
|
||||
isLoading: boolean;
|
||||
|
|
Loading…
Reference in a new issue