[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
|
@ -48,7 +48,7 @@ pageLoadAssetSize:
|
||||||
lensOss: 19341
|
lensOss: 19341
|
||||||
licenseManagement: 41817
|
licenseManagement: 41817
|
||||||
licensing: 39008
|
licensing: 39008
|
||||||
lists: 183665
|
lists: 202261
|
||||||
logstash: 53548
|
logstash: 53548
|
||||||
management: 46112
|
management: 46112
|
||||||
maps: 183610
|
maps: 183610
|
||||||
|
|
|
@ -22,7 +22,7 @@ export const getFindExceptionListSchemaMock = (): FindExceptionListSchema => ({
|
||||||
|
|
||||||
export const getFindExceptionListSchemaDecodedMock = (): FindExceptionListSchemaDecoded => ({
|
export const getFindExceptionListSchemaDecodedMock = (): FindExceptionListSchemaDecoded => ({
|
||||||
filter: FILTER,
|
filter: FILTER,
|
||||||
namespace_type: NAMESPACE_TYPE,
|
namespace_type: [NAMESPACE_TYPE],
|
||||||
page: 1,
|
page: 1,
|
||||||
per_page: 25,
|
per_page: 25,
|
||||||
sort_field: undefined,
|
sort_field: undefined,
|
||||||
|
|
|
@ -37,7 +37,7 @@ describe('find_exception_list_schema', () => {
|
||||||
expect(getPaths(left(message.errors))).toEqual([]);
|
expect(getPaths(left(message.errors))).toEqual([]);
|
||||||
const expected: FindExceptionListSchemaDecoded = {
|
const expected: FindExceptionListSchemaDecoded = {
|
||||||
filter: undefined,
|
filter: undefined,
|
||||||
namespace_type: 'single',
|
namespace_type: ['single'],
|
||||||
page: undefined,
|
page: undefined,
|
||||||
per_page: undefined,
|
per_page: undefined,
|
||||||
sort_field: undefined,
|
sort_field: undefined,
|
||||||
|
|
|
@ -6,15 +6,15 @@
|
||||||
|
|
||||||
import * as t from 'io-ts';
|
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 { RequiredKeepUndefined } from '../../types';
|
||||||
import { StringToPositiveNumber } from '../types/string_to_positive_number';
|
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(
|
export const findExceptionListSchema = t.exact(
|
||||||
t.partial({
|
t.partial({
|
||||||
filter, // defaults to undefined if not set during decode
|
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
|
page: StringToPositiveNumber, // defaults to undefined if not set during decode
|
||||||
per_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
|
sort_field, // defaults to undefined if not set during decode
|
||||||
|
@ -29,5 +29,5 @@ export type FindExceptionListSchemaDecoded = Omit<
|
||||||
RequiredKeepUndefined<t.TypeOf<typeof findExceptionListSchema>>,
|
RequiredKeepUndefined<t.TypeOf<typeof findExceptionListSchema>>,
|
||||||
'namespace_type'
|
'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.
|
* 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
|
* This makes any optional property the same as Required<T> would but also has the
|
||||||
* added benefit of keeping your undefined.
|
* added benefit of keeping your undefined.
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
ExceptionListItemSchema,
|
ExceptionListItemSchema,
|
||||||
ExceptionListSchema,
|
ExceptionListSchema,
|
||||||
} from '../../common/schemas';
|
} from '../../common/schemas';
|
||||||
|
import { getFoundExceptionListSchemaMock } from '../../common/schemas/response/found_exception_list_schema.mock';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addEndpointExceptionList,
|
addEndpointExceptionList,
|
||||||
|
@ -26,11 +27,12 @@ import {
|
||||||
deleteExceptionListItemById,
|
deleteExceptionListItemById,
|
||||||
fetchExceptionListById,
|
fetchExceptionListById,
|
||||||
fetchExceptionListItemById,
|
fetchExceptionListItemById,
|
||||||
|
fetchExceptionLists,
|
||||||
fetchExceptionListsItemsByListIds,
|
fetchExceptionListsItemsByListIds,
|
||||||
updateExceptionList,
|
updateExceptionList,
|
||||||
updateExceptionListItem,
|
updateExceptionListItem,
|
||||||
} from './api';
|
} from './api';
|
||||||
import { ApiCallByIdProps, ApiCallByListIdProps } from './types';
|
import { ApiCallByIdProps, ApiCallByListIdProps, ApiCallFetchExceptionListsProps } from './types';
|
||||||
|
|
||||||
const abortCtrl = new AbortController();
|
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', () => {
|
describe('#fetchExceptionListById', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
httpMock.fetch.mockResolvedValue(getExceptionListSchemaMock());
|
httpMock.fetch.mockResolvedValue(getExceptionListSchemaMock());
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
ExceptionListItemSchema,
|
ExceptionListItemSchema,
|
||||||
ExceptionListSchema,
|
ExceptionListSchema,
|
||||||
FoundExceptionListItemSchema,
|
FoundExceptionListItemSchema,
|
||||||
|
FoundExceptionListSchema,
|
||||||
createEndpointListSchema,
|
createEndpointListSchema,
|
||||||
createExceptionListItemSchema,
|
createExceptionListItemSchema,
|
||||||
createExceptionListSchema,
|
createExceptionListSchema,
|
||||||
|
@ -23,7 +24,9 @@ import {
|
||||||
exceptionListItemSchema,
|
exceptionListItemSchema,
|
||||||
exceptionListSchema,
|
exceptionListSchema,
|
||||||
findExceptionListItemSchema,
|
findExceptionListItemSchema,
|
||||||
|
findExceptionListSchema,
|
||||||
foundExceptionListItemSchema,
|
foundExceptionListItemSchema,
|
||||||
|
foundExceptionListSchema,
|
||||||
readExceptionListItemSchema,
|
readExceptionListItemSchema,
|
||||||
readExceptionListSchema,
|
readExceptionListSchema,
|
||||||
updateExceptionListItemSchema,
|
updateExceptionListItemSchema,
|
||||||
|
@ -37,6 +40,7 @@ import {
|
||||||
AddExceptionListProps,
|
AddExceptionListProps,
|
||||||
ApiCallByIdProps,
|
ApiCallByIdProps,
|
||||||
ApiCallByListIdProps,
|
ApiCallByListIdProps,
|
||||||
|
ApiCallFetchExceptionListsProps,
|
||||||
UpdateExceptionListItemProps,
|
UpdateExceptionListItemProps,
|
||||||
UpdateExceptionListProps,
|
UpdateExceptionListProps,
|
||||||
} from './types';
|
} 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
|
* 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 * as api from '../api';
|
||||||
import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock';
|
import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock';
|
||||||
import { ExceptionListItemSchema } from '../../../common/schemas';
|
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;
|
const mockKibanaHttpService = coreMock.createStart().http;
|
||||||
|
|
||||||
describe('useExceptionList', () => {
|
describe('useExceptionListItems', () => {
|
||||||
const onErrorMock = jest.fn();
|
const onErrorMock = jest.fn();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -36,7 +36,7 @@ describe('useExceptionList', () => {
|
||||||
UseExceptionListProps,
|
UseExceptionListProps,
|
||||||
ReturnExceptionListAndItems
|
ReturnExceptionListAndItems
|
||||||
>(() =>
|
>(() =>
|
||||||
useExceptionList({
|
useExceptionListItems({
|
||||||
filterOptions: [],
|
filterOptions: [],
|
||||||
http: mockKibanaHttpService,
|
http: mockKibanaHttpService,
|
||||||
lists: [
|
lists: [
|
||||||
|
@ -75,7 +75,7 @@ describe('useExceptionList', () => {
|
||||||
UseExceptionListProps,
|
UseExceptionListProps,
|
||||||
ReturnExceptionListAndItems
|
ReturnExceptionListAndItems
|
||||||
>(() =>
|
>(() =>
|
||||||
useExceptionList({
|
useExceptionListItems({
|
||||||
filterOptions: [],
|
filterOptions: [],
|
||||||
http: mockKibanaHttpService,
|
http: mockKibanaHttpService,
|
||||||
lists: [
|
lists: [
|
||||||
|
@ -100,7 +100,7 @@ describe('useExceptionList', () => {
|
||||||
|
|
||||||
const expectedListItemsResult: ExceptionListItemSchema[] = getFoundExceptionListItemSchemaMock()
|
const expectedListItemsResult: ExceptionListItemSchema[] = getFoundExceptionListItemSchemaMock()
|
||||||
.data;
|
.data;
|
||||||
const expectedResult: UseExceptionListSuccess = {
|
const expectedResult: UseExceptionListItemsSuccess = {
|
||||||
exceptions: expectedListItemsResult,
|
exceptions: expectedListItemsResult,
|
||||||
pagination: { page: 1, perPage: 1, total: 1 },
|
pagination: { page: 1, perPage: 1, total: 1 },
|
||||||
};
|
};
|
||||||
|
@ -129,7 +129,7 @@ describe('useExceptionList', () => {
|
||||||
const onSuccessMock = jest.fn();
|
const onSuccessMock = jest.fn();
|
||||||
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
|
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
|
||||||
() =>
|
() =>
|
||||||
useExceptionList({
|
useExceptionListItems({
|
||||||
filterOptions: [],
|
filterOptions: [],
|
||||||
http: mockKibanaHttpService,
|
http: mockKibanaHttpService,
|
||||||
lists: [
|
lists: [
|
||||||
|
@ -179,7 +179,7 @@ describe('useExceptionList', () => {
|
||||||
const onSuccessMock = jest.fn();
|
const onSuccessMock = jest.fn();
|
||||||
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
|
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
|
||||||
() =>
|
() =>
|
||||||
useExceptionList({
|
useExceptionListItems({
|
||||||
filterOptions: [],
|
filterOptions: [],
|
||||||
http: mockKibanaHttpService,
|
http: mockKibanaHttpService,
|
||||||
lists: [
|
lists: [
|
||||||
|
@ -231,7 +231,7 @@ describe('useExceptionList', () => {
|
||||||
UseExceptionListProps,
|
UseExceptionListProps,
|
||||||
ReturnExceptionListAndItems
|
ReturnExceptionListAndItems
|
||||||
>(() =>
|
>(() =>
|
||||||
useExceptionList({
|
useExceptionListItems({
|
||||||
filterOptions: [],
|
filterOptions: [],
|
||||||
http: mockKibanaHttpService,
|
http: mockKibanaHttpService,
|
||||||
lists: [
|
lists: [
|
||||||
|
@ -278,7 +278,7 @@ describe('useExceptionList', () => {
|
||||||
const onSuccessMock = jest.fn();
|
const onSuccessMock = jest.fn();
|
||||||
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
|
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
|
||||||
() =>
|
() =>
|
||||||
useExceptionList({
|
useExceptionListItems({
|
||||||
filterOptions: [{ filter: 'host.name', tags: [] }],
|
filterOptions: [{ filter: 'host.name', tags: [] }],
|
||||||
http: mockKibanaHttpService,
|
http: mockKibanaHttpService,
|
||||||
lists: [
|
lists: [
|
||||||
|
@ -343,7 +343,7 @@ describe('useExceptionList', () => {
|
||||||
showDetectionsListsOnly,
|
showDetectionsListsOnly,
|
||||||
showEndpointListsOnly,
|
showEndpointListsOnly,
|
||||||
}) =>
|
}) =>
|
||||||
useExceptionList({
|
useExceptionListItems({
|
||||||
filterOptions,
|
filterOptions,
|
||||||
http,
|
http,
|
||||||
lists,
|
lists,
|
||||||
|
@ -413,7 +413,7 @@ describe('useExceptionList', () => {
|
||||||
UseExceptionListProps,
|
UseExceptionListProps,
|
||||||
ReturnExceptionListAndItems
|
ReturnExceptionListAndItems
|
||||||
>(() =>
|
>(() =>
|
||||||
useExceptionList({
|
useExceptionListItems({
|
||||||
filterOptions: [],
|
filterOptions: [],
|
||||||
http: mockKibanaHttpService,
|
http: mockKibanaHttpService,
|
||||||
lists: [
|
lists: [
|
||||||
|
@ -455,7 +455,7 @@ describe('useExceptionList', () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
|
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
|
||||||
() =>
|
() =>
|
||||||
useExceptionList({
|
useExceptionListItems({
|
||||||
filterOptions: [],
|
filterOptions: [],
|
||||||
http: mockKibanaHttpService,
|
http: mockKibanaHttpService,
|
||||||
lists: [
|
lists: [
|
|
@ -34,7 +34,7 @@ export type ReturnExceptionListAndItems = [
|
||||||
* @param pagination optional
|
* @param pagination optional
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const useExceptionList = ({
|
export const useExceptionListItems = ({
|
||||||
http,
|
http,
|
||||||
lists,
|
lists,
|
||||||
pagination = {
|
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,
|
UpdateExceptionListItemSchema,
|
||||||
UpdateExceptionListSchema,
|
UpdateExceptionListSchema,
|
||||||
} from '../../common/schemas';
|
} from '../../common/schemas';
|
||||||
import { HttpStart } from '../../../../../src/core/public';
|
import { HttpStart, NotificationsStart } from '../../../../../src/core/public';
|
||||||
|
|
||||||
export interface FilterExceptionsOptions {
|
export interface FilterExceptionsOptions {
|
||||||
filter: string;
|
filter: string;
|
||||||
|
@ -43,7 +43,7 @@ export interface ExceptionList extends ExceptionListSchema {
|
||||||
totalItems: number;
|
totalItems: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UseExceptionListSuccess {
|
export interface UseExceptionListItemsSuccess {
|
||||||
exceptions: ExceptionListItemSchema[];
|
exceptions: ExceptionListItemSchema[];
|
||||||
pagination: Pagination;
|
pagination: Pagination;
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ export interface UseExceptionListProps {
|
||||||
showDetectionsListsOnly: boolean;
|
showDetectionsListsOnly: boolean;
|
||||||
showEndpointListsOnly: boolean;
|
showEndpointListsOnly: boolean;
|
||||||
matchFilters: boolean;
|
matchFilters: boolean;
|
||||||
onSuccess?: (arg: UseExceptionListSuccess) => void;
|
onSuccess?: (arg: UseExceptionListItemsSuccess) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExceptionListIdentifiers {
|
export interface ExceptionListIdentifiers {
|
||||||
|
@ -97,7 +97,35 @@ export interface ApiCallFindListsItemsMemoProps {
|
||||||
showDetectionsListsOnly: boolean;
|
showDetectionsListsOnly: boolean;
|
||||||
showEndpointListsOnly: boolean;
|
showEndpointListsOnly: boolean;
|
||||||
onError: (arg: string[]) => void;
|
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 {
|
export interface AddExceptionListProps {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* 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('Exceptions utils', () => {
|
||||||
describe('#getIdsAndNamespaces', () => {
|
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.
|
* 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 = ({
|
export const getIdsAndNamespaces = ({
|
||||||
lists,
|
lists,
|
||||||
|
@ -34,3 +65,51 @@ export const getIdsAndNamespaces = ({
|
||||||
}),
|
}),
|
||||||
{ ids: [], namespaces: [] }
|
{ 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 { useApi } from './exceptions/hooks/use_api';
|
||||||
export { usePersistExceptionItem } from './exceptions/hooks/persist_exception_item';
|
export { usePersistExceptionItem } from './exceptions/hooks/persist_exception_item';
|
||||||
export { usePersistExceptionList } from './exceptions/hooks/persist_exception_list';
|
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 { useFindLists } from './lists/hooks/use_find_lists';
|
||||||
export { useImportList } from './lists/hooks/use_import_list';
|
export { useImportList } from './lists/hooks/use_import_list';
|
||||||
export { useDeleteList } from './lists/hooks/use_delete_list';
|
export { useDeleteList } from './lists/hooks/use_delete_list';
|
||||||
|
@ -32,5 +33,6 @@ export {
|
||||||
ExceptionList,
|
ExceptionList,
|
||||||
ExceptionListIdentifiers,
|
ExceptionListIdentifiers,
|
||||||
Pagination,
|
Pagination,
|
||||||
UseExceptionListSuccess,
|
UseExceptionListItemsSuccess,
|
||||||
|
UseExceptionListsSuccess,
|
||||||
} from './exceptions/types';
|
} from './exceptions/types';
|
||||||
|
|
|
@ -147,6 +147,7 @@ const getReferencedExceptionLists = async (
|
||||||
.join(' OR ');
|
.join(' OR ');
|
||||||
return exceptionLists.findExceptionList({
|
return exceptionLists.findExceptionList({
|
||||||
filter: `(${filter})`,
|
filter: `(${filter})`,
|
||||||
|
namespaceType: ['agnostic', 'single'],
|
||||||
page: 1,
|
page: 1,
|
||||||
perPage: 10000,
|
perPage: 10000,
|
||||||
sortField: undefined,
|
sortField: undefined,
|
||||||
|
|
|
@ -6,11 +6,12 @@
|
||||||
|
|
||||||
import { SavedObjectsType } from 'kibana/server';
|
import { SavedObjectsType } from 'kibana/server';
|
||||||
|
|
||||||
import { migrations } from './migrations';
|
import {
|
||||||
|
exceptionListAgnosticSavedObjectType,
|
||||||
|
exceptionListSavedObjectType,
|
||||||
|
} from '../../common/types';
|
||||||
|
|
||||||
export const exceptionListSavedObjectType = 'exception-list';
|
import { migrations } from './migrations';
|
||||||
export const exceptionListAgnosticSavedObjectType = 'exception-list-agnostic';
|
|
||||||
export type SavedObjectType = 'exception-list' | 'exception-list-agnostic';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a super set of exception list and exception list items. The switch
|
* This is a super set of exception list and exception list items. The switch
|
||||||
|
|
|
@ -205,7 +205,7 @@ export interface FindValueListExceptionListsItems {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FindExceptionListOptions {
|
export interface FindExceptionListOptions {
|
||||||
namespaceType?: NamespaceType;
|
namespaceType: NamespaceTypeArray;
|
||||||
filter: FilterOrUndefined;
|
filter: FilterOrUndefined;
|
||||||
perPage: PerPageOrUndefined;
|
perPage: PerPageOrUndefined;
|
||||||
page: PageOrUndefined;
|
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 { SavedObjectsClientContract } from 'kibana/server';
|
||||||
|
|
||||||
|
import { NamespaceTypeArray } from '../../../common/schemas/types/default_namespace_array';
|
||||||
|
import { SavedObjectType } from '../../../common/types';
|
||||||
import {
|
import {
|
||||||
ExceptionListSoSchema,
|
ExceptionListSoSchema,
|
||||||
FilterOrUndefined,
|
FilterOrUndefined,
|
||||||
FoundExceptionListSchema,
|
FoundExceptionListSchema,
|
||||||
NamespaceType,
|
|
||||||
PageOrUndefined,
|
PageOrUndefined,
|
||||||
PerPageOrUndefined,
|
PerPageOrUndefined,
|
||||||
SortFieldOrUndefined,
|
SortFieldOrUndefined,
|
||||||
SortOrderOrUndefined,
|
SortOrderOrUndefined,
|
||||||
} from '../../../common/schemas';
|
} from '../../../common/schemas';
|
||||||
import {
|
|
||||||
SavedObjectType,
|
|
||||||
exceptionListAgnosticSavedObjectType,
|
|
||||||
exceptionListSavedObjectType,
|
|
||||||
} from '../../saved_objects';
|
|
||||||
|
|
||||||
import { getSavedObjectType, transformSavedObjectsToFoundExceptionList } from './utils';
|
import { getSavedObjectTypes, transformSavedObjectsToFoundExceptionList } from './utils';
|
||||||
|
|
||||||
interface FindExceptionListOptions {
|
interface FindExceptionListOptions {
|
||||||
namespaceType?: NamespaceType;
|
namespaceType: NamespaceTypeArray;
|
||||||
savedObjectsClient: SavedObjectsClientContract;
|
savedObjectsClient: SavedObjectsClientContract;
|
||||||
filter: FilterOrUndefined;
|
filter: FilterOrUndefined;
|
||||||
perPage: PerPageOrUndefined;
|
perPage: PerPageOrUndefined;
|
||||||
|
@ -43,37 +39,31 @@ export const findExceptionList = async ({
|
||||||
sortField,
|
sortField,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
}: FindExceptionListOptions): Promise<FoundExceptionListSchema> => {
|
}: FindExceptionListOptions): Promise<FoundExceptionListSchema> => {
|
||||||
const savedObjectType: SavedObjectType[] = namespaceType
|
const savedObjectTypes = getSavedObjectTypes({ namespaceType });
|
||||||
? [getSavedObjectType({ namespaceType })]
|
|
||||||
: [exceptionListSavedObjectType, exceptionListAgnosticSavedObjectType];
|
|
||||||
const savedObjectsFindResponse = await savedObjectsClient.find<ExceptionListSoSchema>({
|
const savedObjectsFindResponse = await savedObjectsClient.find<ExceptionListSoSchema>({
|
||||||
filter: getExceptionListFilter({ filter, savedObjectType }),
|
filter: getExceptionListFilter({ filter, savedObjectTypes }),
|
||||||
page,
|
page,
|
||||||
perPage,
|
perPage,
|
||||||
sortField,
|
sortField,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
type: savedObjectType,
|
type: savedObjectTypes,
|
||||||
});
|
});
|
||||||
|
|
||||||
return transformSavedObjectsToFoundExceptionList({ savedObjectsFindResponse });
|
return transformSavedObjectsToFoundExceptionList({ savedObjectsFindResponse });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getExceptionListFilter = ({
|
export const getExceptionListFilter = ({
|
||||||
filter,
|
filter,
|
||||||
savedObjectType,
|
savedObjectTypes,
|
||||||
}: {
|
}: {
|
||||||
filter: FilterOrUndefined;
|
filter: FilterOrUndefined;
|
||||||
savedObjectType: SavedObjectType[];
|
savedObjectTypes: SavedObjectType[];
|
||||||
}): string => {
|
}): string => {
|
||||||
const savedObjectTypeFilter = `(${savedObjectType
|
const listTypesFilter = savedObjectTypes
|
||||||
.map((sot) => `${sot}.attributes.list_type: list`)
|
.map((type) => `${type}.attributes.list_type: list`)
|
||||||
.join(' OR ')})`;
|
.join(' OR ');
|
||||||
if (filter == null) {
|
|
||||||
return savedObjectTypeFilter;
|
if (filter != null) {
|
||||||
} else {
|
return `(${listTypesFilter}) AND ${filter}`;
|
||||||
if (Array.isArray(savedObjectType)) {
|
} else return `(${listTypesFilter})`;
|
||||||
return `${savedObjectTypeFilter} AND ${filter}`;
|
|
||||||
} else {
|
|
||||||
return `${savedObjectType}.attributes.list_type: list AND ${filter}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,11 @@
|
||||||
*/
|
*/
|
||||||
import { SavedObjectsClientContract } from 'kibana/server';
|
import { SavedObjectsClientContract } from 'kibana/server';
|
||||||
|
|
||||||
|
import {
|
||||||
|
SavedObjectType,
|
||||||
|
exceptionListAgnosticSavedObjectType,
|
||||||
|
exceptionListSavedObjectType,
|
||||||
|
} from '../../../common/types';
|
||||||
import { EmptyStringArrayDecoded } from '../../../common/schemas/types/empty_string_array';
|
import { EmptyStringArrayDecoded } from '../../../common/schemas/types/empty_string_array';
|
||||||
import { NamespaceTypeArray } from '../../../common/schemas/types/default_namespace_array';
|
import { NamespaceTypeArray } from '../../../common/schemas/types/default_namespace_array';
|
||||||
import { NonEmptyStringArrayDecoded } from '../../../common/schemas/types/non_empty_string_array';
|
import { NonEmptyStringArrayDecoded } from '../../../common/schemas/types/non_empty_string_array';
|
||||||
|
@ -17,11 +22,6 @@ import {
|
||||||
SortFieldOrUndefined,
|
SortFieldOrUndefined,
|
||||||
SortOrderOrUndefined,
|
SortOrderOrUndefined,
|
||||||
} from '../../../common/schemas';
|
} from '../../../common/schemas';
|
||||||
import {
|
|
||||||
SavedObjectType,
|
|
||||||
exceptionListAgnosticSavedObjectType,
|
|
||||||
exceptionListSavedObjectType,
|
|
||||||
} from '../../saved_objects';
|
|
||||||
|
|
||||||
import { getSavedObjectTypes, transformSavedObjectsToFoundExceptionListItem } from './utils';
|
import { getSavedObjectTypes, transformSavedObjectsToFoundExceptionListItem } from './utils';
|
||||||
import { getExceptionList } from './get_exception_list';
|
import { getExceptionList } from './get_exception_list';
|
||||||
|
|
|
@ -6,6 +6,11 @@
|
||||||
import uuid from 'uuid';
|
import uuid from 'uuid';
|
||||||
import { SavedObject, SavedObjectsFindResponse, SavedObjectsUpdateResponse } from 'kibana/server';
|
import { SavedObject, SavedObjectsFindResponse, SavedObjectsUpdateResponse } from 'kibana/server';
|
||||||
|
|
||||||
|
import {
|
||||||
|
SavedObjectType,
|
||||||
|
exceptionListAgnosticSavedObjectType,
|
||||||
|
exceptionListSavedObjectType,
|
||||||
|
} from '../../../common/types';
|
||||||
import { NamespaceTypeArray } from '../../../common/schemas/types/default_namespace_array';
|
import { NamespaceTypeArray } from '../../../common/schemas/types/default_namespace_array';
|
||||||
import {
|
import {
|
||||||
CommentsArray,
|
CommentsArray,
|
||||||
|
@ -21,11 +26,6 @@ import {
|
||||||
exceptionListItemType,
|
exceptionListItemType,
|
||||||
exceptionListType,
|
exceptionListType,
|
||||||
} from '../../../common/schemas';
|
} from '../../../common/schemas';
|
||||||
import {
|
|
||||||
SavedObjectType,
|
|
||||||
exceptionListAgnosticSavedObjectType,
|
|
||||||
exceptionListSavedObjectType,
|
|
||||||
} from '../../saved_objects';
|
|
||||||
|
|
||||||
export const getSavedObjectType = ({
|
export const getSavedObjectType = ({
|
||||||
namespaceType,
|
namespaceType,
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { ExceptionsViewer } from './';
|
||||||
import { useKibana } from '../../../../common/lib/kibana';
|
import { useKibana } from '../../../../common/lib/kibana';
|
||||||
import {
|
import {
|
||||||
ExceptionListTypeEnum,
|
ExceptionListTypeEnum,
|
||||||
useExceptionList,
|
useExceptionListItems,
|
||||||
useApi,
|
useApi,
|
||||||
} from '../../../../../public/lists_plugin_deps';
|
} from '../../../../../public/lists_plugin_deps';
|
||||||
import { getExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_schema.mock';
|
import { getExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_schema.mock';
|
||||||
|
@ -40,7 +40,7 @@ describe('ExceptionsViewer', () => {
|
||||||
getExceptionListsItems: jest.fn().mockResolvedValue(getFoundExceptionListItemSchemaMock()),
|
getExceptionListsItems: jest.fn().mockResolvedValue(getFoundExceptionListItemSchemaMock()),
|
||||||
});
|
});
|
||||||
|
|
||||||
(useExceptionList as jest.Mock).mockReturnValue([
|
(useExceptionListItems as jest.Mock).mockReturnValue([
|
||||||
false,
|
false,
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
|
@ -54,7 +54,7 @@ describe('ExceptionsViewer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it renders loader if "loadingList" is true', () => {
|
it('it renders loader if "loadingList" is true', () => {
|
||||||
(useExceptionList as jest.Mock).mockReturnValue([
|
(useExceptionListItems as jest.Mock).mockReturnValue([
|
||||||
true,
|
true,
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
|
@ -106,7 +106,7 @@ describe('ExceptionsViewer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it renders empty prompt if no exception items exist', () => {
|
it('it renders empty prompt if no exception items exist', () => {
|
||||||
(useExceptionList as jest.Mock).mockReturnValue([
|
(useExceptionListItems as jest.Mock).mockReturnValue([
|
||||||
false,
|
false,
|
||||||
[getExceptionListSchemaMock()],
|
[getExceptionListSchemaMock()],
|
||||||
[],
|
[],
|
||||||
|
|
|
@ -17,11 +17,11 @@ import { ExceptionsViewerHeader } from './exceptions_viewer_header';
|
||||||
import { ExceptionListItemIdentifiers, Filter } from '../types';
|
import { ExceptionListItemIdentifiers, Filter } from '../types';
|
||||||
import { allExceptionItemsReducer, State, ViewerModalName } from './reducer';
|
import { allExceptionItemsReducer, State, ViewerModalName } from './reducer';
|
||||||
import {
|
import {
|
||||||
useExceptionList,
|
useExceptionListItems,
|
||||||
ExceptionListIdentifiers,
|
ExceptionListIdentifiers,
|
||||||
ExceptionListTypeEnum,
|
ExceptionListTypeEnum,
|
||||||
ExceptionListItemSchema,
|
ExceptionListItemSchema,
|
||||||
UseExceptionListSuccess,
|
UseExceptionListItemsSuccess,
|
||||||
useApi,
|
useApi,
|
||||||
} from '../../../../../public/lists_plugin_deps';
|
} from '../../../../../public/lists_plugin_deps';
|
||||||
import { ExceptionsViewerPagination } from './exceptions_pagination';
|
import { ExceptionsViewerPagination } from './exceptions_pagination';
|
||||||
|
@ -105,7 +105,10 @@ const ExceptionsViewerComponent = ({
|
||||||
const { deleteExceptionItem, getExceptionListsItems } = useApi(services.http);
|
const { deleteExceptionItem, getExceptionListsItems } = useApi(services.http);
|
||||||
|
|
||||||
const setExceptions = useCallback(
|
const setExceptions = useCallback(
|
||||||
({ exceptions: newExceptions, pagination: newPagination }: UseExceptionListSuccess): void => {
|
({
|
||||||
|
exceptions: newExceptions,
|
||||||
|
pagination: newPagination,
|
||||||
|
}: UseExceptionListItemsSuccess): void => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'setExceptions',
|
type: 'setExceptions',
|
||||||
lists: exceptionListsMeta,
|
lists: exceptionListsMeta,
|
||||||
|
@ -115,7 +118,7 @@ const ExceptionsViewerComponent = ({
|
||||||
},
|
},
|
||||||
[dispatch, exceptionListsMeta]
|
[dispatch, exceptionListsMeta]
|
||||||
);
|
);
|
||||||
const [loadingList, , , fetchListItems] = useExceptionList({
|
const [loadingList, , , fetchListItems] = useExceptionListItems({
|
||||||
http: services.http,
|
http: services.http,
|
||||||
lists: exceptionListsMeta,
|
lists: exceptionListsMeta,
|
||||||
filterOptions:
|
filterOptions:
|
||||||
|
|
|
@ -30,7 +30,7 @@ interface FormatUrlOptions {
|
||||||
skipSearch: boolean;
|
skipSearch: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type FormatUrl = (path: string, options?: Partial<FormatUrlOptions>) => string;
|
export type FormatUrl = (path: string, options?: Partial<FormatUrlOptions>) => string;
|
||||||
|
|
||||||
export const useFormatUrl = (page: SecurityPageName) => {
|
export const useFormatUrl = (page: SecurityPageName) => {
|
||||||
const { getUrlForApp } = useKibana().services.application;
|
const { getUrlForApp } = useKibana().services.application;
|
||||||
|
|
|
@ -108,14 +108,16 @@ export const fetchRules = async ({
|
||||||
},
|
},
|
||||||
signal,
|
signal,
|
||||||
}: FetchRulesProps): Promise<FetchRulesResponse> => {
|
}: 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 = [
|
const filtersWithoutTags = [
|
||||||
...(filterOptions.filter.length ? [`alert.attributes.name: ${filterOptions.filter}`] : []),
|
...(filterOptions.filter.length ? [`alert.attributes.name: ${filterOptions.filter}`] : []),
|
||||||
...(filterOptions.showCustomRules
|
...showCustomRuleFilter,
|
||||||
? [`alert.attributes.tags: "__internal_immutable:false"`]
|
...showElasticRuleFilter,
|
||||||
: []),
|
|
||||||
...(filterOptions.showElasticRules
|
|
||||||
? [`alert.attributes.tags: "__internal_immutable:true"`]
|
|
||||||
: []),
|
|
||||||
].join(' AND ');
|
].join(' AND ');
|
||||||
|
|
||||||
const tags = [
|
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 () => {
|
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 () => {
|
it('renders all rules tab by default', async () => {
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<TestProviders>
|
<TestProviders>
|
||||||
|
@ -283,4 +283,33 @@ describe('AllRules', () => {
|
||||||
expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeFalsy();
|
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.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
|
||||||
EuiBasicTable,
|
import React, { useMemo, useState } from 'react';
|
||||||
EuiLoadingContent,
|
|
||||||
EuiSpacer,
|
|
||||||
EuiTab,
|
|
||||||
EuiTabs,
|
|
||||||
EuiProgress,
|
|
||||||
EuiOverlayMask,
|
|
||||||
EuiConfirmModal,
|
|
||||||
EuiWindowEvent,
|
|
||||||
} from '@elastic/eui';
|
|
||||||
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
|
||||||
import { useHistory } from 'react-router-dom';
|
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 { SecurityPageName } from '../../../../../app/types';
|
||||||
import { useFormatUrl } from '../../../../../common/components/link_to';
|
import { useFormatUrl } from '../../../../../common/components/link_to';
|
||||||
import { isBoolean } from '../../../../../common/utils/privileges';
|
import { CreatePreBuiltRules } from '../../../../containers/detection_engine/rules';
|
||||||
import { AllRulesUtilityBar } from './utility_bar';
|
import { RulesTables } from './rules_tables';
|
||||||
import { LastUpdatedAt } from '../../../../../common/components/last_updated';
|
import * as i18n from '../translations';
|
||||||
import { DEFAULT_RULES_TABLE_REFRESH_SETTING } from '../../../../../../common/constants';
|
import { ExceptionListsTable } from './exceptions/exceptions_table';
|
||||||
|
|
||||||
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 AllRulesProps {
|
interface AllRulesProps {
|
||||||
createPrePackagedRules: CreatePreBuiltRules | null;
|
createPrePackagedRules: CreatePreBuiltRules | null;
|
||||||
|
@ -94,6 +31,7 @@ interface AllRulesProps {
|
||||||
export enum AllRulesTabs {
|
export enum AllRulesTabs {
|
||||||
rules = 'rules',
|
rules = 'rules',
|
||||||
monitoring = 'monitoring',
|
monitoring = 'monitoring',
|
||||||
|
exceptions = 'exceptions',
|
||||||
}
|
}
|
||||||
|
|
||||||
const allRulesTabs = [
|
const allRulesTabs = [
|
||||||
|
@ -107,6 +45,11 @@ const allRulesTabs = [
|
||||||
name: i18n.MONITORING_TAB,
|
name: i18n.MONITORING_TAB,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: AllRulesTabs.exceptions,
|
||||||
|
name: i18n.EXCEPTIONS_TAB,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -130,312 +73,9 @@ export const AllRules = React.memo<AllRulesProps>(
|
||||||
rulesNotUpdated,
|
rulesNotUpdated,
|
||||||
setRefreshRulesData,
|
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 history = useHistory();
|
||||||
const [, dispatchToaster] = useStateToaster();
|
|
||||||
const mlCapabilities = useMlCapabilities();
|
|
||||||
const [allRulesTab, setAllRulesTab] = useState(AllRulesTabs.rules);
|
|
||||||
const { formatUrl } = useFormatUrl(SecurityPageName.detections);
|
const { formatUrl } = useFormatUrl(SecurityPageName.detections);
|
||||||
|
const [allRulesTab, setAllRulesTab] = useState(AllRulesTabs.rules);
|
||||||
// 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 tabs = useMemo(
|
const tabs = useMemo(
|
||||||
() => (
|
() => (
|
||||||
|
@ -459,110 +99,36 @@ export const AllRules = React.memo<AllRulesProps>(
|
||||||
|
|
||||||
return (
|
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 />
|
<EuiSpacer />
|
||||||
{tabs}
|
{tabs}
|
||||||
<EuiSpacer />
|
<EuiSpacer />
|
||||||
|
|
||||||
<Panel
|
{(allRulesTab === AllRulesTabs.rules || allRulesTab === AllRulesTabs.monitoring) && (
|
||||||
loading={loading || isLoadingRules || isLoadingRulesStatuses}
|
<RulesTables
|
||||||
data-test-subj="allRulesPanel"
|
history={history}
|
||||||
>
|
formatUrl={formatUrl}
|
||||||
<>
|
selectedTab={allRulesTab}
|
||||||
{(isLoadingRules || isLoadingRulesStatuses) && (
|
createPrePackagedRules={createPrePackagedRules}
|
||||||
<EuiProgress
|
hasNoPermissions={hasNoPermissions}
|
||||||
data-test-subj="loadingRulesInfoProgress"
|
loading={loading}
|
||||||
size="xs"
|
loadingCreatePrePackagedRules={loadingCreatePrePackagedRules}
|
||||||
position="absolute"
|
refetchPrePackagedRulesStatus={refetchPrePackagedRulesStatus}
|
||||||
color="accent"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<HeaderSection
|
|
||||||
split
|
|
||||||
growLeftSplit={false}
|
|
||||||
title={i18n.ALL_RULES}
|
|
||||||
subtitle={
|
|
||||||
<LastUpdatedAt
|
|
||||||
showUpdating={loading || isLoadingRules || isLoadingRulesStatuses}
|
|
||||||
updatedAt={lastUpdated}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<RulesTableFilters
|
|
||||||
onFilterChanged={onFilterChangedCallback}
|
|
||||||
rulesCustomInstalled={rulesCustomInstalled}
|
rulesCustomInstalled={rulesCustomInstalled}
|
||||||
rulesInstalled={rulesInstalled}
|
rulesInstalled={rulesInstalled}
|
||||||
currentFilterTags={filterOptions.tags ?? []}
|
rulesNotInstalled={rulesNotInstalled}
|
||||||
/>
|
rulesNotUpdated={rulesNotUpdated}
|
||||||
</HeaderSection>
|
setRefreshRulesData={setRefreshRulesData}
|
||||||
|
|
||||||
{isLoadingAnActionOnRule && !initLoading && (
|
|
||||||
<Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" />
|
|
||||||
)}
|
|
||||||
{shouldShowPrepackagedRulesPrompt && (
|
|
||||||
<PrePackagedRulesPrompt
|
|
||||||
createPrePackagedRules={handleCreatePrePackagedRules}
|
|
||||||
loading={loadingCreatePrePackagedRules}
|
|
||||||
userHasNoPermissions={hasNoPermissions}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{initLoading && (
|
{allRulesTab === AllRulesTabs.exceptions && (
|
||||||
<EuiLoadingContent data-test-subj="initialLoadingPanelAllRulesTable" lines={10} />
|
<ExceptionListsTable
|
||||||
)}
|
formatUrl={formatUrl}
|
||||||
{showIdleModal && (
|
history={history}
|
||||||
<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}
|
hasNoPermissions={hasNoPermissions}
|
||||||
monitoringColumns={monitoringColumns}
|
loading={loading}
|
||||||
pagination={paginationMemo}
|
|
||||||
rules={rules}
|
|
||||||
rulesColumns={rulesColumns}
|
|
||||||
rulesStatuses={rulesStatuses}
|
|
||||||
sorting={sorting}
|
|
||||||
tableOnChangeCallback={tableOnChangeCallback}
|
|
||||||
tableRef={tableRef}
|
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
</Panel>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}
|
userHasNoPermissions={false}
|
||||||
onRefresh={jest.fn()}
|
onRefresh={jest.fn()}
|
||||||
paginationTotal={4}
|
paginationTotal={4}
|
||||||
numberSelectedRules={1}
|
numberSelectedItems={1}
|
||||||
onGetBatchItemsPopoverContent={jest.fn()}
|
onGetBatchItemsPopoverContent={jest.fn()}
|
||||||
isAutoRefreshOn={true}
|
isAutoRefreshOn={true}
|
||||||
onRefreshSwitch={jest.fn()}
|
onRefreshSwitch={jest.fn()}
|
||||||
|
showBulkActions
|
||||||
/>
|
/>
|
||||||
</ThemeProvider>
|
</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', () => {
|
it('renders utility actions if user has permissions', () => {
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
|
@ -43,10 +67,11 @@ describe('AllRules', () => {
|
||||||
userHasNoPermissions={false}
|
userHasNoPermissions={false}
|
||||||
onRefresh={jest.fn()}
|
onRefresh={jest.fn()}
|
||||||
paginationTotal={4}
|
paginationTotal={4}
|
||||||
numberSelectedRules={1}
|
numberSelectedItems={1}
|
||||||
onGetBatchItemsPopoverContent={jest.fn()}
|
onGetBatchItemsPopoverContent={jest.fn()}
|
||||||
isAutoRefreshOn={true}
|
isAutoRefreshOn={true}
|
||||||
onRefreshSwitch={jest.fn()}
|
onRefreshSwitch={jest.fn()}
|
||||||
|
showBulkActions
|
||||||
/>
|
/>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
@ -61,10 +86,11 @@ describe('AllRules', () => {
|
||||||
userHasNoPermissions
|
userHasNoPermissions
|
||||||
onRefresh={jest.fn()}
|
onRefresh={jest.fn()}
|
||||||
paginationTotal={4}
|
paginationTotal={4}
|
||||||
numberSelectedRules={1}
|
numberSelectedItems={1}
|
||||||
onGetBatchItemsPopoverContent={jest.fn()}
|
onGetBatchItemsPopoverContent={jest.fn()}
|
||||||
isAutoRefreshOn={true}
|
isAutoRefreshOn={true}
|
||||||
onRefreshSwitch={jest.fn()}
|
onRefreshSwitch={jest.fn()}
|
||||||
|
showBulkActions
|
||||||
/>
|
/>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
@ -80,10 +106,11 @@ describe('AllRules', () => {
|
||||||
userHasNoPermissions={false}
|
userHasNoPermissions={false}
|
||||||
onRefresh={mockRefresh}
|
onRefresh={mockRefresh}
|
||||||
paginationTotal={4}
|
paginationTotal={4}
|
||||||
numberSelectedRules={1}
|
numberSelectedItems={1}
|
||||||
onGetBatchItemsPopoverContent={jest.fn()}
|
onGetBatchItemsPopoverContent={jest.fn()}
|
||||||
isAutoRefreshOn={true}
|
isAutoRefreshOn={true}
|
||||||
onRefreshSwitch={jest.fn()}
|
onRefreshSwitch={jest.fn()}
|
||||||
|
showBulkActions
|
||||||
/>
|
/>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
@ -101,10 +128,11 @@ describe('AllRules', () => {
|
||||||
userHasNoPermissions={false}
|
userHasNoPermissions={false}
|
||||||
onRefresh={jest.fn()}
|
onRefresh={jest.fn()}
|
||||||
paginationTotal={4}
|
paginationTotal={4}
|
||||||
numberSelectedRules={1}
|
numberSelectedItems={1}
|
||||||
onGetBatchItemsPopoverContent={jest.fn()}
|
onGetBatchItemsPopoverContent={jest.fn()}
|
||||||
isAutoRefreshOn={true}
|
isAutoRefreshOn={true}
|
||||||
onRefreshSwitch={mockSwitch}
|
onRefreshSwitch={mockSwitch}
|
||||||
|
showBulkActions
|
||||||
/>
|
/>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,12 +18,13 @@ import * as i18n from '../translations';
|
||||||
|
|
||||||
interface AllRulesUtilityBarProps {
|
interface AllRulesUtilityBarProps {
|
||||||
userHasNoPermissions: boolean;
|
userHasNoPermissions: boolean;
|
||||||
numberSelectedRules: number;
|
numberSelectedItems: number;
|
||||||
paginationTotal: number;
|
paginationTotal: number;
|
||||||
isAutoRefreshOn: boolean;
|
isAutoRefreshOn?: boolean;
|
||||||
onRefresh: (refreshRule: boolean) => void;
|
showBulkActions: boolean;
|
||||||
onGetBatchItemsPopoverContent: (closePopover: () => void) => JSX.Element[];
|
onRefresh?: (refreshRule: boolean) => void;
|
||||||
onRefreshSwitch: (checked: boolean) => void;
|
onGetBatchItemsPopoverContent?: (closePopover: () => void) => JSX.Element[];
|
||||||
|
onRefreshSwitch?: (checked: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AllRulesUtilityBar = React.memo<AllRulesUtilityBarProps>(
|
export const AllRulesUtilityBar = React.memo<AllRulesUtilityBarProps>(
|
||||||
|
@ -31,22 +32,29 @@ export const AllRulesUtilityBar = React.memo<AllRulesUtilityBarProps>(
|
||||||
userHasNoPermissions,
|
userHasNoPermissions,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
paginationTotal,
|
paginationTotal,
|
||||||
numberSelectedRules,
|
numberSelectedItems,
|
||||||
onGetBatchItemsPopoverContent,
|
onGetBatchItemsPopoverContent,
|
||||||
isAutoRefreshOn,
|
isAutoRefreshOn,
|
||||||
|
showBulkActions = true,
|
||||||
onRefreshSwitch,
|
onRefreshSwitch,
|
||||||
}) => {
|
}) => {
|
||||||
const handleGetBatchItemsPopoverContent = useCallback(
|
const handleGetBatchItemsPopoverContent = useCallback(
|
||||||
(closePopover: () => void) => (
|
(closePopover: () => void): JSX.Element | null => {
|
||||||
<EuiContextMenuPanel items={onGetBatchItemsPopoverContent(closePopover)} />
|
if (onGetBatchItemsPopoverContent != null) {
|
||||||
),
|
return <EuiContextMenuPanel items={onGetBatchItemsPopoverContent(closePopover)} />;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
[onGetBatchItemsPopoverContent]
|
[onGetBatchItemsPopoverContent]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleAutoRefreshSwitch = useCallback(
|
const handleAutoRefreshSwitch = useCallback(
|
||||||
(closePopover: () => void) => (e: EuiSwitchEvent) => {
|
(closePopover: () => void) => (e: EuiSwitchEvent) => {
|
||||||
|
if (onRefreshSwitch != null) {
|
||||||
onRefreshSwitch(e.target.checked);
|
onRefreshSwitch(e.target.checked);
|
||||||
closePopover();
|
closePopover();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[onRefreshSwitch]
|
[onRefreshSwitch]
|
||||||
);
|
);
|
||||||
|
@ -58,7 +66,7 @@ export const AllRulesUtilityBar = React.memo<AllRulesUtilityBarProps>(
|
||||||
<EuiSwitch
|
<EuiSwitch
|
||||||
key="allRulesAutoRefreshSwitch"
|
key="allRulesAutoRefreshSwitch"
|
||||||
label={i18n.REFRESH_RULE_POPOVER_DESCRIPTION}
|
label={i18n.REFRESH_RULE_POPOVER_DESCRIPTION}
|
||||||
checked={isAutoRefreshOn}
|
checked={isAutoRefreshOn ?? false}
|
||||||
onChange={handleAutoRefreshSwitch(closePopover)}
|
onChange={handleAutoRefreshSwitch(closePopover)}
|
||||||
compressed
|
compressed
|
||||||
data-test-subj="refreshSettingsSwitch"
|
data-test-subj="refreshSettingsSwitch"
|
||||||
|
@ -73,14 +81,22 @@ export const AllRulesUtilityBar = React.memo<AllRulesUtilityBarProps>(
|
||||||
<UtilityBar>
|
<UtilityBar>
|
||||||
<UtilityBarSection>
|
<UtilityBarSection>
|
||||||
<UtilityBarGroup>
|
<UtilityBarGroup>
|
||||||
|
{showBulkActions ? (
|
||||||
<UtilityBarText dataTestSubj="showingRules">
|
<UtilityBarText dataTestSubj="showingRules">
|
||||||
{i18n.SHOWING_RULES(paginationTotal)}
|
{i18n.SHOWING_RULES(paginationTotal)}
|
||||||
</UtilityBarText>
|
</UtilityBarText>
|
||||||
|
) : (
|
||||||
|
<UtilityBarText dataTestSubj="showingExceptionLists">
|
||||||
|
{i18n.SHOWING_EXCEPTION_LISTS(paginationTotal)}
|
||||||
|
</UtilityBarText>
|
||||||
|
)}
|
||||||
</UtilityBarGroup>
|
</UtilityBarGroup>
|
||||||
|
|
||||||
<UtilityBarGroup>
|
{showBulkActions ? (
|
||||||
|
<>
|
||||||
|
<UtilityBarGroup data-test-subj="tableBulkActions">
|
||||||
<UtilityBarText dataTestSubj="selectedRules">
|
<UtilityBarText dataTestSubj="selectedRules">
|
||||||
{i18n.SELECTED_RULES(numberSelectedRules)}
|
{i18n.SELECTED_RULES(numberSelectedItems)}
|
||||||
</UtilityBarText>
|
</UtilityBarText>
|
||||||
{!userHasNoPermissions && (
|
{!userHasNoPermissions && (
|
||||||
<UtilityBarAction
|
<UtilityBarAction
|
||||||
|
@ -92,6 +108,7 @@ export const AllRulesUtilityBar = React.memo<AllRulesUtilityBarProps>(
|
||||||
{i18n.BATCH_ACTIONS}
|
{i18n.BATCH_ACTIONS}
|
||||||
</UtilityBarAction>
|
</UtilityBarAction>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<UtilityBarAction
|
<UtilityBarAction
|
||||||
dataTestSubj="refreshRulesAction"
|
dataTestSubj="refreshRulesAction"
|
||||||
iconSide="left"
|
iconSide="left"
|
||||||
|
@ -109,6 +126,19 @@ export const AllRulesUtilityBar = React.memo<AllRulesUtilityBarProps>(
|
||||||
{i18n.REFRESH_RULE_POPOVER_LABEL}
|
{i18n.REFRESH_RULE_POPOVER_LABEL}
|
||||||
</UtilityBarAction>
|
</UtilityBarAction>
|
||||||
</UtilityBarGroup>
|
</UtilityBarGroup>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<UtilityBarGroup>
|
||||||
|
<UtilityBarAction
|
||||||
|
dataTestSubj="refreshRulesAction"
|
||||||
|
iconSide="left"
|
||||||
|
iconType="refresh"
|
||||||
|
onClick={onRefresh}
|
||||||
|
>
|
||||||
|
{i18n.REFRESH}
|
||||||
|
</UtilityBarAction>
|
||||||
|
</UtilityBarGroup>
|
||||||
|
)}
|
||||||
</UtilityBarSection>
|
</UtilityBarSection>
|
||||||
</UtilityBar>
|
</UtilityBar>
|
||||||
);
|
);
|
||||||
|
|
|
@ -374,7 +374,14 @@ export const RULES_TAB = i18n.translate(
|
||||||
export const MONITORING_TAB = i18n.translate(
|
export const MONITORING_TAB = i18n.translate(
|
||||||
'xpack.securitySolution.detectionEngine.rules.allRules.tabs.monitoring',
|
'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',
|
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,
|
useCursor,
|
||||||
useApi,
|
useApi,
|
||||||
useAsync,
|
useAsync,
|
||||||
useExceptionList,
|
useExceptionListItems,
|
||||||
|
useExceptionLists,
|
||||||
usePersistExceptionItem,
|
usePersistExceptionItem,
|
||||||
usePersistExceptionList,
|
usePersistExceptionList,
|
||||||
useFindLists,
|
useFindLists,
|
||||||
|
@ -52,7 +53,7 @@ export {
|
||||||
ExceptionListIdentifiers,
|
ExceptionListIdentifiers,
|
||||||
ExceptionList,
|
ExceptionList,
|
||||||
Pagination,
|
Pagination,
|
||||||
UseExceptionListSuccess,
|
UseExceptionListItemsSuccess,
|
||||||
addEndpointExceptionList,
|
addEndpointExceptionList,
|
||||||
withOptionalSignal,
|
withOptionalSignal,
|
||||||
} from '../../lists/public';
|
} from '../../lists/public';
|
||||||
|
|
Loading…
Reference in a new issue