mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security Solution][Detections] - Add skeleton exceptions list tab to all rules page (#85465)
## Summary This PR is the first of 2 to complete the addition of a table displaying all exception lists on the all rules page. This PR focuses on the following: - all exception lists displayed - 'number of rules assigned to' displayed - names and links of rules assigned to displayed - refresh action button working - no trusted apps list show - search by `name`, `created_by`, `list_id` - just searching a word will search by list name - to search by `created_by` type `created_by:ytercero` - to search by `list_id` type `list_id:some-list-id` #### TO DO (follow up PR) - [ ] add tests - [ ] wire up export of exception list - [ ] wire up deletion of exception list <img width="1121" alt="Screen Shot 2020-12-09 at 2 10 59 PM" src="https://user-images.githubusercontent.com/10927944/101676548-50498e00-3a29-11eb-90cb-5f56fc8c0a1b.png"> ### Checklist - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
parent
4dccbcad33
commit
be055b85b8
37 changed files with 2268 additions and 613 deletions
|
@ -48,7 +48,7 @@ pageLoadAssetSize:
|
|||
lensOss: 19341
|
||||
licenseManagement: 41817
|
||||
licensing: 39008
|
||||
lists: 183665
|
||||
lists: 202261
|
||||
logstash: 53548
|
||||
management: 46112
|
||||
maps: 183610
|
||||
|
|
|
@ -22,7 +22,7 @@ export const getFindExceptionListSchemaMock = (): FindExceptionListSchema => ({
|
|||
|
||||
export const getFindExceptionListSchemaDecodedMock = (): FindExceptionListSchemaDecoded => ({
|
||||
filter: FILTER,
|
||||
namespace_type: NAMESPACE_TYPE,
|
||||
namespace_type: [NAMESPACE_TYPE],
|
||||
page: 1,
|
||||
per_page: 25,
|
||||
sort_field: undefined,
|
||||
|
|
|
@ -37,7 +37,7 @@ describe('find_exception_list_schema', () => {
|
|||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
const expected: FindExceptionListSchemaDecoded = {
|
||||
filter: undefined,
|
||||
namespace_type: 'single',
|
||||
namespace_type: ['single'],
|
||||
page: undefined,
|
||||
per_page: undefined,
|
||||
sort_field: undefined,
|
||||
|
|
|
@ -6,15 +6,15 @@
|
|||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
import { filter, namespace_type, sort_field, sort_order } from '../common/schemas';
|
||||
import { filter, sort_field, sort_order } from '../common/schemas';
|
||||
import { RequiredKeepUndefined } from '../../types';
|
||||
import { StringToPositiveNumber } from '../types/string_to_positive_number';
|
||||
import { NamespaceType } from '../types';
|
||||
import { DefaultNamespaceArray, NamespaceTypeArray } from '../types/default_namespace_array';
|
||||
|
||||
export const findExceptionListSchema = t.exact(
|
||||
t.partial({
|
||||
filter, // defaults to undefined if not set during decode
|
||||
namespace_type, // defaults to 'single' if not set during decode
|
||||
namespace_type: DefaultNamespaceArray, // defaults to 'single' if not set during decode
|
||||
page: StringToPositiveNumber, // defaults to undefined if not set during decode
|
||||
per_page: StringToPositiveNumber, // defaults to undefined if not set during decode
|
||||
sort_field, // defaults to undefined if not set during decode
|
||||
|
@ -29,5 +29,5 @@ export type FindExceptionListSchemaDecoded = Omit<
|
|||
RequiredKeepUndefined<t.TypeOf<typeof findExceptionListSchema>>,
|
||||
'namespace_type'
|
||||
> & {
|
||||
namespace_type: NamespaceType;
|
||||
namespace_type: NamespaceTypeArray;
|
||||
};
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const exceptionListSavedObjectType = 'exception-list';
|
||||
export const exceptionListAgnosticSavedObjectType = 'exception-list-agnostic';
|
||||
export type SavedObjectType = 'exception-list' | 'exception-list-agnostic';
|
||||
|
||||
/**
|
||||
* This makes any optional property the same as Required<T> would but also has the
|
||||
* added benefit of keeping your undefined.
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
ExceptionListItemSchema,
|
||||
ExceptionListSchema,
|
||||
} from '../../common/schemas';
|
||||
import { getFoundExceptionListSchemaMock } from '../../common/schemas/response/found_exception_list_schema.mock';
|
||||
|
||||
import {
|
||||
addEndpointExceptionList,
|
||||
|
@ -26,11 +27,12 @@ import {
|
|||
deleteExceptionListItemById,
|
||||
fetchExceptionListById,
|
||||
fetchExceptionListItemById,
|
||||
fetchExceptionLists,
|
||||
fetchExceptionListsItemsByListIds,
|
||||
updateExceptionList,
|
||||
updateExceptionListItem,
|
||||
} from './api';
|
||||
import { ApiCallByIdProps, ApiCallByListIdProps } from './types';
|
||||
import { ApiCallByIdProps, ApiCallByListIdProps, ApiCallFetchExceptionListsProps } from './types';
|
||||
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
|
@ -289,6 +291,87 @@ describe('Exceptions Lists API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#fetchExceptionLists', () => {
|
||||
beforeEach(() => {
|
||||
httpMock.fetch.mockResolvedValue(getFoundExceptionListSchemaMock());
|
||||
});
|
||||
|
||||
test('it invokes "fetchExceptionLists" with expected url and body values', async () => {
|
||||
await fetchExceptionLists({
|
||||
filters: 'exception-list.attributes.name: Sample Endpoint',
|
||||
http: httpMock,
|
||||
namespaceTypes: 'single,agnostic',
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/_find', {
|
||||
method: 'GET',
|
||||
query: {
|
||||
filter: 'exception-list.attributes.name: Sample Endpoint',
|
||||
namespace_type: 'single,agnostic',
|
||||
page: '1',
|
||||
per_page: '20',
|
||||
sort_field: 'exception-list.created_at',
|
||||
sort_order: 'desc',
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('it returns expected exception list on success', async () => {
|
||||
const exceptionResponse = await fetchExceptionLists({
|
||||
filters: 'exception-list.attributes.name: Sample Endpoint',
|
||||
http: httpMock,
|
||||
namespaceTypes: 'single,agnostic',
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(exceptionResponse.data).toEqual([getExceptionListSchemaMock()]);
|
||||
});
|
||||
|
||||
test('it returns error and does not make request if request payload fails decode', async () => {
|
||||
const payload = ({
|
||||
filters: 'exception-list.attributes.name: Sample Endpoint',
|
||||
http: httpMock,
|
||||
namespaceTypes: 'notANamespaceType',
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
} as unknown) as ApiCallFetchExceptionListsProps & { namespaceTypes: string[] };
|
||||
await expect(fetchExceptionLists(payload)).rejects.toEqual(
|
||||
'Invalid value "notANamespaceType" supplied to "namespace_type"'
|
||||
);
|
||||
});
|
||||
|
||||
test('it returns error if response payload fails decode', async () => {
|
||||
const badPayload = getExceptionListSchemaMock();
|
||||
// @ts-expect-error
|
||||
delete badPayload.id;
|
||||
httpMock.fetch.mockResolvedValue({ data: [badPayload], page: 1, per_page: 20, total: 1 });
|
||||
|
||||
await expect(
|
||||
fetchExceptionLists({
|
||||
filters: 'exception-list.attributes.name: Sample Endpoint',
|
||||
http: httpMock,
|
||||
namespaceTypes: 'single,agnostic',
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
})
|
||||
).rejects.toEqual('Invalid value "undefined" supplied to "data,id"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#fetchExceptionListById', () => {
|
||||
beforeEach(() => {
|
||||
httpMock.fetch.mockResolvedValue(getExceptionListSchemaMock());
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
ExceptionListItemSchema,
|
||||
ExceptionListSchema,
|
||||
FoundExceptionListItemSchema,
|
||||
FoundExceptionListSchema,
|
||||
createEndpointListSchema,
|
||||
createExceptionListItemSchema,
|
||||
createExceptionListSchema,
|
||||
|
@ -23,7 +24,9 @@ import {
|
|||
exceptionListItemSchema,
|
||||
exceptionListSchema,
|
||||
findExceptionListItemSchema,
|
||||
findExceptionListSchema,
|
||||
foundExceptionListItemSchema,
|
||||
foundExceptionListSchema,
|
||||
readExceptionListItemSchema,
|
||||
readExceptionListSchema,
|
||||
updateExceptionListItemSchema,
|
||||
|
@ -37,6 +40,7 @@ import {
|
|||
AddExceptionListProps,
|
||||
ApiCallByIdProps,
|
||||
ApiCallByListIdProps,
|
||||
ApiCallFetchExceptionListsProps,
|
||||
UpdateExceptionListItemProps,
|
||||
UpdateExceptionListProps,
|
||||
} from './types';
|
||||
|
@ -201,6 +205,58 @@ export const updateExceptionListItem = async ({
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch all ExceptionLists (optionally by namespaceType)
|
||||
*
|
||||
* @param http Kibana http service
|
||||
* @param namespaceTypes ExceptionList namespace_types of lists to find
|
||||
* @param filters search bar filters
|
||||
* @param pagination optional
|
||||
* @param signal to cancel request
|
||||
*
|
||||
* @throws An error if request params or response is not OK
|
||||
*/
|
||||
export const fetchExceptionLists = async ({
|
||||
http,
|
||||
filters,
|
||||
namespaceTypes,
|
||||
pagination,
|
||||
signal,
|
||||
}: ApiCallFetchExceptionListsProps): Promise<FoundExceptionListSchema> => {
|
||||
const query = {
|
||||
filter: filters,
|
||||
namespace_type: namespaceTypes,
|
||||
page: pagination.page ? `${pagination.page}` : '1',
|
||||
per_page: pagination.perPage ? `${pagination.perPage}` : '20',
|
||||
sort_field: 'exception-list.created_at',
|
||||
sort_order: 'desc',
|
||||
};
|
||||
|
||||
const [validatedRequest, errorsRequest] = validate(query, findExceptionListSchema);
|
||||
|
||||
if (validatedRequest != null) {
|
||||
try {
|
||||
const response = await http.fetch<ExceptionListSchema>(`${EXCEPTION_LIST_URL}/_find`, {
|
||||
method: 'GET',
|
||||
query,
|
||||
signal,
|
||||
});
|
||||
|
||||
const [validatedResponse, errorsResponse] = validate(response, foundExceptionListSchema);
|
||||
|
||||
if (errorsResponse != null || validatedResponse == null) {
|
||||
return Promise.reject(errorsResponse);
|
||||
} else {
|
||||
return Promise.resolve(validatedResponse);
|
||||
}
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(errorsRequest);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch an ExceptionList by providing a ExceptionList ID
|
||||
*
|
||||
|
|
|
@ -10,13 +10,13 @@ import { coreMock } from '../../../../../../src/core/public/mocks';
|
|||
import * as api from '../api';
|
||||
import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock';
|
||||
import { ExceptionListItemSchema } from '../../../common/schemas';
|
||||
import { UseExceptionListProps, UseExceptionListSuccess } from '../types';
|
||||
import { UseExceptionListItemsSuccess, UseExceptionListProps } from '../types';
|
||||
|
||||
import { ReturnExceptionListAndItems, useExceptionList } from './use_exception_list';
|
||||
import { ReturnExceptionListAndItems, useExceptionListItems } from './use_exception_list_items';
|
||||
|
||||
const mockKibanaHttpService = coreMock.createStart().http;
|
||||
|
||||
describe('useExceptionList', () => {
|
||||
describe('useExceptionListItems', () => {
|
||||
const onErrorMock = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -36,7 +36,7 @@ describe('useExceptionList', () => {
|
|||
UseExceptionListProps,
|
||||
ReturnExceptionListAndItems
|
||||
>(() =>
|
||||
useExceptionList({
|
||||
useExceptionListItems({
|
||||
filterOptions: [],
|
||||
http: mockKibanaHttpService,
|
||||
lists: [
|
||||
|
@ -75,7 +75,7 @@ describe('useExceptionList', () => {
|
|||
UseExceptionListProps,
|
||||
ReturnExceptionListAndItems
|
||||
>(() =>
|
||||
useExceptionList({
|
||||
useExceptionListItems({
|
||||
filterOptions: [],
|
||||
http: mockKibanaHttpService,
|
||||
lists: [
|
||||
|
@ -100,7 +100,7 @@ describe('useExceptionList', () => {
|
|||
|
||||
const expectedListItemsResult: ExceptionListItemSchema[] = getFoundExceptionListItemSchemaMock()
|
||||
.data;
|
||||
const expectedResult: UseExceptionListSuccess = {
|
||||
const expectedResult: UseExceptionListItemsSuccess = {
|
||||
exceptions: expectedListItemsResult,
|
||||
pagination: { page: 1, perPage: 1, total: 1 },
|
||||
};
|
||||
|
@ -129,7 +129,7 @@ describe('useExceptionList', () => {
|
|||
const onSuccessMock = jest.fn();
|
||||
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
|
||||
() =>
|
||||
useExceptionList({
|
||||
useExceptionListItems({
|
||||
filterOptions: [],
|
||||
http: mockKibanaHttpService,
|
||||
lists: [
|
||||
|
@ -179,7 +179,7 @@ describe('useExceptionList', () => {
|
|||
const onSuccessMock = jest.fn();
|
||||
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
|
||||
() =>
|
||||
useExceptionList({
|
||||
useExceptionListItems({
|
||||
filterOptions: [],
|
||||
http: mockKibanaHttpService,
|
||||
lists: [
|
||||
|
@ -231,7 +231,7 @@ describe('useExceptionList', () => {
|
|||
UseExceptionListProps,
|
||||
ReturnExceptionListAndItems
|
||||
>(() =>
|
||||
useExceptionList({
|
||||
useExceptionListItems({
|
||||
filterOptions: [],
|
||||
http: mockKibanaHttpService,
|
||||
lists: [
|
||||
|
@ -278,7 +278,7 @@ describe('useExceptionList', () => {
|
|||
const onSuccessMock = jest.fn();
|
||||
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
|
||||
() =>
|
||||
useExceptionList({
|
||||
useExceptionListItems({
|
||||
filterOptions: [{ filter: 'host.name', tags: [] }],
|
||||
http: mockKibanaHttpService,
|
||||
lists: [
|
||||
|
@ -343,7 +343,7 @@ describe('useExceptionList', () => {
|
|||
showDetectionsListsOnly,
|
||||
showEndpointListsOnly,
|
||||
}) =>
|
||||
useExceptionList({
|
||||
useExceptionListItems({
|
||||
filterOptions,
|
||||
http,
|
||||
lists,
|
||||
|
@ -413,7 +413,7 @@ describe('useExceptionList', () => {
|
|||
UseExceptionListProps,
|
||||
ReturnExceptionListAndItems
|
||||
>(() =>
|
||||
useExceptionList({
|
||||
useExceptionListItems({
|
||||
filterOptions: [],
|
||||
http: mockKibanaHttpService,
|
||||
lists: [
|
||||
|
@ -455,7 +455,7 @@ describe('useExceptionList', () => {
|
|||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
|
||||
() =>
|
||||
useExceptionList({
|
||||
useExceptionListItems({
|
||||
filterOptions: [],
|
||||
http: mockKibanaHttpService,
|
||||
lists: [
|
|
@ -34,7 +34,7 @@ export type ReturnExceptionListAndItems = [
|
|||
* @param pagination optional
|
||||
*
|
||||
*/
|
||||
export const useExceptionList = ({
|
||||
export const useExceptionListItems = ({
|
||||
http,
|
||||
lists,
|
||||
pagination = {
|
|
@ -0,0 +1,348 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { coreMock } from '../../../../../../src/core/public/mocks';
|
||||
import * as api from '../api';
|
||||
import { getFoundExceptionListSchemaMock } from '../../../common/schemas/response/found_exception_list_schema.mock';
|
||||
import { ExceptionListSchema } from '../../../common/schemas';
|
||||
import { UseExceptionListsProps } from '../types';
|
||||
|
||||
import { ReturnExceptionLists, useExceptionLists } from './use_exception_lists';
|
||||
|
||||
const mockKibanaHttpService = coreMock.createStart().http;
|
||||
const mockKibanaNotificationsService = coreMock.createStart().notifications;
|
||||
|
||||
describe('useExceptionLists', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(api, 'fetchExceptionLists').mockResolvedValue(getFoundExceptionListSchemaMock());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('initializes hook', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<
|
||||
UseExceptionListsProps,
|
||||
ReturnExceptionLists
|
||||
>(() =>
|
||||
useExceptionLists({
|
||||
errorMessage: 'Uh oh',
|
||||
filterOptions: {},
|
||||
http: mockKibanaHttpService,
|
||||
namespaceTypes: ['single', 'agnostic'],
|
||||
notifications: mockKibanaNotificationsService,
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
},
|
||||
showTrustedApps: false,
|
||||
})
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual([
|
||||
true,
|
||||
[],
|
||||
{
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
},
|
||||
null,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('fetches exception lists', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<
|
||||
UseExceptionListsProps,
|
||||
ReturnExceptionLists
|
||||
>(() =>
|
||||
useExceptionLists({
|
||||
errorMessage: 'Uh oh',
|
||||
filterOptions: {},
|
||||
http: mockKibanaHttpService,
|
||||
namespaceTypes: ['single', 'agnostic'],
|
||||
notifications: mockKibanaNotificationsService,
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
},
|
||||
showTrustedApps: false,
|
||||
})
|
||||
);
|
||||
// NOTE: First `waitForNextUpdate` is initialization
|
||||
// Second call applies the params
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
const expectedListItemsResult: ExceptionListSchema[] = getFoundExceptionListSchemaMock().data;
|
||||
|
||||
expect(result.current).toEqual([
|
||||
false,
|
||||
expectedListItemsResult,
|
||||
{
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 1,
|
||||
},
|
||||
result.current[3],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('fetches trusted apps lists if "showTrustedApps" is true', async () => {
|
||||
const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists');
|
||||
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook<UseExceptionListsProps, ReturnExceptionLists>(() =>
|
||||
useExceptionLists({
|
||||
errorMessage: 'Uh oh',
|
||||
filterOptions: {},
|
||||
http: mockKibanaHttpService,
|
||||
namespaceTypes: ['single', 'agnostic'],
|
||||
notifications: mockKibanaNotificationsService,
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
},
|
||||
showTrustedApps: true,
|
||||
})
|
||||
);
|
||||
// NOTE: First `waitForNextUpdate` is initialization
|
||||
// Second call applies the params
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
|
||||
filters:
|
||||
'(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)',
|
||||
http: mockKibanaHttpService,
|
||||
namespaceTypes: 'single,agnostic',
|
||||
pagination: { page: 1, perPage: 20 },
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('does not fetch trusted apps lists if "showTrustedApps" is false', async () => {
|
||||
const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists');
|
||||
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook<UseExceptionListsProps, ReturnExceptionLists>(() =>
|
||||
useExceptionLists({
|
||||
errorMessage: 'Uh oh',
|
||||
filterOptions: {},
|
||||
http: mockKibanaHttpService,
|
||||
namespaceTypes: ['single', 'agnostic'],
|
||||
notifications: mockKibanaNotificationsService,
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
},
|
||||
showTrustedApps: false,
|
||||
})
|
||||
);
|
||||
// NOTE: First `waitForNextUpdate` is initialization
|
||||
// Second call applies the params
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
|
||||
filters:
|
||||
'(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)',
|
||||
http: mockKibanaHttpService,
|
||||
namespaceTypes: 'single,agnostic',
|
||||
pagination: { page: 1, perPage: 20 },
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('applies filters to query', async () => {
|
||||
const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists');
|
||||
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook<UseExceptionListsProps, ReturnExceptionLists>(() =>
|
||||
useExceptionLists({
|
||||
errorMessage: 'Uh oh',
|
||||
filterOptions: {
|
||||
created_by: 'Moi',
|
||||
name: 'Sample Endpoint',
|
||||
},
|
||||
http: mockKibanaHttpService,
|
||||
namespaceTypes: ['single', 'agnostic'],
|
||||
notifications: mockKibanaNotificationsService,
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
},
|
||||
showTrustedApps: false,
|
||||
})
|
||||
);
|
||||
// NOTE: First `waitForNextUpdate` is initialization
|
||||
// Second call applies the params
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
|
||||
filters:
|
||||
'(exception-list.attributes.created_by:Moi* OR exception-list-agnostic.attributes.created_by:Moi*) AND (exception-list.attributes.name:Sample Endpoint* OR exception-list-agnostic.attributes.name:Sample Endpoint*) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)',
|
||||
http: mockKibanaHttpService,
|
||||
namespaceTypes: 'single,agnostic',
|
||||
pagination: { page: 1, perPage: 20 },
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('fetches a new exception list and its items when props change', async () => {
|
||||
const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists');
|
||||
await act(async () => {
|
||||
const { rerender, waitForNextUpdate } = renderHook<
|
||||
UseExceptionListsProps,
|
||||
ReturnExceptionLists
|
||||
>(
|
||||
({
|
||||
errorMessage,
|
||||
filterOptions,
|
||||
http,
|
||||
namespaceTypes,
|
||||
notifications,
|
||||
pagination,
|
||||
showTrustedApps,
|
||||
}) =>
|
||||
useExceptionLists({
|
||||
errorMessage,
|
||||
filterOptions,
|
||||
http,
|
||||
namespaceTypes,
|
||||
notifications,
|
||||
pagination,
|
||||
showTrustedApps,
|
||||
}),
|
||||
{
|
||||
initialProps: {
|
||||
errorMessage: 'Uh oh',
|
||||
filterOptions: {},
|
||||
http: mockKibanaHttpService,
|
||||
namespaceTypes: ['single'],
|
||||
notifications: mockKibanaNotificationsService,
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
},
|
||||
showTrustedApps: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
// NOTE: First `waitForNextUpdate` is initialization
|
||||
// Second call applies the params
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
rerender({
|
||||
errorMessage: 'Uh oh',
|
||||
filterOptions: {},
|
||||
http: mockKibanaHttpService,
|
||||
namespaceTypes: ['single', 'agnostic'],
|
||||
notifications: mockKibanaNotificationsService,
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
},
|
||||
showTrustedApps: false,
|
||||
});
|
||||
// NOTE: Only need one call here because hook already initilaized
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(spyOnfetchExceptionLists).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
test('fetches list when refreshExceptionList callback invoked', async () => {
|
||||
const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists');
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<
|
||||
UseExceptionListsProps,
|
||||
ReturnExceptionLists
|
||||
>(() =>
|
||||
useExceptionLists({
|
||||
errorMessage: 'Uh oh',
|
||||
filterOptions: {},
|
||||
http: mockKibanaHttpService,
|
||||
namespaceTypes: ['single', 'agnostic'],
|
||||
notifications: mockKibanaNotificationsService,
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
},
|
||||
showTrustedApps: false,
|
||||
})
|
||||
);
|
||||
// NOTE: First `waitForNextUpdate` is initialization
|
||||
// Second call applies the params
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(typeof result.current[3]).toEqual('function');
|
||||
|
||||
if (result.current[3] != null) {
|
||||
result.current[3]();
|
||||
}
|
||||
// NOTE: Only need one call here because hook already initilaized
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(spyOnfetchExceptionLists).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
test('invokes notifications service if "fetchExceptionLists" fails', async () => {
|
||||
const mockError = new Error('failed to fetches list items');
|
||||
const spyOnfetchExceptionLists = jest
|
||||
.spyOn(api, 'fetchExceptionLists')
|
||||
.mockRejectedValue(mockError);
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook<UseExceptionListsProps, ReturnExceptionLists>(() =>
|
||||
useExceptionLists({
|
||||
errorMessage: 'Uh oh',
|
||||
filterOptions: {},
|
||||
http: mockKibanaHttpService,
|
||||
namespaceTypes: ['single', 'agnostic'],
|
||||
notifications: mockKibanaNotificationsService,
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
},
|
||||
showTrustedApps: false,
|
||||
})
|
||||
);
|
||||
// NOTE: First `waitForNextUpdate` is initialization
|
||||
// Second call applies the params
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(mockKibanaNotificationsService.toasts.addError).toHaveBeenCalledWith(mockError, {
|
||||
title: 'Uh oh',
|
||||
});
|
||||
expect(spyOnfetchExceptionLists).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { fetchExceptionLists } from '../api';
|
||||
import { Pagination, UseExceptionListsProps } from '../types';
|
||||
import { ExceptionListSchema } from '../../../common/schemas';
|
||||
import { getFilters } from '../utils';
|
||||
|
||||
export type Func = () => void;
|
||||
export type ReturnExceptionLists = [boolean, ExceptionListSchema[], Pagination, Func | null];
|
||||
|
||||
/**
|
||||
* Hook for fetching ExceptionLists
|
||||
*
|
||||
* @param http Kibana http service
|
||||
* @param errorMessage message shown to user if error occurs
|
||||
* @param filterOptions filter by certain fields
|
||||
* @param namespaceTypes spaces to be searched
|
||||
* @param notifications kibana service for displaying toasters
|
||||
* @param showTrustedApps boolean - include/exclude trusted app lists
|
||||
* @param pagination
|
||||
*
|
||||
*/
|
||||
export const useExceptionLists = ({
|
||||
errorMessage,
|
||||
http,
|
||||
pagination = {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
},
|
||||
filterOptions = {},
|
||||
namespaceTypes,
|
||||
notifications,
|
||||
showTrustedApps = false,
|
||||
}: UseExceptionListsProps): ReturnExceptionLists => {
|
||||
const [exceptionLists, setExceptionLists] = useState<ExceptionListSchema[]>([]);
|
||||
const [paginationInfo, setPagination] = useState<Pagination>(pagination);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const fetchExceptionListsRef = useRef<Func | null>(null);
|
||||
|
||||
const namespaceTypesAsString = useMemo(() => namespaceTypes.join(','), [namespaceTypes]);
|
||||
const filters = useMemo(
|
||||
(): string => getFilters(filterOptions, namespaceTypes, showTrustedApps),
|
||||
[namespaceTypes, filterOptions, showTrustedApps]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
const fetchData = async (): Promise<void> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const { page, per_page: perPage, total, data } = await fetchExceptionLists({
|
||||
filters,
|
||||
http,
|
||||
namespaceTypes: namespaceTypesAsString,
|
||||
pagination: {
|
||||
page: pagination.page,
|
||||
perPage: pagination.perPage,
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
if (isSubscribed) {
|
||||
setPagination({
|
||||
page,
|
||||
perPage,
|
||||
total,
|
||||
});
|
||||
setExceptionLists(data);
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isSubscribed) {
|
||||
notifications.toasts.addError(error, {
|
||||
title: errorMessage,
|
||||
});
|
||||
setExceptionLists([]);
|
||||
setPagination({
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
|
||||
fetchExceptionListsRef.current = fetchData;
|
||||
return (): void => {
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, [
|
||||
errorMessage,
|
||||
notifications,
|
||||
pagination.page,
|
||||
pagination.perPage,
|
||||
filters,
|
||||
namespaceTypesAsString,
|
||||
http,
|
||||
]);
|
||||
|
||||
return [loading, exceptionLists, paginationInfo, fetchExceptionListsRef.current];
|
||||
};
|
|
@ -17,7 +17,7 @@ import {
|
|||
UpdateExceptionListItemSchema,
|
||||
UpdateExceptionListSchema,
|
||||
} from '../../common/schemas';
|
||||
import { HttpStart } from '../../../../../src/core/public';
|
||||
import { HttpStart, NotificationsStart } from '../../../../../src/core/public';
|
||||
|
||||
export interface FilterExceptionsOptions {
|
||||
filter: string;
|
||||
|
@ -43,7 +43,7 @@ export interface ExceptionList extends ExceptionListSchema {
|
|||
totalItems: number;
|
||||
}
|
||||
|
||||
export interface UseExceptionListSuccess {
|
||||
export interface UseExceptionListItemsSuccess {
|
||||
exceptions: ExceptionListItemSchema[];
|
||||
pagination: Pagination;
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ export interface UseExceptionListProps {
|
|||
showDetectionsListsOnly: boolean;
|
||||
showEndpointListsOnly: boolean;
|
||||
matchFilters: boolean;
|
||||
onSuccess?: (arg: UseExceptionListSuccess) => void;
|
||||
onSuccess?: (arg: UseExceptionListItemsSuccess) => void;
|
||||
}
|
||||
|
||||
export interface ExceptionListIdentifiers {
|
||||
|
@ -97,7 +97,35 @@ export interface ApiCallFindListsItemsMemoProps {
|
|||
showDetectionsListsOnly: boolean;
|
||||
showEndpointListsOnly: boolean;
|
||||
onError: (arg: string[]) => void;
|
||||
onSuccess: (arg: UseExceptionListSuccess) => void;
|
||||
onSuccess: (arg: UseExceptionListItemsSuccess) => void;
|
||||
}
|
||||
export interface ApiCallFetchExceptionListsProps {
|
||||
http: HttpStart;
|
||||
namespaceTypes: string;
|
||||
pagination: Partial<Pagination>;
|
||||
filters: string;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface UseExceptionListsSuccess {
|
||||
exceptions: ExceptionListSchema[];
|
||||
pagination: Pagination;
|
||||
}
|
||||
|
||||
export interface ExceptionListFilter {
|
||||
name?: string | null;
|
||||
list_id?: string | null;
|
||||
created_by?: string | null;
|
||||
}
|
||||
|
||||
export interface UseExceptionListsProps {
|
||||
errorMessage: string;
|
||||
filterOptions?: ExceptionListFilter;
|
||||
http: HttpStart;
|
||||
namespaceTypes: NamespaceType[];
|
||||
notifications: NotificationsStart;
|
||||
pagination?: Pagination;
|
||||
showTrustedApps: boolean;
|
||||
}
|
||||
|
||||
export interface AddExceptionListProps {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getIdsAndNamespaces } from './utils';
|
||||
import { getFilters, getGeneralFilters, getIdsAndNamespaces, getTrustedAppsFilter } from './utils';
|
||||
|
||||
describe('Exceptions utils', () => {
|
||||
describe('#getIdsAndNamespaces', () => {
|
||||
|
@ -102,4 +102,169 @@ describe('Exceptions utils', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGeneralFilters', () => {
|
||||
test('it returns empty string if no filters', () => {
|
||||
const filters = getGeneralFilters({}, ['exception-list']);
|
||||
|
||||
expect(filters).toEqual('');
|
||||
});
|
||||
|
||||
test('it properly formats filters when one namespace type passed in', () => {
|
||||
const filters = getGeneralFilters({ created_by: 'moi', name: 'Sample' }, ['exception-list']);
|
||||
|
||||
expect(filters).toEqual(
|
||||
'(exception-list.attributes.created_by:moi*) AND (exception-list.attributes.name:Sample*)'
|
||||
);
|
||||
});
|
||||
|
||||
test('it properly formats filters when two namespace types passed in', () => {
|
||||
const filters = getGeneralFilters({ created_by: 'moi', name: 'Sample' }, [
|
||||
'exception-list',
|
||||
'exception-list-agnostic',
|
||||
]);
|
||||
|
||||
expect(filters).toEqual(
|
||||
'(exception-list.attributes.created_by:moi* OR exception-list-agnostic.attributes.created_by:moi*) AND (exception-list.attributes.name:Sample* OR exception-list-agnostic.attributes.name:Sample*)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTrustedAppsFilter', () => {
|
||||
test('it returns filter to search for "exception-list" namespace trusted apps', () => {
|
||||
const filter = getTrustedAppsFilter(true, ['exception-list']);
|
||||
|
||||
expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_trusted_apps*)');
|
||||
});
|
||||
|
||||
test('it returns filter to search for "exception-list" and "agnostic" namespace trusted apps', () => {
|
||||
const filter = getTrustedAppsFilter(true, ['exception-list', 'exception-list-agnostic']);
|
||||
|
||||
expect(filter).toEqual(
|
||||
'(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
|
||||
);
|
||||
});
|
||||
|
||||
test('it returns filter to exclude "exception-list" namespace trusted apps', () => {
|
||||
const filter = getTrustedAppsFilter(false, ['exception-list']);
|
||||
|
||||
expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_trusted_apps*)');
|
||||
});
|
||||
|
||||
test('it returns filter to exclude "exception-list" and "agnostic" namespace trusted apps', () => {
|
||||
const filter = getTrustedAppsFilter(false, ['exception-list', 'exception-list-agnostic']);
|
||||
|
||||
expect(filter).toEqual(
|
||||
'(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFilters', () => {
|
||||
describe('single', () => {
|
||||
test('it properly formats when no filters passed and "showTrustedApps" is false', () => {
|
||||
const filter = getFilters({}, ['single'], false);
|
||||
|
||||
expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_trusted_apps*)');
|
||||
});
|
||||
|
||||
test('it properly formats when no filters passed and "showTrustedApps" is true', () => {
|
||||
const filter = getFilters({}, ['single'], true);
|
||||
|
||||
expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_trusted_apps*)');
|
||||
});
|
||||
|
||||
test('it properly formats when filters passed and "showTrustedApps" is false', () => {
|
||||
const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], false);
|
||||
|
||||
expect(filter).toEqual(
|
||||
'(exception-list.attributes.created_by:moi*) AND (exception-list.attributes.name:Sample*) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*)'
|
||||
);
|
||||
});
|
||||
|
||||
test('it if filters passed and "showTrustedApps" is true', () => {
|
||||
const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], true);
|
||||
|
||||
expect(filter).toEqual(
|
||||
'(exception-list.attributes.created_by:moi*) AND (exception-list.attributes.name:Sample*) AND (exception-list.attributes.list_id: endpoint_trusted_apps*)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('agnostic', () => {
|
||||
test('it properly formats when no filters passed and "showTrustedApps" is false', () => {
|
||||
const filter = getFilters({}, ['agnostic'], false);
|
||||
|
||||
expect(filter).toEqual(
|
||||
'(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
|
||||
);
|
||||
});
|
||||
|
||||
test('it properly formats when no filters passed and "showTrustedApps" is true', () => {
|
||||
const filter = getFilters({}, ['agnostic'], true);
|
||||
|
||||
expect(filter).toEqual(
|
||||
'(exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
|
||||
);
|
||||
});
|
||||
|
||||
test('it properly formats when filters passed and "showTrustedApps" is false', () => {
|
||||
const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], false);
|
||||
|
||||
expect(filter).toEqual(
|
||||
'(exception-list-agnostic.attributes.created_by:moi*) AND (exception-list-agnostic.attributes.name:Sample*) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
|
||||
);
|
||||
});
|
||||
|
||||
test('it if filters passed and "showTrustedApps" is true', () => {
|
||||
const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], true);
|
||||
|
||||
expect(filter).toEqual(
|
||||
'(exception-list-agnostic.attributes.created_by:moi*) AND (exception-list-agnostic.attributes.name:Sample*) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('single, agnostic', () => {
|
||||
test('it properly formats when no filters passed and "showTrustedApps" is false', () => {
|
||||
const filter = getFilters({}, ['single', 'agnostic'], false);
|
||||
|
||||
expect(filter).toEqual(
|
||||
'(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
|
||||
);
|
||||
});
|
||||
|
||||
test('it properly formats when no filters passed and "showTrustedApps" is true', () => {
|
||||
const filter = getFilters({}, ['single', 'agnostic'], true);
|
||||
|
||||
expect(filter).toEqual(
|
||||
'(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
|
||||
);
|
||||
});
|
||||
|
||||
test('it properly formats when filters passed and "showTrustedApps" is false', () => {
|
||||
const filter = getFilters(
|
||||
{ created_by: 'moi', name: 'Sample' },
|
||||
['single', 'agnostic'],
|
||||
false
|
||||
);
|
||||
|
||||
expect(filter).toEqual(
|
||||
'(exception-list.attributes.created_by:moi* OR exception-list-agnostic.attributes.created_by:moi*) AND (exception-list.attributes.name:Sample* OR exception-list-agnostic.attributes.name:Sample*) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
|
||||
);
|
||||
});
|
||||
|
||||
test('it properly formats when filters passed and "showTrustedApps" is true', () => {
|
||||
const filter = getFilters(
|
||||
{ created_by: 'moi', name: 'Sample' },
|
||||
['single', 'agnostic'],
|
||||
true
|
||||
);
|
||||
|
||||
expect(filter).toEqual(
|
||||
'(exception-list.attributes.created_by:moi* OR exception-list-agnostic.attributes.created_by:moi*) AND (exception-list.attributes.name:Sample* OR exception-list-agnostic.attributes.name:Sample*) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,9 +4,40 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { NamespaceType } from '../../common/schemas';
|
||||
import { get } from 'lodash/fp';
|
||||
|
||||
import { ExceptionListIdentifiers } from './types';
|
||||
import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../common/constants';
|
||||
import { NamespaceType } from '../../common/schemas';
|
||||
import { NamespaceTypeArray } from '../../common/schemas/types/default_namespace_array';
|
||||
import {
|
||||
SavedObjectType,
|
||||
exceptionListAgnosticSavedObjectType,
|
||||
exceptionListSavedObjectType,
|
||||
} from '../../common/types';
|
||||
|
||||
import { ExceptionListFilter, ExceptionListIdentifiers } from './types';
|
||||
|
||||
export const getSavedObjectType = ({
|
||||
namespaceType,
|
||||
}: {
|
||||
namespaceType: NamespaceType;
|
||||
}): SavedObjectType => {
|
||||
if (namespaceType === 'agnostic') {
|
||||
return exceptionListAgnosticSavedObjectType;
|
||||
} else {
|
||||
return exceptionListSavedObjectType;
|
||||
}
|
||||
};
|
||||
|
||||
export const getSavedObjectTypes = ({
|
||||
namespaceType,
|
||||
}: {
|
||||
namespaceType: NamespaceTypeArray;
|
||||
}): SavedObjectType[] => {
|
||||
return namespaceType.map((singleNamespaceType) =>
|
||||
getSavedObjectType({ namespaceType: singleNamespaceType })
|
||||
);
|
||||
};
|
||||
|
||||
export const getIdsAndNamespaces = ({
|
||||
lists,
|
||||
|
@ -34,3 +65,51 @@ export const getIdsAndNamespaces = ({
|
|||
}),
|
||||
{ ids: [], namespaces: [] }
|
||||
);
|
||||
|
||||
export const getGeneralFilters = (
|
||||
filters: ExceptionListFilter,
|
||||
namespaceTypes: SavedObjectType[]
|
||||
): string => {
|
||||
return Object.keys(filters)
|
||||
.map((filterKey) => {
|
||||
const value = get(filterKey, filters);
|
||||
if (value != null) {
|
||||
const filtersByNamespace = namespaceTypes
|
||||
.map((namespace) => {
|
||||
return `${namespace}.attributes.${filterKey}:${value}*`;
|
||||
})
|
||||
.join(' OR ');
|
||||
return `(${filtersByNamespace})`;
|
||||
} else return null;
|
||||
})
|
||||
.filter((item) => item != null)
|
||||
.join(' AND ');
|
||||
};
|
||||
|
||||
export const getTrustedAppsFilter = (
|
||||
showTrustedApps: boolean,
|
||||
namespaceTypes: SavedObjectType[]
|
||||
): string => {
|
||||
if (showTrustedApps) {
|
||||
const filters = namespaceTypes.map((namespace) => {
|
||||
return `${namespace}.attributes.list_id: ${ENDPOINT_TRUSTED_APPS_LIST_ID}*`;
|
||||
});
|
||||
return `(${filters.join(' OR ')})`;
|
||||
} else {
|
||||
const filters = namespaceTypes.map((namespace) => {
|
||||
return `not ${namespace}.attributes.list_id: ${ENDPOINT_TRUSTED_APPS_LIST_ID}*`;
|
||||
});
|
||||
return `(${filters.join(' AND ')})`;
|
||||
}
|
||||
};
|
||||
|
||||
export const getFilters = (
|
||||
filters: ExceptionListFilter,
|
||||
namespaceTypes: NamespaceType[],
|
||||
showTrustedApps: boolean
|
||||
): string => {
|
||||
const namespaces = getSavedObjectTypes({ namespaceType: namespaceTypes });
|
||||
const generalFilters = getGeneralFilters(filters, namespaces);
|
||||
const trustedAppsFilter = getTrustedAppsFilter(showTrustedApps, namespaces);
|
||||
return [generalFilters, trustedAppsFilter].filter((filter) => filter.trim() !== '').join(' AND ');
|
||||
};
|
||||
|
|
|
@ -11,7 +11,8 @@ export { useAsync } from './common/hooks/use_async';
|
|||
export { useApi } from './exceptions/hooks/use_api';
|
||||
export { usePersistExceptionItem } from './exceptions/hooks/persist_exception_item';
|
||||
export { usePersistExceptionList } from './exceptions/hooks/persist_exception_list';
|
||||
export { useExceptionList } from './exceptions/hooks/use_exception_list';
|
||||
export { useExceptionListItems } from './exceptions/hooks/use_exception_list_items';
|
||||
export { useExceptionLists } from './exceptions/hooks/use_exception_lists';
|
||||
export { useFindLists } from './lists/hooks/use_find_lists';
|
||||
export { useImportList } from './lists/hooks/use_import_list';
|
||||
export { useDeleteList } from './lists/hooks/use_delete_list';
|
||||
|
@ -32,5 +33,6 @@ export {
|
|||
ExceptionList,
|
||||
ExceptionListIdentifiers,
|
||||
Pagination,
|
||||
UseExceptionListSuccess,
|
||||
UseExceptionListItemsSuccess,
|
||||
UseExceptionListsSuccess,
|
||||
} from './exceptions/types';
|
||||
|
|
|
@ -147,6 +147,7 @@ const getReferencedExceptionLists = async (
|
|||
.join(' OR ');
|
||||
return exceptionLists.findExceptionList({
|
||||
filter: `(${filter})`,
|
||||
namespaceType: ['agnostic', 'single'],
|
||||
page: 1,
|
||||
perPage: 10000,
|
||||
sortField: undefined,
|
||||
|
|
|
@ -6,11 +6,12 @@
|
|||
|
||||
import { SavedObjectsType } from 'kibana/server';
|
||||
|
||||
import { migrations } from './migrations';
|
||||
import {
|
||||
exceptionListAgnosticSavedObjectType,
|
||||
exceptionListSavedObjectType,
|
||||
} from '../../common/types';
|
||||
|
||||
export const exceptionListSavedObjectType = 'exception-list';
|
||||
export const exceptionListAgnosticSavedObjectType = 'exception-list-agnostic';
|
||||
export type SavedObjectType = 'exception-list' | 'exception-list-agnostic';
|
||||
import { migrations } from './migrations';
|
||||
|
||||
/**
|
||||
* This is a super set of exception list and exception list items. The switch
|
||||
|
|
|
@ -205,7 +205,7 @@ export interface FindValueListExceptionListsItems {
|
|||
}
|
||||
|
||||
export interface FindExceptionListOptions {
|
||||
namespaceType?: NamespaceType;
|
||||
namespaceType: NamespaceTypeArray;
|
||||
filter: FilterOrUndefined;
|
||||
perPage: PerPageOrUndefined;
|
||||
page: PageOrUndefined;
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { getExceptionListFilter } from './find_exception_list';
|
||||
|
||||
describe('find_exception_list', () => {
|
||||
describe('getExceptionListFilter', () => {
|
||||
test('it should create a filter for agnostic lists if only searching for agnostic lists', () => {
|
||||
const filter = getExceptionListFilter({
|
||||
filter: undefined,
|
||||
savedObjectTypes: ['exception-list-agnostic'],
|
||||
});
|
||||
expect(filter).toEqual('(exception-list-agnostic.attributes.list_type: list)');
|
||||
});
|
||||
|
||||
test('it should create a filter for agnostic lists with additional filters if only searching for agnostic lists', () => {
|
||||
const filter = getExceptionListFilter({
|
||||
filter: 'exception-list-agnostic.attributes.name: "Sample Endpoint Exception List"',
|
||||
savedObjectTypes: ['exception-list-agnostic'],
|
||||
});
|
||||
expect(filter).toEqual(
|
||||
'(exception-list-agnostic.attributes.list_type: list) AND exception-list-agnostic.attributes.name: "Sample Endpoint Exception List"'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should create a filter for single lists if only searching for single lists', () => {
|
||||
const filter = getExceptionListFilter({
|
||||
filter: undefined,
|
||||
savedObjectTypes: ['exception-list'],
|
||||
});
|
||||
expect(filter).toEqual('(exception-list.attributes.list_type: list)');
|
||||
});
|
||||
|
||||
test('it should create a filter for single lists with additional filters if only searching for single lists', () => {
|
||||
const filter = getExceptionListFilter({
|
||||
filter: 'exception-list.attributes.name: "Sample Endpoint Exception List"',
|
||||
savedObjectTypes: ['exception-list'],
|
||||
});
|
||||
expect(filter).toEqual(
|
||||
'(exception-list.attributes.list_type: list) AND exception-list.attributes.name: "Sample Endpoint Exception List"'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should create a filter that searches for both agnostic and single lists', () => {
|
||||
const filter = getExceptionListFilter({
|
||||
filter: undefined,
|
||||
savedObjectTypes: ['exception-list-agnostic', 'exception-list'],
|
||||
});
|
||||
expect(filter).toEqual(
|
||||
'(exception-list-agnostic.attributes.list_type: list OR exception-list.attributes.list_type: list)'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should create a filter that searches for both agnostic and single lists with additional filters if only searching for agnostic lists', () => {
|
||||
const filter = getExceptionListFilter({
|
||||
filter: 'exception-list-agnostic.attributes.name: "Sample Endpoint Exception List"',
|
||||
savedObjectTypes: ['exception-list-agnostic', 'exception-list'],
|
||||
});
|
||||
expect(filter).toEqual(
|
||||
'(exception-list-agnostic.attributes.list_type: list OR exception-list.attributes.list_type: list) AND exception-list-agnostic.attributes.name: "Sample Endpoint Exception List"'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,26 +6,22 @@
|
|||
|
||||
import { SavedObjectsClientContract } from 'kibana/server';
|
||||
|
||||
import { NamespaceTypeArray } from '../../../common/schemas/types/default_namespace_array';
|
||||
import { SavedObjectType } from '../../../common/types';
|
||||
import {
|
||||
ExceptionListSoSchema,
|
||||
FilterOrUndefined,
|
||||
FoundExceptionListSchema,
|
||||
NamespaceType,
|
||||
PageOrUndefined,
|
||||
PerPageOrUndefined,
|
||||
SortFieldOrUndefined,
|
||||
SortOrderOrUndefined,
|
||||
} from '../../../common/schemas';
|
||||
import {
|
||||
SavedObjectType,
|
||||
exceptionListAgnosticSavedObjectType,
|
||||
exceptionListSavedObjectType,
|
||||
} from '../../saved_objects';
|
||||
|
||||
import { getSavedObjectType, transformSavedObjectsToFoundExceptionList } from './utils';
|
||||
import { getSavedObjectTypes, transformSavedObjectsToFoundExceptionList } from './utils';
|
||||
|
||||
interface FindExceptionListOptions {
|
||||
namespaceType?: NamespaceType;
|
||||
namespaceType: NamespaceTypeArray;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
filter: FilterOrUndefined;
|
||||
perPage: PerPageOrUndefined;
|
||||
|
@ -43,37 +39,31 @@ export const findExceptionList = async ({
|
|||
sortField,
|
||||
sortOrder,
|
||||
}: FindExceptionListOptions): Promise<FoundExceptionListSchema> => {
|
||||
const savedObjectType: SavedObjectType[] = namespaceType
|
||||
? [getSavedObjectType({ namespaceType })]
|
||||
: [exceptionListSavedObjectType, exceptionListAgnosticSavedObjectType];
|
||||
const savedObjectTypes = getSavedObjectTypes({ namespaceType });
|
||||
const savedObjectsFindResponse = await savedObjectsClient.find<ExceptionListSoSchema>({
|
||||
filter: getExceptionListFilter({ filter, savedObjectType }),
|
||||
filter: getExceptionListFilter({ filter, savedObjectTypes }),
|
||||
page,
|
||||
perPage,
|
||||
sortField,
|
||||
sortOrder,
|
||||
type: savedObjectType,
|
||||
type: savedObjectTypes,
|
||||
});
|
||||
|
||||
return transformSavedObjectsToFoundExceptionList({ savedObjectsFindResponse });
|
||||
};
|
||||
|
||||
export const getExceptionListFilter = ({
|
||||
filter,
|
||||
savedObjectType,
|
||||
savedObjectTypes,
|
||||
}: {
|
||||
filter: FilterOrUndefined;
|
||||
savedObjectType: SavedObjectType[];
|
||||
savedObjectTypes: SavedObjectType[];
|
||||
}): string => {
|
||||
const savedObjectTypeFilter = `(${savedObjectType
|
||||
.map((sot) => `${sot}.attributes.list_type: list`)
|
||||
.join(' OR ')})`;
|
||||
if (filter == null) {
|
||||
return savedObjectTypeFilter;
|
||||
} else {
|
||||
if (Array.isArray(savedObjectType)) {
|
||||
return `${savedObjectTypeFilter} AND ${filter}`;
|
||||
} else {
|
||||
return `${savedObjectType}.attributes.list_type: list AND ${filter}`;
|
||||
}
|
||||
}
|
||||
const listTypesFilter = savedObjectTypes
|
||||
.map((type) => `${type}.attributes.list_type: list`)
|
||||
.join(' OR ');
|
||||
|
||||
if (filter != null) {
|
||||
return `(${listTypesFilter}) AND ${filter}`;
|
||||
} else return `(${listTypesFilter})`;
|
||||
};
|
||||
|
|
|
@ -5,6 +5,11 @@
|
|||
*/
|
||||
import { SavedObjectsClientContract } from 'kibana/server';
|
||||
|
||||
import {
|
||||
SavedObjectType,
|
||||
exceptionListAgnosticSavedObjectType,
|
||||
exceptionListSavedObjectType,
|
||||
} from '../../../common/types';
|
||||
import { EmptyStringArrayDecoded } from '../../../common/schemas/types/empty_string_array';
|
||||
import { NamespaceTypeArray } from '../../../common/schemas/types/default_namespace_array';
|
||||
import { NonEmptyStringArrayDecoded } from '../../../common/schemas/types/non_empty_string_array';
|
||||
|
@ -17,11 +22,6 @@ import {
|
|||
SortFieldOrUndefined,
|
||||
SortOrderOrUndefined,
|
||||
} from '../../../common/schemas';
|
||||
import {
|
||||
SavedObjectType,
|
||||
exceptionListAgnosticSavedObjectType,
|
||||
exceptionListSavedObjectType,
|
||||
} from '../../saved_objects';
|
||||
|
||||
import { getSavedObjectTypes, transformSavedObjectsToFoundExceptionListItem } from './utils';
|
||||
import { getExceptionList } from './get_exception_list';
|
||||
|
|
|
@ -6,6 +6,11 @@
|
|||
import uuid from 'uuid';
|
||||
import { SavedObject, SavedObjectsFindResponse, SavedObjectsUpdateResponse } from 'kibana/server';
|
||||
|
||||
import {
|
||||
SavedObjectType,
|
||||
exceptionListAgnosticSavedObjectType,
|
||||
exceptionListSavedObjectType,
|
||||
} from '../../../common/types';
|
||||
import { NamespaceTypeArray } from '../../../common/schemas/types/default_namespace_array';
|
||||
import {
|
||||
CommentsArray,
|
||||
|
@ -21,11 +26,6 @@ import {
|
|||
exceptionListItemType,
|
||||
exceptionListType,
|
||||
} from '../../../common/schemas';
|
||||
import {
|
||||
SavedObjectType,
|
||||
exceptionListAgnosticSavedObjectType,
|
||||
exceptionListSavedObjectType,
|
||||
} from '../../saved_objects';
|
||||
|
||||
export const getSavedObjectType = ({
|
||||
namespaceType,
|
||||
|
|
|
@ -13,7 +13,7 @@ import { ExceptionsViewer } from './';
|
|||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import {
|
||||
ExceptionListTypeEnum,
|
||||
useExceptionList,
|
||||
useExceptionListItems,
|
||||
useApi,
|
||||
} from '../../../../../public/lists_plugin_deps';
|
||||
import { getExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_schema.mock';
|
||||
|
@ -40,7 +40,7 @@ describe('ExceptionsViewer', () => {
|
|||
getExceptionListsItems: jest.fn().mockResolvedValue(getFoundExceptionListItemSchemaMock()),
|
||||
});
|
||||
|
||||
(useExceptionList as jest.Mock).mockReturnValue([
|
||||
(useExceptionListItems as jest.Mock).mockReturnValue([
|
||||
false,
|
||||
[],
|
||||
[],
|
||||
|
@ -54,7 +54,7 @@ describe('ExceptionsViewer', () => {
|
|||
});
|
||||
|
||||
it('it renders loader if "loadingList" is true', () => {
|
||||
(useExceptionList as jest.Mock).mockReturnValue([
|
||||
(useExceptionListItems as jest.Mock).mockReturnValue([
|
||||
true,
|
||||
[],
|
||||
[],
|
||||
|
@ -106,7 +106,7 @@ describe('ExceptionsViewer', () => {
|
|||
});
|
||||
|
||||
it('it renders empty prompt if no exception items exist', () => {
|
||||
(useExceptionList as jest.Mock).mockReturnValue([
|
||||
(useExceptionListItems as jest.Mock).mockReturnValue([
|
||||
false,
|
||||
[getExceptionListSchemaMock()],
|
||||
[],
|
||||
|
|
|
@ -17,11 +17,11 @@ import { ExceptionsViewerHeader } from './exceptions_viewer_header';
|
|||
import { ExceptionListItemIdentifiers, Filter } from '../types';
|
||||
import { allExceptionItemsReducer, State, ViewerModalName } from './reducer';
|
||||
import {
|
||||
useExceptionList,
|
||||
useExceptionListItems,
|
||||
ExceptionListIdentifiers,
|
||||
ExceptionListTypeEnum,
|
||||
ExceptionListItemSchema,
|
||||
UseExceptionListSuccess,
|
||||
UseExceptionListItemsSuccess,
|
||||
useApi,
|
||||
} from '../../../../../public/lists_plugin_deps';
|
||||
import { ExceptionsViewerPagination } from './exceptions_pagination';
|
||||
|
@ -105,7 +105,10 @@ const ExceptionsViewerComponent = ({
|
|||
const { deleteExceptionItem, getExceptionListsItems } = useApi(services.http);
|
||||
|
||||
const setExceptions = useCallback(
|
||||
({ exceptions: newExceptions, pagination: newPagination }: UseExceptionListSuccess): void => {
|
||||
({
|
||||
exceptions: newExceptions,
|
||||
pagination: newPagination,
|
||||
}: UseExceptionListItemsSuccess): void => {
|
||||
dispatch({
|
||||
type: 'setExceptions',
|
||||
lists: exceptionListsMeta,
|
||||
|
@ -115,7 +118,7 @@ const ExceptionsViewerComponent = ({
|
|||
},
|
||||
[dispatch, exceptionListsMeta]
|
||||
);
|
||||
const [loadingList, , , fetchListItems] = useExceptionList({
|
||||
const [loadingList, , , fetchListItems] = useExceptionListItems({
|
||||
http: services.http,
|
||||
lists: exceptionListsMeta,
|
||||
filterOptions:
|
||||
|
|
|
@ -30,7 +30,7 @@ interface FormatUrlOptions {
|
|||
skipSearch: boolean;
|
||||
}
|
||||
|
||||
type FormatUrl = (path: string, options?: Partial<FormatUrlOptions>) => string;
|
||||
export type FormatUrl = (path: string, options?: Partial<FormatUrlOptions>) => string;
|
||||
|
||||
export const useFormatUrl = (page: SecurityPageName) => {
|
||||
const { getUrlForApp } = useKibana().services.application;
|
||||
|
|
|
@ -108,14 +108,16 @@ export const fetchRules = async ({
|
|||
},
|
||||
signal,
|
||||
}: FetchRulesProps): Promise<FetchRulesResponse> => {
|
||||
const showCustomRuleFilter = filterOptions.showCustomRules
|
||||
? [`alert.attributes.tags: "__internal_immutable:false"`]
|
||||
: [];
|
||||
const showElasticRuleFilter = filterOptions.showElasticRules
|
||||
? [`alert.attributes.tags: "__internal_immutable:true"`]
|
||||
: [];
|
||||
const filtersWithoutTags = [
|
||||
...(filterOptions.filter.length ? [`alert.attributes.name: ${filterOptions.filter}`] : []),
|
||||
...(filterOptions.showCustomRules
|
||||
? [`alert.attributes.tags: "__internal_immutable:false"`]
|
||||
: []),
|
||||
...(filterOptions.showElasticRules
|
||||
? [`alert.attributes.tags: "__internal_immutable:true"`]
|
||||
: []),
|
||||
...showCustomRuleFilter,
|
||||
...showElasticRuleFilter,
|
||||
].join(' AND ');
|
||||
|
||||
const tags = [
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
import React from 'react';
|
||||
import { EuiButtonIcon, EuiBasicTableColumn, EuiToolTip } from '@elastic/eui';
|
||||
import { History } from 'history';
|
||||
|
||||
import { FormatUrl } from '../../../../../../common/components/link_to';
|
||||
import { LinkAnchor } from '../../../../../../common/components/links';
|
||||
import * as i18n from './translations';
|
||||
import { ExceptionListInfo } from './use_all_exception_lists';
|
||||
import { getRuleDetailsUrl } from '../../../../../../common/components/link_to/redirect_to_detection_engine';
|
||||
|
||||
export type AllExceptionListsColumns = EuiBasicTableColumn<ExceptionListInfo>;
|
||||
export type Func = (listId: string) => () => void;
|
||||
|
||||
export const getAllExceptionListsColumns = (
|
||||
onExport: Func,
|
||||
onDelete: Func,
|
||||
history: History,
|
||||
formatUrl: FormatUrl
|
||||
): AllExceptionListsColumns[] => [
|
||||
{
|
||||
align: 'left',
|
||||
field: 'list_id',
|
||||
name: i18n.EXCEPTION_LIST_ID_TITLE,
|
||||
truncateText: true,
|
||||
dataType: 'string',
|
||||
width: '15%',
|
||||
render: (value: ExceptionListInfo['list_id']) => (
|
||||
<EuiToolTip position="left" content={value}>
|
||||
<>{value}</>
|
||||
</EuiToolTip>
|
||||
),
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
field: 'rules',
|
||||
name: i18n.NUMBER_RULES_ASSIGNED_TO_TITLE,
|
||||
truncateText: true,
|
||||
dataType: 'number',
|
||||
width: '10%',
|
||||
render: (value: ExceptionListInfo['rules']) => {
|
||||
return <p>{value.length}</p>;
|
||||
},
|
||||
},
|
||||
{
|
||||
align: 'left',
|
||||
field: 'rules',
|
||||
name: i18n.RULES_ASSIGNED_TO_TITLE,
|
||||
truncateText: true,
|
||||
dataType: 'string',
|
||||
width: '20%',
|
||||
render: (value: ExceptionListInfo['rules']) => {
|
||||
return (
|
||||
<>
|
||||
{value.map(({ id, name }, index) => (
|
||||
<>
|
||||
<LinkAnchor
|
||||
data-test-subj="ruleName"
|
||||
onClick={(ev: { preventDefault: () => void }) => {
|
||||
ev.preventDefault();
|
||||
history.push(getRuleDetailsUrl(id));
|
||||
}}
|
||||
href={formatUrl(getRuleDetailsUrl(id))}
|
||||
>
|
||||
{name}
|
||||
</LinkAnchor>
|
||||
{index !== value.length - 1 ? ', ' : ''}
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
align: 'left',
|
||||
field: 'created_at',
|
||||
name: i18n.LIST_DATE_CREATED_TITLE,
|
||||
truncateText: true,
|
||||
dataType: 'date',
|
||||
width: '14%',
|
||||
},
|
||||
{
|
||||
align: 'left',
|
||||
field: 'updated_at',
|
||||
name: i18n.LIST_DATE_UPDATED_TITLE,
|
||||
truncateText: true,
|
||||
width: '14%',
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
isExpander: false,
|
||||
width: '25px',
|
||||
render: (list: ExceptionListInfo) => (
|
||||
<EuiButtonIcon
|
||||
onClick={onExport(list.id)}
|
||||
aria-label="Export exception list"
|
||||
iconType="exportAction"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
width: '25px',
|
||||
isExpander: false,
|
||||
render: (list: ExceptionListInfo) => (
|
||||
<EuiButtonIcon
|
||||
color="danger"
|
||||
onClick={onDelete(list.id)}
|
||||
aria-label="Delete exception list"
|
||||
iconType="trash"
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useEffect, useCallback, useState, ChangeEvent } from 'react';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiEmptyPrompt,
|
||||
EuiLoadingContent,
|
||||
EuiProgress,
|
||||
EuiFieldSearch,
|
||||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { History } from 'history';
|
||||
import { set } from 'lodash/fp';
|
||||
|
||||
import { useKibana } from '../../../../../../common/lib/kibana';
|
||||
import { useExceptionLists } from '../../../../../../shared_imports';
|
||||
import { FormatUrl } from '../../../../../../common/components/link_to';
|
||||
import { HeaderSection } from '../../../../../../common/components/header_section';
|
||||
import { Loader } from '../../../../../../common/components/loader';
|
||||
import { Panel } from '../../../../../../common/components/panel';
|
||||
import * as i18n from './translations';
|
||||
import { AllRulesUtilityBar } from '../utility_bar';
|
||||
import { LastUpdatedAt } from '../../../../../../common/components/last_updated';
|
||||
import { AllExceptionListsColumns, getAllExceptionListsColumns } from './columns';
|
||||
import { useAllExceptionLists } from './use_all_exception_lists';
|
||||
|
||||
// Known lost battle with Eui :(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const MyEuiBasicTable = styled(EuiBasicTable as any)`` as any;
|
||||
|
||||
export type Func = () => void;
|
||||
export interface ExceptionListFilter {
|
||||
name?: string | null;
|
||||
list_id?: string | null;
|
||||
created_by?: string | null;
|
||||
}
|
||||
|
||||
interface ExceptionListsTableProps {
|
||||
history: History;
|
||||
hasNoPermissions: boolean;
|
||||
loading: boolean;
|
||||
formatUrl: FormatUrl;
|
||||
}
|
||||
|
||||
export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
|
||||
({ formatUrl, history, hasNoPermissions, loading }) => {
|
||||
const {
|
||||
services: { http, notifications },
|
||||
} = useKibana();
|
||||
const [filters, setFilters] = useState<ExceptionListFilter>({
|
||||
name: null,
|
||||
list_id: null,
|
||||
created_by: null,
|
||||
});
|
||||
const [loadingExceptions, exceptions, pagination, refreshExceptions] = useExceptionLists({
|
||||
errorMessage: i18n.ERROR_EXCEPTION_LISTS,
|
||||
filterOptions: filters,
|
||||
http,
|
||||
namespaceTypes: ['single', 'agnostic'],
|
||||
notifications,
|
||||
showTrustedApps: false,
|
||||
});
|
||||
const [loadingTableInfo, data] = useAllExceptionLists({
|
||||
exceptionLists: exceptions ?? [],
|
||||
});
|
||||
const [initLoading, setInitLoading] = useState(true);
|
||||
const [lastUpdated, setLastUpdated] = useState(Date.now());
|
||||
|
||||
const handleDelete = useCallback((id: string) => () => {}, []);
|
||||
|
||||
const handleExport = useCallback((id: string) => () => {}, []);
|
||||
|
||||
const exceptionsColumns = useMemo((): AllExceptionListsColumns[] => {
|
||||
return getAllExceptionListsColumns(handleExport, handleDelete, history, formatUrl);
|
||||
}, [handleExport, handleDelete, history, formatUrl]);
|
||||
|
||||
const handleRefresh = useCallback((): void => {
|
||||
if (refreshExceptions != null) {
|
||||
setLastUpdated(Date.now());
|
||||
refreshExceptions();
|
||||
}
|
||||
}, [refreshExceptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initLoading && !loading && !loadingExceptions && !loadingTableInfo) {
|
||||
setInitLoading(false);
|
||||
}
|
||||
}, [initLoading, loading, loadingExceptions, loadingTableInfo]);
|
||||
|
||||
const emptyPrompt = useMemo((): JSX.Element => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={<h3>{i18n.NO_EXCEPTION_LISTS}</h3>}
|
||||
titleSize="xs"
|
||||
body={i18n.NO_LISTS_BODY}
|
||||
/>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleSearch = useCallback((search: string) => {
|
||||
const regex = search.split(/\s+(?=([^"]*"[^"]*")*[^"]*$)/);
|
||||
const formattedFilter = regex
|
||||
.filter((c) => c != null)
|
||||
.reduce<ExceptionListFilter>(
|
||||
(filter, term) => {
|
||||
const [qualifier, value] = term.split(':');
|
||||
|
||||
if (qualifier == null) {
|
||||
filter.name = search;
|
||||
} else if (value != null && Object.keys(filter).includes(qualifier)) {
|
||||
return set(qualifier, value, filter);
|
||||
}
|
||||
|
||||
return filter;
|
||||
},
|
||||
{ name: null, list_id: null, created_by: null }
|
||||
);
|
||||
setFilters(formattedFilter);
|
||||
}, []);
|
||||
|
||||
const handleSearchChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const val = event.target.value;
|
||||
handleSearch(val);
|
||||
},
|
||||
[handleSearch]
|
||||
);
|
||||
|
||||
const paginationMemo = useMemo(
|
||||
() => ({
|
||||
pageIndex: pagination.page - 1,
|
||||
pageSize: pagination.perPage,
|
||||
totalItemCount: pagination.total,
|
||||
pageSizeOptions: [5, 10, 20, 50, 100, 200, 300],
|
||||
}),
|
||||
[pagination]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel loading={!initLoading && loadingTableInfo} data-test-subj="allExceptionListsPanel">
|
||||
<>
|
||||
{loadingTableInfo && (
|
||||
<EuiProgress
|
||||
data-test-subj="loadingRulesInfoProgress"
|
||||
size="xs"
|
||||
position="absolute"
|
||||
color="accent"
|
||||
/>
|
||||
)}
|
||||
<HeaderSection
|
||||
split
|
||||
title={i18n.ALL_EXCEPTIONS}
|
||||
subtitle={<LastUpdatedAt showUpdating={loading} updatedAt={lastUpdated} />}
|
||||
>
|
||||
<EuiFieldSearch
|
||||
data-test-subj="exceptionsHeaderSearch"
|
||||
aria-label={i18n.EXCEPTIONS_LISTS_SEARCH_PLACEHOLDER}
|
||||
placeholder={i18n.EXCEPTIONS_LISTS_SEARCH_PLACEHOLDER}
|
||||
onSearch={handleSearch}
|
||||
onChange={handleSearchChange}
|
||||
disabled={initLoading}
|
||||
incremental={false}
|
||||
fullWidth
|
||||
/>
|
||||
</HeaderSection>
|
||||
|
||||
{loadingTableInfo && !initLoading && (
|
||||
<Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" />
|
||||
)}
|
||||
{initLoading ? (
|
||||
<EuiLoadingContent data-test-subj="initialLoadingPanelAllRulesTable" lines={10} />
|
||||
) : (
|
||||
<>
|
||||
<AllRulesUtilityBar
|
||||
showBulkActions={false}
|
||||
userHasNoPermissions={hasNoPermissions}
|
||||
paginationTotal={data.length ?? 0}
|
||||
numberSelectedItems={0}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
<MyEuiBasicTable
|
||||
data-test-subj="exceptions-table"
|
||||
columns={exceptionsColumns}
|
||||
isSelectable={!hasNoPermissions ?? false}
|
||||
itemId="id"
|
||||
items={data ?? []}
|
||||
noItemsMessage={emptyPrompt}
|
||||
onChange={() => {}}
|
||||
pagination={paginationMemo}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</Panel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ExceptionListsTable.displayName = 'ExceptionListsTable';
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const EXCEPTION_LIST_ID_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.all.exceptions.idTitle',
|
||||
{
|
||||
defaultMessage: 'List ID',
|
||||
}
|
||||
);
|
||||
|
||||
export const NUMBER_RULES_ASSIGNED_TO_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.all.exceptions.numberRulesAssignedTitle',
|
||||
{
|
||||
defaultMessage: 'Number of rules assigned to',
|
||||
}
|
||||
);
|
||||
|
||||
export const RULES_ASSIGNED_TO_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.all.exceptions.rulesAssignedTitle',
|
||||
{
|
||||
defaultMessage: 'Rules assigned to',
|
||||
}
|
||||
);
|
||||
|
||||
export const LIST_DATE_CREATED_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.all.exceptions.dateCreatedTitle',
|
||||
{
|
||||
defaultMessage: 'Date created',
|
||||
}
|
||||
);
|
||||
|
||||
export const LIST_DATE_UPDATED_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.all.exceptions.dateUPdatedTitle',
|
||||
{
|
||||
defaultMessage: 'Last edited',
|
||||
}
|
||||
);
|
||||
|
||||
export const ERROR_EXCEPTION_LISTS = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.all.exceptions.errorFetching',
|
||||
{
|
||||
defaultMessage: 'Error fetching exception lists',
|
||||
}
|
||||
);
|
||||
|
||||
export const NO_EXCEPTION_LISTS = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allExceptionLists.filters.noExceptionsTitle',
|
||||
{
|
||||
defaultMessage: 'No exception lists found',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTIONS_LISTS_SEARCH_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allExceptionLists.search.placeholder',
|
||||
{
|
||||
defaultMessage: 'Search exception lists',
|
||||
}
|
||||
);
|
||||
|
||||
export const ALL_EXCEPTIONS = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allExceptions.tableTitle',
|
||||
{
|
||||
defaultMessage: 'Exception Lists',
|
||||
}
|
||||
);
|
||||
|
||||
export const NO_LISTS_BODY = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allExceptions.filters.noListsBody',
|
||||
{
|
||||
defaultMessage: "We weren't able to find any exception lists.",
|
||||
}
|
||||
);
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ExceptionListSchema } from '../../../../../../../../lists/common';
|
||||
|
||||
import { fetchRules } from '../../../../../containers/detection_engine/rules/api';
|
||||
|
||||
export interface ExceptionListInfo extends ExceptionListSchema {
|
||||
rules: Array<{ name: string; id: string }>;
|
||||
}
|
||||
|
||||
export type UseAllExceptionListsReturn = [boolean, ExceptionListInfo[]];
|
||||
|
||||
/**
|
||||
* Hook for preparing exception lists table info. For now, we need to do a table scan
|
||||
* of all rules to figure out which exception lists are used in what rules. This is very
|
||||
* slow, however, there is an issue open that would push all this work to Kiaban/ES and
|
||||
* speed things up a ton - https://github.com/elastic/kibana/issues/85173
|
||||
*
|
||||
* @param exceptionLists ExceptionListSchema(s) to evaluate
|
||||
*
|
||||
*/
|
||||
export const useAllExceptionLists = ({
|
||||
exceptionLists,
|
||||
}: {
|
||||
exceptionLists: ExceptionListSchema[];
|
||||
}): UseAllExceptionListsReturn => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [exceptionsListInfo, setExceptionsListInfo] = useState<ExceptionListInfo[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
const fetchData = async (): Promise<void> => {
|
||||
if (exceptionLists.length === 0 && isSubscribed) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const listsSkeleton = exceptionLists.reduce<Record<string, ExceptionListInfo>>(
|
||||
(acc, { id, ...rest }) => {
|
||||
acc[id] = {
|
||||
...rest,
|
||||
id,
|
||||
rules: [],
|
||||
};
|
||||
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const { data: rules } = await fetchRules({
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 500,
|
||||
total: 0,
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
const updatedLists = rules.reduce<Record<string, ExceptionListInfo>>((acc, rule) => {
|
||||
const exceptions = rule.exceptions_list;
|
||||
|
||||
if (exceptions != null && exceptions.length > 0) {
|
||||
exceptions.forEach((ex) => {
|
||||
const list = acc[ex.id];
|
||||
if (list != null) {
|
||||
acc[ex.id] = {
|
||||
...list,
|
||||
rules: [...list.rules, { id: rule.id, name: rule.name }],
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, listsSkeleton);
|
||||
|
||||
const lists = Object.keys(updatedLists).map<ExceptionListInfo>(
|
||||
(listKey) => updatedLists[listKey]
|
||||
);
|
||||
|
||||
setExceptionsListInfo(lists);
|
||||
|
||||
if (isSubscribed) {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isSubscribed) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
|
||||
return (): void => {
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, [exceptionLists.length, exceptionLists]);
|
||||
|
||||
return [loading, exceptionsListInfo];
|
||||
};
|
|
@ -158,7 +158,7 @@ describe('AllRules', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[title="All rules"]')).toHaveLength(1);
|
||||
expect(wrapper.find('[data-test-subj="allRulesTableTab-rules"]')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('it pulls from uiSettings to determine default refresh values', async () => {
|
||||
|
@ -230,7 +230,7 @@ describe('AllRules', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('rules tab', () => {
|
||||
describe('tabs', () => {
|
||||
it('renders all rules tab by default', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
|
@ -283,4 +283,33 @@ describe('AllRules', () => {
|
|||
expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders exceptions lists tab when tab clicked', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllRules
|
||||
createPrePackagedRules={jest.fn()}
|
||||
hasNoPermissions={false}
|
||||
loading={false}
|
||||
loadingCreatePrePackagedRules={false}
|
||||
refetchPrePackagedRulesStatus={jest.fn()}
|
||||
rulesCustomInstalled={1}
|
||||
rulesInstalled={0}
|
||||
rulesNotInstalled={0}
|
||||
rulesNotUpdated={0}
|
||||
setRefreshRulesData={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const exceptionsTab = wrapper.find('[data-test-subj="allRulesTableTab-exceptions"] button');
|
||||
exceptionsTab.simulate('click');
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.exists('[data-test-subj="allExceptionListsPanel"]')).toBeTruthy();
|
||||
expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeFalsy();
|
||||
expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,79 +4,16 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiLoadingContent,
|
||||
EuiSpacer,
|
||||
EuiTab,
|
||||
EuiTabs,
|
||||
EuiProgress,
|
||||
EuiOverlayMask,
|
||||
EuiConfirmModal,
|
||||
EuiWindowEvent,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
||||
import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import uuid from 'uuid';
|
||||
import { debounce } from 'lodash/fp';
|
||||
|
||||
import {
|
||||
useRules,
|
||||
useRulesStatuses,
|
||||
CreatePreBuiltRules,
|
||||
FilterOptions,
|
||||
Rule,
|
||||
PaginationOptions,
|
||||
exportRules,
|
||||
RulesSortingFields,
|
||||
} from '../../../../containers/detection_engine/rules';
|
||||
import { HeaderSection } from '../../../../../common/components/header_section';
|
||||
import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana';
|
||||
import { useStateToaster } from '../../../../../common/components/toasters';
|
||||
import { Loader } from '../../../../../common/components/loader';
|
||||
import { Panel } from '../../../../../common/components/panel';
|
||||
import { PrePackagedRulesPrompt } from '../../../../components/rules/pre_packaged_rules/load_empty_prompt';
|
||||
import { GenericDownloader } from '../../../../../common/components/generic_downloader';
|
||||
import { AllRulesTables, SortingType } from '../../../../components/rules/all_rules_tables';
|
||||
import { getPrePackagedRuleStatus } from '../helpers';
|
||||
import * as i18n from '../translations';
|
||||
import { EuiBasicTableOnChange } from '../types';
|
||||
import { getBatchItems } from './batch_actions';
|
||||
import { getColumns, getMonitoringColumns } from './columns';
|
||||
import { showRulesTable } from './helpers';
|
||||
import { allRulesReducer, State } from './reducer';
|
||||
import { RulesTableFilters } from './rules_table_filters/rules_table_filters';
|
||||
import { useMlCapabilities } from '../../../../../common/components/ml/hooks/use_ml_capabilities';
|
||||
import { hasMlAdminPermissions } from '../../../../../../common/machine_learning/has_ml_admin_permissions';
|
||||
import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_license';
|
||||
import { SecurityPageName } from '../../../../../app/types';
|
||||
import { useFormatUrl } from '../../../../../common/components/link_to';
|
||||
import { isBoolean } from '../../../../../common/utils/privileges';
|
||||
import { AllRulesUtilityBar } from './utility_bar';
|
||||
import { LastUpdatedAt } from '../../../../../common/components/last_updated';
|
||||
import { DEFAULT_RULES_TABLE_REFRESH_SETTING } from '../../../../../../common/constants';
|
||||
|
||||
const INITIAL_SORT_FIELD = 'enabled';
|
||||
const initialState: State = {
|
||||
exportRuleIds: [],
|
||||
filterOptions: {
|
||||
filter: '',
|
||||
sortField: INITIAL_SORT_FIELD,
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
loadingRuleIds: [],
|
||||
loadingRulesAction: null,
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
},
|
||||
rules: [],
|
||||
selectedRuleIds: [],
|
||||
lastUpdated: 0,
|
||||
showIdleModal: false,
|
||||
isRefreshOn: true,
|
||||
};
|
||||
import { CreatePreBuiltRules } from '../../../../containers/detection_engine/rules';
|
||||
import { RulesTables } from './rules_tables';
|
||||
import * as i18n from '../translations';
|
||||
import { ExceptionListsTable } from './exceptions/exceptions_table';
|
||||
|
||||
interface AllRulesProps {
|
||||
createPrePackagedRules: CreatePreBuiltRules | null;
|
||||
|
@ -94,6 +31,7 @@ interface AllRulesProps {
|
|||
export enum AllRulesTabs {
|
||||
rules = 'rules',
|
||||
monitoring = 'monitoring',
|
||||
exceptions = 'exceptions',
|
||||
}
|
||||
|
||||
const allRulesTabs = [
|
||||
|
@ -107,6 +45,11 @@ const allRulesTabs = [
|
|||
name: i18n.MONITORING_TAB,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: AllRulesTabs.exceptions,
|
||||
name: i18n.EXCEPTIONS_TAB,
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -130,312 +73,9 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
rulesNotUpdated,
|
||||
setRefreshRulesData,
|
||||
}) => {
|
||||
const [initLoading, setInitLoading] = useState(true);
|
||||
const tableRef = useRef<EuiBasicTable>();
|
||||
const {
|
||||
services: {
|
||||
application: {
|
||||
capabilities: { actions },
|
||||
},
|
||||
},
|
||||
} = useKibana();
|
||||
const [defaultAutoRefreshSetting] = useUiSetting$<{
|
||||
on: boolean;
|
||||
value: number;
|
||||
idleTimeout: number;
|
||||
}>(DEFAULT_RULES_TABLE_REFRESH_SETTING);
|
||||
const [
|
||||
{
|
||||
exportRuleIds,
|
||||
filterOptions,
|
||||
loadingRuleIds,
|
||||
loadingRulesAction,
|
||||
pagination,
|
||||
rules,
|
||||
selectedRuleIds,
|
||||
lastUpdated,
|
||||
showIdleModal,
|
||||
isRefreshOn,
|
||||
},
|
||||
dispatch,
|
||||
] = useReducer(allRulesReducer(tableRef), {
|
||||
...initialState,
|
||||
lastUpdated: Date.now(),
|
||||
isRefreshOn: defaultAutoRefreshSetting.on,
|
||||
});
|
||||
const { loading: isLoadingRulesStatuses, rulesStatuses } = useRulesStatuses(rules);
|
||||
const history = useHistory();
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
const mlCapabilities = useMlCapabilities();
|
||||
const [allRulesTab, setAllRulesTab] = useState(AllRulesTabs.rules);
|
||||
const { formatUrl } = useFormatUrl(SecurityPageName.detections);
|
||||
|
||||
// TODO: Refactor license check + hasMlAdminPermissions to common check
|
||||
const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities);
|
||||
|
||||
const setRules = useCallback((newRules: Rule[], newPagination: Partial<PaginationOptions>) => {
|
||||
dispatch({
|
||||
type: 'setRules',
|
||||
rules: newRules,
|
||||
pagination: newPagination,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setShowIdleModal = useCallback((show: boolean) => {
|
||||
dispatch({
|
||||
type: 'setShowIdleModal',
|
||||
show,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setLastRefreshDate = useCallback(() => {
|
||||
dispatch({
|
||||
type: 'setLastRefreshDate',
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setAutoRefreshOn = useCallback((on: boolean) => {
|
||||
dispatch({
|
||||
type: 'setAutoRefreshOn',
|
||||
on,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const [isLoadingRules, , reFetchRulesData] = useRules({
|
||||
pagination,
|
||||
filterOptions,
|
||||
refetchPrePackagedRulesStatus,
|
||||
dispatchRulesInReducer: setRules,
|
||||
});
|
||||
|
||||
const sorting = useMemo(
|
||||
(): SortingType => ({
|
||||
sort: {
|
||||
field: filterOptions.sortField,
|
||||
direction: filterOptions.sortOrder,
|
||||
},
|
||||
}),
|
||||
[filterOptions]
|
||||
);
|
||||
|
||||
const prePackagedRuleStatus = getPrePackagedRuleStatus(
|
||||
rulesInstalled,
|
||||
rulesNotInstalled,
|
||||
rulesNotUpdated
|
||||
);
|
||||
|
||||
const hasActionsPrivileges = useMemo(() => (isBoolean(actions.show) ? actions.show : true), [
|
||||
actions,
|
||||
]);
|
||||
|
||||
const getBatchItemsPopoverContent = useCallback(
|
||||
(closePopover: () => void): JSX.Element[] => {
|
||||
return getBatchItems({
|
||||
closePopover,
|
||||
dispatch,
|
||||
dispatchToaster,
|
||||
hasMlPermissions,
|
||||
hasActionsPrivileges,
|
||||
loadingRuleIds,
|
||||
selectedRuleIds,
|
||||
reFetchRules: reFetchRulesData,
|
||||
rules,
|
||||
});
|
||||
},
|
||||
[
|
||||
dispatch,
|
||||
dispatchToaster,
|
||||
hasMlPermissions,
|
||||
loadingRuleIds,
|
||||
reFetchRulesData,
|
||||
rules,
|
||||
selectedRuleIds,
|
||||
hasActionsPrivileges,
|
||||
]
|
||||
);
|
||||
|
||||
const paginationMemo = useMemo(
|
||||
() => ({
|
||||
pageIndex: pagination.page - 1,
|
||||
pageSize: pagination.perPage,
|
||||
totalItemCount: pagination.total,
|
||||
pageSizeOptions: [5, 10, 20, 50, 100, 200, 300],
|
||||
}),
|
||||
[pagination]
|
||||
);
|
||||
|
||||
const tableOnChangeCallback = useCallback(
|
||||
({ page, sort }: EuiBasicTableOnChange) => {
|
||||
dispatch({
|
||||
type: 'updateFilterOptions',
|
||||
filterOptions: {
|
||||
sortField: (sort?.field as RulesSortingFields) ?? INITIAL_SORT_FIELD, // Narrowing EuiBasicTable sorting types
|
||||
sortOrder: sort?.direction ?? 'desc',
|
||||
},
|
||||
pagination: { page: page.index + 1, perPage: page.size },
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const rulesColumns = useMemo(() => {
|
||||
return getColumns({
|
||||
dispatch,
|
||||
dispatchToaster,
|
||||
formatUrl,
|
||||
history,
|
||||
hasMlPermissions,
|
||||
hasNoPermissions,
|
||||
loadingRuleIds:
|
||||
loadingRulesAction != null &&
|
||||
(loadingRulesAction === 'enable' || loadingRulesAction === 'disable')
|
||||
? loadingRuleIds
|
||||
: [],
|
||||
reFetchRules: reFetchRulesData,
|
||||
hasReadActionsPrivileges: hasActionsPrivileges,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
dispatch,
|
||||
dispatchToaster,
|
||||
formatUrl,
|
||||
hasMlPermissions,
|
||||
history,
|
||||
loadingRuleIds,
|
||||
loadingRulesAction,
|
||||
reFetchRulesData,
|
||||
]);
|
||||
|
||||
const monitoringColumns = useMemo(() => getMonitoringColumns(history, formatUrl), [
|
||||
history,
|
||||
formatUrl,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (reFetchRulesData != null) {
|
||||
setRefreshRulesData(reFetchRulesData);
|
||||
}
|
||||
}, [reFetchRulesData, setRefreshRulesData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initLoading && !loading && !isLoadingRules && !isLoadingRulesStatuses) {
|
||||
setInitLoading(false);
|
||||
}
|
||||
}, [initLoading, loading, isLoadingRules, isLoadingRulesStatuses]);
|
||||
|
||||
const handleCreatePrePackagedRules = useCallback(async () => {
|
||||
if (createPrePackagedRules != null && reFetchRulesData != null) {
|
||||
await createPrePackagedRules();
|
||||
reFetchRulesData(true);
|
||||
}
|
||||
}, [createPrePackagedRules, reFetchRulesData]);
|
||||
|
||||
const euiBasicTableSelectionProps = useMemo(
|
||||
() => ({
|
||||
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>) => {
|
||||
dispatch({
|
||||
type: 'updateFilterOptions',
|
||||
filterOptions: {
|
||||
...newFilterOptions,
|
||||
},
|
||||
pagination: { page: 1 },
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isLoadingAnActionOnRule = useMemo(() => {
|
||||
if (
|
||||
loadingRuleIds.length > 0 &&
|
||||
(loadingRulesAction === 'disable' || loadingRulesAction === 'enable')
|
||||
) {
|
||||
return false;
|
||||
} else if (loadingRuleIds.length > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [loadingRuleIds, loadingRulesAction]);
|
||||
|
||||
const handleRefreshData = useCallback((): void => {
|
||||
if (reFetchRulesData != null && !isLoadingAnActionOnRule) {
|
||||
reFetchRulesData(true);
|
||||
setLastRefreshDate();
|
||||
}
|
||||
}, [reFetchRulesData, isLoadingAnActionOnRule, setLastRefreshDate]);
|
||||
|
||||
const handleResetIdleTimer = useCallback((): void => {
|
||||
if (isRefreshOn) {
|
||||
setShowIdleModal(true);
|
||||
setAutoRefreshOn(false);
|
||||
}
|
||||
}, [setShowIdleModal, setAutoRefreshOn, isRefreshOn]);
|
||||
|
||||
const debounceResetIdleTimer = useMemo(() => {
|
||||
return debounce(defaultAutoRefreshSetting.idleTimeout, handleResetIdleTimer);
|
||||
}, [handleResetIdleTimer, defaultAutoRefreshSetting.idleTimeout]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (isRefreshOn) {
|
||||
handleRefreshData();
|
||||
}
|
||||
}, defaultAutoRefreshSetting.value);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [isRefreshOn, handleRefreshData, defaultAutoRefreshSetting.value]);
|
||||
|
||||
const handleIdleModalContinue = useCallback((): void => {
|
||||
setShowIdleModal(false);
|
||||
handleRefreshData();
|
||||
setAutoRefreshOn(true);
|
||||
}, [setShowIdleModal, setAutoRefreshOn, handleRefreshData]);
|
||||
|
||||
const handleAutoRefreshSwitch = useCallback(
|
||||
(refreshOn: boolean) => {
|
||||
if (refreshOn) {
|
||||
handleRefreshData();
|
||||
}
|
||||
setAutoRefreshOn(refreshOn);
|
||||
},
|
||||
[setAutoRefreshOn, handleRefreshData]
|
||||
);
|
||||
|
||||
const shouldShowRulesTable = useMemo(
|
||||
(): boolean => showRulesTable({ rulesCustomInstalled, rulesInstalled }) && !initLoading,
|
||||
[initLoading, rulesCustomInstalled, rulesInstalled]
|
||||
);
|
||||
|
||||
const shouldShowPrepackagedRulesPrompt = useMemo(
|
||||
(): boolean =>
|
||||
rulesCustomInstalled != null &&
|
||||
rulesCustomInstalled === 0 &&
|
||||
prePackagedRuleStatus === 'ruleNotInstalled' &&
|
||||
!initLoading,
|
||||
[initLoading, prePackagedRuleStatus, rulesCustomInstalled]
|
||||
);
|
||||
|
||||
const handleGenericDownloaderSuccess = useCallback(
|
||||
(exportCount) => {
|
||||
dispatch({ type: 'loadingRuleIds', ids: [], actionType: null });
|
||||
dispatchToaster({
|
||||
type: 'addToaster',
|
||||
toast: {
|
||||
id: uuid.v4(),
|
||||
title: i18n.SUCCESSFULLY_EXPORTED_RULES(exportCount),
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
},
|
||||
});
|
||||
},
|
||||
[dispatchToaster]
|
||||
);
|
||||
const [allRulesTab, setAllRulesTab] = useState(AllRulesTabs.rules);
|
||||
|
||||
const tabs = useMemo(
|
||||
() => (
|
||||
|
@ -459,109 +99,35 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
|
||||
return (
|
||||
<>
|
||||
<EuiWindowEvent event="mousemove" handler={debounceResetIdleTimer} />
|
||||
<EuiWindowEvent event="mousedown" handler={debounceResetIdleTimer} />
|
||||
<EuiWindowEvent event="click" handler={debounceResetIdleTimer} />
|
||||
<EuiWindowEvent event="keydown" handler={debounceResetIdleTimer} />
|
||||
<EuiWindowEvent event="scroll" handler={debounceResetIdleTimer} />
|
||||
<EuiWindowEvent event="load" handler={debounceResetIdleTimer} />
|
||||
<GenericDownloader
|
||||
filename={`${i18n.EXPORT_FILENAME}.ndjson`}
|
||||
ids={exportRuleIds}
|
||||
onExportSuccess={handleGenericDownloaderSuccess}
|
||||
exportSelectedData={exportRules}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
{tabs}
|
||||
<EuiSpacer />
|
||||
|
||||
<Panel
|
||||
loading={loading || isLoadingRules || isLoadingRulesStatuses}
|
||||
data-test-subj="allRulesPanel"
|
||||
>
|
||||
<>
|
||||
{(isLoadingRules || isLoadingRulesStatuses) && (
|
||||
<EuiProgress
|
||||
data-test-subj="loadingRulesInfoProgress"
|
||||
size="xs"
|
||||
position="absolute"
|
||||
color="accent"
|
||||
/>
|
||||
)}
|
||||
<HeaderSection
|
||||
split
|
||||
growLeftSplit={false}
|
||||
title={i18n.ALL_RULES}
|
||||
subtitle={
|
||||
<LastUpdatedAt
|
||||
showUpdating={loading || isLoadingRules || isLoadingRulesStatuses}
|
||||
updatedAt={lastUpdated}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<RulesTableFilters
|
||||
onFilterChanged={onFilterChangedCallback}
|
||||
rulesCustomInstalled={rulesCustomInstalled}
|
||||
rulesInstalled={rulesInstalled}
|
||||
currentFilterTags={filterOptions.tags ?? []}
|
||||
/>
|
||||
</HeaderSection>
|
||||
|
||||
{isLoadingAnActionOnRule && !initLoading && (
|
||||
<Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" />
|
||||
)}
|
||||
{shouldShowPrepackagedRulesPrompt && (
|
||||
<PrePackagedRulesPrompt
|
||||
createPrePackagedRules={handleCreatePrePackagedRules}
|
||||
loading={loadingCreatePrePackagedRules}
|
||||
userHasNoPermissions={hasNoPermissions}
|
||||
/>
|
||||
)}
|
||||
{initLoading && (
|
||||
<EuiLoadingContent data-test-subj="initialLoadingPanelAllRulesTable" lines={10} />
|
||||
)}
|
||||
{showIdleModal && (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={i18n.REFRESH_PROMPT_TITLE}
|
||||
onCancel={handleIdleModalContinue}
|
||||
onConfirm={handleIdleModalContinue}
|
||||
confirmButtonText={i18n.REFRESH_PROMPT_CONFIRM}
|
||||
defaultFocusedButton="confirm"
|
||||
data-test-subj="allRulesIdleModal"
|
||||
>
|
||||
<p>{i18n.REFRESH_PROMPT_BODY}</p>
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
)}
|
||||
{shouldShowRulesTable && (
|
||||
<>
|
||||
<AllRulesUtilityBar
|
||||
userHasNoPermissions={hasNoPermissions}
|
||||
paginationTotal={pagination.total ?? 0}
|
||||
numberSelectedRules={selectedRuleIds.length}
|
||||
onGetBatchItemsPopoverContent={getBatchItemsPopoverContent}
|
||||
onRefresh={handleRefreshData}
|
||||
isAutoRefreshOn={isRefreshOn}
|
||||
onRefreshSwitch={handleAutoRefreshSwitch}
|
||||
/>
|
||||
<AllRulesTables
|
||||
selectedTab={allRulesTab}
|
||||
euiBasicTableSelectionProps={euiBasicTableSelectionProps}
|
||||
hasNoPermissions={hasNoPermissions}
|
||||
monitoringColumns={monitoringColumns}
|
||||
pagination={paginationMemo}
|
||||
rules={rules}
|
||||
rulesColumns={rulesColumns}
|
||||
rulesStatuses={rulesStatuses}
|
||||
sorting={sorting}
|
||||
tableOnChangeCallback={tableOnChangeCallback}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</Panel>
|
||||
{(allRulesTab === AllRulesTabs.rules || allRulesTab === AllRulesTabs.monitoring) && (
|
||||
<RulesTables
|
||||
history={history}
|
||||
formatUrl={formatUrl}
|
||||
selectedTab={allRulesTab}
|
||||
createPrePackagedRules={createPrePackagedRules}
|
||||
hasNoPermissions={hasNoPermissions}
|
||||
loading={loading}
|
||||
loadingCreatePrePackagedRules={loadingCreatePrePackagedRules}
|
||||
refetchPrePackagedRulesStatus={refetchPrePackagedRulesStatus}
|
||||
rulesCustomInstalled={rulesCustomInstalled}
|
||||
rulesInstalled={rulesInstalled}
|
||||
rulesNotInstalled={rulesNotInstalled}
|
||||
rulesNotUpdated={rulesNotUpdated}
|
||||
setRefreshRulesData={setRefreshRulesData}
|
||||
/>
|
||||
)}
|
||||
{allRulesTab === AllRulesTabs.exceptions && (
|
||||
<ExceptionListsTable
|
||||
formatUrl={formatUrl}
|
||||
history={history}
|
||||
hasNoPermissions={hasNoPermissions}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,530 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiLoadingContent,
|
||||
EuiProgress,
|
||||
EuiOverlayMask,
|
||||
EuiConfirmModal,
|
||||
EuiWindowEvent,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
||||
import uuid from 'uuid';
|
||||
import { debounce } from 'lodash/fp';
|
||||
import { History } from 'history';
|
||||
|
||||
import {
|
||||
useRules,
|
||||
useRulesStatuses,
|
||||
CreatePreBuiltRules,
|
||||
FilterOptions,
|
||||
Rule,
|
||||
PaginationOptions,
|
||||
exportRules,
|
||||
RulesSortingFields,
|
||||
} from '../../../../containers/detection_engine/rules';
|
||||
import { FormatUrl } from '../../../../../common/components/link_to';
|
||||
import { HeaderSection } from '../../../../../common/components/header_section';
|
||||
import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana';
|
||||
import { useStateToaster } from '../../../../../common/components/toasters';
|
||||
import { Loader } from '../../../../../common/components/loader';
|
||||
import { Panel } from '../../../../../common/components/panel';
|
||||
import { PrePackagedRulesPrompt } from '../../../../components/rules/pre_packaged_rules/load_empty_prompt';
|
||||
import { GenericDownloader } from '../../../../../common/components/generic_downloader';
|
||||
import { AllRulesTables, SortingType } from '../../../../components/rules/all_rules_tables';
|
||||
import { getPrePackagedRuleStatus } from '../helpers';
|
||||
import * as i18n from '../translations';
|
||||
import { EuiBasicTableOnChange } from '../types';
|
||||
import { getBatchItems } from './batch_actions';
|
||||
import { getColumns, getMonitoringColumns } from './columns';
|
||||
import { showRulesTable } from './helpers';
|
||||
import { allRulesReducer, State } from './reducer';
|
||||
import { RulesTableFilters } from './rules_table_filters/rules_table_filters';
|
||||
import { useMlCapabilities } from '../../../../../common/components/ml/hooks/use_ml_capabilities';
|
||||
import { hasMlAdminPermissions } from '../../../../../../common/machine_learning/has_ml_admin_permissions';
|
||||
import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_license';
|
||||
import { isBoolean } from '../../../../../common/utils/privileges';
|
||||
import { AllRulesUtilityBar } from './utility_bar';
|
||||
import { LastUpdatedAt } from '../../../../../common/components/last_updated';
|
||||
import { DEFAULT_RULES_TABLE_REFRESH_SETTING } from '../../../../../../common/constants';
|
||||
import { AllRulesTabs } from '.';
|
||||
|
||||
const INITIAL_SORT_FIELD = 'enabled';
|
||||
const initialState: State = {
|
||||
exportRuleIds: [],
|
||||
filterOptions: {
|
||||
filter: '',
|
||||
sortField: INITIAL_SORT_FIELD,
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
loadingRuleIds: [],
|
||||
loadingRulesAction: null,
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
},
|
||||
rules: [],
|
||||
selectedRuleIds: [],
|
||||
lastUpdated: 0,
|
||||
showIdleModal: false,
|
||||
isRefreshOn: true,
|
||||
};
|
||||
|
||||
interface RulesTableProps {
|
||||
history: History;
|
||||
formatUrl: FormatUrl;
|
||||
createPrePackagedRules: CreatePreBuiltRules | null;
|
||||
hasNoPermissions: boolean;
|
||||
loading: boolean;
|
||||
loadingCreatePrePackagedRules: boolean;
|
||||
refetchPrePackagedRulesStatus: () => void;
|
||||
rulesCustomInstalled: number | null;
|
||||
rulesInstalled: number | null;
|
||||
rulesNotInstalled: number | null;
|
||||
rulesNotUpdated: number | null;
|
||||
setRefreshRulesData: (refreshRule: (refreshPrePackagedRule?: boolean) => void) => void;
|
||||
selectedTab: AllRulesTabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Table Component for displaying all Rules for a given cluster. Provides the ability to filter
|
||||
* by name, sort by enabled, and perform the following actions:
|
||||
* * Enable/Disable
|
||||
* * Duplicate
|
||||
* * Delete
|
||||
* * Import/Export
|
||||
*/
|
||||
export const RulesTables = React.memo<RulesTableProps>(
|
||||
({
|
||||
history,
|
||||
formatUrl,
|
||||
createPrePackagedRules,
|
||||
hasNoPermissions,
|
||||
loading,
|
||||
loadingCreatePrePackagedRules,
|
||||
refetchPrePackagedRulesStatus,
|
||||
rulesCustomInstalled,
|
||||
rulesInstalled,
|
||||
rulesNotInstalled,
|
||||
rulesNotUpdated,
|
||||
setRefreshRulesData,
|
||||
selectedTab,
|
||||
}) => {
|
||||
const [initLoading, setInitLoading] = useState(true);
|
||||
const tableRef = useRef<EuiBasicTable>();
|
||||
const {
|
||||
services: {
|
||||
application: {
|
||||
capabilities: { actions },
|
||||
},
|
||||
},
|
||||
} = useKibana();
|
||||
const [defaultAutoRefreshSetting] = useUiSetting$<{
|
||||
on: boolean;
|
||||
value: number;
|
||||
idleTimeout: number;
|
||||
}>(DEFAULT_RULES_TABLE_REFRESH_SETTING);
|
||||
const [
|
||||
{
|
||||
exportRuleIds,
|
||||
filterOptions,
|
||||
loadingRuleIds,
|
||||
loadingRulesAction,
|
||||
pagination,
|
||||
rules,
|
||||
selectedRuleIds,
|
||||
lastUpdated,
|
||||
showIdleModal,
|
||||
isRefreshOn,
|
||||
},
|
||||
dispatch,
|
||||
] = useReducer(allRulesReducer(tableRef), {
|
||||
...initialState,
|
||||
lastUpdated: Date.now(),
|
||||
isRefreshOn: defaultAutoRefreshSetting.on,
|
||||
});
|
||||
const { loading: isLoadingRulesStatuses, rulesStatuses } = useRulesStatuses(rules);
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
const mlCapabilities = useMlCapabilities();
|
||||
|
||||
// TODO: Refactor license check + hasMlAdminPermissions to common check
|
||||
const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities);
|
||||
|
||||
const setRules = useCallback((newRules: Rule[], newPagination: Partial<PaginationOptions>) => {
|
||||
dispatch({
|
||||
type: 'setRules',
|
||||
rules: newRules,
|
||||
pagination: newPagination,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setShowIdleModal = useCallback((show: boolean) => {
|
||||
dispatch({
|
||||
type: 'setShowIdleModal',
|
||||
show,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setLastRefreshDate = useCallback(() => {
|
||||
dispatch({
|
||||
type: 'setLastRefreshDate',
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setAutoRefreshOn = useCallback((on: boolean) => {
|
||||
dispatch({
|
||||
type: 'setAutoRefreshOn',
|
||||
on,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const [isLoadingRules, , reFetchRulesData] = useRules({
|
||||
pagination,
|
||||
filterOptions,
|
||||
refetchPrePackagedRulesStatus,
|
||||
dispatchRulesInReducer: setRules,
|
||||
});
|
||||
|
||||
const sorting = useMemo(
|
||||
(): SortingType => ({
|
||||
sort: {
|
||||
field: filterOptions.sortField,
|
||||
direction: filterOptions.sortOrder,
|
||||
},
|
||||
}),
|
||||
[filterOptions]
|
||||
);
|
||||
|
||||
const prePackagedRuleStatus = getPrePackagedRuleStatus(
|
||||
rulesInstalled,
|
||||
rulesNotInstalled,
|
||||
rulesNotUpdated
|
||||
);
|
||||
|
||||
const hasActionsPrivileges = useMemo(() => (isBoolean(actions.show) ? actions.show : true), [
|
||||
actions,
|
||||
]);
|
||||
|
||||
const getBatchItemsPopoverContent = useCallback(
|
||||
(closePopover: () => void): JSX.Element[] => {
|
||||
return getBatchItems({
|
||||
closePopover,
|
||||
dispatch,
|
||||
dispatchToaster,
|
||||
hasMlPermissions,
|
||||
hasActionsPrivileges,
|
||||
loadingRuleIds,
|
||||
selectedRuleIds,
|
||||
reFetchRules: reFetchRulesData,
|
||||
rules,
|
||||
});
|
||||
},
|
||||
[
|
||||
dispatch,
|
||||
dispatchToaster,
|
||||
hasMlPermissions,
|
||||
loadingRuleIds,
|
||||
reFetchRulesData,
|
||||
rules,
|
||||
selectedRuleIds,
|
||||
hasActionsPrivileges,
|
||||
]
|
||||
);
|
||||
|
||||
const paginationMemo = useMemo(
|
||||
() => ({
|
||||
pageIndex: pagination.page - 1,
|
||||
pageSize: pagination.perPage,
|
||||
totalItemCount: pagination.total,
|
||||
pageSizeOptions: [5, 10, 20, 50, 100, 200, 300],
|
||||
}),
|
||||
[pagination]
|
||||
);
|
||||
|
||||
const tableOnChangeCallback = useCallback(
|
||||
({ page, sort }: EuiBasicTableOnChange) => {
|
||||
dispatch({
|
||||
type: 'updateFilterOptions',
|
||||
filterOptions: {
|
||||
sortField: (sort?.field as RulesSortingFields) ?? INITIAL_SORT_FIELD, // Narrowing EuiBasicTable sorting types
|
||||
sortOrder: sort?.direction ?? 'desc',
|
||||
},
|
||||
pagination: { page: page.index + 1, perPage: page.size },
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const rulesColumns = useMemo(() => {
|
||||
return getColumns({
|
||||
dispatch,
|
||||
dispatchToaster,
|
||||
formatUrl,
|
||||
history,
|
||||
hasMlPermissions,
|
||||
hasNoPermissions,
|
||||
loadingRuleIds:
|
||||
loadingRulesAction != null &&
|
||||
(loadingRulesAction === 'enable' || loadingRulesAction === 'disable')
|
||||
? loadingRuleIds
|
||||
: [],
|
||||
reFetchRules: reFetchRulesData,
|
||||
hasReadActionsPrivileges: hasActionsPrivileges,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
dispatch,
|
||||
dispatchToaster,
|
||||
formatUrl,
|
||||
hasMlPermissions,
|
||||
history,
|
||||
loadingRuleIds,
|
||||
loadingRulesAction,
|
||||
reFetchRulesData,
|
||||
]);
|
||||
|
||||
const monitoringColumns = useMemo(() => getMonitoringColumns(history, formatUrl), [
|
||||
history,
|
||||
formatUrl,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (reFetchRulesData != null) {
|
||||
setRefreshRulesData(reFetchRulesData);
|
||||
}
|
||||
}, [reFetchRulesData, setRefreshRulesData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initLoading && !loading && !isLoadingRules && !isLoadingRulesStatuses) {
|
||||
setInitLoading(false);
|
||||
}
|
||||
}, [initLoading, loading, isLoadingRules, isLoadingRulesStatuses]);
|
||||
|
||||
const handleCreatePrePackagedRules = useCallback(async () => {
|
||||
if (createPrePackagedRules != null && reFetchRulesData != null) {
|
||||
await createPrePackagedRules();
|
||||
reFetchRulesData(true);
|
||||
}
|
||||
}, [createPrePackagedRules, reFetchRulesData]);
|
||||
|
||||
const euiBasicTableSelectionProps = useMemo(
|
||||
() => ({
|
||||
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>) => {
|
||||
dispatch({
|
||||
type: 'updateFilterOptions',
|
||||
filterOptions: {
|
||||
...newFilterOptions,
|
||||
},
|
||||
pagination: { page: 1 },
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isLoadingAnActionOnRule = useMemo(() => {
|
||||
if (
|
||||
loadingRuleIds.length > 0 &&
|
||||
(loadingRulesAction === 'disable' || loadingRulesAction === 'enable')
|
||||
) {
|
||||
return false;
|
||||
} else if (loadingRuleIds.length > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [loadingRuleIds, loadingRulesAction]);
|
||||
|
||||
const handleRefreshData = useCallback((): void => {
|
||||
if (reFetchRulesData != null && !isLoadingAnActionOnRule) {
|
||||
reFetchRulesData(true);
|
||||
setLastRefreshDate();
|
||||
}
|
||||
}, [reFetchRulesData, isLoadingAnActionOnRule, setLastRefreshDate]);
|
||||
|
||||
const handleResetIdleTimer = useCallback((): void => {
|
||||
if (isRefreshOn) {
|
||||
setShowIdleModal(true);
|
||||
setAutoRefreshOn(false);
|
||||
}
|
||||
}, [setShowIdleModal, setAutoRefreshOn, isRefreshOn]);
|
||||
|
||||
const debounceResetIdleTimer = useMemo(() => {
|
||||
return debounce(defaultAutoRefreshSetting.idleTimeout, handleResetIdleTimer);
|
||||
}, [handleResetIdleTimer, defaultAutoRefreshSetting.idleTimeout]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (isRefreshOn) {
|
||||
handleRefreshData();
|
||||
}
|
||||
}, defaultAutoRefreshSetting.value);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [isRefreshOn, handleRefreshData, defaultAutoRefreshSetting.value]);
|
||||
|
||||
const handleIdleModalContinue = useCallback((): void => {
|
||||
setShowIdleModal(false);
|
||||
handleRefreshData();
|
||||
setAutoRefreshOn(true);
|
||||
}, [setShowIdleModal, setAutoRefreshOn, handleRefreshData]);
|
||||
|
||||
const handleAutoRefreshSwitch = useCallback(
|
||||
(refreshOn: boolean) => {
|
||||
if (refreshOn) {
|
||||
handleRefreshData();
|
||||
}
|
||||
setAutoRefreshOn(refreshOn);
|
||||
},
|
||||
[setAutoRefreshOn, handleRefreshData]
|
||||
);
|
||||
|
||||
const shouldShowRulesTable = useMemo(
|
||||
(): boolean => showRulesTable({ rulesCustomInstalled, rulesInstalled }) && !initLoading,
|
||||
[initLoading, rulesCustomInstalled, rulesInstalled]
|
||||
);
|
||||
|
||||
const shouldShowPrepackagedRulesPrompt = useMemo(
|
||||
(): boolean =>
|
||||
rulesCustomInstalled != null &&
|
||||
rulesCustomInstalled === 0 &&
|
||||
prePackagedRuleStatus === 'ruleNotInstalled' &&
|
||||
!initLoading,
|
||||
[initLoading, prePackagedRuleStatus, rulesCustomInstalled]
|
||||
);
|
||||
|
||||
const handleGenericDownloaderSuccess = useCallback(
|
||||
(exportCount) => {
|
||||
dispatch({ type: 'loadingRuleIds', ids: [], actionType: null });
|
||||
dispatchToaster({
|
||||
type: 'addToaster',
|
||||
toast: {
|
||||
id: uuid.v4(),
|
||||
title: i18n.SUCCESSFULLY_EXPORTED_RULES(exportCount),
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
},
|
||||
});
|
||||
},
|
||||
[dispatchToaster]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiWindowEvent event="mousemove" handler={debounceResetIdleTimer} />
|
||||
<EuiWindowEvent event="mousedown" handler={debounceResetIdleTimer} />
|
||||
<EuiWindowEvent event="click" handler={debounceResetIdleTimer} />
|
||||
<EuiWindowEvent event="keydown" handler={debounceResetIdleTimer} />
|
||||
<EuiWindowEvent event="scroll" handler={debounceResetIdleTimer} />
|
||||
<EuiWindowEvent event="load" handler={debounceResetIdleTimer} />
|
||||
<GenericDownloader
|
||||
filename={`${i18n.EXPORT_FILENAME}.ndjson`}
|
||||
ids={exportRuleIds}
|
||||
onExportSuccess={handleGenericDownloaderSuccess}
|
||||
exportSelectedData={exportRules}
|
||||
/>
|
||||
|
||||
<Panel
|
||||
loading={loading || isLoadingRules || isLoadingRulesStatuses}
|
||||
data-test-subj="allRulesPanel"
|
||||
>
|
||||
<>
|
||||
{(isLoadingRules || isLoadingRulesStatuses) && (
|
||||
<EuiProgress
|
||||
data-test-subj="loadingRulesInfoProgress"
|
||||
size="xs"
|
||||
position="absolute"
|
||||
color="accent"
|
||||
/>
|
||||
)}
|
||||
<HeaderSection
|
||||
split
|
||||
growLeftSplit={false}
|
||||
title={i18n.ALL_RULES}
|
||||
subtitle={
|
||||
<LastUpdatedAt
|
||||
showUpdating={loading || isLoadingRules || isLoadingRulesStatuses}
|
||||
updatedAt={lastUpdated}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<RulesTableFilters
|
||||
onFilterChanged={onFilterChangedCallback}
|
||||
rulesCustomInstalled={rulesCustomInstalled}
|
||||
rulesInstalled={rulesInstalled}
|
||||
currentFilterTags={filterOptions.tags ?? []}
|
||||
/>
|
||||
</HeaderSection>
|
||||
|
||||
{isLoadingAnActionOnRule && !initLoading && (
|
||||
<Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" />
|
||||
)}
|
||||
{shouldShowPrepackagedRulesPrompt && (
|
||||
<PrePackagedRulesPrompt
|
||||
createPrePackagedRules={handleCreatePrePackagedRules}
|
||||
loading={loadingCreatePrePackagedRules}
|
||||
userHasNoPermissions={hasNoPermissions}
|
||||
/>
|
||||
)}
|
||||
{initLoading && (
|
||||
<EuiLoadingContent data-test-subj="initialLoadingPanelAllRulesTable" lines={10} />
|
||||
)}
|
||||
{showIdleModal && (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={i18n.REFRESH_PROMPT_TITLE}
|
||||
onCancel={handleIdleModalContinue}
|
||||
onConfirm={handleIdleModalContinue}
|
||||
confirmButtonText={i18n.REFRESH_PROMPT_CONFIRM}
|
||||
defaultFocusedButton="confirm"
|
||||
data-test-subj="allRulesIdleModal"
|
||||
>
|
||||
<p>{i18n.REFRESH_PROMPT_BODY}</p>
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
)}
|
||||
{shouldShowRulesTable && (
|
||||
<>
|
||||
<AllRulesUtilityBar
|
||||
userHasNoPermissions={hasNoPermissions}
|
||||
paginationTotal={pagination.total ?? 0}
|
||||
numberSelectedItems={selectedRuleIds.length}
|
||||
onGetBatchItemsPopoverContent={getBatchItemsPopoverContent}
|
||||
onRefresh={handleRefreshData}
|
||||
isAutoRefreshOn={isRefreshOn}
|
||||
onRefreshSwitch={handleAutoRefreshSwitch}
|
||||
showBulkActions
|
||||
/>
|
||||
<AllRulesTables
|
||||
selectedTab={selectedTab}
|
||||
euiBasicTableSelectionProps={euiBasicTableSelectionProps}
|
||||
hasNoPermissions={hasNoPermissions}
|
||||
monitoringColumns={monitoringColumns}
|
||||
pagination={paginationMemo}
|
||||
rules={rules}
|
||||
rulesColumns={rulesColumns}
|
||||
rulesStatuses={rulesStatuses}
|
||||
sorting={sorting}
|
||||
tableOnChangeCallback={tableOnChangeCallback}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</Panel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
RulesTables.displayName = 'RulesTables';
|
|
@ -22,10 +22,11 @@ describe('AllRules', () => {
|
|||
userHasNoPermissions={false}
|
||||
onRefresh={jest.fn()}
|
||||
paginationTotal={4}
|
||||
numberSelectedRules={1}
|
||||
numberSelectedItems={1}
|
||||
onGetBatchItemsPopoverContent={jest.fn()}
|
||||
isAutoRefreshOn={true}
|
||||
onRefreshSwitch={jest.fn()}
|
||||
showBulkActions
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
@ -36,6 +37,29 @@ describe('AllRules', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('does not render total selected and bulk actions when "showBulkActions" is false', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={theme}>
|
||||
<AllRulesUtilityBar
|
||||
userHasNoPermissions={false}
|
||||
onRefresh={jest.fn()}
|
||||
paginationTotal={4}
|
||||
numberSelectedItems={1}
|
||||
onGetBatchItemsPopoverContent={jest.fn()}
|
||||
isAutoRefreshOn={true}
|
||||
onRefreshSwitch={jest.fn()}
|
||||
showBulkActions={false}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="showingRules"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="tableBulkActions"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="showingExceptionLists"]').at(0).text()).toEqual(
|
||||
'Showing 4 lists'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders utility actions if user has permissions', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={theme}>
|
||||
|
@ -43,10 +67,11 @@ describe('AllRules', () => {
|
|||
userHasNoPermissions={false}
|
||||
onRefresh={jest.fn()}
|
||||
paginationTotal={4}
|
||||
numberSelectedRules={1}
|
||||
numberSelectedItems={1}
|
||||
onGetBatchItemsPopoverContent={jest.fn()}
|
||||
isAutoRefreshOn={true}
|
||||
onRefreshSwitch={jest.fn()}
|
||||
showBulkActions
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
@ -61,10 +86,11 @@ describe('AllRules', () => {
|
|||
userHasNoPermissions
|
||||
onRefresh={jest.fn()}
|
||||
paginationTotal={4}
|
||||
numberSelectedRules={1}
|
||||
numberSelectedItems={1}
|
||||
onGetBatchItemsPopoverContent={jest.fn()}
|
||||
isAutoRefreshOn={true}
|
||||
onRefreshSwitch={jest.fn()}
|
||||
showBulkActions
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
@ -80,10 +106,11 @@ describe('AllRules', () => {
|
|||
userHasNoPermissions={false}
|
||||
onRefresh={mockRefresh}
|
||||
paginationTotal={4}
|
||||
numberSelectedRules={1}
|
||||
numberSelectedItems={1}
|
||||
onGetBatchItemsPopoverContent={jest.fn()}
|
||||
isAutoRefreshOn={true}
|
||||
onRefreshSwitch={jest.fn()}
|
||||
showBulkActions
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
@ -101,10 +128,11 @@ describe('AllRules', () => {
|
|||
userHasNoPermissions={false}
|
||||
onRefresh={jest.fn()}
|
||||
paginationTotal={4}
|
||||
numberSelectedRules={1}
|
||||
numberSelectedItems={1}
|
||||
onGetBatchItemsPopoverContent={jest.fn()}
|
||||
isAutoRefreshOn={true}
|
||||
onRefreshSwitch={mockSwitch}
|
||||
showBulkActions
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
|
|
@ -18,12 +18,13 @@ import * as i18n from '../translations';
|
|||
|
||||
interface AllRulesUtilityBarProps {
|
||||
userHasNoPermissions: boolean;
|
||||
numberSelectedRules: number;
|
||||
numberSelectedItems: number;
|
||||
paginationTotal: number;
|
||||
isAutoRefreshOn: boolean;
|
||||
onRefresh: (refreshRule: boolean) => void;
|
||||
onGetBatchItemsPopoverContent: (closePopover: () => void) => JSX.Element[];
|
||||
onRefreshSwitch: (checked: boolean) => void;
|
||||
isAutoRefreshOn?: boolean;
|
||||
showBulkActions: boolean;
|
||||
onRefresh?: (refreshRule: boolean) => void;
|
||||
onGetBatchItemsPopoverContent?: (closePopover: () => void) => JSX.Element[];
|
||||
onRefreshSwitch?: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
export const AllRulesUtilityBar = React.memo<AllRulesUtilityBarProps>(
|
||||
|
@ -31,22 +32,29 @@ export const AllRulesUtilityBar = React.memo<AllRulesUtilityBarProps>(
|
|||
userHasNoPermissions,
|
||||
onRefresh,
|
||||
paginationTotal,
|
||||
numberSelectedRules,
|
||||
numberSelectedItems,
|
||||
onGetBatchItemsPopoverContent,
|
||||
isAutoRefreshOn,
|
||||
showBulkActions = true,
|
||||
onRefreshSwitch,
|
||||
}) => {
|
||||
const handleGetBatchItemsPopoverContent = useCallback(
|
||||
(closePopover: () => void) => (
|
||||
<EuiContextMenuPanel items={onGetBatchItemsPopoverContent(closePopover)} />
|
||||
),
|
||||
(closePopover: () => void): JSX.Element | null => {
|
||||
if (onGetBatchItemsPopoverContent != null) {
|
||||
return <EuiContextMenuPanel items={onGetBatchItemsPopoverContent(closePopover)} />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[onGetBatchItemsPopoverContent]
|
||||
);
|
||||
|
||||
const handleAutoRefreshSwitch = useCallback(
|
||||
(closePopover: () => void) => (e: EuiSwitchEvent) => {
|
||||
onRefreshSwitch(e.target.checked);
|
||||
closePopover();
|
||||
if (onRefreshSwitch != null) {
|
||||
onRefreshSwitch(e.target.checked);
|
||||
closePopover();
|
||||
}
|
||||
},
|
||||
[onRefreshSwitch]
|
||||
);
|
||||
|
@ -58,7 +66,7 @@ export const AllRulesUtilityBar = React.memo<AllRulesUtilityBarProps>(
|
|||
<EuiSwitch
|
||||
key="allRulesAutoRefreshSwitch"
|
||||
label={i18n.REFRESH_RULE_POPOVER_DESCRIPTION}
|
||||
checked={isAutoRefreshOn}
|
||||
checked={isAutoRefreshOn ?? false}
|
||||
onChange={handleAutoRefreshSwitch(closePopover)}
|
||||
compressed
|
||||
data-test-subj="refreshSettingsSwitch"
|
||||
|
@ -73,42 +81,64 @@ export const AllRulesUtilityBar = React.memo<AllRulesUtilityBarProps>(
|
|||
<UtilityBar>
|
||||
<UtilityBarSection>
|
||||
<UtilityBarGroup>
|
||||
<UtilityBarText dataTestSubj="showingRules">
|
||||
{i18n.SHOWING_RULES(paginationTotal)}
|
||||
</UtilityBarText>
|
||||
{showBulkActions ? (
|
||||
<UtilityBarText dataTestSubj="showingRules">
|
||||
{i18n.SHOWING_RULES(paginationTotal)}
|
||||
</UtilityBarText>
|
||||
) : (
|
||||
<UtilityBarText dataTestSubj="showingExceptionLists">
|
||||
{i18n.SHOWING_EXCEPTION_LISTS(paginationTotal)}
|
||||
</UtilityBarText>
|
||||
)}
|
||||
</UtilityBarGroup>
|
||||
|
||||
<UtilityBarGroup>
|
||||
<UtilityBarText dataTestSubj="selectedRules">
|
||||
{i18n.SELECTED_RULES(numberSelectedRules)}
|
||||
</UtilityBarText>
|
||||
{!userHasNoPermissions && (
|
||||
{showBulkActions ? (
|
||||
<>
|
||||
<UtilityBarGroup data-test-subj="tableBulkActions">
|
||||
<UtilityBarText dataTestSubj="selectedRules">
|
||||
{i18n.SELECTED_RULES(numberSelectedItems)}
|
||||
</UtilityBarText>
|
||||
{!userHasNoPermissions && (
|
||||
<UtilityBarAction
|
||||
dataTestSubj="bulkActions"
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
popoverContent={handleGetBatchItemsPopoverContent}
|
||||
>
|
||||
{i18n.BATCH_ACTIONS}
|
||||
</UtilityBarAction>
|
||||
)}
|
||||
|
||||
<UtilityBarAction
|
||||
dataTestSubj="refreshRulesAction"
|
||||
iconSide="left"
|
||||
iconType="refresh"
|
||||
onClick={onRefresh}
|
||||
>
|
||||
{i18n.REFRESH}
|
||||
</UtilityBarAction>
|
||||
<UtilityBarAction
|
||||
dataTestSubj="refreshSettings"
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
popoverContent={handleGetRefreshSettingsPopoverContent}
|
||||
>
|
||||
{i18n.REFRESH_RULE_POPOVER_LABEL}
|
||||
</UtilityBarAction>
|
||||
</UtilityBarGroup>
|
||||
</>
|
||||
) : (
|
||||
<UtilityBarGroup>
|
||||
<UtilityBarAction
|
||||
dataTestSubj="bulkActions"
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
popoverContent={handleGetBatchItemsPopoverContent}
|
||||
dataTestSubj="refreshRulesAction"
|
||||
iconSide="left"
|
||||
iconType="refresh"
|
||||
onClick={onRefresh}
|
||||
>
|
||||
{i18n.BATCH_ACTIONS}
|
||||
{i18n.REFRESH}
|
||||
</UtilityBarAction>
|
||||
)}
|
||||
<UtilityBarAction
|
||||
dataTestSubj="refreshRulesAction"
|
||||
iconSide="left"
|
||||
iconType="refresh"
|
||||
onClick={onRefresh}
|
||||
>
|
||||
{i18n.REFRESH}
|
||||
</UtilityBarAction>
|
||||
<UtilityBarAction
|
||||
dataTestSubj="refreshSettings"
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
popoverContent={handleGetRefreshSettingsPopoverContent}
|
||||
>
|
||||
{i18n.REFRESH_RULE_POPOVER_LABEL}
|
||||
</UtilityBarAction>
|
||||
</UtilityBarGroup>
|
||||
</UtilityBarGroup>
|
||||
)}
|
||||
</UtilityBarSection>
|
||||
</UtilityBar>
|
||||
);
|
||||
|
|
|
@ -374,7 +374,14 @@ export const RULES_TAB = i18n.translate(
|
|||
export const MONITORING_TAB = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allRules.tabs.monitoring',
|
||||
{
|
||||
defaultMessage: 'Monitoring',
|
||||
defaultMessage: 'Rule Monitoring',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTIONS_TAB = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allRules.tabs.exceptions',
|
||||
{
|
||||
defaultMessage: 'Exception Lists',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -589,3 +596,9 @@ export const REFRESH_RULE_POPOVER_LABEL = i18n.translate(
|
|||
defaultMessage: 'Refresh settings',
|
||||
}
|
||||
);
|
||||
|
||||
export const SHOWING_EXCEPTION_LISTS = (totalLists: number) =>
|
||||
i18n.translate('xpack.securitySolution.detectionEngine.rules.allRules.showingExceptionLists', {
|
||||
values: { totalLists },
|
||||
defaultMessage: 'Showing {totalLists} {totalLists, plural, =1 {list} other {lists}}',
|
||||
});
|
||||
|
|
|
@ -36,7 +36,8 @@ export {
|
|||
useCursor,
|
||||
useApi,
|
||||
useAsync,
|
||||
useExceptionList,
|
||||
useExceptionListItems,
|
||||
useExceptionLists,
|
||||
usePersistExceptionItem,
|
||||
usePersistExceptionList,
|
||||
useFindLists,
|
||||
|
@ -52,7 +53,7 @@ export {
|
|||
ExceptionListIdentifiers,
|
||||
ExceptionList,
|
||||
Pagination,
|
||||
UseExceptionListSuccess,
|
||||
UseExceptionListItemsSuccess,
|
||||
addEndpointExceptionList,
|
||||
withOptionalSignal,
|
||||
} from '../../lists/public';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue