diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts index 1909bcb1bcc2e..31f763101c258 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts @@ -40,7 +40,7 @@ export interface UseExceptionListsProps { http: HttpStart; namespaceTypes: NamespaceType[]; notifications: NotificationsStart; - pagination?: Pagination; + initialPagination?: Pagination; showTrustedApps: boolean; showEventFilters: boolean; } diff --git a/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts index 0bd4c6c705668..44a8af20b36b2 100644 --- a/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts +++ b/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { ExceptionListSchema, UseExceptionListsProps, @@ -17,7 +17,19 @@ import { fetchExceptionLists } from '@kbn/securitysolution-list-api'; import { getFilters } from '@kbn/securitysolution-list-utils'; export type Func = () => void; -export type ReturnExceptionLists = [boolean, ExceptionListSchema[], Pagination, Func | null]; +export type ReturnExceptionLists = [ + loading: boolean, + exceptionLists: ExceptionListSchema[], + pagination: Pagination, + setPagination: React.Dispatch>, + fetchLists: Func | null +]; + +const DEFAULT_PAGINATION = { + page: 1, + perPage: 20, + total: 0, +}; /** * Hook for fetching ExceptionLists @@ -29,17 +41,13 @@ export type ReturnExceptionLists = [boolean, ExceptionListSchema[], Pagination, * @param notifications kibana service for displaying toasters * @param showTrustedApps boolean - include/exclude trusted app lists * @param showEventFilters boolean - include/exclude event filters lists - * @param pagination + * @param initialPagination * */ export const useExceptionLists = ({ errorMessage, http, - pagination = { - page: 1, - perPage: 20, - total: 0, - }, + initialPagination = DEFAULT_PAGINATION, filterOptions = {}, namespaceTypes, notifications, @@ -47,9 +55,9 @@ export const useExceptionLists = ({ showEventFilters = false, }: UseExceptionListsProps): ReturnExceptionLists => { const [exceptionLists, setExceptionLists] = useState([]); - const [paginationInfo, setPagination] = useState(pagination); + const [pagination, setPagination] = useState(initialPagination); const [loading, setLoading] = useState(true); - const fetchExceptionListsRef = useRef(null); + const abortCtrlRef = useRef(); const namespaceTypesAsString = useMemo(() => namespaceTypes.join(','), [namespaceTypes]); const filters = useMemo( @@ -58,66 +66,59 @@ export const useExceptionLists = ({ [namespaceTypes, filterOptions, showTrustedApps, showEventFilters] ); - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); + const fetchData = useCallback(async (): Promise => { + try { + setLoading(true); - const fetchData = async (): Promise => { - try { - setLoading(true); + abortCtrlRef.current = new AbortController(); - const { page, per_page: perPage, total, data } = await fetchExceptionLists({ - filters, - http, - namespaceTypes: namespaceTypesAsString, - pagination: { - page: pagination.page, - perPage: pagination.perPage, - }, - signal: abortCtrl.signal, - }); + const { page, per_page: perPage, total, data } = await fetchExceptionLists({ + filters, + http, + namespaceTypes: namespaceTypesAsString, + pagination: { + page: pagination.page, + perPage: pagination.perPage, + }, + signal: abortCtrlRef.current.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); - } + setPagination({ + page, + perPage, + total, + }); + setExceptionLists(data); + setLoading(false); + } catch (error) { + if (error.name !== 'AbortError') { + notifications.toasts.addError(error, { + title: errorMessage, + }); + setExceptionLists([]); + setPagination(DEFAULT_PAGINATION); + setLoading(false); } - }; - - fetchData(); - - fetchExceptionListsRef.current = fetchData; - return (): void => { - isSubscribed = false; - abortCtrl.abort(); - }; + } }, [ errorMessage, - notifications, - pagination.page, - pagination.perPage, filters, - namespaceTypesAsString, http, + namespaceTypesAsString, + notifications.toasts, + pagination.page, + pagination.perPage, ]); - return [loading, exceptionLists, paginationInfo, fetchExceptionListsRef.current]; + useEffect(() => { + fetchData(); + + return (): void => { + if (abortCtrlRef.current) { + abortCtrlRef.current.abort(); + } + }; + }, [fetchData]); + + return [loading, exceptionLists, pagination, setPagination, fetchData]; }; diff --git a/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts index e3611120348f4..e5e41b5fe4a85 100644 --- a/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts @@ -12,6 +12,6 @@ import { getExceptionListSchemaMock } from './exception_list_schema.mock'; export const getFoundExceptionListSchemaMock = (): FoundExceptionListSchema => ({ data: [getExceptionListSchemaMock()], page: 1, - per_page: 1, + per_page: 20, total: 1, }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts index 4987de321c556..810fcaa15494f 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts @@ -41,13 +41,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: false, }) @@ -62,7 +62,8 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, - null, + expect.any(Function), + expect.any(Function), ]); }); }); @@ -77,13 +78,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: false, }) @@ -100,10 +101,11 @@ describe('useExceptionLists', () => { expectedListItemsResult, { page: 1, - perPage: 1, + perPage: 20, total: 1, }, - result.current[3], + expect.any(Function), + expect.any(Function), ]); }); }); @@ -117,13 +119,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: true, }) @@ -153,13 +155,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: false, }) @@ -189,13 +191,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: true, showTrustedApps: false, }) @@ -225,13 +227,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: false, }) @@ -264,13 +266,13 @@ describe('useExceptionLists', () => { name: 'Sample Endpoint', }, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: false, }) @@ -302,9 +304,9 @@ describe('useExceptionLists', () => { errorMessage, filterOptions, http, + initialPagination, namespaceTypes, notifications, - pagination, showEventFilters, showTrustedApps, }) => @@ -312,9 +314,9 @@ describe('useExceptionLists', () => { errorMessage, filterOptions, http, + initialPagination, namespaceTypes, notifications, - pagination, showEventFilters, showTrustedApps, }), @@ -323,13 +325,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: false, }, @@ -344,13 +346,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: false, }); @@ -372,13 +374,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: false, }) @@ -390,8 +392,8 @@ describe('useExceptionLists', () => { expect(typeof result.current[3]).toEqual('function'); - if (result.current[3] != null) { - result.current[3](); + if (result.current[4] != null) { + result.current[4](); } // NOTE: Only need one call here because hook already initilaized await waitForNextUpdate(); @@ -411,13 +413,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: false, }) diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx index 582ca0252604c..1ef3c3d3c5414 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx @@ -15,8 +15,9 @@ import { FormatUrl } from '../../../../../../common/components/link_to'; import * as i18n from './translations'; import { ExceptionListInfo } from './use_all_exception_lists'; import { ExceptionOverflowDisplay } from './exceptions_overflow_display'; +import { ExceptionsTableItem } from './types'; -export type AllExceptionListsColumns = EuiBasicTableColumn; +export type AllExceptionListsColumns = EuiBasicTableColumn; export const getAllExceptionListsColumns = ( onExport: (arg: { id: string; listId: string; namespaceType: NamespaceType }) => () => void, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index 206976e6c0c1a..23bf634cb1081 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -7,6 +7,7 @@ import React, { useMemo, useEffect, useCallback, useState } from 'react'; import { + CriteriaWithPagination, EuiBasicTable, EuiEmptyPrompt, EuiLoadingContent, @@ -37,6 +38,7 @@ import { SecurityPageName } from '../../../../../../../common/constants'; import { useUserData } from '../../../../../components/user_info'; import { userHasPermissions } from '../../helpers'; import { useListsConfig } from '../../../../../containers/detection_engine/lists/use_lists_config'; +import { ExceptionsTableItem } from './types'; export type Func = () => Promise; @@ -74,7 +76,13 @@ export const ExceptionListsTable = React.memo(() => { exceptionReferenceModalInitialState ); const [filters, setFilters] = useState(undefined); - const [loadingExceptions, exceptions, pagination, refreshExceptions] = useExceptionLists({ + const [ + loadingExceptions, + exceptions, + pagination, + setPagination, + refreshExceptions, + ] = useExceptionLists({ errorMessage: i18n.ERROR_EXCEPTION_LISTS, filterOptions: filters, http, @@ -125,7 +133,7 @@ export const ExceptionListsTable = React.memo(() => { try { setDeletingListIds((ids) => [...ids, id]); if (refreshExceptions != null) { - await refreshExceptions(); + refreshExceptions(); } if (exceptionsListsRef[id] != null && exceptionsListsRef[id].rules.length === 0) { @@ -153,7 +161,7 @@ export const ExceptionListsTable = React.memo(() => { } catch (error) { handleDeleteError(error); } finally { - setDeletingListIds((ids) => [...ids.filter((_id) => _id !== id)]); + setDeletingListIds((ids) => ids.filter((_id) => _id !== id)); } }, [ @@ -326,11 +334,27 @@ export const ExceptionListsTable = React.memo(() => { setExportDownload({}); }, []); - const tableItems = (exceptionListsWithRuleRefs ?? []).map((item) => ({ - ...item, - isDeleting: deletingListIds.includes(item.id), - isExporting: exportingListIds.includes(item.id), - })); + const tableItems = useMemo( + () => + (exceptionListsWithRuleRefs ?? []).map((item) => ({ + ...item, + isDeleting: deletingListIds.includes(item.id), + isExporting: exportingListIds.includes(item.id), + })), + [deletingListIds, exceptionListsWithRuleRefs, exportingListIds] + ); + + const handlePaginationChange = useCallback( + (criteria: CriteriaWithPagination) => { + const { index, size } = criteria.page; + setPagination((currentPagination) => ({ + ...currentPagination, + perPage: size, + page: index + 1, + })); + }, + [setPagination] + ); return ( <> @@ -367,14 +391,14 @@ export const ExceptionListsTable = React.memo(() => { numberSelectedItems={0} onRefresh={handleRefresh} /> - data-test-subj="exceptions-table" columns={exceptionsColumns} isSelectable={hasPermissions} itemId="id" items={tableItems} noItemsMessage={emptyPrompt} - onChange={() => {}} + onChange={handlePaginationChange} pagination={paginationMemo} /> @@ -400,3 +424,5 @@ export const ExceptionListsTable = React.memo(() => { ); }); + +ExceptionListsTable.displayName = 'ExceptionListsTable'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/types.ts new file mode 100644 index 0000000000000..d7cbb924071f2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExceptionListInfo } from './use_all_exception_lists'; + +export interface ExceptionsTableItem extends ExceptionListInfo { + isDeleting: boolean; + isExporting: boolean; +}