Fix exceptions page table pagination (#111000) (#111209)

Co-authored-by: Dmitry Shevchenko <dmshevch@gmail.com>
This commit is contained in:
Kibana Machine 2021-09-06 14:09:54 -04:00 committed by GitHub
parent 5d599f1e6a
commit 3a53492f29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 156 additions and 115 deletions

View file

@ -40,7 +40,7 @@ export interface UseExceptionListsProps {
http: HttpStart;
namespaceTypes: NamespaceType[];
notifications: NotificationsStart;
pagination?: Pagination;
initialPagination?: Pagination;
showTrustedApps: boolean;
showEventFilters: boolean;
}

View file

@ -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<React.SetStateAction<Pagination>>,
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<ExceptionListSchema[]>([]);
const [paginationInfo, setPagination] = useState<Pagination>(pagination);
const [pagination, setPagination] = useState<Pagination>(initialPagination);
const [loading, setLoading] = useState(true);
const fetchExceptionListsRef = useRef<Func | null>(null);
const abortCtrlRef = useRef<AbortController>();
const namespaceTypesAsString = useMemo(() => namespaceTypes.join(','), [namespaceTypes]);
const filters = useMemo(
@ -58,66 +66,57 @@ export const useExceptionLists = ({
[namespaceTypes, filterOptions, showTrustedApps, showEventFilters]
);
useEffect(() => {
let isSubscribed = true;
const abortCtrl = new AbortController();
const fetchData = useCallback(async (): Promise<void> => {
try {
setLoading(true);
const fetchData = async (): Promise<void> => {
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,
});
setPagination({
page,
perPage,
total,
});
setExceptionLists(data);
setLoading(false);
} catch (error) {
if (error.name !== 'AbortError') {
notifications.toasts.addError(error, {
title: errorMessage,
});
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);
}
setExceptionLists([]);
setPagination(DEFAULT_PAGINATION);
setLoading(false);
}
};
fetchData();
fetchExceptionListsRef.current = fetchData;
return (): void => {
isSubscribed = false;
abortCtrl.abort();
};
}
}, [
errorMessage,
notifications,
filters,
http,
namespaceTypesAsString,
notifications.toasts,
pagination.page,
pagination.perPage,
filters,
namespaceTypesAsString,
http,
]);
return [loading, exceptionLists, paginationInfo, fetchExceptionListsRef.current];
useEffect(() => {
fetchData();
return (): void => {
abortCtrlRef.current?.abort();
};
}, [fetchData]);
return [loading, exceptionLists, pagination, setPagination, fetchData];
};

View file

@ -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,
});

View file

@ -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,
})

View file

@ -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<ExceptionListInfo>;
export type AllExceptionListsColumns = EuiBasicTableColumn<ExceptionsTableItem>;
export const getAllExceptionListsColumns = (
onExport: (arg: { id: string; listId: string; namespaceType: NamespaceType }) => () => void,

View file

@ -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<void>;
@ -74,7 +76,13 @@ export const ExceptionListsTable = React.memo(() => {
exceptionReferenceModalInitialState
);
const [filters, setFilters] = useState<ExceptionListFilter | undefined>(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<ExceptionsTableItem[]>(
() =>
(exceptionListsWithRuleRefs ?? []).map((item) => ({
...item,
isDeleting: deletingListIds.includes(item.id),
isExporting: exportingListIds.includes(item.id),
})),
[deletingListIds, exceptionListsWithRuleRefs, exportingListIds]
);
const handlePaginationChange = useCallback(
(criteria: CriteriaWithPagination<ExceptionsTableItem>) => {
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}
/>
<EuiBasicTable
<EuiBasicTable<ExceptionsTableItem>
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';

View file

@ -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;
}