From 6ad9cefa7c378061d9e8a7af8aacd016bc043253 Mon Sep 17 00:00:00 2001
From: Chandler Prall
Date: Tue, 15 Sep 2020 13:59:13 -0600
Subject: [PATCH 1/3] Adds pageSize and pageIndex controlled pagination fields
to EuiInMemoryTable's pagination
---
.../in_memory_controlled_pagination.js | 119 ++++++++++
...in_memory_controlled_pagination_section.js | 44 ++++
src-docs/src/views/tables/in_memory/index.js | 1 +
.../src/views/tables/in_memory/props_info.js | 12 +
.../views/tables/tables_in_memory_example.js | 2 +
.../basic_table/in_memory_table.test.tsx | 215 ++++++++++++++++++
.../basic_table/in_memory_table.tsx | 80 +++++--
7 files changed, 457 insertions(+), 16 deletions(-)
create mode 100644 src-docs/src/views/tables/in_memory/in_memory_controlled_pagination.js
create mode 100644 src-docs/src/views/tables/in_memory/in_memory_controlled_pagination_section.js
diff --git a/src-docs/src/views/tables/in_memory/in_memory_controlled_pagination.js b/src-docs/src/views/tables/in_memory/in_memory_controlled_pagination.js
new file mode 100644
index 00000000000..b230097aaea
--- /dev/null
+++ b/src-docs/src/views/tables/in_memory/in_memory_controlled_pagination.js
@@ -0,0 +1,119 @@
+import React, { useEffect, useState } from 'react';
+import { formatDate } from '../../../../../src/services/format';
+import { createDataStore } from '../data_store';
+import {
+ EuiInMemoryTable,
+ EuiLink,
+ EuiHealth,
+} from '../../../../../src/components';
+
+/*
+Example user object:
+
+{
+ id: '1',
+ firstName: 'john',
+ lastName: 'doe',
+ github: 'johndoe',
+ dateOfBirth: Date.now(),
+ nationality: 'NL',
+ online: true
+}
+
+Example country object:
+
+{
+ code: 'NL',
+ name: 'Netherlands',
+ flag: '🇳🇱'
+}
+*/
+
+const store = createDataStore();
+
+export const Table = () => {
+ const columns = [
+ {
+ field: 'firstName',
+ name: 'First Name',
+ sortable: true,
+ truncateText: true,
+ },
+ {
+ field: 'lastName',
+ name: 'Last Name',
+ truncateText: true,
+ },
+ {
+ field: 'github',
+ name: 'Github',
+ render: username => (
+
+ {username}
+
+ ),
+ },
+ {
+ field: 'dateOfBirth',
+ name: 'Date of Birth',
+ dataType: 'date',
+ render: date => formatDate(date, 'dobLong'),
+ sortable: true,
+ },
+ {
+ field: 'nationality',
+ name: 'Nationality',
+ render: countryCode => {
+ const country = store.getCountry(countryCode);
+ return `${country.flag} ${country.name}`;
+ },
+ },
+ {
+ field: 'online',
+ name: 'Online',
+ dataType: 'boolean',
+ render: online => {
+ const color = online ? 'success' : 'danger';
+ const label = online ? 'Online' : 'Offline';
+ return {label};
+ },
+ sortable: true,
+ },
+ ];
+
+ const sorting = {
+ sort: {
+ field: 'dateOfBirth',
+ direction: 'desc',
+ },
+ };
+
+ const [users, setUsers] = useState(store.users);
+
+ useEffect(() => {
+ const updateInterval = setInterval(() => {
+ setUsers(users =>
+ // randomly toggle some of the online statuses
+ users.map(({ online, ...user }) => ({
+ ...user,
+ online: Math.random() > 0.7 ? !online : online,
+ }))
+ );
+ }, 1000);
+ return () => clearInterval(updateInterval);
+ }, []);
+
+ const [pagination, setPagination] = useState({ pageIndex: 0 });
+
+ return (
+
+ setPagination({ pageIndex: index })
+ }
+ items={users}
+ columns={columns}
+ pagination={pagination}
+ sorting={sorting}
+ />
+ );
+};
diff --git a/src-docs/src/views/tables/in_memory/in_memory_controlled_pagination_section.js b/src-docs/src/views/tables/in_memory/in_memory_controlled_pagination_section.js
new file mode 100644
index 00000000000..33d2c31ba8d
--- /dev/null
+++ b/src-docs/src/views/tables/in_memory/in_memory_controlled_pagination_section.js
@@ -0,0 +1,44 @@
+import React from 'react';
+import { EuiCode } from '../../../../../src/components';
+import { GuideSectionTypes } from '../../../components';
+import { renderToHtml } from '../../../services';
+
+import { Table } from './in_memory_controlled_pagination';
+import { propsInfo } from './props_info';
+
+const source = require('!!raw-loader!./in_memory_controlled_pagination');
+const html = renderToHtml(Table);
+
+export const controlledPaginationSection = {
+ title: 'In-memory table with controlled pagination',
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: source,
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: html,
+ },
+ ],
+ text: (
+
+
+ By default EuiInMemoryTable resets its page index
+ when receiving a new EuiInMemoryTable array. To avoid
+ this behavior the pagination object optionally takes a
+ pageIndex value to control this yourself.
+ Additionally, pageSize can also be controlled the
+ same way. available. Both of these are provided to your app during the
+ onTableChange callback.
+
+
+ The example below updates the array of users every second, randomly
+ toggling their online status. Pagination state is maintained by the app,
+ preventing it from being reset by the updates.
+
+
+ ),
+ props: propsInfo,
+ demo: ,
+};
diff --git a/src-docs/src/views/tables/in_memory/index.js b/src-docs/src/views/tables/in_memory/index.js
index f7b303a0e9b..a820ed11be7 100644
--- a/src-docs/src/views/tables/in_memory/index.js
+++ b/src-docs/src/views/tables/in_memory/index.js
@@ -4,3 +4,4 @@ export { searchSection } from './in_memory_search_section';
export { searchExternalSection } from './in_memory_search_external_section';
export { searchCallbackSection } from './in_memory_search_callback_section';
export { customSortingSection } from './in_memory_custom_sorting_section';
+export { controlledPaginationSection } from './in_memory_controlled_pagination_section';
diff --git a/src-docs/src/views/tables/in_memory/props_info.js b/src-docs/src/views/tables/in_memory/props_info.js
index 8dc550818b2..6055eed75e8 100644
--- a/src-docs/src/views/tables/in_memory/props_info.js
+++ b/src-docs/src/views/tables/in_memory/props_info.js
@@ -85,6 +85,18 @@ export const propsInfo = {
required: false,
type: { name: 'number' },
},
+ pageIndex: {
+ description:
+ "When present, controls the table's pagination index. You must listen to the onTableChange callback to respond to user actions. Ignores any initialPageIndex value",
+ required: false,
+ type: { name: 'number' },
+ },
+ pageSize: {
+ description:
+ "When present, controls the table's page size. You must listen to the onTableChange callback to respond to user actions. Ignores any initialPageSize value",
+ required: false,
+ type: { name: 'number' },
+ },
pageSizeOptions:
basicPropsInfo.Pagination.__docgenInfo.props.pageSizeOptions,
hidePerPageOptions:
diff --git a/src-docs/src/views/tables/tables_in_memory_example.js b/src-docs/src/views/tables/tables_in_memory_example.js
index 204dfd4663e..d778b126e67 100644
--- a/src-docs/src/views/tables/tables_in_memory_example.js
+++ b/src-docs/src/views/tables/tables_in_memory_example.js
@@ -5,6 +5,7 @@ import {
searchExternalSection as inMemorySearchExternalSection,
searchCallbackSection as inMemorySearchCallbackSection,
customSortingSection as inMemoryCustomSortingSection,
+ controlledPaginationSection as inMemoryControlledPaginationSection,
} from './in_memory';
export const TableInMemoryExample = {
@@ -16,5 +17,6 @@ export const TableInMemoryExample = {
inMemorySearchCallbackSection,
inMemorySearchExternalSection,
inMemoryCustomSortingSection,
+ inMemoryControlledPaginationSection,
],
};
diff --git a/src/components/basic_table/in_memory_table.test.tsx b/src/components/basic_table/in_memory_table.test.tsx
index b6e8ed25082..00e90ce12ab 100644
--- a/src/components/basic_table/in_memory_table.test.tsx
+++ b/src/components/basic_table/in_memory_table.test.tsx
@@ -1001,4 +1001,219 @@ describe('EuiInMemoryTable', () => {
});
});
});
+
+ describe('controlled pagination', () => {
+ it('respects pageIndex', () => {
+ const pagination = {
+ initialPageIndex: 2,
+ pageIndex: 1,
+ pageSizeOptions: [2],
+ };
+ const items = [
+ { index: 0 },
+ { index: 1 },
+ { index: 2 },
+ { index: 3 },
+ { index: 4 },
+ { index: 5 },
+ ];
+ const columns = [
+ {
+ field: 'index',
+ name: 'Index',
+ },
+ ];
+ const onTableChange = jest.fn();
+ const component = mount(
+
+ );
+
+ // ensure table is on 2nd page (pageIndex=1)
+ expect(
+ component.find('button[data-test-subj="pagination-button-1"][disabled]')
+ .length
+ ).toBe(1);
+ expect(
+ component
+ .find('td')
+ .at(0)
+ .text()
+ ).toBe('Index2');
+ expect(
+ component
+ .find('td')
+ .at(1)
+ .text()
+ ).toBe('Index3');
+
+ // click the first pagination button
+ component
+ .find('EuiButtonEmpty[data-test-subj="pagination-button-0"]')
+ .simulate('click');
+ expect(onTableChange).toHaveBeenCalledTimes(1);
+ expect(onTableChange).toHaveBeenCalledWith({
+ sort: {},
+ page: {
+ index: 0,
+ size: 2,
+ },
+ });
+
+ // ensure table is still on the 2nd page (pageIndex=1)
+ expect(
+ component.find('button[data-test-subj="pagination-button-1"][disabled]')
+ .length
+ ).toBe(1);
+ expect(
+ component
+ .find('td')
+ .at(0)
+ .text()
+ ).toBe('Index2');
+ expect(
+ component
+ .find('td')
+ .at(1)
+ .text()
+ ).toBe('Index3');
+
+ // re-render with an updated `pageIndex` value
+ pagination.pageIndex = 2;
+ component.setProps({ pagination });
+
+ // ensure table is on 3rd page (pageIndex=2)
+ expect(
+ component.find('button[data-test-subj="pagination-button-2"][disabled]')
+ .length
+ ).toBe(1);
+ expect(
+ component
+ .find('td')
+ .at(0)
+ .text()
+ ).toBe('Index4');
+ expect(
+ component
+ .find('td')
+ .at(1)
+ .text()
+ ).toBe('Index5');
+ });
+
+ it('respects pageSize', () => {
+ const pagination = {
+ pageSize: 2,
+ initialPageSize: 4,
+ pageSizeOptions: [1, 2, 4],
+ };
+ const items = [
+ { index: 0 },
+ { index: 1 },
+ { index: 2 },
+ { index: 3 },
+ { index: 4 },
+ { index: 5 },
+ ];
+ const columns = [
+ {
+ field: 'index',
+ name: 'Index',
+ },
+ ];
+ const onTableChange = jest.fn();
+ const component = mount(
+
+ );
+
+ // check that the first 2 items rendered
+ expect(component.find('td').length).toBe(2);
+ expect(
+ component
+ .find('td')
+ .at(0)
+ .text()
+ ).toBe('Index0');
+ expect(
+ component
+ .find('td')
+ .at(1)
+ .text()
+ ).toBe('Index1');
+
+ // change the page size
+ component
+ .find('button[data-test-subj="tablePaginationPopoverButton"]')
+ .simulate('click');
+ component.update();
+ component
+ .find('button[data-test-subj="tablePagination-4-rows"]')
+ .simulate('click');
+
+ // check callback
+ expect(onTableChange).toHaveBeenCalledTimes(1);
+ expect(onTableChange).toHaveBeenCalledWith({
+ sort: {},
+ page: {
+ index: 0,
+ size: 4,
+ },
+ });
+
+ // verify still only rendering the first 2 rows
+ expect(component.find('td').length).toBe(2);
+ expect(
+ component
+ .find('td')
+ .at(0)
+ .text()
+ ).toBe('Index0');
+ expect(
+ component
+ .find('td')
+ .at(1)
+ .text()
+ ).toBe('Index1');
+
+ // update the controlled page size
+ pagination.pageSize = 4;
+ component.setProps({ pagination });
+
+ // verify it now renders 4 rows
+ expect(component.find('td').length).toBe(4);
+ expect(
+ component
+ .find('td')
+ .at(0)
+ .text()
+ ).toBe('Index0');
+ expect(
+ component
+ .find('td')
+ .at(1)
+ .text()
+ ).toBe('Index1');
+ expect(
+ component
+ .find('td')
+ .at(2)
+ .text()
+ ).toBe('Index2');
+ expect(
+ component
+ .find('td')
+ .at(3)
+ .text()
+ ).toBe('Index3');
+ });
+ });
});
diff --git a/src/components/basic_table/in_memory_table.tsx b/src/components/basic_table/in_memory_table.tsx
index 9a8e97b3b8e..2c7f7ab5d41 100644
--- a/src/components/basic_table/in_memory_table.tsx
+++ b/src/components/basic_table/in_memory_table.tsx
@@ -58,10 +58,12 @@ function isEuiSearchBarProps(
type Search = boolean | EuiSearchBarProps;
interface PaginationOptions {
- initialPageIndex?: number;
- initialPageSize?: number;
pageSizeOptions?: number[];
hidePerPageOptions?: boolean;
+ initialPageIndex?: number;
+ initialPageSize?: number;
+ pageIndex?: number;
+ pageSize?: number;
}
type Pagination = boolean | PaginationOptions;
@@ -149,12 +151,23 @@ const getInitialPagination = (pagination: Pagination | undefined) => {
}
const {
- initialPageIndex = 0,
- initialPageSize,
pageSizeOptions = paginationBarDefaults.pageSizeOptions,
hidePerPageOptions,
} = pagination as PaginationOptions;
+ const defaultPageSize = pageSizeOptions
+ ? pageSizeOptions[0]
+ : paginationBarDefaults.pageSizeOptions[0];
+
+ const initialPageIndex =
+ pagination === true
+ ? 0
+ : pagination.pageIndex || pagination.initialPageIndex || 0;
+ const initialPageSize =
+ pagination === true
+ ? defaultPageSize
+ : pagination.pageSize || pagination.initialPageSize || defaultPageSize;
+
if (
!hidePerPageOptions &&
initialPageSize &&
@@ -165,13 +178,9 @@ const getInitialPagination = (pagination: Pagination | undefined) => {
);
}
- const defaultPageSize = pageSizeOptions
- ? pageSizeOptions[0]
- : paginationBarDefaults.pageSizeOptions[0];
-
return {
pageIndex: initialPageIndex,
- pageSize: initialPageSize || defaultPageSize,
+ pageSize: initialPageSize,
pageSizeOptions,
hidePerPageOptions,
};
@@ -247,19 +256,52 @@ export class EuiInMemoryTable extends Component<
prevState: State
) {
let updatedPrevState = prevState;
- let componentShouldUpdate = false;
if (nextProps.items !== prevState.prevProps.items) {
// We have new items because an external search has completed, so reset pagination state.
- componentShouldUpdate = true;
+
+ let nextPageIndex = 0;
+ if (
+ nextProps.pagination != null &&
+ typeof nextProps.pagination !== 'boolean'
+ ) {
+ nextPageIndex = nextProps.pagination.pageIndex || 0;
+ }
+
updatedPrevState = {
...updatedPrevState,
prevProps: {
...updatedPrevState.prevProps,
items: nextProps.items,
},
- pageIndex: 0,
+ pageIndex: nextPageIndex,
};
}
+
+ // apply changes to controlled pagination
+ if (
+ nextProps.pagination != null &&
+ typeof nextProps.pagination !== 'boolean'
+ ) {
+ if (
+ nextProps.pagination.pageSize != null &&
+ nextProps.pagination.pageSize !== updatedPrevState.pageIndex
+ ) {
+ updatedPrevState = {
+ ...updatedPrevState,
+ pageSize: nextProps.pagination.pageSize,
+ };
+ }
+ if (
+ nextProps.pagination.pageIndex != null &&
+ nextProps.pagination.pageIndex !== updatedPrevState.pageIndex
+ ) {
+ updatedPrevState = {
+ ...updatedPrevState,
+ pageIndex: nextProps.pagination.pageIndex,
+ };
+ }
+ }
+
const { sortName, sortDirection } = getInitialSorting(
nextProps.columns,
nextProps.sorting
@@ -268,7 +310,6 @@ export class EuiInMemoryTable extends Component<
sortName !== prevState.prevProps.sortName ||
sortDirection !== prevState.prevProps.sortDirection
) {
- componentShouldUpdate = true;
updatedPrevState = {
...updatedPrevState,
sortName,
@@ -284,7 +325,6 @@ export class EuiInMemoryTable extends Component<
: '';
if (nextQuery !== prevQuery) {
- componentShouldUpdate = true;
updatedPrevState = {
...updatedPrevState,
prevProps: {
@@ -294,7 +334,7 @@ export class EuiInMemoryTable extends Component<
query: getQueryFromSearch(nextProps.search, false),
};
}
- if (componentShouldUpdate) {
+ if (updatedPrevState !== prevState) {
return updatedPrevState;
}
return null;
@@ -340,11 +380,19 @@ export class EuiInMemoryTable extends Component<
}
onTableChange = ({ page, sort }: Criteria) => {
- const { index: pageIndex, size: pageSize } = (page || {}) as {
+ let { index: pageIndex, size: pageSize } = (page || {}) as {
index: number;
size: number;
};
+ // don't apply pagination changes that are otherwise controlled
+ // `page` is left unchanged as it goes to the consumer's `onTableChange` callback, allowing the app to respond
+ const { pagination } = this.props;
+ if (pagination != null && typeof pagination !== 'boolean') {
+ if (pagination.pageSize != null) pageSize = pagination.pageSize;
+ if (pagination.pageIndex != null) pageIndex = pagination.pageIndex;
+ }
+
let { field: sortName, direction: sortDirection } = (sort || {}) as {
field: keyof T;
direction: Direction;
From 085eed92d571c66c450efef682d22661ebe1e9bb Mon Sep 17 00:00:00 2001
From: Chandler Prall
Date: Thu, 17 Sep 2020 13:45:40 -0600
Subject: [PATCH 2/3] Update
src-docs/src/views/tables/in_memory/in_memory_controlled_pagination_section.js
Co-authored-by: Greg Thompson
---
.../tables/in_memory/in_memory_controlled_pagination_section.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src-docs/src/views/tables/in_memory/in_memory_controlled_pagination_section.js b/src-docs/src/views/tables/in_memory/in_memory_controlled_pagination_section.js
index 33d2c31ba8d..2867c236802 100644
--- a/src-docs/src/views/tables/in_memory/in_memory_controlled_pagination_section.js
+++ b/src-docs/src/views/tables/in_memory/in_memory_controlled_pagination_section.js
@@ -29,7 +29,7 @@ export const controlledPaginationSection = {
this behavior the pagination object optionally takes a
pageIndex value to control this yourself.
Additionally, pageSize can also be controlled the
- same way. available. Both of these are provided to your app during the
+ same way. Both of these are provided to your app during the
onTableChange callback.
From 5f3594b61bd90aad4fe6f227c02c60631d37fada Mon Sep 17 00:00:00 2001
From: Chandler Prall
Date: Thu, 17 Sep 2020 13:48:28 -0600
Subject: [PATCH 3/3] changelog
---
CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6a4ce2176e1..625c34aeb33 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
- Added `boolean` type to the `notification` prop of `EuiHeaderSectionItemButton` to show a simple dot ([#4008](https://github.com/elastic/eui/pull/4008))
- Added `popoverButton` and `popoverButtonBreakpoints` props to `EuiSelectableTemplateSitewide` for responsive capabilities ([#4008](https://github.com/elastic/eui/pull/4008))
- Added `isWithinMaxBreakpoint` service ([#4008](https://github.com/elastic/eui/pull/4008))
+- Added controlled pagination props to `EuiInMemoryTablee` ([#4038](https://github.com/elastic/eui/pull/4038))
**Bug fixes**