mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Add the ability to filter by index pattern to the rules management table (#128245)
This commit is contained in:
parent
52f0bf0a6a
commit
eb51ea64c5
22 changed files with 220 additions and 302 deletions
|
@ -13,7 +13,7 @@ import {
|
|||
FOURTH_RULE,
|
||||
RULES_TABLE,
|
||||
pageSelector,
|
||||
RULES_TABLE_REFRESH_INDICATOR,
|
||||
RULES_ROW,
|
||||
} from '../../screens/alerts_detection_rules';
|
||||
|
||||
import { goToManageAlertsDetectionRules, waitForAlertsPanelToBeLoaded } from '../../tasks/alerts';
|
||||
|
@ -90,14 +90,10 @@ describe('Alerts detection rules', () => {
|
|||
.invoke('text')
|
||||
.then((ruleNameFirstPage) => {
|
||||
goToPage(2);
|
||||
cy.get(RULES_TABLE_REFRESH_INDICATOR).should('not.exist');
|
||||
cy.get(RULES_TABLE)
|
||||
.find(RULE_NAME)
|
||||
.first()
|
||||
.invoke('text')
|
||||
.should((ruleNameSecondPage) => {
|
||||
expect(ruleNameFirstPage).not.to.eq(ruleNameSecondPage);
|
||||
});
|
||||
// Check that the rules table shows at least one row
|
||||
cy.get(RULES_TABLE).find(RULES_ROW).should('have.length.gte', 1);
|
||||
// Check that the rules table doesn't show the rule from the first page
|
||||
cy.get(RULES_TABLE).should('not.contain', ruleNameFirstPage);
|
||||
});
|
||||
|
||||
cy.get(RULES_TABLE)
|
||||
|
|
|
@ -154,7 +154,7 @@ export const goToRuleDetails = () => {
|
|||
};
|
||||
|
||||
export const goToTheRuleDetailsOf = (ruleName: string) => {
|
||||
cy.get(RULE_NAME).contains(ruleName).click();
|
||||
cy.get(RULE_NAME).should('contain', ruleName).contains(ruleName).click();
|
||||
};
|
||||
|
||||
export const loadPrebuiltDetectionRules = () => {
|
||||
|
|
|
@ -5,12 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
export const toggleSelectedGroup = (
|
||||
group: string,
|
||||
selectedGroups: string[],
|
||||
setSelectedGroups: Dispatch<SetStateAction<string[]>>
|
||||
setSelectedGroups: (groups: string[]) => void
|
||||
): void => {
|
||||
const selectedGroupIndex = selectedGroups.indexOf(group);
|
||||
const updatedSelectedGroups = [...selectedGroups];
|
||||
|
|
|
@ -178,7 +178,7 @@ describe('RuleActionsOverflow', () => {
|
|||
).toEqual(false);
|
||||
});
|
||||
|
||||
test('it calls duplicateRulesAction when rules-details-duplicate-rule is clicked', () => {
|
||||
test('it calls duplicate action when rules-details-duplicate-rule is clicked', () => {
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow
|
||||
rule={mockRule('id')}
|
||||
|
@ -195,7 +195,7 @@ describe('RuleActionsOverflow', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('it calls duplicateRulesAction with the rule and rule.id when rules-details-duplicate-rule is clicked', () => {
|
||||
test('it calls duplicate action with the rule and rule.id when rules-details-duplicate-rule is clicked', () => {
|
||||
const rule = mockRule('id');
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow rule={rule} userHasPermissions canDuplicateRuleWithActions={true} />
|
||||
|
@ -210,7 +210,7 @@ describe('RuleActionsOverflow', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('it calls editRuleAction after the rule is duplicated', async () => {
|
||||
test('it navigates to edit page after the rule is duplicated', async () => {
|
||||
const rule = mockRule('id');
|
||||
const ruleDuplicate = mockRule('newRule');
|
||||
executeRulesBulkActionMock.mockImplementation(() =>
|
||||
|
|
|
@ -142,7 +142,37 @@ describe('Detections Rules API', () => {
|
|||
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', {
|
||||
method: 'GET',
|
||||
query: {
|
||||
filter: 'alert.attributes.name: hello world',
|
||||
filter:
|
||||
'(alert.attributes.name: "hello world" OR alert.attributes.params.index: "hello world" OR alert.attributes.params.threat.tactic.id: "hello world" OR alert.attributes.params.threat.tactic.name: "hello world" OR alert.attributes.params.threat.technique.id: "hello world" OR alert.attributes.params.threat.technique.name: "hello world")',
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
sort_field: 'enabled',
|
||||
sort_order: 'desc',
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('check parameter url, query with a filter get escaped correctly', async () => {
|
||||
await fetchRules({
|
||||
filterOptions: {
|
||||
filter: '" OR (foo:bar)',
|
||||
showCustomRules: false,
|
||||
showElasticRules: false,
|
||||
tags: [],
|
||||
},
|
||||
sortingOptions: {
|
||||
field: 'enabled',
|
||||
order: 'desc',
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', {
|
||||
method: 'GET',
|
||||
query: {
|
||||
filter:
|
||||
'(alert.attributes.name: "\\" OR (foo:bar)" OR alert.attributes.params.index: "\\" OR (foo:bar)" OR alert.attributes.params.threat.tactic.id: "\\" OR (foo:bar)" OR alert.attributes.params.threat.tactic.name: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.id: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.name: "\\" OR (foo:bar)")',
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
sort_field: 'enabled',
|
||||
|
@ -226,7 +256,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" AND "world")',
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
sort_field: 'enabled',
|
||||
|
@ -254,7 +284,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" AND "world")',
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
sort_field: 'updatedAt',
|
||||
|
@ -353,7 +383,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.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags:("hello" AND "world") AND (alert.attributes.name: "ruleName" OR alert.attributes.params.index: "ruleName" OR alert.attributes.params.threat.tactic.id: "ruleName" OR alert.attributes.params.threat.tactic.name: "ruleName" OR alert.attributes.params.threat.technique.id: "ruleName" OR alert.attributes.params.threat.technique.name: "ruleName")',
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
sort_field: 'enabled',
|
||||
|
|
|
@ -26,7 +26,17 @@ describe('convertRulesFilterToKQL', () => {
|
|||
it('handles presence of "filter" properly', () => {
|
||||
const kql = convertRulesFilterToKQL({ ...filterOptions, filter: 'foo' });
|
||||
|
||||
expect(kql).toBe('alert.attributes.name: foo');
|
||||
expect(kql).toBe(
|
||||
'(alert.attributes.name: "foo" OR alert.attributes.params.index: "foo" OR alert.attributes.params.threat.tactic.id: "foo" OR alert.attributes.params.threat.tactic.name: "foo" OR alert.attributes.params.threat.technique.id: "foo" OR alert.attributes.params.threat.technique.name: "foo")'
|
||||
);
|
||||
});
|
||||
|
||||
it('escapes "filter" value properly', () => {
|
||||
const kql = convertRulesFilterToKQL({ ...filterOptions, filter: '" OR (foo: bar)' });
|
||||
|
||||
expect(kql).toBe(
|
||||
'(alert.attributes.name: "\\" OR (foo: bar)" OR alert.attributes.params.index: "\\" OR (foo: bar)" OR alert.attributes.params.threat.tactic.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.tactic.name: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.name: "\\" OR (foo: bar)")'
|
||||
);
|
||||
});
|
||||
|
||||
it('handles presence of "showCustomRules" properly', () => {
|
||||
|
@ -44,7 +54,7 @@ describe('convertRulesFilterToKQL', () => {
|
|||
it('handles presence of "tags" properly', () => {
|
||||
const kql = convertRulesFilterToKQL({ ...filterOptions, tags: ['tag1', 'tag2'] });
|
||||
|
||||
expect(kql).toBe('alert.attributes.tags: "tag1" AND alert.attributes.tags: "tag2"');
|
||||
expect(kql).toBe('alert.attributes.tags:("tag1" AND "tag2")');
|
||||
});
|
||||
|
||||
it('handles combination of different properties properly', () => {
|
||||
|
@ -56,7 +66,7 @@ describe('convertRulesFilterToKQL', () => {
|
|||
});
|
||||
|
||||
expect(kql).toBe(
|
||||
`alert.attributes.name: foo AND alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true" AND (alert.attributes.tags: "tag1" AND alert.attributes.tags: "tag2")`
|
||||
`alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true" AND alert.attributes.tags:(\"tag1\" AND \"tag2\") AND (alert.attributes.name: \"foo\" OR alert.attributes.params.index: \"foo\" OR alert.attributes.params.threat.tactic.id: \"foo\" OR alert.attributes.params.threat.tactic.name: \"foo\" OR alert.attributes.params.threat.technique.id: \"foo\" OR alert.attributes.params.threat.technique.name: \"foo\")`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,8 +6,18 @@
|
|||
*/
|
||||
|
||||
import { INTERNAL_IMMUTABLE_KEY } from '../../../../../common/constants';
|
||||
import { escapeKuery } from '../../../../common/lib/keury';
|
||||
import { FilterOptions } from './types';
|
||||
|
||||
const SEARCHABLE_RULE_PARAMS = [
|
||||
'alert.attributes.name',
|
||||
'alert.attributes.params.index',
|
||||
'alert.attributes.params.threat.tactic.id',
|
||||
'alert.attributes.params.threat.tactic.name',
|
||||
'alert.attributes.params.threat.technique.id',
|
||||
'alert.attributes.params.threat.technique.name',
|
||||
];
|
||||
|
||||
/**
|
||||
* Convert rules filter options object to KQL query
|
||||
*
|
||||
|
@ -15,27 +25,35 @@ import { FilterOptions } from './types';
|
|||
*
|
||||
* @returns KQL string
|
||||
*/
|
||||
export const convertRulesFilterToKQL = (filterOptions: FilterOptions): string => {
|
||||
const showCustomRuleFilter = filterOptions.showCustomRules
|
||||
? [`alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:false"`]
|
||||
: [];
|
||||
const showElasticRuleFilter = filterOptions.showElasticRules
|
||||
? [`alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true"`]
|
||||
: [];
|
||||
const filtersWithoutTags = [
|
||||
...(filterOptions.filter.length ? [`alert.attributes.name: ${filterOptions.filter}`] : []),
|
||||
...showCustomRuleFilter,
|
||||
...showElasticRuleFilter,
|
||||
].join(' AND ');
|
||||
export const convertRulesFilterToKQL = ({
|
||||
showCustomRules,
|
||||
showElasticRules,
|
||||
filter,
|
||||
tags,
|
||||
}: FilterOptions): string => {
|
||||
const filters: string[] = [];
|
||||
|
||||
const tags = filterOptions.tags
|
||||
.map((t) => `alert.attributes.tags: "${t.replace(/"/g, '\\"')}"`)
|
||||
.join(' AND ');
|
||||
if (showCustomRules) {
|
||||
filters.push(`alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:false"`);
|
||||
}
|
||||
|
||||
const filterString =
|
||||
filtersWithoutTags !== '' && tags !== ''
|
||||
? `${filtersWithoutTags} AND (${tags})`
|
||||
: filtersWithoutTags + tags;
|
||||
if (showElasticRules) {
|
||||
filters.push(`alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true"`);
|
||||
}
|
||||
|
||||
return filterString;
|
||||
if (tags.length > 0) {
|
||||
filters.push(
|
||||
`alert.attributes.tags:(${tags.map((tag) => `"${escapeKuery(tag)}"`).join(' AND ')})`
|
||||
);
|
||||
}
|
||||
|
||||
if (filter.length) {
|
||||
const searchQuery = SEARCHABLE_RULE_PARAMS.map(
|
||||
(param) => `${param}: "${escapeKuery(filter)}"`
|
||||
).join(' OR ');
|
||||
|
||||
filters.push(`(${searchQuery})`);
|
||||
}
|
||||
|
||||
return filters.join(' AND ');
|
||||
};
|
||||
|
|
|
@ -81,25 +81,6 @@ export const executeRulesBulkAction = async ({
|
|||
}
|
||||
};
|
||||
|
||||
export const initRulesBulkAction = (params: Omit<ExecuteRulesBulkActionArgs, 'search'>) => {
|
||||
const byQuery = (query: string) =>
|
||||
executeRulesBulkAction({
|
||||
...params,
|
||||
search: { query },
|
||||
});
|
||||
|
||||
const byIds = (ids: string[]) =>
|
||||
executeRulesBulkAction({
|
||||
...params,
|
||||
search: { ids },
|
||||
});
|
||||
|
||||
return {
|
||||
byQuery,
|
||||
byIds,
|
||||
};
|
||||
};
|
||||
|
||||
function defaultErrorHandler(toasts: UseAppToasts, action: BulkAction, error: HTTPError) {
|
||||
// if response doesn't have number of failed rules, it means the whole bulk action failed
|
||||
// and general error toast will be shown. Otherwise - error toast for partial failure
|
||||
|
|
|
@ -30,7 +30,7 @@ import { canEditRuleWithActions } from '../../../../../../common/utils/privilege
|
|||
import { useRulesTableContext } from '../rules_table/rules_table_context';
|
||||
import * as detectionI18n from '../../../translations';
|
||||
import * as i18n from '../../translations';
|
||||
import { executeRulesBulkAction, initRulesBulkAction } from '../actions';
|
||||
import { executeRulesBulkAction } from '../actions';
|
||||
import { useHasActionsPrivileges } from '../use_has_actions_privileges';
|
||||
import { useHasMlPermissions } from '../use_has_ml_permissions';
|
||||
import { getCustomRulesCountFromCache } from './use_custom_rules_count';
|
||||
|
@ -239,26 +239,23 @@ export const useBulkActions = ({
|
|||
);
|
||||
}, 5 * 1000);
|
||||
|
||||
const rulesBulkAction = initRulesBulkAction({
|
||||
await executeRulesBulkAction({
|
||||
visibleRuleIds: selectedRuleIds,
|
||||
action: BulkAction.edit,
|
||||
setLoadingRules,
|
||||
toasts,
|
||||
payload: { edit: [editPayload] },
|
||||
onFinish: () => hideWarningToast(),
|
||||
search: isAllSelected
|
||||
? {
|
||||
query: convertRulesFilterToKQL({
|
||||
...filterOptions,
|
||||
showCustomRules: true, // only edit custom rules, as elastic rule are immutable
|
||||
}),
|
||||
}
|
||||
: { ids: customSelectedRuleIds },
|
||||
});
|
||||
|
||||
// only edit custom rules, as elastic rule are immutable
|
||||
if (isAllSelected) {
|
||||
const customRulesOnlyFilterQuery = convertRulesFilterToKQL({
|
||||
...filterOptions,
|
||||
showCustomRules: true,
|
||||
});
|
||||
await rulesBulkAction.byQuery(customRulesOnlyFilterQuery);
|
||||
} else {
|
||||
await rulesBulkAction.byIds(customSelectedRuleIds);
|
||||
}
|
||||
|
||||
isBulkEditFinished = true;
|
||||
invalidateRules();
|
||||
if (getIsMounted()) {
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
SortingOptions,
|
||||
} from '../../../../../containers/detection_engine/rules/types';
|
||||
import { useFindRules } from './use_find_rules';
|
||||
import { getRulesComparator, getRulesPredicate } from './utils';
|
||||
import { getRulesComparator } from './utils';
|
||||
|
||||
export interface RulesTableState {
|
||||
/**
|
||||
|
@ -114,7 +114,7 @@ export interface LoadingRules {
|
|||
|
||||
export interface RulesTableActions {
|
||||
reFetchRules: ReturnType<typeof useFindRules>['refetch'];
|
||||
setFilterOptions: React.Dispatch<React.SetStateAction<FilterOptions>>;
|
||||
setFilterOptions: (newFilter: Partial<FilterOptions>) => void;
|
||||
setIsAllSelected: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setIsInMemorySorting: (value: boolean) => void;
|
||||
setIsRefreshOn: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
|
@ -186,6 +186,13 @@ export const RulesTableContextProvider = ({
|
|||
|
||||
const pagination = useMemo(() => ({ page, perPage }), [page, perPage]);
|
||||
|
||||
const handleFilterOptionsChange = useCallback((newFilter: Partial<FilterOptions>) => {
|
||||
setFilterOptions((currentFilter) => ({ ...currentFilter, ...newFilter }));
|
||||
setPage(1);
|
||||
setSelectedRuleIds([]);
|
||||
setIsAllSelected(false);
|
||||
}, []);
|
||||
|
||||
// Fetch rules
|
||||
const {
|
||||
data: { rules, total } = { rules: [], total: 0 },
|
||||
|
@ -210,15 +217,10 @@ export const RulesTableContextProvider = ({
|
|||
}
|
||||
}, [isFetched, isRefetching, refetchPrePackagedRulesStatus]);
|
||||
|
||||
// Filter rules
|
||||
const filteredRules = isInMemorySorting ? rules.filter(getRulesPredicate(filterOptions)) : rules;
|
||||
|
||||
// Paginate and sort rules
|
||||
const rulesToDisplay = isInMemorySorting
|
||||
? filteredRules
|
||||
.sort(getRulesComparator(sortingOptions))
|
||||
.slice((page - 1) * perPage, page * perPage)
|
||||
: filteredRules;
|
||||
? rules.sort(getRulesComparator(sortingOptions)).slice((page - 1) * perPage, page * perPage)
|
||||
: rules;
|
||||
|
||||
const providerValue = useMemo(
|
||||
() => ({
|
||||
|
@ -227,7 +229,7 @@ export const RulesTableContextProvider = ({
|
|||
pagination: {
|
||||
page,
|
||||
perPage,
|
||||
total: isInMemorySorting ? filteredRules.length : total,
|
||||
total: isInMemorySorting ? rules.length : total,
|
||||
},
|
||||
filterOptions,
|
||||
isActionInProgress,
|
||||
|
@ -246,7 +248,7 @@ export const RulesTableContextProvider = ({
|
|||
},
|
||||
actions: {
|
||||
reFetchRules: refetch,
|
||||
setFilterOptions,
|
||||
setFilterOptions: handleFilterOptionsChange,
|
||||
setIsAllSelected,
|
||||
setIsInMemorySorting: toggleInMemorySorting,
|
||||
setIsRefreshOn,
|
||||
|
@ -260,7 +262,7 @@ export const RulesTableContextProvider = ({
|
|||
[
|
||||
dataUpdatedAt,
|
||||
filterOptions,
|
||||
filteredRules.length,
|
||||
handleFilterOptionsChange,
|
||||
isActionInProgress,
|
||||
isAllSelected,
|
||||
isFetched,
|
||||
|
@ -274,6 +276,7 @@ export const RulesTableContextProvider = ({
|
|||
page,
|
||||
perPage,
|
||||
refetch,
|
||||
rules.length,
|
||||
rulesToDisplay,
|
||||
selectedRuleIds,
|
||||
sortingOptions,
|
||||
|
|
|
@ -31,8 +31,12 @@ export const useFindRules = (args: UseFindRulesArgs) => {
|
|||
// Use this query result when isInMemorySorting = true
|
||||
const allRules = useFindRulesQuery(
|
||||
['all'],
|
||||
{ pagination: { page: 1, perPage: MAX_RULES_PER_PAGE } },
|
||||
{ refetchInterval, enabled: isInMemorySorting }
|
||||
{ pagination: { page: 1, perPage: MAX_RULES_PER_PAGE }, filterOptions },
|
||||
{
|
||||
refetchInterval,
|
||||
enabled: isInMemorySorting,
|
||||
keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change
|
||||
}
|
||||
);
|
||||
|
||||
// Use this query result when isInMemorySorting = false
|
||||
|
|
|
@ -6,11 +6,7 @@
|
|||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import {
|
||||
FilterOptions,
|
||||
Rule,
|
||||
SortingOptions,
|
||||
} from '../../../../../containers/detection_engine/rules/types';
|
||||
import { Rule, SortingOptions } from '../../../../../containers/detection_engine/rules/types';
|
||||
|
||||
/**
|
||||
* Returns a comparator function to be used with .sort()
|
||||
|
@ -79,29 +75,3 @@ const compareNumbers = (a: number, b: number, direction: number) => {
|
|||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a predicate function to be used with .filter()
|
||||
*
|
||||
* @param filterOptions Current table filter
|
||||
*/
|
||||
export function getRulesPredicate(filterOptions: FilterOptions) {
|
||||
return (rule: Rule) => {
|
||||
if (
|
||||
filterOptions.filter &&
|
||||
!rule.name.toLowerCase().includes(filterOptions.filter.toLowerCase())
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (filterOptions.showCustomRules && rule.immutable) {
|
||||
return false;
|
||||
}
|
||||
if (filterOptions.showElasticRules && !rule.immutable) {
|
||||
return false;
|
||||
}
|
||||
if (filterOptions.tags.length && !filterOptions.tags.every((tag) => rule.tags.includes(tag))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,65 +7,38 @@
|
|||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from '@testing-library/react';
|
||||
|
||||
import { RulesTableFilters } from './rules_table_filters';
|
||||
import { useAppToastsMock } from '../../../../../../common/hooks/use_app_toasts.mock';
|
||||
import { useAppToasts } from '../../../../../../common/hooks/use_app_toasts';
|
||||
jest.mock('../../../../../../common/hooks/use_app_toasts');
|
||||
import { TestProviders } from '../../../../../../common/mock';
|
||||
|
||||
jest.mock('../rules_table/rules_table_context');
|
||||
|
||||
describe('RulesTableFilters', () => {
|
||||
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
appToastsMock = useAppToastsMock.create();
|
||||
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
|
||||
});
|
||||
|
||||
it('renders no numbers next to rule type button filter if none exist', async () => {
|
||||
await act(async () => {
|
||||
const wrapper = mount(
|
||||
<RulesTableFilters
|
||||
onFilterChanged={jest.fn()}
|
||||
rulesCustomInstalled={null}
|
||||
rulesInstalled={null}
|
||||
currentFilterTags={[]}
|
||||
tags={[]}
|
||||
isLoadingTags={false}
|
||||
reFetchTags={() => ({})}
|
||||
/>
|
||||
);
|
||||
const wrapper = mount(
|
||||
<RulesTableFilters rulesCustomInstalled={null} rulesInstalled={null} allTags={[]} />,
|
||||
{ wrappingComponent: TestProviders }
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="showElasticRulesFilterButton"]').at(0).text()).toEqual(
|
||||
'Elastic rules'
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="showCustomRulesFilterButton"]').at(0).text()).toEqual(
|
||||
'Custom rules'
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('[data-test-subj="showElasticRulesFilterButton"]').at(0).text()).toEqual(
|
||||
'Elastic rules'
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="showCustomRulesFilterButton"]').at(0).text()).toEqual(
|
||||
'Custom rules'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders number of custom and prepackaged rules', async () => {
|
||||
await act(async () => {
|
||||
const wrapper = mount(
|
||||
<RulesTableFilters
|
||||
onFilterChanged={jest.fn()}
|
||||
rulesCustomInstalled={10}
|
||||
rulesInstalled={9}
|
||||
currentFilterTags={[]}
|
||||
tags={[]}
|
||||
isLoadingTags={false}
|
||||
reFetchTags={() => ({})}
|
||||
/>
|
||||
);
|
||||
const wrapper = mount(
|
||||
<RulesTableFilters rulesCustomInstalled={10} rulesInstalled={9} allTags={[]} />,
|
||||
{ wrappingComponent: TestProviders }
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="showElasticRulesFilterButton"]').at(0).text()).toEqual(
|
||||
'Elastic rules (9)'
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="showCustomRulesFilterButton"]').at(0).text()).toEqual(
|
||||
'Custom rules (10)'
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('[data-test-subj="showElasticRulesFilterButton"]').at(0).text()).toEqual(
|
||||
'Elastic rules (9)'
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="showCustomRulesFilterButton"]').at(0).text()).toEqual(
|
||||
'Custom rules (10)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
EuiFieldSearch,
|
||||
EuiFilterButton,
|
||||
|
@ -15,76 +13,63 @@ import {
|
|||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { isEqual } from 'lodash/fp';
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import * as i18n from '../../translations';
|
||||
|
||||
import { FilterOptions } from '../../../../../containers/detection_engine/rules';
|
||||
import { useRulesTableContext } from '../rules_table/rules_table_context';
|
||||
import { TagsFilterPopover } from './tags_filter_popover';
|
||||
|
||||
const FilterWrapper = styled(EuiFlexGroup)`
|
||||
margin-bottom: ${({ theme }) => theme.eui.euiSizeXS};
|
||||
`;
|
||||
|
||||
interface RulesTableFiltersProps {
|
||||
onFilterChanged: (filterOptions: Partial<FilterOptions>) => void;
|
||||
rulesCustomInstalled: number | null;
|
||||
rulesInstalled: number | null;
|
||||
currentFilterTags: string[];
|
||||
tags: string[];
|
||||
isLoadingTags: boolean;
|
||||
reFetchTags: () => void;
|
||||
allTags: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
rulesCustomInstalled,
|
||||
rulesInstalled,
|
||||
currentFilterTags,
|
||||
tags,
|
||||
isLoadingTags,
|
||||
reFetchTags,
|
||||
allTags,
|
||||
}: RulesTableFiltersProps) => {
|
||||
const [filter, setFilter] = useState<string>('');
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [showCustomRules, setShowCustomRules] = useState<boolean>(false);
|
||||
const [showElasticRules, setShowElasticRules] = useState<boolean>(false);
|
||||
const {
|
||||
state: { filterOptions },
|
||||
actions: { setFilterOptions },
|
||||
} = useRulesTableContext();
|
||||
|
||||
useEffect(() => {
|
||||
reFetchTags();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [rulesCustomInstalled, rulesInstalled]);
|
||||
const { showCustomRules, showElasticRules, tags: selectedTags } = filterOptions;
|
||||
|
||||
// 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 handleOnSearch = useCallback(
|
||||
(filterString) => setFilterOptions({ filter: filterString.trim() }),
|
||||
[setFilterOptions]
|
||||
);
|
||||
|
||||
const handleElasticRulesClick = useCallback(() => {
|
||||
setShowElasticRules(!showElasticRules);
|
||||
setShowCustomRules(false);
|
||||
}, [setShowElasticRules, showElasticRules, setShowCustomRules]);
|
||||
setFilterOptions({ showElasticRules: !showElasticRules, showCustomRules: false });
|
||||
}, [setFilterOptions, showElasticRules]);
|
||||
|
||||
const handleCustomRulesClick = useCallback(() => {
|
||||
setShowCustomRules(!showCustomRules);
|
||||
setShowElasticRules(false);
|
||||
}, [setShowElasticRules, showCustomRules, setShowCustomRules]);
|
||||
setFilterOptions({ showCustomRules: !showCustomRules, showElasticRules: false });
|
||||
}, [setFilterOptions, showCustomRules]);
|
||||
|
||||
const handleSelectedTags = useCallback(
|
||||
(newTags) => {
|
||||
(newTags: string[]) => {
|
||||
if (!isEqual(newTags, selectedTags)) {
|
||||
setSelectedTags(newTags);
|
||||
setFilterOptions({ tags: newTags });
|
||||
}
|
||||
},
|
||||
[selectedTags]
|
||||
[selectedTags, setFilterOptions]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="m" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<FilterWrapper gutterSize="m" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow>
|
||||
<EuiFieldSearch
|
||||
aria-label={i18n.SEARCH_RULES}
|
||||
fullWidth
|
||||
|
@ -93,15 +78,12 @@ const RulesTableFiltersComponent = ({
|
|||
onSearch={handleOnSearch}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFilterGroup>
|
||||
<TagsFilterPopover
|
||||
isLoading={isLoadingTags}
|
||||
onSelectedTagsChanged={handleSelectedTags}
|
||||
selectedTags={selectedTags}
|
||||
tags={tags}
|
||||
currentFilterTags={currentFilterTags}
|
||||
tags={allTags}
|
||||
data-test-subj="allRulesTagPopover"
|
||||
/>
|
||||
</EuiFilterGroup>
|
||||
|
@ -123,14 +105,12 @@ const RulesTableFiltersComponent = ({
|
|||
onClick={handleCustomRulesClick}
|
||||
data-test-subj="showCustomRulesFilterButton"
|
||||
>
|
||||
<>
|
||||
{i18n.CUSTOM_RULES}
|
||||
{rulesCustomInstalled != null ? ` (${rulesCustomInstalled})` : ''}
|
||||
</>
|
||||
{i18n.CUSTOM_RULES}
|
||||
{rulesCustomInstalled != null ? ` (${rulesCustomInstalled})` : ''}
|
||||
</EuiFilterButton>
|
||||
</EuiFilterGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</FilterWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -13,13 +13,7 @@ import { TagsFilterPopover } from './tags_filter_popover';
|
|||
describe('TagsFilterPopover', () => {
|
||||
it('renders correctly', () => {
|
||||
const wrapper = shallow(
|
||||
<TagsFilterPopover
|
||||
tags={[]}
|
||||
selectedTags={[]}
|
||||
onSelectedTagsChanged={jest.fn()}
|
||||
currentFilterTags={[]}
|
||||
isLoading={false}
|
||||
/>
|
||||
<TagsFilterPopover tags={[]} selectedTags={[]} onSelectedTagsChanged={jest.fn()} />
|
||||
);
|
||||
|
||||
expect(wrapper.find('EuiPopover')).toHaveLength(1);
|
||||
|
|
|
@ -5,15 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiFilterButton,
|
||||
EuiFilterSelectItem,
|
||||
|
@ -33,10 +25,7 @@ import { caseInsensitiveSort } from '../helpers';
|
|||
interface TagsFilterPopoverProps {
|
||||
selectedTags: string[];
|
||||
tags: string[];
|
||||
onSelectedTagsChanged: Dispatch<SetStateAction<string[]>>;
|
||||
currentFilterTags: string[];
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
isLoading: boolean; // TO DO reimplement?
|
||||
onSelectedTagsChanged: (newTags: string[]) => void;
|
||||
}
|
||||
|
||||
const PopoverContentWrapper = styled.div`
|
||||
|
@ -64,11 +53,10 @@ const TagsFilterPopoverComponent = ({
|
|||
tags,
|
||||
selectedTags,
|
||||
onSelectedTagsChanged,
|
||||
currentFilterTags,
|
||||
}: TagsFilterPopoverProps) => {
|
||||
const sortedTags = useMemo(
|
||||
() => caseInsensitiveSort(Array.from(new Set([...tags, ...currentFilterTags]))),
|
||||
[tags, currentFilterTags]
|
||||
() => caseInsensitiveSort(Array.from(new Set([...tags, ...selectedTags]))),
|
||||
[selectedTags, tags]
|
||||
);
|
||||
const [isTagPopoverOpen, setIsTagPopoverOpen] = useState(false);
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
|
|
|
@ -14,19 +14,16 @@ import {
|
|||
EuiLoadingContent,
|
||||
EuiProgress,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { partition } from 'lodash/fp';
|
||||
|
||||
import { AllRulesTabs } from './rules_table_toolbar';
|
||||
import { HeaderSection } from '../../../../../common/components/header_section';
|
||||
import { Loader } from '../../../../../common/components/loader';
|
||||
import { useBoolState } from '../../../../../common/hooks/use_bool_state';
|
||||
import { useValueChanged } from '../../../../../common/hooks/use_value_changed';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import { PrePackagedRulesPrompt } from '../../../../components/rules/pre_packaged_rules/load_empty_prompt';
|
||||
import {
|
||||
CreatePreBuiltRules,
|
||||
FilterOptions,
|
||||
Rule,
|
||||
RulesSortingFields,
|
||||
} from '../../../../containers/detection_engine/rules';
|
||||
|
@ -85,7 +82,6 @@ export const RulesTables = React.memo<RulesTableProps>(
|
|||
rulesNotUpdated,
|
||||
selectedTab,
|
||||
}) => {
|
||||
const { timelines } = useKibana().services;
|
||||
const tableRef = useRef<EuiBasicTable>(null);
|
||||
const rulesTableContext = useRulesTableContext();
|
||||
|
||||
|
@ -96,11 +92,9 @@ export const RulesTables = React.memo<RulesTableProps>(
|
|||
isActionInProgress,
|
||||
isAllSelected,
|
||||
isFetched,
|
||||
isFetching,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isRefreshOn,
|
||||
lastUpdated,
|
||||
loadingRuleIds,
|
||||
loadingRulesAction,
|
||||
pagination,
|
||||
|
@ -109,7 +103,6 @@ export const RulesTables = React.memo<RulesTableProps>(
|
|||
},
|
||||
actions: {
|
||||
reFetchRules,
|
||||
setFilterOptions,
|
||||
setIsAllSelected,
|
||||
setIsRefreshOn,
|
||||
setPage,
|
||||
|
@ -125,7 +118,12 @@ export const RulesTables = React.memo<RulesTableProps>(
|
|||
rulesNotUpdated
|
||||
);
|
||||
|
||||
const [isLoadingTags, tags, reFetchTags] = useTags();
|
||||
const [, allTags, reFetchTags] = useTags();
|
||||
|
||||
useEffect(() => {
|
||||
reFetchTags();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [rulesCustomInstalled, rulesInstalled]);
|
||||
|
||||
const [isDeleteConfirmationVisible, showDeleteConfirmation, hideDeleteConfirmation] =
|
||||
useBoolState();
|
||||
|
@ -183,16 +181,6 @@ export const RulesTables = React.memo<RulesTableProps>(
|
|||
[pagination]
|
||||
);
|
||||
|
||||
const onFilterChangedCallback = useCallback(
|
||||
(newFilter: Partial<FilterOptions>) => {
|
||||
setFilterOptions((currentFilter) => ({ ...currentFilter, ...newFilter }));
|
||||
setPage(1);
|
||||
setSelectedRuleIds([]);
|
||||
setIsAllSelected(false);
|
||||
},
|
||||
[setFilterOptions, setIsAllSelected, setPage, setSelectedRuleIds]
|
||||
);
|
||||
|
||||
const tableOnChangeCallback = useCallback(
|
||||
({ page, sort }: EuiBasicTableOnChange) => {
|
||||
setSortingOptions({
|
||||
|
@ -286,9 +274,11 @@ export const RulesTables = React.memo<RulesTableProps>(
|
|||
}
|
||||
: { 'data-test-subj': 'monitoring-table', columns: monitoringColumns };
|
||||
|
||||
const shouldShowLinearProgress = isFetched && isRefetching;
|
||||
const shouldShowLoadingOverlay = (!isFetched && isRefetching) || isActionInProgress;
|
||||
return (
|
||||
<>
|
||||
{isFetched && isRefetching && (
|
||||
{shouldShowLinearProgress && (
|
||||
<EuiProgress
|
||||
data-test-subj="loadingRulesInfoProgress"
|
||||
size="xs"
|
||||
|
@ -296,30 +286,16 @@ export const RulesTables = React.memo<RulesTableProps>(
|
|||
color="accent"
|
||||
/>
|
||||
)}
|
||||
{((!isFetched && isRefetching) || isActionInProgress) && (
|
||||
{shouldShowLoadingOverlay && (
|
||||
<Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" />
|
||||
)}
|
||||
<HeaderSection
|
||||
split
|
||||
growLeftSplit={false}
|
||||
title={i18n.ALL_RULES}
|
||||
subtitle={timelines.getLastUpdated({
|
||||
showUpdating: loading || isFetching,
|
||||
updatedAt: lastUpdated,
|
||||
})}
|
||||
>
|
||||
{shouldShowRulesTable && (
|
||||
<RulesTableFilters
|
||||
onFilterChanged={onFilterChangedCallback}
|
||||
rulesCustomInstalled={rulesCustomInstalled}
|
||||
rulesInstalled={rulesInstalled}
|
||||
currentFilterTags={filterOptions.tags}
|
||||
isLoadingTags={isLoadingTags}
|
||||
tags={tags}
|
||||
reFetchTags={reFetchTags}
|
||||
/>
|
||||
)}
|
||||
</HeaderSection>
|
||||
{shouldShowRulesTable && (
|
||||
<RulesTableFilters
|
||||
rulesCustomInstalled={rulesCustomInstalled}
|
||||
rulesInstalled={rulesInstalled}
|
||||
allTags={allTags}
|
||||
/>
|
||||
)}
|
||||
{shouldShowPrepackagedRulesPrompt && (
|
||||
<PrePackagedRulesPrompt
|
||||
createPrePackagedRules={handleCreatePrePackagedRules}
|
||||
|
@ -362,7 +338,7 @@ export const RulesTables = React.memo<RulesTableProps>(
|
|||
editAction={bulkEditActionType}
|
||||
onClose={handleBulkEditFormCancel}
|
||||
onConfirm={handleBulkEditFormConfirm}
|
||||
tags={tags}
|
||||
tags={allTags}
|
||||
/>
|
||||
)}
|
||||
{shouldShowRulesTable && (
|
||||
|
|
|
@ -22,6 +22,8 @@ import {
|
|||
UtilityBarText,
|
||||
} from '../../../../../common/components/utility_bar';
|
||||
import * as i18n from '../translations';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import { useRulesTableContextOptional } from './rules_table/rules_table_context';
|
||||
|
||||
interface AllRulesUtilityBarProps {
|
||||
canBulkEdit: boolean;
|
||||
|
@ -55,6 +57,9 @@ export const AllRulesUtilityBar = React.memo<AllRulesUtilityBarProps>(
|
|||
isBulkActionInProgress,
|
||||
hasDisabledActions,
|
||||
}) => {
|
||||
const { timelines } = useKibana().services;
|
||||
const rulesTableContext = useRulesTableContextOptional();
|
||||
|
||||
const handleGetBulkItemsPopoverContent = useCallback(
|
||||
(closePopover: () => void): JSX.Element | null => {
|
||||
if (onGetBulkItemsPopoverContent != null) {
|
||||
|
@ -100,7 +105,7 @@ export const AllRulesUtilityBar = React.memo<AllRulesUtilityBarProps>(
|
|||
);
|
||||
|
||||
return (
|
||||
<UtilityBar>
|
||||
<UtilityBar border>
|
||||
<UtilityBarSection>
|
||||
<UtilityBarGroup>
|
||||
{hasBulkActions ? (
|
||||
|
@ -180,6 +185,14 @@ export const AllRulesUtilityBar = React.memo<AllRulesUtilityBarProps>(
|
|||
</UtilityBarGroup>
|
||||
)}
|
||||
</UtilityBarSection>
|
||||
{rulesTableContext && (
|
||||
<UtilityBarSection>
|
||||
{timelines.getLastUpdated({
|
||||
showUpdating: rulesTableContext.state.isFetching,
|
||||
updatedAt: rulesTableContext.state.lastUpdated,
|
||||
})}
|
||||
</UtilityBarSection>
|
||||
)}
|
||||
</UtilityBar>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -391,13 +391,6 @@ export const EXPORT_FILENAME = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const ALL_RULES = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allRules.tableTitle',
|
||||
{
|
||||
defaultMessage: 'All rules',
|
||||
}
|
||||
);
|
||||
|
||||
export const SEARCH_RULES = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allRules.searchAriaLabel',
|
||||
{
|
||||
|
@ -408,7 +401,7 @@ export const SEARCH_RULES = i18n.translate(
|
|||
export const SEARCH_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allRules.searchPlaceholder',
|
||||
{
|
||||
defaultMessage: 'e.g. rule name',
|
||||
defaultMessage: 'Search by rule name, index pattern, or MITRE ATT&CK tactic or technique',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -20830,14 +20830,12 @@
|
|||
"xpack.securitySolution.detectionEngine.rules.allRules.inactiveRuleDescription": "inactive",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.refreshTitle": "Actualiser",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.searchAriaLabel": "Rechercher les règles",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.searchPlaceholder": "par ex. nom de règle",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.selectAllRulesTitle": "Sélection totale de {totalRules} {totalRules, plural, =1 {règle} other {règles}} effectuée",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.selectedRulesTitle": "Sélection de {selectedRules} {selectedRules, plural, =1 {règle} other {règles}} effectuée",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.showingExceptionLists": "Affichage de {totalLists} {totalLists, plural, =1 {liste} other {listes}}",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.showingRulesTitle": "Affichage de {totalRules} {totalRules, plural, =1 {règle} other {règles}}",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastDescription": "Duplication réussie de {totalRules, plural, =1 {{totalRules} règle} other {{totalRules} règles}}",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.export.successToastDescription": "Exportation réussie de {exportedRules} sur {totalRules} {totalRules, plural, =1 {règle} other {règles}}. Les règles prédéfinies ont été exclues du fichier résultant.",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.tableTitle": "Toutes les règles",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.tabs.exceptions": "Listes d'exceptions",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.tabs.monitoring": "Monitoring des règles",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.tabs.rules": "Règles",
|
||||
|
|
|
@ -23836,14 +23836,12 @@
|
|||
"xpack.securitySolution.detectionEngine.rules.allRules.inactiveRuleDescription": "非アクティブ",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.refreshTitle": "更新",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.searchAriaLabel": "ルールの検索",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.searchPlaceholder": "例:ルール名",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.selectAllRulesTitle": "すべての{totalRules} {totalRules, plural, other {個のルール}}を選択",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.selectedRulesTitle": "{selectedRules} {selectedRules, plural, other {ルール}}を選択しました",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.showingExceptionLists": "{totalLists} {totalLists, plural, other {件のリスト}}を表示しています。",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.showingRulesTitle": "{totalRules} {totalRules, plural, other {ルール}}を表示中",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastDescription": "{totalRules, plural, other {{totalRules}ルール}}を正常に複製しました",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.export.successToastDescription": "{exportedRules}/{totalRules} {totalRules, plural, other {件のルール}}を正常にエクスポートしました事前構築済みルールは結果のファイルから除外されました。",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.tableTitle": "すべてのルール",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.tabs.exceptions": "例外リスト",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.tabs.monitoring": "ルール監視",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.tabs.rules": "ルール",
|
||||
|
|
|
@ -23863,14 +23863,12 @@
|
|||
"xpack.securitySolution.detectionEngine.rules.allRules.inactiveRuleDescription": "非活动",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.refreshTitle": "刷新",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.searchAriaLabel": "搜索规则",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.searchPlaceholder": "例如,规则名",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.selectAllRulesTitle": "选择所有 {totalRules} 个{totalRules, plural, other {规则}}",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.selectedRulesTitle": "已选择 {selectedRules} 个{selectedRules, plural, other {规则}}",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.showingExceptionLists": "正在显示 {totalLists} 个{totalLists, plural, other {列表}}",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.showingRulesTitle": "正在显示 {totalRules} 个{totalRules, plural, other {规则}}",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastDescription": "已成功复制 {totalRules, plural, other {{totalRules} 个规则}}",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.export.successToastDescription": "已成功导出 {exportedRules}/{totalRules} 个{totalRules, plural, other {规则}}。预置规则已从结果文件中排除。",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.tableTitle": "所有规则",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.tabs.exceptions": "例外列表",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.tabs.monitoring": "规则监测",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.tabs.rules": "规则",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue