[Discover] Fix pagination when applying filter (#110763)
* [Discover] fix pagination when applying filter * [Discover] refactoring to forward ref usage * [Discover] remove console log debug * [Discover] hide pagination on empty result * [Discover] add usePager test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
a1a717862b
commit
9792c1079e
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import './index.scss';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
|
@ -24,30 +24,61 @@ export interface DocTableEmbeddableProps extends DocTableProps {
|
|||
const DocTableWrapperMemoized = memo(DocTableWrapper);
|
||||
|
||||
export const DocTableEmbeddable = (props: DocTableEmbeddableProps) => {
|
||||
const pager = usePager({ totalItems: props.rows.length });
|
||||
const tableWrapperRef = useRef<HTMLDivElement>(null);
|
||||
const {
|
||||
currentPage,
|
||||
pageSize,
|
||||
totalPages,
|
||||
startIndex,
|
||||
hasNextPage,
|
||||
changePage,
|
||||
changePageSize,
|
||||
} = usePager({
|
||||
totalItems: props.rows.length,
|
||||
});
|
||||
const showPagination = totalPages !== 0;
|
||||
|
||||
const pageOfItems = useMemo(
|
||||
() => props.rows.slice(pager.startIndex, pager.pageSize + pager.startIndex),
|
||||
[pager.pageSize, pager.startIndex, props.rows]
|
||||
const scrollTop = useCallback(() => {
|
||||
if (tableWrapperRef.current) {
|
||||
tableWrapperRef.current.scrollTo(0, 0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const pageOfItems = useMemo(() => props.rows.slice(startIndex, pageSize + startIndex), [
|
||||
pageSize,
|
||||
startIndex,
|
||||
props.rows,
|
||||
]);
|
||||
|
||||
const onPageChange = useCallback(
|
||||
(page: number) => {
|
||||
scrollTop();
|
||||
changePage(page);
|
||||
},
|
||||
[changePage, scrollTop]
|
||||
);
|
||||
|
||||
const shouldShowLimitedResultsWarning = () =>
|
||||
!pager.hasNextPage && props.rows.length < props.totalHitCount;
|
||||
const onPageSizeChange = useCallback(
|
||||
(size: number) => {
|
||||
scrollTop();
|
||||
changePageSize(size);
|
||||
},
|
||||
[changePageSize, scrollTop]
|
||||
);
|
||||
|
||||
const scrollTop = () => {
|
||||
const scrollDiv = document.querySelector('.kbnDocTableWrapper') as HTMLElement;
|
||||
scrollDiv.scrollTo(0, 0);
|
||||
};
|
||||
/**
|
||||
* Go to the first page if the current is no longer available
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (totalPages < currentPage + 1) {
|
||||
onPageChange(0);
|
||||
}
|
||||
}, [currentPage, totalPages, onPageChange]);
|
||||
|
||||
const onPageChange = (page: number) => {
|
||||
scrollTop();
|
||||
pager.onPageChange(page);
|
||||
};
|
||||
|
||||
const onPageSizeChange = (size: number) => {
|
||||
scrollTop();
|
||||
pager.onPageSizeChange(size);
|
||||
};
|
||||
const shouldShowLimitedResultsWarning = useMemo(
|
||||
() => !hasNextPage && props.rows.length < props.totalHitCount,
|
||||
[hasNextPage, props.rows.length, props.totalHitCount]
|
||||
);
|
||||
|
||||
const sampleSize = useMemo(() => {
|
||||
return getServices().uiSettings.get(SAMPLE_SIZE_SETTING, 500);
|
||||
|
@ -77,7 +108,7 @@ export const DocTableEmbeddable = (props: DocTableEmbeddableProps) => {
|
|||
responsive={false}
|
||||
wrap={true}
|
||||
>
|
||||
{shouldShowLimitedResultsWarning() && (
|
||||
{shouldShowLimitedResultsWarning && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText grow={false} size="s" color="subdued">
|
||||
<FormattedMessage
|
||||
|
@ -97,18 +128,20 @@ export const DocTableEmbeddable = (props: DocTableEmbeddableProps) => {
|
|||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem style={{ minHeight: 0 }}>
|
||||
<DocTableWrapperMemoized {...props} render={renderDocTable} />
|
||||
<DocTableWrapperMemoized ref={tableWrapperRef} {...props} render={renderDocTable} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<ToolBarPagination
|
||||
pageSize={pager.pageSize}
|
||||
pageCount={pager.totalPages}
|
||||
activePage={pager.currentPage}
|
||||
onPageClick={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{showPagination && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<ToolBarPagination
|
||||
pageSize={pageSize}
|
||||
pageCount={totalPages}
|
||||
activePage={currentPage}
|
||||
onPageClick={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { Fragment, memo, useCallback, useEffect, useState } from 'react';
|
||||
import React, { Fragment, memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import './index.scss';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { debounce } from 'lodash';
|
||||
|
@ -17,24 +17,88 @@ import { shouldLoadNextDocPatch } from './lib/should_load_next_doc_patch';
|
|||
|
||||
const FOOTER_PADDING = { padding: 0 };
|
||||
|
||||
const DocTableInfiniteContent = (props: DocTableRenderProps) => {
|
||||
const [limit, setLimit] = useState(props.minimumVisibleRows);
|
||||
const DocTableWrapperMemoized = memo(DocTableWrapper);
|
||||
|
||||
// Reset infinite scroll limit
|
||||
useEffect(() => {
|
||||
setLimit(props.minimumVisibleRows);
|
||||
}, [props.rows, props.minimumVisibleRows]);
|
||||
interface DocTableInfiniteContentProps extends DocTableRenderProps {
|
||||
limit: number;
|
||||
onSetMaxLimit: () => void;
|
||||
onBackToTop: () => void;
|
||||
}
|
||||
|
||||
const DocTableInfiniteContent = ({
|
||||
rows,
|
||||
columnLength,
|
||||
sampleSize,
|
||||
limit,
|
||||
onSkipBottomButtonClick,
|
||||
renderHeader,
|
||||
renderRows,
|
||||
onSetMaxLimit,
|
||||
onBackToTop,
|
||||
}: DocTableInfiniteContentProps) => {
|
||||
const onSkipBottomButton = useCallback(() => {
|
||||
onSetMaxLimit();
|
||||
onSkipBottomButtonClick();
|
||||
}, [onSetMaxLimit, onSkipBottomButtonClick]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SkipBottomButton onClick={onSkipBottomButton} />
|
||||
<table className="kbn-table table" data-test-subj="docTable">
|
||||
<thead>{renderHeader()}</thead>
|
||||
<tbody>{renderRows(rows.slice(0, limit))}</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={(columnLength || 1) + 2} style={FOOTER_PADDING}>
|
||||
{rows.length === sampleSize ? (
|
||||
<div
|
||||
className="kbnDocTable__footer"
|
||||
data-test-subj="discoverDocTableFooter"
|
||||
tabIndex={-1}
|
||||
id="discoverBottomMarker"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="discover.howToSeeOtherMatchingDocumentsDescription"
|
||||
defaultMessage="These are the first {sampleSize} documents matching
|
||||
your search, refine your search to see others."
|
||||
values={{ sampleSize }}
|
||||
/>
|
||||
<EuiButtonEmpty onClick={onBackToTop} data-test-subj="discoverBackToTop">
|
||||
<FormattedMessage
|
||||
id="discover.backToTopLinkText"
|
||||
defaultMessage="Back to top."
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</div>
|
||||
) : (
|
||||
<span tabIndex={-1} id="discoverBottomMarker">
|
||||
​
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export const DocTableInfinite = (props: DocTableProps) => {
|
||||
const tableWrapperRef = useRef<HTMLDivElement>(null);
|
||||
const [limit, setLimit] = useState(50);
|
||||
|
||||
/**
|
||||
* depending on which version of Discover is displayed, different elements are scrolling
|
||||
* and have therefore to be considered for calculation of infinite scrolling
|
||||
*/
|
||||
useEffect(() => {
|
||||
const scrollDiv = document.querySelector('.kbnDocTableWrapper') as HTMLElement;
|
||||
// After mounting table wrapper should be initialized
|
||||
const scrollDiv = tableWrapperRef.current as HTMLDivElement;
|
||||
const scrollMobileElem = document.documentElement;
|
||||
|
||||
const scheduleCheck = debounce(() => {
|
||||
const isMobileView = document.getElementsByClassName('dscSidebar__mobile').length > 0;
|
||||
|
||||
const usedScrollDiv = isMobileView ? scrollMobileElem : scrollDiv;
|
||||
if (shouldLoadNextDocPatch(usedScrollDiv)) {
|
||||
setLimit((prevLimit) => prevLimit + 50);
|
||||
|
@ -58,63 +122,26 @@ const DocTableInfiniteContent = (props: DocTableRenderProps) => {
|
|||
focusElem.focus();
|
||||
|
||||
// Only the desktop one needs to target a specific container
|
||||
if (!isMobileView) {
|
||||
const scrollDiv = document.querySelector('.kbnDocTableWrapper') as HTMLElement;
|
||||
scrollDiv.scrollTo(0, 0);
|
||||
if (!isMobileView && tableWrapperRef.current) {
|
||||
tableWrapperRef.current.scrollTo(0, 0);
|
||||
} else if (window) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SkipBottomButton onClick={props.onSkipBottomButtonClick} />
|
||||
<table className="kbn-table table" data-test-subj="docTable">
|
||||
<thead>{props.renderHeader()}</thead>
|
||||
<tbody>{props.renderRows(props.rows.slice(0, limit))}</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={(props.columnLength || 1) + 2} style={FOOTER_PADDING}>
|
||||
{props.rows.length === props.sampleSize ? (
|
||||
<div
|
||||
className="kbnDocTable__footer"
|
||||
data-test-subj="discoverDocTableFooter"
|
||||
tabIndex={-1}
|
||||
id="discoverBottomMarker"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="discover.howToSeeOtherMatchingDocumentsDescription"
|
||||
defaultMessage="These are the first {sampleSize} documents matching
|
||||
your search, refine your search to see others."
|
||||
values={{ sampleSize: props.sampleSize }}
|
||||
/>
|
||||
<EuiButtonEmpty onClick={onBackToTop} data-test-subj="discoverBackToTop">
|
||||
<FormattedMessage
|
||||
id="discover.backToTopLinkText"
|
||||
defaultMessage="Back to top."
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</div>
|
||||
) : (
|
||||
<span tabIndex={-1} id="discoverBottomMarker">
|
||||
​
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</Fragment>
|
||||
const setMaxLimit = useCallback(() => setLimit(props.rows.length), [props.rows.length]);
|
||||
|
||||
const renderDocTable = useCallback(
|
||||
(tableProps: DocTableRenderProps) => (
|
||||
<DocTableInfiniteContent
|
||||
{...tableProps}
|
||||
limit={limit}
|
||||
onSetMaxLimit={setMaxLimit}
|
||||
onBackToTop={onBackToTop}
|
||||
/>
|
||||
),
|
||||
[limit, onBackToTop, setMaxLimit]
|
||||
);
|
||||
};
|
||||
|
||||
const DocTableWrapperMemoized = memo(DocTableWrapper);
|
||||
const DocTableInfiniteContentMemoized = memo(DocTableInfiniteContent);
|
||||
|
||||
const renderDocTable = (tableProps: DocTableRenderProps) => (
|
||||
<DocTableInfiniteContentMemoized {...tableProps} />
|
||||
);
|
||||
|
||||
export const DocTableInfinite = (props: DocTableProps) => {
|
||||
return <DocTableWrapperMemoized {...props} render={renderDocTable} />;
|
||||
|
||||
return <DocTableWrapperMemoized ref={tableWrapperRef} render={renderDocTable} {...props} />;
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { forwardRef, useCallback, useMemo } from 'react';
|
||||
import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import type { IndexPattern, IndexPatternField } from 'src/plugins/data/common';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
@ -86,7 +86,6 @@ export interface DocTableProps {
|
|||
export interface DocTableRenderProps {
|
||||
columnLength: number;
|
||||
rows: DocTableRow[];
|
||||
minimumVisibleRows: number;
|
||||
sampleSize: number;
|
||||
renderRows: (row: DocTableRow[]) => JSX.Element[];
|
||||
renderHeader: () => JSX.Element;
|
||||
|
@ -100,163 +99,166 @@ export interface DocTableWrapperProps extends DocTableProps {
|
|||
render: (params: DocTableRenderProps) => JSX.Element;
|
||||
}
|
||||
|
||||
export const DocTableWrapper = ({
|
||||
render,
|
||||
columns,
|
||||
rows,
|
||||
indexPattern,
|
||||
onSort,
|
||||
onAddColumn,
|
||||
onMoveColumn,
|
||||
onRemoveColumn,
|
||||
sort,
|
||||
onFilter,
|
||||
useNewFieldsApi,
|
||||
searchDescription,
|
||||
sharedItemTitle,
|
||||
dataTestSubj,
|
||||
isLoading,
|
||||
}: DocTableWrapperProps) => {
|
||||
const [minimumVisibleRows, setMinimumVisibleRows] = useState(50);
|
||||
const [
|
||||
defaultSortOrder,
|
||||
hideTimeColumn,
|
||||
isShortDots,
|
||||
sampleSize,
|
||||
showMultiFields,
|
||||
filterManager,
|
||||
addBasePath,
|
||||
] = useMemo(() => {
|
||||
const services = getServices();
|
||||
return [
|
||||
services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc'),
|
||||
services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false),
|
||||
services.uiSettings.get(FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE),
|
||||
services.uiSettings.get(SAMPLE_SIZE_SETTING, 500),
|
||||
services.uiSettings.get(SHOW_MULTIFIELDS, false),
|
||||
services.filterManager,
|
||||
services.addBasePath,
|
||||
];
|
||||
}, []);
|
||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const onSkipBottomButtonClick = useCallback(async () => {
|
||||
// delay scrolling to after the rows have been rendered
|
||||
const bottomMarker = document.getElementById('discoverBottomMarker');
|
||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
// show all the rows
|
||||
setMinimumVisibleRows(rows.length);
|
||||
|
||||
while (rows.length !== document.getElementsByClassName('kbnDocTable__row').length) {
|
||||
await wait(50);
|
||||
}
|
||||
bottomMarker!.focus();
|
||||
await wait(50);
|
||||
bottomMarker!.blur();
|
||||
}, [setMinimumVisibleRows, rows]);
|
||||
|
||||
const fieldsToShow = useMemo(
|
||||
() =>
|
||||
getFieldsToShow(
|
||||
indexPattern.fields.map((field: IndexPatternField) => field.name),
|
||||
indexPattern,
|
||||
showMultiFields
|
||||
),
|
||||
[indexPattern, showMultiFields]
|
||||
);
|
||||
|
||||
const renderHeader = useCallback(
|
||||
() => (
|
||||
<TableHeader
|
||||
columns={columns}
|
||||
defaultSortOrder={defaultSortOrder}
|
||||
hideTimeColumn={hideTimeColumn}
|
||||
indexPattern={indexPattern}
|
||||
isShortDots={isShortDots}
|
||||
onChangeSortOrder={onSort}
|
||||
onMoveColumn={onMoveColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
sortOrder={sort as SortOrder[]}
|
||||
/>
|
||||
),
|
||||
[
|
||||
export const DocTableWrapper = forwardRef(
|
||||
(
|
||||
{
|
||||
render,
|
||||
columns,
|
||||
defaultSortOrder,
|
||||
hideTimeColumn,
|
||||
rows,
|
||||
indexPattern,
|
||||
isShortDots,
|
||||
onSort,
|
||||
onAddColumn,
|
||||
onMoveColumn,
|
||||
onRemoveColumn,
|
||||
onSort,
|
||||
sort,
|
||||
]
|
||||
);
|
||||
|
||||
const renderRows = useCallback(
|
||||
(rowsToRender: DocTableRow[]) => {
|
||||
return rowsToRender.map((current) => (
|
||||
<TableRow
|
||||
key={`${current._index}${current._type ?? ''}${current._id}${current._score}${
|
||||
current._version
|
||||
}${current._routing}`}
|
||||
columns={columns}
|
||||
filter={onFilter}
|
||||
indexPattern={indexPattern}
|
||||
row={current}
|
||||
useNewFieldsApi={useNewFieldsApi}
|
||||
hideTimeColumn={hideTimeColumn}
|
||||
onAddColumn={onAddColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
filterManager={filterManager}
|
||||
addBasePath={addBasePath}
|
||||
fieldsToShow={fieldsToShow}
|
||||
/>
|
||||
));
|
||||
},
|
||||
[
|
||||
columns,
|
||||
onFilter,
|
||||
indexPattern,
|
||||
useNewFieldsApi,
|
||||
searchDescription,
|
||||
sharedItemTitle,
|
||||
dataTestSubj,
|
||||
isLoading,
|
||||
}: DocTableWrapperProps,
|
||||
ref
|
||||
) => {
|
||||
const [
|
||||
defaultSortOrder,
|
||||
hideTimeColumn,
|
||||
onAddColumn,
|
||||
onRemoveColumn,
|
||||
isShortDots,
|
||||
sampleSize,
|
||||
showMultiFields,
|
||||
filterManager,
|
||||
addBasePath,
|
||||
fieldsToShow,
|
||||
]
|
||||
);
|
||||
] = useMemo(() => {
|
||||
const services = getServices();
|
||||
return [
|
||||
services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc'),
|
||||
services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false),
|
||||
services.uiSettings.get(FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE),
|
||||
services.uiSettings.get(SAMPLE_SIZE_SETTING, 500),
|
||||
services.uiSettings.get(SHOW_MULTIFIELDS, false),
|
||||
services.filterManager,
|
||||
services.addBasePath,
|
||||
];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="kbnDocTableWrapper eui-yScroll eui-xScroll"
|
||||
data-shared-item
|
||||
data-title={sharedItemTitle}
|
||||
data-description={searchDescription}
|
||||
data-test-subj={dataTestSubj}
|
||||
data-render-complete={!isLoading}
|
||||
>
|
||||
{rows.length !== 0 &&
|
||||
render({
|
||||
columnLength: columns.length,
|
||||
rows,
|
||||
minimumVisibleRows,
|
||||
sampleSize,
|
||||
onSkipBottomButtonClick,
|
||||
renderHeader,
|
||||
renderRows,
|
||||
})}
|
||||
{!rows.length && (
|
||||
<div className="kbnDocTable__error">
|
||||
<EuiText size="xs" color="subdued">
|
||||
<EuiIcon type="visualizeApp" size="m" color="subdued" />
|
||||
<EuiSpacer size="m" />
|
||||
<FormattedMessage
|
||||
id="discover.docTable.noResultsTitle"
|
||||
defaultMessage="No results found"
|
||||
/>
|
||||
</EuiText>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const onSkipBottomButtonClick = useCallback(async () => {
|
||||
// delay scrolling to after the rows have been rendered
|
||||
const bottomMarker = document.getElementById('discoverBottomMarker');
|
||||
|
||||
while (rows.length !== document.getElementsByClassName('kbnDocTable__row').length) {
|
||||
await wait(50);
|
||||
}
|
||||
bottomMarker!.focus();
|
||||
await wait(50);
|
||||
bottomMarker!.blur();
|
||||
}, [rows]);
|
||||
|
||||
const fieldsToShow = useMemo(
|
||||
() =>
|
||||
getFieldsToShow(
|
||||
indexPattern.fields.map((field: IndexPatternField) => field.name),
|
||||
indexPattern,
|
||||
showMultiFields
|
||||
),
|
||||
[indexPattern, showMultiFields]
|
||||
);
|
||||
|
||||
const renderHeader = useCallback(
|
||||
() => (
|
||||
<TableHeader
|
||||
columns={columns}
|
||||
defaultSortOrder={defaultSortOrder}
|
||||
hideTimeColumn={hideTimeColumn}
|
||||
indexPattern={indexPattern}
|
||||
isShortDots={isShortDots}
|
||||
onChangeSortOrder={onSort}
|
||||
onMoveColumn={onMoveColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
sortOrder={sort as SortOrder[]}
|
||||
/>
|
||||
),
|
||||
[
|
||||
columns,
|
||||
defaultSortOrder,
|
||||
hideTimeColumn,
|
||||
indexPattern,
|
||||
isShortDots,
|
||||
onMoveColumn,
|
||||
onRemoveColumn,
|
||||
onSort,
|
||||
sort,
|
||||
]
|
||||
);
|
||||
|
||||
const renderRows = useCallback(
|
||||
(rowsToRender: DocTableRow[]) => {
|
||||
return rowsToRender.map((current) => (
|
||||
<TableRow
|
||||
key={`${current._index}${current._type ?? ''}${current._id}${current._score}${
|
||||
current._version
|
||||
}${current._routing}`}
|
||||
columns={columns}
|
||||
filter={onFilter}
|
||||
indexPattern={indexPattern}
|
||||
row={current}
|
||||
useNewFieldsApi={useNewFieldsApi}
|
||||
hideTimeColumn={hideTimeColumn}
|
||||
onAddColumn={onAddColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
filterManager={filterManager}
|
||||
addBasePath={addBasePath}
|
||||
fieldsToShow={fieldsToShow}
|
||||
/>
|
||||
));
|
||||
},
|
||||
[
|
||||
columns,
|
||||
onFilter,
|
||||
indexPattern,
|
||||
useNewFieldsApi,
|
||||
hideTimeColumn,
|
||||
onAddColumn,
|
||||
onRemoveColumn,
|
||||
filterManager,
|
||||
addBasePath,
|
||||
fieldsToShow,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="kbnDocTableWrapper eui-yScroll eui-xScroll"
|
||||
data-shared-item
|
||||
data-title={sharedItemTitle}
|
||||
data-description={searchDescription}
|
||||
data-test-subj={dataTestSubj}
|
||||
data-render-complete={!isLoading}
|
||||
ref={ref as React.MutableRefObject<HTMLDivElement>}
|
||||
>
|
||||
{rows.length !== 0 &&
|
||||
render({
|
||||
columnLength: columns.length,
|
||||
rows,
|
||||
sampleSize,
|
||||
onSkipBottomButtonClick,
|
||||
renderHeader,
|
||||
renderRows,
|
||||
})}
|
||||
{!rows.length && (
|
||||
<div className="kbnDocTable__error">
|
||||
<EuiText size="xs" color="subdued">
|
||||
<EuiIcon type="visualizeApp" size="m" color="subdued" />
|
||||
<EuiSpacer size="m" />
|
||||
<FormattedMessage
|
||||
id="discover.docTable.noResultsTitle"
|
||||
defaultMessage="No results found"
|
||||
/>
|
||||
</EuiText>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { usePager } from './use_pager';
|
||||
|
||||
describe('usePager', () => {
|
||||
const defaultProps = {
|
||||
totalItems: 745,
|
||||
};
|
||||
|
||||
test('should initialize the first page', () => {
|
||||
const { result } = renderHook(() => {
|
||||
return usePager(defaultProps);
|
||||
});
|
||||
|
||||
expect(result.current.currentPage).toEqual(0);
|
||||
expect(result.current.pageSize).toEqual(50);
|
||||
expect(result.current.totalPages).toEqual(15);
|
||||
expect(result.current.startIndex).toEqual(0);
|
||||
expect(result.current.hasNextPage).toEqual(true);
|
||||
});
|
||||
|
||||
test('should change the page', () => {
|
||||
const { result } = renderHook(() => {
|
||||
return usePager(defaultProps);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.changePage(5);
|
||||
});
|
||||
|
||||
expect(result.current.currentPage).toEqual(5);
|
||||
expect(result.current.pageSize).toEqual(50);
|
||||
expect(result.current.totalPages).toEqual(15);
|
||||
expect(result.current.startIndex).toEqual(250);
|
||||
expect(result.current.hasNextPage).toEqual(true);
|
||||
});
|
||||
|
||||
test('should go to the last page', () => {
|
||||
const { result } = renderHook(() => {
|
||||
return usePager(defaultProps);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.changePage(15);
|
||||
});
|
||||
|
||||
expect(result.current.currentPage).toEqual(15);
|
||||
expect(result.current.pageSize).toEqual(50);
|
||||
expect(result.current.totalPages).toEqual(15);
|
||||
expect(result.current.startIndex).toEqual(750);
|
||||
expect(result.current.hasNextPage).toEqual(false);
|
||||
});
|
||||
|
||||
test('should change page size and stay on the current page', () => {
|
||||
const { result } = renderHook(() => usePager(defaultProps));
|
||||
|
||||
act(() => {
|
||||
result.current.changePage(5);
|
||||
result.current.changePageSize(100);
|
||||
});
|
||||
|
||||
expect(result.current.currentPage).toEqual(5);
|
||||
expect(result.current.pageSize).toEqual(100);
|
||||
expect(result.current.totalPages).toEqual(8);
|
||||
expect(result.current.startIndex).toEqual(500);
|
||||
expect(result.current.hasNextPage).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -6,73 +6,38 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
interface MetaParams {
|
||||
currentPage: number;
|
||||
totalItems: number;
|
||||
totalPages: number;
|
||||
startIndex: number;
|
||||
hasNextPage: boolean;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
interface ProvidedMeta {
|
||||
updatedPageSize?: number;
|
||||
updatedCurrentPage?: number;
|
||||
}
|
||||
|
||||
const INITIAL_PAGE_SIZE = 50;
|
||||
|
||||
export const usePager = ({ totalItems }: { totalItems: number }) => {
|
||||
const [meta, setMeta] = useState<MetaParams>({
|
||||
currentPage: 0,
|
||||
totalItems,
|
||||
startIndex: 0,
|
||||
totalPages: Math.ceil(totalItems / INITIAL_PAGE_SIZE),
|
||||
hasNextPage: true,
|
||||
pageSize: INITIAL_PAGE_SIZE,
|
||||
});
|
||||
const [pageSize, setPageSize] = useState(INITIAL_PAGE_SIZE);
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
|
||||
const getNewMeta = useCallback(
|
||||
(newMeta: ProvidedMeta) => {
|
||||
const actualCurrentPage = newMeta.updatedCurrentPage ?? meta.currentPage;
|
||||
const actualPageSize = newMeta.updatedPageSize ?? meta.pageSize;
|
||||
const meta: MetaParams = useMemo(() => {
|
||||
const totalPages = Math.ceil(totalItems / pageSize);
|
||||
return {
|
||||
totalPages,
|
||||
startIndex: pageSize * currentPage,
|
||||
hasNextPage: currentPage + 1 < totalPages,
|
||||
};
|
||||
}, [currentPage, pageSize, totalItems]);
|
||||
|
||||
const newTotalPages = Math.ceil(totalItems / actualPageSize);
|
||||
const newStartIndex = actualPageSize * actualCurrentPage;
|
||||
const changePage = useCallback((pageIndex: number) => setCurrentPage(pageIndex), []);
|
||||
|
||||
return {
|
||||
currentPage: actualCurrentPage,
|
||||
totalPages: newTotalPages,
|
||||
startIndex: newStartIndex,
|
||||
totalItems,
|
||||
hasNextPage: meta.currentPage + 1 < meta.totalPages,
|
||||
pageSize: actualPageSize,
|
||||
};
|
||||
},
|
||||
[meta.currentPage, meta.pageSize, meta.totalPages, totalItems]
|
||||
);
|
||||
|
||||
const onPageChange = useCallback(
|
||||
(pageIndex: number) => setMeta(getNewMeta({ updatedCurrentPage: pageIndex })),
|
||||
[getNewMeta]
|
||||
);
|
||||
|
||||
const onPageSizeChange = useCallback(
|
||||
(newPageSize: number) =>
|
||||
setMeta(getNewMeta({ updatedPageSize: newPageSize, updatedCurrentPage: 0 })),
|
||||
[getNewMeta]
|
||||
);
|
||||
|
||||
/**
|
||||
* Update meta on totalItems change
|
||||
*/
|
||||
useEffect(() => setMeta(getNewMeta({})), [getNewMeta, totalItems]);
|
||||
const changePageSize = useCallback((newPageSize: number) => setPageSize(newPageSize), []);
|
||||
|
||||
return {
|
||||
...meta,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
currentPage,
|
||||
pageSize,
|
||||
changePage,
|
||||
changePageSize,
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue