[SIEM] [Detections] Refactor the all rules page (#58428)
* Refactor the all rules page to be easier to test * review with Garrett * bring back utility bar under condition * fix bugs tags and allow switch to show loading when enable/disable rule * fix rules selection when trigerring new rules * fix imports/exports can only use rule_id as learned today * review I
This commit is contained in:
parent
24b8ecf74d
commit
b56eead4ac
21 changed files with 463 additions and 577 deletions
|
@ -5,9 +5,8 @@
|
|||
*/
|
||||
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useRules, ReturnRules } from './use_rules';
|
||||
import { useRules, UseRules, ReturnRules } from './use_rules';
|
||||
import * as api from './api';
|
||||
import { PaginationOptions, FilterOptions } from '.';
|
||||
|
||||
jest.mock('./api');
|
||||
|
||||
|
@ -17,55 +16,40 @@ describe('useRules', () => {
|
|||
});
|
||||
test('init', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<
|
||||
[PaginationOptions, FilterOptions],
|
||||
ReturnRules
|
||||
>(props =>
|
||||
useRules(
|
||||
{
|
||||
const { result, waitForNextUpdate } = renderHook<UseRules, ReturnRules>(props =>
|
||||
useRules({
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
total: 100,
|
||||
},
|
||||
{
|
||||
filterOptions: {
|
||||
filter: '',
|
||||
sortField: 'created_at',
|
||||
sortOrder: 'desc',
|
||||
}
|
||||
)
|
||||
},
|
||||
})
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual([
|
||||
true,
|
||||
{
|
||||
data: [],
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
},
|
||||
null,
|
||||
]);
|
||||
expect(result.current).toEqual([true, null, result.current[2]]);
|
||||
});
|
||||
});
|
||||
|
||||
test('fetch rules', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<
|
||||
[PaginationOptions, FilterOptions],
|
||||
ReturnRules
|
||||
>(() =>
|
||||
useRules(
|
||||
{
|
||||
const { result, waitForNextUpdate } = renderHook<UseRules, ReturnRules>(() =>
|
||||
useRules({
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
total: 100,
|
||||
},
|
||||
{
|
||||
filterOptions: {
|
||||
filter: '',
|
||||
sortField: 'created_at',
|
||||
sortOrder: 'desc',
|
||||
}
|
||||
)
|
||||
},
|
||||
})
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
@ -148,22 +132,19 @@ describe('useRules', () => {
|
|||
test('re-fetch rules', async () => {
|
||||
const spyOnfetchRules = jest.spyOn(api, 'fetchRules');
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<
|
||||
[PaginationOptions, FilterOptions],
|
||||
ReturnRules
|
||||
>(id =>
|
||||
useRules(
|
||||
{
|
||||
const { result, waitForNextUpdate } = renderHook<UseRules, ReturnRules>(id =>
|
||||
useRules({
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
total: 100,
|
||||
},
|
||||
{
|
||||
filterOptions: {
|
||||
filter: '',
|
||||
sortField: 'created_at',
|
||||
sortOrder: 'desc',
|
||||
}
|
||||
)
|
||||
},
|
||||
})
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
@ -178,37 +159,37 @@ describe('useRules', () => {
|
|||
test('fetch rules if props changes', async () => {
|
||||
const spyOnfetchRules = jest.spyOn(api, 'fetchRules');
|
||||
await act(async () => {
|
||||
const { rerender, waitForNextUpdate } = renderHook<
|
||||
[PaginationOptions, FilterOptions],
|
||||
ReturnRules
|
||||
>(args => useRules(args[0], args[1]), {
|
||||
initialProps: [
|
||||
{
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
total: 100,
|
||||
},
|
||||
{
|
||||
filter: '',
|
||||
sortField: 'created_at',
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
],
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
rerender([
|
||||
const { rerender, waitForNextUpdate } = renderHook<UseRules, ReturnRules>(
|
||||
args => useRules(args),
|
||||
{
|
||||
initialProps: {
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
total: 100,
|
||||
},
|
||||
filterOptions: {
|
||||
filter: '',
|
||||
sortField: 'created_at',
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
rerender({
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
total: 100,
|
||||
},
|
||||
{
|
||||
filterOptions: {
|
||||
filter: 'hello world',
|
||||
sortField: 'created_at',
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
]);
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
expect(spyOnfetchRules).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
|
|
@ -4,16 +4,27 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { noop } from 'lodash/fp';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
|
||||
import { FetchRulesResponse, FilterOptions, PaginationOptions } from './types';
|
||||
import { FetchRulesResponse, FilterOptions, PaginationOptions, Rule } from './types';
|
||||
import { useStateToaster } from '../../../components/toasters';
|
||||
import { fetchRules } from './api';
|
||||
import { errorToToaster } from '../../../components/ml/api/error_to_toaster';
|
||||
import * as i18n from './translations';
|
||||
|
||||
type Func = () => void;
|
||||
export type ReturnRules = [boolean, FetchRulesResponse, Func | null];
|
||||
export type ReturnRules = [
|
||||
boolean,
|
||||
FetchRulesResponse | null,
|
||||
(refreshPrePackagedRule?: boolean) => void
|
||||
];
|
||||
|
||||
export interface UseRules {
|
||||
pagination: PaginationOptions;
|
||||
filterOptions: FilterOptions;
|
||||
refetchPrePackagedRulesStatus?: () => void;
|
||||
dispatchRulesInReducer?: (rules: Rule[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for using the list of Rules from the Detection Engine API
|
||||
|
@ -21,17 +32,14 @@ export type ReturnRules = [boolean, FetchRulesResponse, Func | null];
|
|||
* @param pagination desired pagination options (e.g. page/perPage)
|
||||
* @param filterOptions desired filters (e.g. filter/sortField/sortOrder)
|
||||
*/
|
||||
export const useRules = (
|
||||
pagination: PaginationOptions,
|
||||
filterOptions: FilterOptions
|
||||
): ReturnRules => {
|
||||
const [rules, setRules] = useState<FetchRulesResponse>({
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
data: [],
|
||||
});
|
||||
const reFetchRules = useRef<Func | null>(null);
|
||||
export const useRules = ({
|
||||
pagination,
|
||||
filterOptions,
|
||||
refetchPrePackagedRulesStatus,
|
||||
dispatchRulesInReducer,
|
||||
}: UseRules): ReturnRules => {
|
||||
const [rules, setRules] = useState<FetchRulesResponse | null>(null);
|
||||
const reFetchRules = useRef<(refreshPrePackagedRule?: boolean) => void>(noop);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
||||
|
@ -50,10 +58,16 @@ export const useRules = (
|
|||
|
||||
if (isSubscribed) {
|
||||
setRules(fetchRulesResult);
|
||||
if (dispatchRulesInReducer != null) {
|
||||
dispatchRulesInReducer(fetchRulesResult.data);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (isSubscribed) {
|
||||
errorToToaster({ title: i18n.RULE_FETCH_FAILURE, error, dispatchToaster });
|
||||
if (dispatchRulesInReducer != null) {
|
||||
dispatchRulesInReducer([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isSubscribed) {
|
||||
|
@ -62,7 +76,12 @@ export const useRules = (
|
|||
}
|
||||
|
||||
fetchData();
|
||||
reFetchRules.current = fetchData.bind(null, true);
|
||||
reFetchRules.current = (refreshPrePackagedRule: boolean = false) => {
|
||||
fetchData(true);
|
||||
if (refreshPrePackagedRule && refetchPrePackagedRulesStatus != null) {
|
||||
refetchPrePackagedRulesStatus();
|
||||
}
|
||||
};
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
|
@ -76,6 +95,7 @@ export const useRules = (
|
|||
filterOptions.tags?.sort().join(),
|
||||
filterOptions.showCustomRules,
|
||||
filterOptions.showElasticRules,
|
||||
refetchPrePackagedRulesStatus,
|
||||
]);
|
||||
|
||||
return [loading, rules, reFetchRules.current];
|
||||
|
|
|
@ -14,7 +14,7 @@ describe('useTags', () => {
|
|||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<unknown, ReturnTags>(() => useTags());
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual([true, []]);
|
||||
expect(result.current).toEqual([true, [], result.current[2]]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -23,7 +23,11 @@ describe('useTags', () => {
|
|||
const { result, waitForNextUpdate } = renderHook<unknown, ReturnTags>(() => useTags());
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual([false, ['elastic', 'love', 'quality', 'code']]);
|
||||
expect(result.current).toEqual([
|
||||
false,
|
||||
['elastic', 'love', 'quality', 'code'],
|
||||
result.current[2],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,13 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { noop } from 'lodash/fp';
|
||||
import { useEffect, useState, useRef } 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';
|
||||
|
||||
export type ReturnTags = [boolean, string[]];
|
||||
export type ReturnTags = [boolean, string[], () => void];
|
||||
|
||||
/**
|
||||
* Hook for using the list of Tags from the Detection Engine API
|
||||
|
@ -20,6 +21,7 @@ export const useTags = (): ReturnTags => {
|
|||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
const reFetchTags = useRef<() => void>(noop);
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
|
@ -46,6 +48,7 @@ export const useTags = (): ReturnTags => {
|
|||
}
|
||||
|
||||
fetchData();
|
||||
reFetchTags.current = fetchData;
|
||||
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
|
@ -53,5 +56,5 @@ export const useTags = (): ReturnTags => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
return [loading, tags];
|
||||
return [loading, tags, reFetchTags.current];
|
||||
};
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import { Rule, RuleError } from '../../../../../containers/detection_engine/rules';
|
||||
import { TableData } from '../../types';
|
||||
|
||||
export const mockRule = (id: string): Rule => ({
|
||||
created_at: '2020-01-10T21:11:45.839Z',
|
||||
|
@ -50,103 +49,3 @@ export const mockRules: Rule[] = [
|
|||
mockRule('abe6c564-050d-45a5-aaf0-386c37dd1f61'),
|
||||
mockRule('63f06f34-c181-4b2d-af35-f2ace572a1ee'),
|
||||
];
|
||||
export const mockTableData: TableData[] = [
|
||||
{
|
||||
activate: true,
|
||||
id: 'abe6c564-050d-45a5-aaf0-386c37dd1f61',
|
||||
immutable: false,
|
||||
isLoading: false,
|
||||
risk_score: 21,
|
||||
rule: {
|
||||
href: '#/detections/rules/id/abe6c564-050d-45a5-aaf0-386c37dd1f61',
|
||||
name: 'Home Grown!',
|
||||
},
|
||||
rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea',
|
||||
severity: 'low',
|
||||
sourceRule: {
|
||||
created_at: '2020-01-10T21:11:45.839Z',
|
||||
created_by: 'elastic',
|
||||
description: '24/7',
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
filters: [],
|
||||
from: 'now-300s',
|
||||
id: 'abe6c564-050d-45a5-aaf0-386c37dd1f61',
|
||||
immutable: false,
|
||||
index: ['auditbeat-*'],
|
||||
interval: '5m',
|
||||
language: 'kuery',
|
||||
max_signals: 100,
|
||||
meta: { from: '0m' },
|
||||
name: 'Home Grown!',
|
||||
output_index: '.siem-signals-default',
|
||||
query: '',
|
||||
references: [],
|
||||
risk_score: 21,
|
||||
rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea',
|
||||
saved_id: "Garrett's IP",
|
||||
severity: 'low',
|
||||
tags: [],
|
||||
threat: [],
|
||||
timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2',
|
||||
timeline_title: 'Untitled timeline',
|
||||
to: 'now',
|
||||
type: 'saved_query',
|
||||
updated_at: '2020-01-10T21:11:45.839Z',
|
||||
updated_by: 'elastic',
|
||||
version: 1,
|
||||
},
|
||||
status: null,
|
||||
statusDate: null,
|
||||
tags: [],
|
||||
},
|
||||
{
|
||||
activate: true,
|
||||
id: '63f06f34-c181-4b2d-af35-f2ace572a1ee',
|
||||
immutable: false,
|
||||
isLoading: false,
|
||||
risk_score: 21,
|
||||
rule: {
|
||||
href: '#/detections/rules/id/63f06f34-c181-4b2d-af35-f2ace572a1ee',
|
||||
name: 'Home Grown!',
|
||||
},
|
||||
rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea',
|
||||
severity: 'low',
|
||||
sourceRule: {
|
||||
created_at: '2020-01-10T21:11:45.839Z',
|
||||
created_by: 'elastic',
|
||||
description: '24/7',
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
filters: [],
|
||||
from: 'now-300s',
|
||||
id: '63f06f34-c181-4b2d-af35-f2ace572a1ee',
|
||||
immutable: false,
|
||||
index: ['auditbeat-*'],
|
||||
interval: '5m',
|
||||
language: 'kuery',
|
||||
max_signals: 100,
|
||||
meta: { from: '0m' },
|
||||
name: 'Home Grown!',
|
||||
output_index: '.siem-signals-default',
|
||||
query: '',
|
||||
references: [],
|
||||
risk_score: 21,
|
||||
rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea',
|
||||
saved_id: "Garrett's IP",
|
||||
severity: 'low',
|
||||
tags: [],
|
||||
threat: [],
|
||||
timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2',
|
||||
timeline_title: 'Untitled timeline',
|
||||
to: 'now',
|
||||
type: 'saved_query',
|
||||
updated_at: '2020-01-10T21:11:45.839Z',
|
||||
updated_by: 'elastic',
|
||||
version: 1,
|
||||
},
|
||||
status: null,
|
||||
statusDate: null,
|
||||
tags: [],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -32,53 +32,58 @@ export const editRuleAction = (rule: Rule, history: H.History) => {
|
|||
|
||||
export const duplicateRulesAction = async (
|
||||
rules: Rule[],
|
||||
ruleIds: string[],
|
||||
dispatch: React.Dispatch<Action>,
|
||||
dispatchToaster: Dispatch<ActionToaster>
|
||||
) => {
|
||||
try {
|
||||
const ruleIds = rules.map(r => r.id);
|
||||
dispatch({ type: 'updateLoading', ids: ruleIds, isLoading: true });
|
||||
const duplicatedRules = await duplicateRules({ rules });
|
||||
dispatch({ type: 'refresh' });
|
||||
displaySuccessToast(
|
||||
i18n.SUCCESSFULLY_DUPLICATED_RULES(duplicatedRules.length),
|
||||
dispatchToaster
|
||||
);
|
||||
dispatch({ type: 'loadingRuleIds', ids: ruleIds, actionType: 'duplicate' });
|
||||
const response = await duplicateRules({ rules });
|
||||
const { errors } = bucketRulesResponse(response);
|
||||
if (errors.length > 0) {
|
||||
displayErrorToast(
|
||||
i18n.DUPLICATE_RULE_ERROR,
|
||||
errors.map(e => e.error.message),
|
||||
dispatchToaster
|
||||
);
|
||||
} else {
|
||||
displaySuccessToast(i18n.SUCCESSFULLY_DUPLICATED_RULES(ruleIds.length), dispatchToaster);
|
||||
}
|
||||
dispatch({ type: 'loadingRuleIds', ids: [], actionType: null });
|
||||
} catch (e) {
|
||||
dispatch({ type: 'loadingRuleIds', ids: [], actionType: null });
|
||||
displayErrorToast(i18n.DUPLICATE_RULE_ERROR, [e.message], dispatchToaster);
|
||||
}
|
||||
};
|
||||
|
||||
export const exportRulesAction = async (rules: Rule[], dispatch: React.Dispatch<Action>) => {
|
||||
dispatch({ type: 'setExportPayload', exportPayload: rules });
|
||||
export const exportRulesAction = (exportRuleId: string[], dispatch: React.Dispatch<Action>) => {
|
||||
dispatch({ type: 'exportRuleIds', ids: exportRuleId });
|
||||
};
|
||||
|
||||
export const deleteRulesAction = async (
|
||||
ids: string[],
|
||||
ruleIds: string[],
|
||||
dispatch: React.Dispatch<Action>,
|
||||
dispatchToaster: Dispatch<ActionToaster>,
|
||||
onRuleDeleted?: () => void
|
||||
) => {
|
||||
try {
|
||||
dispatch({ type: 'loading', isLoading: true });
|
||||
|
||||
const response = await deleteRules({ ids });
|
||||
dispatch({ type: 'loadingRuleIds', ids: ruleIds, actionType: 'delete' });
|
||||
const response = await deleteRules({ ids: ruleIds });
|
||||
const { errors } = bucketRulesResponse(response);
|
||||
|
||||
dispatch({ type: 'refresh' });
|
||||
dispatch({ type: 'loadingRuleIds', ids: [], actionType: null });
|
||||
if (errors.length > 0) {
|
||||
displayErrorToast(
|
||||
i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ids.length),
|
||||
i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ruleIds.length),
|
||||
errors.map(e => e.error.message),
|
||||
dispatchToaster
|
||||
);
|
||||
} else {
|
||||
// FP: See https://github.com/typescript-eslint/typescript-eslint/issues/1138#issuecomment-566929566
|
||||
onRuleDeleted?.(); // eslint-disable-line no-unused-expressions
|
||||
} else if (onRuleDeleted) {
|
||||
onRuleDeleted();
|
||||
}
|
||||
} catch (e) {
|
||||
dispatch({ type: 'loadingRuleIds', ids: [], actionType: null });
|
||||
displayErrorToast(
|
||||
i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ids.length),
|
||||
i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ruleIds.length),
|
||||
[e.message],
|
||||
dispatchToaster
|
||||
);
|
||||
|
@ -96,7 +101,7 @@ export const enableRulesAction = async (
|
|||
: i18n.BATCH_ACTION_DEACTIVATE_SELECTED_ERROR(ids.length);
|
||||
|
||||
try {
|
||||
dispatch({ type: 'updateLoading', ids, isLoading: true });
|
||||
dispatch({ type: 'loadingRuleIds', ids, actionType: enabled ? 'enable' : 'disable' });
|
||||
|
||||
const response = await enableRules({ ids, enabled });
|
||||
const { rules, errors } = bucketRulesResponse(response);
|
||||
|
@ -125,6 +130,6 @@ export const enableRulesAction = async (
|
|||
}
|
||||
} catch (e) {
|
||||
displayErrorToast(errorTitle, [e.message], dispatchToaster);
|
||||
dispatch({ type: 'updateLoading', ids, isLoading: false });
|
||||
dispatch({ type: 'loadingRuleIds', ids: [], actionType: null });
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,9 +6,7 @@
|
|||
|
||||
import { EuiContextMenuItem } from '@elastic/eui';
|
||||
import React, { Dispatch } from 'react';
|
||||
import * as H from 'history';
|
||||
import * as i18n from '../translations';
|
||||
import { TableData } from '../types';
|
||||
import { Action } from './reducer';
|
||||
import {
|
||||
deleteRulesAction,
|
||||
|
@ -17,18 +15,37 @@ import {
|
|||
exportRulesAction,
|
||||
} from './actions';
|
||||
import { ActionToaster } from '../../../../components/toasters';
|
||||
import { Rule } from '../../../../containers/detection_engine/rules';
|
||||
|
||||
export const getBatchItems = (
|
||||
selectedState: TableData[],
|
||||
dispatch: Dispatch<Action>,
|
||||
dispatchToaster: Dispatch<ActionToaster>,
|
||||
history: H.History,
|
||||
closePopover: () => void
|
||||
) => {
|
||||
const containsEnabled = selectedState.some(v => v.activate);
|
||||
const containsDisabled = selectedState.some(v => !v.activate);
|
||||
const containsLoading = selectedState.some(v => v.isLoading);
|
||||
const containsImmutable = selectedState.some(v => v.immutable);
|
||||
interface GetBatchItems {
|
||||
closePopover: () => void;
|
||||
dispatch: Dispatch<Action>;
|
||||
dispatchToaster: Dispatch<ActionToaster>;
|
||||
loadingRuleIds: string[];
|
||||
reFetchRules: (refreshPrePackagedRule?: boolean) => void;
|
||||
rules: Rule[];
|
||||
selectedRuleIds: string[];
|
||||
}
|
||||
|
||||
export const getBatchItems = ({
|
||||
closePopover,
|
||||
dispatch,
|
||||
dispatchToaster,
|
||||
loadingRuleIds,
|
||||
reFetchRules,
|
||||
rules,
|
||||
selectedRuleIds,
|
||||
}: GetBatchItems) => {
|
||||
const containsEnabled = selectedRuleIds.some(
|
||||
id => rules.find(r => r.id === id)?.enabled ?? false
|
||||
);
|
||||
const containsDisabled = selectedRuleIds.some(
|
||||
id => !rules.find(r => r.id === id)?.enabled ?? false
|
||||
);
|
||||
const containsLoading = selectedRuleIds.some(id => loadingRuleIds.includes(id));
|
||||
const containsImmutable = selectedRuleIds.some(
|
||||
id => rules.find(r => r.id === id)?.immutable ?? false
|
||||
);
|
||||
|
||||
return [
|
||||
<EuiContextMenuItem
|
||||
|
@ -37,7 +54,9 @@ export const getBatchItems = (
|
|||
disabled={containsLoading || !containsDisabled}
|
||||
onClick={async () => {
|
||||
closePopover();
|
||||
const deactivatedIds = selectedState.filter(s => !s.activate).map(s => s.id);
|
||||
const deactivatedIds = selectedRuleIds.filter(
|
||||
id => !rules.find(r => r.id === id)?.enabled ?? false
|
||||
);
|
||||
await enableRulesAction(deactivatedIds, true, dispatch, dispatchToaster);
|
||||
}}
|
||||
>
|
||||
|
@ -49,7 +68,9 @@ export const getBatchItems = (
|
|||
disabled={containsLoading || !containsEnabled}
|
||||
onClick={async () => {
|
||||
closePopover();
|
||||
const activatedIds = selectedState.filter(s => s.activate).map(s => s.id);
|
||||
const activatedIds = selectedRuleIds.filter(
|
||||
id => rules.find(r => r.id === id)?.enabled ?? false
|
||||
);
|
||||
await enableRulesAction(activatedIds, false, dispatch, dispatchToaster);
|
||||
}}
|
||||
>
|
||||
|
@ -58,11 +79,11 @@ export const getBatchItems = (
|
|||
<EuiContextMenuItem
|
||||
key={i18n.BATCH_ACTION_EXPORT_SELECTED}
|
||||
icon="exportAction"
|
||||
disabled={containsImmutable || containsLoading || selectedState.length === 0}
|
||||
onClick={async () => {
|
||||
disabled={containsImmutable || containsLoading || selectedRuleIds.length === 0}
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
await exportRulesAction(
|
||||
selectedState.map(s => s.sourceRule),
|
||||
exportRulesAction(
|
||||
rules.filter(r => selectedRuleIds.includes(r.id)).map(r => r.rule_id),
|
||||
dispatch
|
||||
);
|
||||
}}
|
||||
|
@ -72,14 +93,16 @@ export const getBatchItems = (
|
|||
<EuiContextMenuItem
|
||||
key={i18n.BATCH_ACTION_DUPLICATE_SELECTED}
|
||||
icon="copy"
|
||||
disabled={containsLoading || selectedState.length === 0}
|
||||
disabled={containsLoading || selectedRuleIds.length === 0}
|
||||
onClick={async () => {
|
||||
closePopover();
|
||||
await duplicateRulesAction(
|
||||
selectedState.map(s => s.sourceRule),
|
||||
rules.filter(r => selectedRuleIds.includes(r.id)),
|
||||
selectedRuleIds,
|
||||
dispatch,
|
||||
dispatchToaster
|
||||
);
|
||||
reFetchRules(true);
|
||||
}}
|
||||
>
|
||||
{i18n.BATCH_ACTION_DUPLICATE_SELECTED}
|
||||
|
@ -88,14 +111,11 @@ export const getBatchItems = (
|
|||
key={i18n.BATCH_ACTION_DELETE_SELECTED}
|
||||
icon="trash"
|
||||
title={containsImmutable ? i18n.BATCH_ACTION_DELETE_SELECTED_IMMUTABLE : undefined}
|
||||
disabled={containsLoading || selectedState.length === 0}
|
||||
disabled={containsLoading || selectedRuleIds.length === 0}
|
||||
onClick={async () => {
|
||||
closePopover();
|
||||
await deleteRulesAction(
|
||||
selectedState.map(({ sourceRule: { id } }) => id),
|
||||
dispatch,
|
||||
dispatchToaster
|
||||
);
|
||||
await deleteRulesAction(selectedRuleIds, dispatch, dispatchToaster);
|
||||
reFetchRules(true);
|
||||
}}
|
||||
>
|
||||
{i18n.BATCH_ACTION_DELETE_SELECTED}
|
||||
|
|
|
@ -15,72 +15,92 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import * as H from 'history';
|
||||
import React, { Dispatch } from 'react';
|
||||
|
||||
import { Rule } from '../../../../containers/detection_engine/rules';
|
||||
import { getEmptyTagValue } from '../../../../components/empty_value';
|
||||
import { FormattedDate } from '../../../../components/formatted_date';
|
||||
import { getRuleDetailsUrl } from '../../../../components/link_to/redirect_to_detection_engine';
|
||||
import { ActionToaster } from '../../../../components/toasters';
|
||||
import { TruncatableText } from '../../../../components/truncatable_text';
|
||||
import { getStatusColor } from '../components/rule_status/helpers';
|
||||
import { RuleSwitch } from '../components/rule_switch';
|
||||
import { SeverityBadge } from '../components/severity_badge';
|
||||
import * as i18n from '../translations';
|
||||
import {
|
||||
deleteRulesAction,
|
||||
duplicateRulesAction,
|
||||
editRuleAction,
|
||||
exportRulesAction,
|
||||
} from './actions';
|
||||
|
||||
import { Action } from './reducer';
|
||||
import { TableData } from '../types';
|
||||
import * as i18n from '../translations';
|
||||
import { FormattedDate } from '../../../../components/formatted_date';
|
||||
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>,
|
||||
dispatchToaster: Dispatch<ActionToaster>,
|
||||
history: H.History
|
||||
history: H.History,
|
||||
reFetchRules: (refreshPrePackagedRule?: boolean) => void
|
||||
) => [
|
||||
{
|
||||
description: i18n.EDIT_RULE_SETTINGS,
|
||||
icon: 'visControls',
|
||||
name: i18n.EDIT_RULE_SETTINGS,
|
||||
onClick: (rowItem: TableData) => editRuleAction(rowItem.sourceRule, history),
|
||||
enabled: (rowItem: TableData) => !rowItem.sourceRule.immutable,
|
||||
onClick: (rowItem: Rule) => editRuleAction(rowItem, history),
|
||||
enabled: (rowItem: Rule) => !rowItem.immutable,
|
||||
},
|
||||
{
|
||||
description: i18n.DUPLICATE_RULE,
|
||||
icon: 'copy',
|
||||
name: i18n.DUPLICATE_RULE,
|
||||
onClick: (rowItem: TableData) =>
|
||||
duplicateRulesAction([rowItem.sourceRule], dispatch, dispatchToaster),
|
||||
onClick: (rowItem: Rule) => {
|
||||
duplicateRulesAction([rowItem], [rowItem.id], dispatch, dispatchToaster);
|
||||
reFetchRules(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
description: i18n.EXPORT_RULE,
|
||||
icon: 'exportAction',
|
||||
name: i18n.EXPORT_RULE,
|
||||
onClick: (rowItem: TableData) => exportRulesAction([rowItem.sourceRule], dispatch),
|
||||
enabled: (rowItem: TableData) => !rowItem.immutable,
|
||||
onClick: (rowItem: Rule) => exportRulesAction([rowItem.rule_id], dispatch),
|
||||
enabled: (rowItem: Rule) => !rowItem.immutable,
|
||||
},
|
||||
{
|
||||
description: i18n.DELETE_RULE,
|
||||
icon: 'trash',
|
||||
name: i18n.DELETE_RULE,
|
||||
onClick: (rowItem: TableData) => deleteRulesAction([rowItem.id], dispatch, dispatchToaster),
|
||||
onClick: (rowItem: Rule) => {
|
||||
deleteRulesAction([rowItem.id], dispatch, dispatchToaster);
|
||||
reFetchRules(true);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
type RulesColumns = EuiBasicTableColumn<TableData> | EuiTableActionsColumnType<TableData>;
|
||||
type RulesColumns = EuiBasicTableColumn<Rule> | EuiTableActionsColumnType<Rule>;
|
||||
|
||||
interface GetColumns {
|
||||
dispatch: React.Dispatch<Action>;
|
||||
dispatchToaster: Dispatch<ActionToaster>;
|
||||
history: H.History;
|
||||
hasNoPermissions: boolean;
|
||||
loadingRuleIds: string[];
|
||||
reFetchRules: (refreshPrePackagedRule?: boolean) => void;
|
||||
}
|
||||
|
||||
// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes?
|
||||
export const getColumns = (
|
||||
dispatch: React.Dispatch<Action>,
|
||||
dispatchToaster: Dispatch<ActionToaster>,
|
||||
history: H.History,
|
||||
hasNoPermissions: boolean
|
||||
): RulesColumns[] => {
|
||||
export const getColumns = ({
|
||||
dispatch,
|
||||
dispatchToaster,
|
||||
history,
|
||||
hasNoPermissions,
|
||||
loadingRuleIds,
|
||||
reFetchRules,
|
||||
}: GetColumns): RulesColumns[] => {
|
||||
const cols: RulesColumns[] = [
|
||||
{
|
||||
field: 'rule',
|
||||
field: 'name',
|
||||
name: i18n.COLUMN_RULE,
|
||||
render: (value: TableData['rule']) => <EuiLink href={value.href}>{value.name}</EuiLink>,
|
||||
render: (value: Rule['name'], item: Rule) => (
|
||||
<EuiLink href={getRuleDetailsUrl(item.id)}>{value}</EuiLink>
|
||||
),
|
||||
truncateText: true,
|
||||
width: '24%',
|
||||
},
|
||||
|
@ -93,14 +113,14 @@ export const getColumns = (
|
|||
{
|
||||
field: 'severity',
|
||||
name: i18n.COLUMN_SEVERITY,
|
||||
render: (value: TableData['severity']) => <SeverityBadge value={value} />,
|
||||
render: (value: Rule['severity']) => <SeverityBadge value={value} />,
|
||||
truncateText: true,
|
||||
width: '16%',
|
||||
},
|
||||
{
|
||||
field: 'statusDate',
|
||||
field: 'status_date',
|
||||
name: i18n.COLUMN_LAST_COMPLETE_RUN,
|
||||
render: (value: TableData['statusDate']) => {
|
||||
render: (value: Rule['status_date']) => {
|
||||
return value == null ? (
|
||||
getEmptyTagValue()
|
||||
) : (
|
||||
|
@ -114,7 +134,7 @@ export const getColumns = (
|
|||
{
|
||||
field: 'status',
|
||||
name: i18n.COLUMN_LAST_RESPONSE,
|
||||
render: (value: TableData['status']) => {
|
||||
render: (value: Rule['status']) => {
|
||||
return (
|
||||
<>
|
||||
<EuiHealth color={getStatusColor(value ?? null)}>
|
||||
|
@ -129,7 +149,7 @@ export const getColumns = (
|
|||
{
|
||||
field: 'tags',
|
||||
name: i18n.COLUMN_TAGS,
|
||||
render: (value: TableData['tags']) => (
|
||||
render: (value: Rule['tags']) => (
|
||||
<TruncatableText>
|
||||
{value.map((tag, i) => (
|
||||
<EuiBadge color="hollow" key={`${tag}-${i}`}>
|
||||
|
@ -145,13 +165,13 @@ export const getColumns = (
|
|||
align: 'center',
|
||||
field: 'activate',
|
||||
name: i18n.COLUMN_ACTIVATE,
|
||||
render: (value: TableData['activate'], item: TableData) => (
|
||||
render: (value: Rule['enabled'], item: Rule) => (
|
||||
<RuleSwitch
|
||||
dispatch={dispatch}
|
||||
id={item.id}
|
||||
enabled={item.activate}
|
||||
enabled={item.enabled}
|
||||
isDisabled={hasNoPermissions}
|
||||
isLoading={item.isLoading}
|
||||
isLoading={loadingRuleIds.includes(item.id)}
|
||||
/>
|
||||
),
|
||||
sortable: true,
|
||||
|
@ -160,9 +180,9 @@ export const getColumns = (
|
|||
];
|
||||
const actions: RulesColumns[] = [
|
||||
{
|
||||
actions: getActions(dispatch, dispatchToaster, history),
|
||||
actions: getActions(dispatch, dispatchToaster, history, reFetchRules),
|
||||
width: '40px',
|
||||
} as EuiTableActionsColumnType<TableData>,
|
||||
} as EuiTableActionsColumnType<Rule>,
|
||||
];
|
||||
|
||||
return hasNoPermissions ? cols : [...cols, ...actions];
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { bucketRulesResponse, formatRules } from './helpers';
|
||||
import { mockRule, mockRuleError, mockRules, mockTableData } from './__mocks__/mock';
|
||||
import { bucketRulesResponse } from './helpers';
|
||||
import { mockRule, mockRuleError } from './__mocks__/mock';
|
||||
import uuid from 'uuid';
|
||||
import { Rule, RuleError } from '../../../../containers/detection_engine/rules';
|
||||
|
||||
|
@ -15,20 +15,6 @@ describe('AllRulesTable Helpers', () => {
|
|||
const mockRuleError1: Readonly<RuleError> = mockRuleError(uuid.v4());
|
||||
const mockRuleError2: Readonly<RuleError> = mockRuleError(uuid.v4());
|
||||
|
||||
describe('formatRules', () => {
|
||||
test('formats rules with no selection', () => {
|
||||
const formattedRules = formatRules(mockRules);
|
||||
expect(formattedRules).toEqual(mockTableData);
|
||||
});
|
||||
|
||||
test('formats rules with selection', () => {
|
||||
const mockTableDataWithSelected = [...mockTableData];
|
||||
mockTableDataWithSelected[0].isLoading = true;
|
||||
const formattedRules = formatRules(mockRules, [mockRules[0].id]);
|
||||
expect(formattedRules).toEqual(mockTableDataWithSelected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bucketRulesResponse', () => {
|
||||
test('buckets empty response', () => {
|
||||
const bucketedResponse = bucketRulesResponse([]);
|
||||
|
|
|
@ -9,32 +9,6 @@ import {
|
|||
RuleError,
|
||||
RuleResponseBuckets,
|
||||
} from '../../../../containers/detection_engine/rules';
|
||||
import { TableData } from '../types';
|
||||
|
||||
/**
|
||||
* Formats rules into the correct format for the AllRulesTable
|
||||
*
|
||||
* @param rules as returned from the Rules API
|
||||
* @param selectedIds ids of the currently selected rules
|
||||
*/
|
||||
export const formatRules = (rules: Rule[], selectedIds?: string[]): TableData[] =>
|
||||
rules.map(rule => ({
|
||||
id: rule.id,
|
||||
immutable: rule.immutable,
|
||||
rule_id: rule.rule_id,
|
||||
rule: {
|
||||
href: `#/detections/rules/id/${encodeURIComponent(rule.id)}`,
|
||||
name: rule.name,
|
||||
},
|
||||
risk_score: rule.risk_score,
|
||||
severity: rule.severity,
|
||||
tags: rule.tags ?? [],
|
||||
activate: rule.enabled,
|
||||
status: rule.status ?? null,
|
||||
statusDate: rule.status_date ?? null,
|
||||
sourceRule: rule,
|
||||
isLoading: selectedIds?.includes(rule.id) ?? false,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Separates rules/errors from bulk rules API response (create/update/delete)
|
||||
|
@ -52,14 +26,11 @@ export const bucketRulesResponse = (response: Array<Rule | RuleError>) =>
|
|||
);
|
||||
|
||||
export const showRulesTable = ({
|
||||
isInitialLoad,
|
||||
rulesCustomInstalled,
|
||||
rulesInstalled,
|
||||
}: {
|
||||
isInitialLoad: boolean;
|
||||
rulesCustomInstalled: number | null;
|
||||
rulesInstalled: number | null;
|
||||
}) =>
|
||||
!isInitialLoad &&
|
||||
((rulesCustomInstalled != null && rulesCustomInstalled > 0) ||
|
||||
(rulesInstalled != null && rulesInstalled > 0));
|
||||
(rulesCustomInstalled != null && rulesCustomInstalled > 0) ||
|
||||
(rulesInstalled != null && rulesInstalled > 0);
|
||||
|
|
|
@ -11,15 +11,16 @@ import {
|
|||
EuiLoadingContent,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import {
|
||||
useRules,
|
||||
CreatePreBuiltRules,
|
||||
FilterOptions,
|
||||
Rule,
|
||||
} from '../../../../containers/detection_engine/rules';
|
||||
import { HeaderSection } from '../../../../components/header_section';
|
||||
import {
|
||||
|
@ -36,35 +37,39 @@ import { PrePackagedRulesPrompt } from '../components/pre_packaged_rules/load_em
|
|||
import { RuleDownloader } from '../components/rule_downloader';
|
||||
import { getPrePackagedRuleStatus } from '../helpers';
|
||||
import * as i18n from '../translations';
|
||||
import { EuiBasicTableOnChange, TableData } from '../types';
|
||||
import { EuiBasicTableOnChange } from '../types';
|
||||
import { getBatchItems } from './batch_actions';
|
||||
import { getColumns } from './columns';
|
||||
import { showRulesTable } from './helpers';
|
||||
import { allRulesReducer, State } from './reducer';
|
||||
import { RulesTableFilters } from './rules_table_filters/rules_table_filters';
|
||||
|
||||
// EuiBasicTable give me a hardtime with adding the ref attributes so I went the easy way
|
||||
// after few hours of fight with typescript !!!! I lost :(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const MyEuiBasicTable = styled(EuiBasicTable as any)`` as any;
|
||||
|
||||
const initialState: State = {
|
||||
isLoading: true,
|
||||
rules: [],
|
||||
tableData: [],
|
||||
selectedItems: [],
|
||||
refreshToggle: true,
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
},
|
||||
exportRuleIds: [],
|
||||
filterOptions: {
|
||||
filter: '',
|
||||
sortField: 'enabled',
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
loadingRuleIds: [],
|
||||
loadingRulesAction: null,
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
},
|
||||
rules: [],
|
||||
selectedRuleIds: [],
|
||||
};
|
||||
|
||||
interface AllRulesProps {
|
||||
createPrePackagedRules: CreatePreBuiltRules | null;
|
||||
hasNoPermissions: boolean;
|
||||
importCompleteToggle: boolean;
|
||||
loading: boolean;
|
||||
loadingCreatePrePackagedRules: boolean;
|
||||
refetchPrePackagedRulesStatus: () => void;
|
||||
|
@ -72,7 +77,7 @@ interface AllRulesProps {
|
|||
rulesInstalled: number | null;
|
||||
rulesNotInstalled: number | null;
|
||||
rulesNotUpdated: number | null;
|
||||
setRefreshRulesData: (refreshRule: () => void) => void;
|
||||
setRefreshRulesData: (refreshRule: (refreshPrePackagedRule?: boolean) => void) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -87,7 +92,6 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
({
|
||||
createPrePackagedRules,
|
||||
hasNoPermissions,
|
||||
importCompleteToggle,
|
||||
loading,
|
||||
loadingCreatePrePackagedRules,
|
||||
refetchPrePackagedRulesStatus,
|
||||
|
@ -97,24 +101,36 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
rulesNotUpdated,
|
||||
setRefreshRulesData,
|
||||
}) => {
|
||||
const [initLoading, setInitLoading] = useState(true);
|
||||
const tableRef = useRef<EuiBasicTable>();
|
||||
const [
|
||||
{
|
||||
exportPayload,
|
||||
exportRuleIds,
|
||||
filterOptions,
|
||||
isLoading,
|
||||
refreshToggle,
|
||||
selectedItems,
|
||||
tableData,
|
||||
loadingRuleIds,
|
||||
loadingRulesAction,
|
||||
pagination,
|
||||
rules,
|
||||
selectedRuleIds,
|
||||
},
|
||||
dispatch,
|
||||
] = useReducer(allRulesReducer, initialState);
|
||||
] = useReducer(allRulesReducer(tableRef), initialState);
|
||||
const history = useHistory();
|
||||
const [oldRefreshToggle, setOldRefreshToggle] = useState(refreshToggle);
|
||||
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
||||
const [isGlobalLoading, setIsGlobalLoad] = useState(false);
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
const [isLoadingRules, rulesData, reFetchRulesData] = useRules(pagination, filterOptions);
|
||||
|
||||
const setRules = useCallback((newRules: Rule[]) => {
|
||||
dispatch({
|
||||
type: 'setRules',
|
||||
rules: newRules,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const [isLoadingRules, , reFetchRulesData] = useRules({
|
||||
pagination,
|
||||
filterOptions,
|
||||
refetchPrePackagedRulesStatus,
|
||||
dispatchRulesInReducer: setRules,
|
||||
});
|
||||
|
||||
const prePackagedRuleStatus = getPrePackagedRuleStatus(
|
||||
rulesInstalled,
|
||||
|
@ -125,10 +141,18 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
const getBatchItemsPopoverContent = useCallback(
|
||||
(closePopover: () => void) => (
|
||||
<EuiContextMenuPanel
|
||||
items={getBatchItems(selectedItems, dispatch, dispatchToaster, history, closePopover)}
|
||||
items={getBatchItems({
|
||||
closePopover,
|
||||
dispatch,
|
||||
dispatchToaster,
|
||||
loadingRuleIds,
|
||||
selectedRuleIds,
|
||||
reFetchRules: reFetchRulesData,
|
||||
rules,
|
||||
})}
|
||||
/>
|
||||
),
|
||||
[selectedItems, dispatch, dispatchToaster, history]
|
||||
[dispatch, dispatchToaster, loadingRuleIds, reFetchRulesData, rules, selectedRuleIds]
|
||||
);
|
||||
|
||||
const tableOnChangeCallback = useCallback(
|
||||
|
@ -146,46 +170,19 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return getColumns(dispatch, dispatchToaster, history, hasNoPermissions);
|
||||
}, [dispatch, dispatchToaster, history]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: 'loading', isLoading: isLoadingRules });
|
||||
}, [isLoadingRules]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoadingRules && !loading && isInitialLoad) {
|
||||
setIsInitialLoad(false);
|
||||
}
|
||||
}, [isInitialLoad, isLoadingRules, loading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGlobalLoading && (isLoadingRules || isLoading)) {
|
||||
setIsGlobalLoad(true);
|
||||
} else if (isGlobalLoading && !isLoadingRules && !isLoading) {
|
||||
setIsGlobalLoad(false);
|
||||
}
|
||||
}, [setIsGlobalLoad, isGlobalLoading, isLoadingRules, isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitialLoad) {
|
||||
dispatch({ type: 'refresh' });
|
||||
}
|
||||
}, [importCompleteToggle]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitialLoad && reFetchRulesData != null && oldRefreshToggle !== refreshToggle) {
|
||||
setOldRefreshToggle(refreshToggle);
|
||||
reFetchRulesData();
|
||||
refetchPrePackagedRulesStatus();
|
||||
}
|
||||
}, [
|
||||
isInitialLoad,
|
||||
refreshToggle,
|
||||
oldRefreshToggle,
|
||||
reFetchRulesData,
|
||||
refetchPrePackagedRulesStatus,
|
||||
]);
|
||||
return getColumns({
|
||||
dispatch,
|
||||
dispatchToaster,
|
||||
history,
|
||||
hasNoPermissions,
|
||||
loadingRuleIds:
|
||||
loadingRulesAction != null &&
|
||||
(loadingRulesAction === 'enable' || loadingRulesAction === 'disable')
|
||||
? loadingRuleIds
|
||||
: [],
|
||||
reFetchRules: reFetchRulesData,
|
||||
});
|
||||
}, [dispatch, dispatchToaster, history, loadingRuleIds, loadingRulesAction, reFetchRulesData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (reFetchRulesData != null) {
|
||||
|
@ -194,31 +191,25 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
}, [reFetchRulesData, setRefreshRulesData]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({
|
||||
type: 'updateRules',
|
||||
rules: rulesData.data,
|
||||
pagination: {
|
||||
page: rulesData.page,
|
||||
perPage: rulesData.perPage,
|
||||
total: rulesData.total,
|
||||
},
|
||||
});
|
||||
}, [rulesData]);
|
||||
if (initLoading && !loading && !isLoadingRules) {
|
||||
setInitLoading(false);
|
||||
}
|
||||
}, [initLoading, loading, isLoadingRules]);
|
||||
|
||||
const handleCreatePrePackagedRules = useCallback(async () => {
|
||||
if (createPrePackagedRules != null) {
|
||||
if (createPrePackagedRules != null && reFetchRulesData != null) {
|
||||
await createPrePackagedRules();
|
||||
dispatch({ type: 'refresh' });
|
||||
reFetchRulesData(true);
|
||||
}
|
||||
}, [createPrePackagedRules]);
|
||||
}, [createPrePackagedRules, reFetchRulesData]);
|
||||
|
||||
const euiBasicTableSelectionProps = useMemo(
|
||||
() => ({
|
||||
selectable: (item: TableData) => !item.isLoading,
|
||||
onSelectionChange: (selected: TableData[]) =>
|
||||
dispatch({ type: 'setSelected', selectedItems: selected }),
|
||||
selectable: (item: Rule) => !loadingRuleIds.includes(item.id),
|
||||
onSelectionChange: (selected: Rule[]) =>
|
||||
dispatch({ type: 'selectedRuleIds', ids: selected.map(r => r.id) }),
|
||||
}),
|
||||
[]
|
||||
[loadingRuleIds]
|
||||
);
|
||||
|
||||
const onFilterChangedCallback = useCallback((newFilterOptions: Partial<FilterOptions>) => {
|
||||
|
@ -237,12 +228,25 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
);
|
||||
}, []);
|
||||
|
||||
const isLoadingAnActionOnRule = useMemo(() => {
|
||||
if (
|
||||
loadingRuleIds.length > 0 &&
|
||||
(loadingRulesAction === 'disable' || loadingRulesAction === 'enable')
|
||||
) {
|
||||
return false;
|
||||
} else if (loadingRuleIds.length > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [loadingRuleIds, loadingRulesAction]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<RuleDownloader
|
||||
filename={`${i18n.EXPORT_FILENAME}.ndjson`}
|
||||
rules={exportPayload}
|
||||
ruleIds={exportRuleIds}
|
||||
onExportComplete={exportCount => {
|
||||
dispatch({ type: 'loadingRuleIds', ids: [], actionType: null });
|
||||
dispatchToaster({
|
||||
type: 'addToaster',
|
||||
toast: {
|
||||
|
@ -256,22 +260,17 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
/>
|
||||
<EuiSpacer />
|
||||
|
||||
<Panel loading={isGlobalLoading}>
|
||||
<Panel loading={loading || isLoadingRules}>
|
||||
<>
|
||||
{((rulesCustomInstalled && rulesCustomInstalled > 0) ||
|
||||
(rulesInstalled != null && rulesInstalled > 0)) && (
|
||||
<HeaderSection split title={i18n.ALL_RULES}>
|
||||
<RulesTableFilters
|
||||
onFilterChanged={onFilterChangedCallback}
|
||||
rulesCustomInstalled={rulesCustomInstalled}
|
||||
rulesInstalled={rulesInstalled}
|
||||
/>
|
||||
</HeaderSection>
|
||||
)}
|
||||
{isInitialLoad && (
|
||||
<EuiLoadingContent data-test-subj="initialLoadingPanelAllRulesTable" lines={10} />
|
||||
)}
|
||||
{isGlobalLoading && !isEmpty(tableData) && !isInitialLoad && (
|
||||
<HeaderSection split title={i18n.ALL_RULES}>
|
||||
<RulesTableFilters
|
||||
onFilterChanged={onFilterChangedCallback}
|
||||
rulesCustomInstalled={rulesCustomInstalled}
|
||||
rulesInstalled={rulesInstalled}
|
||||
/>
|
||||
</HeaderSection>
|
||||
|
||||
{(loading || isLoadingRules || isLoadingAnActionOnRule) && !initLoading && (
|
||||
<Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" />
|
||||
)}
|
||||
{rulesCustomInstalled != null &&
|
||||
|
@ -283,7 +282,10 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
userHasNoPermissions={hasNoPermissions}
|
||||
/>
|
||||
)}
|
||||
{showRulesTable({ isInitialLoad, rulesCustomInstalled, rulesInstalled }) && (
|
||||
{initLoading && (
|
||||
<EuiLoadingContent data-test-subj="initialLoadingPanelAllRulesTable" lines={10} />
|
||||
)}
|
||||
{showRulesTable({ rulesCustomInstalled, rulesInstalled }) && !initLoading && (
|
||||
<>
|
||||
<UtilityBar border>
|
||||
<UtilityBarSection>
|
||||
|
@ -292,7 +294,7 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
</UtilityBarGroup>
|
||||
|
||||
<UtilityBarGroup>
|
||||
<UtilityBarText>{i18n.SELECTED_RULES(selectedItems.length)}</UtilityBarText>
|
||||
<UtilityBarText>{i18n.SELECTED_RULES(selectedRuleIds.length)}</UtilityBarText>
|
||||
{!hasNoPermissions && (
|
||||
<UtilityBarAction
|
||||
iconSide="right"
|
||||
|
@ -303,21 +305,20 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
</UtilityBarAction>
|
||||
)}
|
||||
<UtilityBarAction
|
||||
iconSide="right"
|
||||
iconSide="left"
|
||||
iconType="refresh"
|
||||
onClick={() => dispatch({ type: 'refresh' })}
|
||||
onClick={() => reFetchRulesData(true)}
|
||||
>
|
||||
{i18n.REFRESH}
|
||||
</UtilityBarAction>
|
||||
</UtilityBarGroup>
|
||||
</UtilityBarSection>
|
||||
</UtilityBar>
|
||||
|
||||
<EuiBasicTable
|
||||
<MyEuiBasicTable
|
||||
columns={columns}
|
||||
isSelectable={!hasNoPermissions ?? false}
|
||||
itemId="id"
|
||||
items={tableData}
|
||||
items={rules ?? []}
|
||||
noItemsMessage={emptyPrompt}
|
||||
onChange={tableOnChangeCallback}
|
||||
pagination={{
|
||||
|
@ -326,7 +327,8 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
totalItemCount: pagination.total,
|
||||
pageSizeOptions: [5, 10, 20, 50, 100, 200, 300],
|
||||
}}
|
||||
sorting={{ sort: { field: 'activate', direction: filterOptions.sortOrder } }}
|
||||
ref={tableRef}
|
||||
sorting={{ sort: { field: 'enabled', direction: filterOptions.sortOrder } }}
|
||||
selection={hasNoPermissions ? undefined : euiBasicTableSelectionProps}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -4,34 +4,30 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiBasicTable } from '@elastic/eui';
|
||||
import {
|
||||
FilterOptions,
|
||||
PaginationOptions,
|
||||
Rule,
|
||||
} from '../../../../containers/detection_engine/rules';
|
||||
import { TableData } from '../types';
|
||||
import { formatRules } from './helpers';
|
||||
|
||||
type LoadingRuleAction = 'duplicate' | 'enable' | 'disable' | 'export' | 'delete' | null;
|
||||
export interface State {
|
||||
isLoading: boolean;
|
||||
rules: Rule[];
|
||||
selectedItems: TableData[];
|
||||
pagination: PaginationOptions;
|
||||
exportRuleIds: string[];
|
||||
filterOptions: FilterOptions;
|
||||
refreshToggle: boolean;
|
||||
tableData: TableData[];
|
||||
exportPayload?: Rule[];
|
||||
loadingRuleIds: string[];
|
||||
loadingRulesAction: LoadingRuleAction;
|
||||
pagination: PaginationOptions;
|
||||
rules: Rule[];
|
||||
selectedRuleIds: string[];
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| { type: 'refresh' }
|
||||
| { type: 'loading'; isLoading: boolean }
|
||||
| { type: 'deleteRules'; rules: Rule[] }
|
||||
| { type: 'duplicate'; rule: Rule }
|
||||
| { type: 'setExportPayload'; exportPayload?: Rule[] }
|
||||
| { type: 'setSelected'; selectedItems: TableData[] }
|
||||
| { type: 'updateLoading'; ids: string[]; isLoading: boolean }
|
||||
| { type: 'updateRules'; rules: Rule[]; pagination?: PaginationOptions }
|
||||
| { type: 'exportRuleIds'; ids: string[] }
|
||||
| { type: 'loadingRuleIds'; ids: string[]; actionType: LoadingRuleAction }
|
||||
| { type: 'selectedRuleIds'; ids: string[] }
|
||||
| { type: 'setRules'; rules: Rule[] }
|
||||
| { type: 'updateRules'; rules: Rule[] }
|
||||
| { type: 'updatePagination'; pagination: Partial<PaginationOptions> }
|
||||
| {
|
||||
type: 'updateFilterOptions';
|
||||
|
@ -40,53 +36,70 @@ export type Action =
|
|||
}
|
||||
| { type: 'failure' };
|
||||
|
||||
export const allRulesReducer = (state: State, action: Action): State => {
|
||||
export const allRulesReducer = (
|
||||
tableRef: React.MutableRefObject<EuiBasicTable<unknown> | undefined>
|
||||
) => (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'refresh': {
|
||||
case 'exportRuleIds': {
|
||||
return {
|
||||
...state,
|
||||
refreshToggle: !state.refreshToggle,
|
||||
loadingRuleIds: action.ids,
|
||||
loadingRulesAction: 'export',
|
||||
exportRuleIds: action.ids,
|
||||
};
|
||||
}
|
||||
case 'loadingRuleIds': {
|
||||
return {
|
||||
...state,
|
||||
loadingRuleIds: action.actionType == null ? [] : [...state.loadingRuleIds, ...action.ids],
|
||||
loadingRulesAction: action.actionType,
|
||||
};
|
||||
}
|
||||
case 'selectedRuleIds': {
|
||||
return {
|
||||
...state,
|
||||
selectedRuleIds: action.ids,
|
||||
};
|
||||
}
|
||||
case 'setRules': {
|
||||
if (
|
||||
tableRef != null &&
|
||||
tableRef.current != null &&
|
||||
tableRef.current.changeSelection != null
|
||||
) {
|
||||
tableRef.current.changeSelection([]);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
rules: action.rules,
|
||||
selectedRuleIds: [],
|
||||
loadingRuleIds: [],
|
||||
loadingRulesAction: null,
|
||||
};
|
||||
}
|
||||
case 'updateRules': {
|
||||
// If pagination included, this was a hard refresh
|
||||
if (action.pagination) {
|
||||
if (state.rules != null) {
|
||||
const ruleIds = state.rules.map(r => r.id);
|
||||
const updatedRules = action.rules.reduce((rules, updatedRule) => {
|
||||
let newRules = rules;
|
||||
if (ruleIds.includes(updatedRule.id)) {
|
||||
newRules = newRules.map(r => (updatedRule.id === r.id ? updatedRule : r));
|
||||
} else {
|
||||
newRules = [...newRules, updatedRule];
|
||||
}
|
||||
return newRules;
|
||||
}, state.rules);
|
||||
const updatedRuleIds = action.rules.map(r => r.id);
|
||||
const newLoadingRuleIds = state.loadingRuleIds.filter(id => !updatedRuleIds.includes(id));
|
||||
return {
|
||||
...state,
|
||||
rules: action.rules,
|
||||
pagination: action.pagination,
|
||||
tableData: formatRules(action.rules),
|
||||
rules: updatedRules,
|
||||
loadingRuleIds: newLoadingRuleIds,
|
||||
loadingRulesAction: newLoadingRuleIds.length === 0 ? null : state.loadingRulesAction,
|
||||
};
|
||||
}
|
||||
|
||||
const ruleIds = state.rules.map(r => r.rule_id);
|
||||
const updatedRules = action.rules.reverse().reduce((rules, updatedRule) => {
|
||||
let newRules = rules;
|
||||
if (ruleIds.includes(updatedRule.rule_id)) {
|
||||
newRules = newRules.map(r => (updatedRule.rule_id === r.rule_id ? updatedRule : r));
|
||||
} else {
|
||||
newRules = [...newRules, updatedRule];
|
||||
}
|
||||
return newRules;
|
||||
}, state.rules);
|
||||
|
||||
// Update enabled on selectedItems so that batch actions show correct available actions
|
||||
const updatedRuleIdToState = action.rules.reduce<Record<string, boolean>>(
|
||||
(acc, r) => ({ ...acc, [r.id]: r.enabled }),
|
||||
{}
|
||||
);
|
||||
const updatedSelectedItems = state.selectedItems.map(selectedItem =>
|
||||
Object.keys(updatedRuleIdToState).includes(selectedItem.id)
|
||||
? { ...selectedItem, activate: updatedRuleIdToState[selectedItem.id] }
|
||||
: selectedItem
|
||||
);
|
||||
|
||||
return {
|
||||
...state,
|
||||
rules: updatedRules,
|
||||
tableData: formatRules(updatedRules),
|
||||
selectedItems: updatedSelectedItems,
|
||||
};
|
||||
return state;
|
||||
}
|
||||
case 'updatePagination': {
|
||||
return {
|
||||
|
@ -110,51 +123,12 @@ export const allRulesReducer = (state: State, action: Action): State => {
|
|||
},
|
||||
};
|
||||
}
|
||||
case 'deleteRules': {
|
||||
const deletedRuleIds = action.rules.map(r => r.rule_id);
|
||||
const updatedRules = state.rules.reduce<Rule[]>(
|
||||
(rules, rule) => (deletedRuleIds.includes(rule.rule_id) ? rules : [...rules, rule]),
|
||||
[]
|
||||
);
|
||||
return {
|
||||
...state,
|
||||
rules: updatedRules,
|
||||
tableData: formatRules(updatedRules),
|
||||
refreshToggle: !state.refreshToggle,
|
||||
};
|
||||
}
|
||||
case 'setSelected': {
|
||||
return {
|
||||
...state,
|
||||
selectedItems: action.selectedItems,
|
||||
};
|
||||
}
|
||||
case 'updateLoading': {
|
||||
return {
|
||||
...state,
|
||||
rules: state.rules,
|
||||
tableData: formatRules(state.rules, action.ids),
|
||||
};
|
||||
}
|
||||
case 'loading': {
|
||||
return {
|
||||
...state,
|
||||
isLoading: action.isLoading,
|
||||
};
|
||||
}
|
||||
case 'failure': {
|
||||
return {
|
||||
...state,
|
||||
isLoading: false,
|
||||
rules: [],
|
||||
};
|
||||
}
|
||||
case 'setExportPayload': {
|
||||
return {
|
||||
...state,
|
||||
exportPayload: [...(action.exportPayload ?? [])],
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -41,7 +41,11 @@ const RulesTableFiltersComponent = ({
|
|||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [showCustomRules, setShowCustomRules] = useState<boolean>(false);
|
||||
const [showElasticRules, setShowElasticRules] = useState<boolean>(false);
|
||||
const [isLoadingTags, tags] = useTags();
|
||||
const [isLoadingTags, tags, reFetchTags] = useTags();
|
||||
|
||||
useEffect(() => {
|
||||
reFetchTags();
|
||||
}, [rulesCustomInstalled, rulesInstalled]);
|
||||
|
||||
// Propagate filter changes to parent
|
||||
useEffect(() => {
|
||||
|
|
|
@ -58,6 +58,7 @@ exports[`RuleActionsOverflow renders correctly against snapshot 1`] = `
|
|||
<RuleDownloader
|
||||
filename="rules_export.ndjson"
|
||||
onExportComplete={[Function]}
|
||||
ruleIds={Array []}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
|
|
@ -48,7 +48,7 @@ const RuleActionsOverflowComponent = ({
|
|||
userHasNoPermissions,
|
||||
}: RuleActionsOverflowComponentProps) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const [rulesToExport, setRulesToExport] = useState<Rule[] | undefined>(undefined);
|
||||
const [rulesToExport, setRulesToExport] = useState<string[]>([]);
|
||||
const history = useHistory();
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
||||
|
@ -66,7 +66,7 @@ const RuleActionsOverflowComponent = ({
|
|||
disabled={userHasNoPermissions}
|
||||
onClick={async () => {
|
||||
setIsPopoverOpen(false);
|
||||
await duplicateRulesAction([rule], noop, dispatchToaster);
|
||||
await duplicateRulesAction([rule], [rule.id], noop, dispatchToaster);
|
||||
}}
|
||||
>
|
||||
{i18nActions.DUPLICATE_RULE}
|
||||
|
@ -75,9 +75,9 @@ const RuleActionsOverflowComponent = ({
|
|||
key={i18nActions.EXPORT_RULE}
|
||||
icon="indexEdit"
|
||||
disabled={userHasNoPermissions || rule.immutable}
|
||||
onClick={async () => {
|
||||
onClick={() => {
|
||||
setIsPopoverOpen(false);
|
||||
setRulesToExport([rule]);
|
||||
setRulesToExport([rule.id]);
|
||||
}}
|
||||
>
|
||||
{i18nActions.EXPORT_RULE}
|
||||
|
@ -131,7 +131,7 @@ const RuleActionsOverflowComponent = ({
|
|||
</EuiPopover>
|
||||
<RuleDownloader
|
||||
filename={`${i18nActions.EXPORT_FILENAME}.ndjson`}
|
||||
rules={rulesToExport}
|
||||
ruleIds={rulesToExport}
|
||||
onExportComplete={exportCount => {
|
||||
displaySuccessToast(
|
||||
i18nActions.SUCCESSFULLY_EXPORTED_RULES(exportCount),
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { isFunction } from 'lodash/fp';
|
||||
import { exportRules, Rule } from '../../../../../containers/detection_engine/rules';
|
||||
import { exportRules } from '../../../../../containers/detection_engine/rules';
|
||||
import { displayErrorToast, useStateToaster } from '../../../../../components/toasters';
|
||||
import * as i18n from './translations';
|
||||
|
||||
|
@ -17,7 +17,7 @@ const InvisibleAnchor = styled.a`
|
|||
|
||||
export interface RuleDownloaderProps {
|
||||
filename: string;
|
||||
rules?: Rule[];
|
||||
ruleIds?: string[];
|
||||
onExportComplete: (exportCount: number) => void;
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ export interface RuleDownloaderProps {
|
|||
*/
|
||||
export const RuleDownloaderComponent = ({
|
||||
filename,
|
||||
rules,
|
||||
ruleIds,
|
||||
onExportComplete,
|
||||
}: RuleDownloaderProps) => {
|
||||
const anchorRef = useRef<HTMLAnchorElement>(null);
|
||||
|
@ -41,10 +41,10 @@ export const RuleDownloaderComponent = ({
|
|||
const abortCtrl = new AbortController();
|
||||
|
||||
async function exportData() {
|
||||
if (anchorRef && anchorRef.current && rules != null) {
|
||||
if (anchorRef && anchorRef.current && ruleIds != null && ruleIds.length > 0) {
|
||||
try {
|
||||
const exportResponse = await exportRules({
|
||||
ruleIds: rules.map(r => r.rule_id),
|
||||
ruleIds,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
|
@ -61,7 +61,7 @@ export const RuleDownloaderComponent = ({
|
|||
window.URL.revokeObjectURL(objectURL);
|
||||
}
|
||||
|
||||
onExportComplete(rules.length);
|
||||
onExportComplete(ruleIds.length);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isSubscribed) {
|
||||
|
@ -77,7 +77,7 @@ export const RuleDownloaderComponent = ({
|
|||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, [rules]);
|
||||
}, [ruleIds]);
|
||||
|
||||
return <InvisibleAnchor ref={anchorRef} />;
|
||||
};
|
||||
|
|
|
@ -26,11 +26,10 @@ import { UpdatePrePackagedRulesCallOut } from './components/pre_packaged_rules/u
|
|||
import { getPrePackagedRuleStatus, redirectToDetections } from './helpers';
|
||||
import * as i18n from './translations';
|
||||
|
||||
type Func = () => void;
|
||||
type Func = (refreshPrePackagedRule?: boolean) => void;
|
||||
|
||||
const RulesPageComponent: React.FC = () => {
|
||||
const [showImportModal, setShowImportModal] = useState(false);
|
||||
const [importCompleteToggle, setImportCompleteToggle] = useState(false);
|
||||
const refreshRulesData = useRef<null | Func>(null);
|
||||
const {
|
||||
loading,
|
||||
|
@ -67,14 +66,18 @@ const RulesPageComponent: React.FC = () => {
|
|||
const userHasNoPermissions =
|
||||
canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false;
|
||||
|
||||
const handleRefreshRules = useCallback(async () => {
|
||||
if (refreshRulesData.current != null) {
|
||||
refreshRulesData.current(true);
|
||||
}
|
||||
}, [refreshRulesData]);
|
||||
|
||||
const handleCreatePrePackagedRules = useCallback(async () => {
|
||||
if (createPrePackagedRules != null) {
|
||||
await createPrePackagedRules();
|
||||
if (refreshRulesData.current != null) {
|
||||
refreshRulesData.current();
|
||||
}
|
||||
handleRefreshRules();
|
||||
}
|
||||
}, [createPrePackagedRules, refreshRulesData]);
|
||||
}, [createPrePackagedRules, handleRefreshRules]);
|
||||
|
||||
const handleRefetchPrePackagedRulesStatus = useCallback(() => {
|
||||
if (refetchPrePackagedRulesStatus != null) {
|
||||
|
@ -96,7 +99,7 @@ const RulesPageComponent: React.FC = () => {
|
|||
<ImportRuleModal
|
||||
showModal={showImportModal}
|
||||
closeModal={() => setShowImportModal(false)}
|
||||
importComplete={() => setImportCompleteToggle(!importCompleteToggle)}
|
||||
importComplete={handleRefreshRules}
|
||||
/>
|
||||
<WrapperPage>
|
||||
<DetectionEngineHeaderPage
|
||||
|
@ -166,7 +169,6 @@ const RulesPageComponent: React.FC = () => {
|
|||
loading={loading || prePackagedRuleLoading}
|
||||
loadingCreatePrePackagedRules={loadingCreatePrePackagedRules}
|
||||
hasNoPermissions={userHasNoPermissions}
|
||||
importCompleteToggle={importCompleteToggle}
|
||||
refetchPrePackagedRulesStatus={handleRefetchPrePackagedRulesStatus}
|
||||
rulesCustomInstalled={rulesCustomInstalled}
|
||||
rulesInstalled={rulesInstalled}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import { Filter } from '../../../../../../../../src/plugins/data/common';
|
||||
import { Rule } from '../../../containers/detection_engine/rules';
|
||||
import { FieldValueQueryBar } from './components/query_bar';
|
||||
import { FormData, FormHook } from '../../shared_imports';
|
||||
import { FieldValueTimeline } from './components/pick_timeline';
|
||||
|
@ -23,24 +22,6 @@ export interface EuiBasicTableOnChange {
|
|||
sort?: EuiBasicTableSortTypes;
|
||||
}
|
||||
|
||||
export interface TableData {
|
||||
id: string;
|
||||
immutable: boolean;
|
||||
rule_id: string;
|
||||
rule: {
|
||||
href: string;
|
||||
name: string;
|
||||
};
|
||||
risk_score: number;
|
||||
severity: string;
|
||||
tags: string[];
|
||||
activate: boolean;
|
||||
isLoading: boolean;
|
||||
sourceRule: Rule;
|
||||
status?: string | null;
|
||||
statusDate?: string | null;
|
||||
}
|
||||
|
||||
export enum RuleStep {
|
||||
defineRule = 'define-rule',
|
||||
aboutRule = 'about-rule',
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import Hapi from 'hapi';
|
||||
import { countBy } from 'lodash/fp';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
|
||||
|
@ -45,8 +44,7 @@ export const createCreateRulesBulkRoute = (
|
|||
}
|
||||
|
||||
const ruleDefinitions = request.payload;
|
||||
const mappedDuplicates = countBy('rule_id', ruleDefinitions);
|
||||
const dupes = getDuplicates(mappedDuplicates);
|
||||
const dupes = getDuplicates(ruleDefinitions, 'rule_id');
|
||||
|
||||
const rules = await Promise.all(
|
||||
ruleDefinitions
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
} from './utils';
|
||||
import { getResult } from '../__mocks__/request_responses';
|
||||
import { INTERNAL_IDENTIFIER } from '../../../../../common/constants';
|
||||
import { OutputRuleAlertRest, ImportRuleAlertRest } from '../../types';
|
||||
import { OutputRuleAlertRest, ImportRuleAlertRest, RuleAlertParamsRest } from '../../types';
|
||||
import { BulkError, ImportSuccessError } from '../utils';
|
||||
import { sampleRule } from '../../signals/__mocks__/es_results';
|
||||
import { getSimpleRule } from '../__mocks__/utils';
|
||||
|
@ -1222,20 +1222,32 @@ describe('utils', () => {
|
|||
|
||||
describe('getDuplicates', () => {
|
||||
test("returns array of ruleIds showing the duplicate keys of 'value2' and 'value3'", () => {
|
||||
const output = getDuplicates({
|
||||
value1: 1,
|
||||
value2: 2,
|
||||
value3: 2,
|
||||
});
|
||||
const output = getDuplicates(
|
||||
[
|
||||
{ rule_id: 'value1' },
|
||||
{ rule_id: 'value2' },
|
||||
{ rule_id: 'value2' },
|
||||
{ rule_id: 'value3' },
|
||||
{ rule_id: 'value3' },
|
||||
{},
|
||||
{},
|
||||
] as RuleAlertParamsRest[],
|
||||
'rule_id'
|
||||
);
|
||||
const expected = ['value2', 'value3'];
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
test('returns null when given a map of no duplicates', () => {
|
||||
const output = getDuplicates({
|
||||
value1: 1,
|
||||
value2: 1,
|
||||
value3: 1,
|
||||
});
|
||||
const output = getDuplicates(
|
||||
[
|
||||
{ rule_id: 'value1' },
|
||||
{ rule_id: 'value2' },
|
||||
{ rule_id: 'value3' },
|
||||
{},
|
||||
{},
|
||||
] as RuleAlertParamsRest[],
|
||||
'rule_id'
|
||||
);
|
||||
const expected: string[] = [];
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
|
|
@ -4,8 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { pickBy } from 'lodash/fp';
|
||||
import { Dictionary } from 'lodash';
|
||||
import { pickBy, countBy } from 'lodash/fp';
|
||||
import { SavedObject } from 'kibana/server';
|
||||
import uuid from 'uuid';
|
||||
import { INTERNAL_IDENTIFIER } from '../../../../../common/constants';
|
||||
|
@ -18,7 +17,7 @@ import {
|
|||
isRuleStatusFindTypes,
|
||||
isRuleStatusSavedObjectType,
|
||||
} from '../../rules/types';
|
||||
import { OutputRuleAlertRest, ImportRuleAlertRest } from '../../types';
|
||||
import { OutputRuleAlertRest, ImportRuleAlertRest, RuleAlertParamsRest } from '../../types';
|
||||
import {
|
||||
createBulkErrorObject,
|
||||
BulkError,
|
||||
|
@ -224,10 +223,14 @@ export const transformOrImportError = (
|
|||
}
|
||||
};
|
||||
|
||||
export const getDuplicates = (lodashDict: Dictionary<number>): string[] => {
|
||||
const hasDuplicates = Object.values(lodashDict).some(i => i > 1);
|
||||
export const getDuplicates = (ruleDefinitions: RuleAlertParamsRest[], by: 'rule_id'): string[] => {
|
||||
const mappedDuplicates = countBy(
|
||||
by,
|
||||
ruleDefinitions.filter(r => r[by] != null)
|
||||
);
|
||||
const hasDuplicates = Object.values(mappedDuplicates).some(i => i > 1);
|
||||
if (hasDuplicates) {
|
||||
return Object.keys(lodashDict).filter(key => lodashDict[key] > 1);
|
||||
return Object.keys(mappedDuplicates).filter(key => mappedDuplicates[key] > 1);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue