diff --git a/CHANGELOG.md b/CHANGELOG.md index e62bed01beb..7aa2ccb1973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,17 @@ - Added new `renderCellPopover` prop to `EuiDataGrid` ([#5640](https://github.com/elastic/eui/pull/5640)) - Added cell `schema` info to `EuiDataGrid`'s `renderCellValue` props ([#5640](https://github.com/elastic/eui/pull/5640)) - Added `isLoading` prop to `EuiDualRange` ([#5648](https://github.com/elastic/eui/pull/5648)) +- Added an `'all'` option to `EuiTablePagination.itemsPerPage` and `itemsPerPageOptions` to render a "Show all" option and updated `EuiBasicTable` and `EuiDataGrid` usages ([#5547](https://github.com/elastic/eui/issues/5547)) + +**Bug fixes** + +- Fixed `EuiImage` images' width in small containers by adding `max-width: 100%` ([#5547](https://github.com/elastic/eui/issues/5547)) +- Fixed `EuiTablePagination` layout in small containers by adding `wrap` ([#5547](https://github.com/elastic/eui/issues/5547)) **Breaking changes** - Removed `popoverContents` props from `EuiDataGrid` (use new `renderCellPopover` instead) ([#5640](https://github.com/elastic/eui/pull/5640)) +- Changed the `EuiTablePagination` prop `hidePerPageOptions` to the positive form `showPerPageOptions` ([#5547](https://github.com/elastic/eui/issues/5547)) ## [`48.1.1`](https://github.com/elastic/eui/tree/v48.1.1) diff --git a/src-docs/src/images/pagination_filters.gif b/src-docs/src/images/pagination_filters.gif new file mode 100644 index 00000000000..1bff08c6751 Binary files /dev/null and b/src-docs/src/images/pagination_filters.gif differ diff --git a/src-docs/src/images/pagination_infinite_do.svg b/src-docs/src/images/pagination_infinite_do.svg new file mode 100644 index 00000000000..4b5d2cbbdc1 --- /dev/null +++ b/src-docs/src/images/pagination_infinite_do.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src-docs/src/images/pagination_infinite_dont.svg b/src-docs/src/images/pagination_infinite_dont.svg new file mode 100644 index 00000000000..62c2c2be304 --- /dev/null +++ b/src-docs/src/images/pagination_infinite_dont.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src-docs/src/views/datagrid/in_memory_pagination.js b/src-docs/src/views/datagrid/in_memory_pagination.js index 016d66863ea..f901bcdd4fe 100644 --- a/src-docs/src/views/datagrid/in_memory_pagination.js +++ b/src-docs/src/views/datagrid/in_memory_pagination.js @@ -117,7 +117,7 @@ export default () => { sorting={{ columns: sortingColumns, onSort }} pagination={{ ...pagination, - pageSizeOptions: [10, 50, 100], + pageSizeOptions: [10, 50, 'all'], onChangeItemsPerPage: onChangeItemsPerPage, onChangePage: onChangePage, }} diff --git a/src-docs/src/views/pagination/guidelines.js b/src-docs/src/views/pagination/guidelines.js new file mode 100644 index 00000000000..fe909a3bd9e --- /dev/null +++ b/src-docs/src/views/pagination/guidelines.js @@ -0,0 +1,384 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { + GuideRule, + GuideRuleExample, + GuideSectionTypes, +} from '../../components'; + +import { + EuiText, + EuiSpacer, + EuiPanel, + EuiImage, + EuiHorizontalRule, + EuiBasicTable, + EuiLink, + EuiCode, + EuiButtonEmpty, + EuiContextMenuPanel, + EuiContextMenuItem, +} from '../../../../src/components'; + +import animatedGif from '../../images/pagination_filters.gif'; +import infiniteDo from '../../images/pagination_infinite_do.svg'; +import infiniteDont from '../../images/pagination_infinite_dont.svg'; + +import Table from './paginated_table'; +import { GuideSection } from '../../components/guide_section/guide_section'; +const source = require('!!raw-loader!./paginated_table'); + +const items = [ + + 10 rows + , + + 20 rows + , + + 50 rows + , +]; + +const tooManyItems = [ + + 10 rows + , + + 15 rows + , + + 20 rows + , + + 30 rows + , + + 50 rows + , +]; + +export default () => ( + <> + +

+ Don’t rely on pagination for users to find what they’re looking for +

+

+ For any results-style table, always provide ways to filter, search, etc + for the thing that the user wants. Pagination is only helpful once the + user has reduced the 1000+ results to just 100 (for example). +

+
+ + + + + + + + Full prototype + + + + + + +

Don’t rely on pagination to indicate total results

+

+ When possible, always present a clear indicator of how many (and if not + all results) have been returned. Just a simple count will do. Including + a detailed summary of results at the top of the table or list goes a + long way to signify what paging can’t. +

+

Indicate indeterminate results

+

+ If you cannot provide a concrete number of results, you still have to + communicate what the current results showcase. For instance, say + "Showing first 100 results" or "Search results maxed at + 1000" or "Results fetched at runtime". +

+

+ + Remember that not all users understand how your data API works. They + just care about the data that's being shown to them. + +

+
+ + + + + + + + +

Give users control of pagination

+

+ Providing a{' '} + + “Rows per page” option + {' '} + is often helpful enough to provide users control over the amount of data + they see at once. +

+

+ Keep the choices simple and only show “Rows per page” if there are more + rows than the smallest option. For example, if there are only 9 rows and + the smallest option is 10 rows per page, hide the selector. +

+
+ + + +
+ + Rows per page: 10 + + + + + +
+
+ +
+ + Rows per page: 10 + + + + + +
+
+
+ + + + +

Optimize your defaults

+

+ Most users don’t customize the default view. Therefore, it’s vital that + you provide optimal defaults and reduce complexity as the number of + entries increase. This means choosing a default “Rows per page” that + best corresponds to the total results. For instance, 1000+ results + shouldn’t start with 10 rows per page, but rather 20 or 50. +

+

+ Here are some samples of what controls to provide based + on the number of data entries. +

+
+ + + + rows, + valign: 'top', + }, + { + field: 'style', + name: 'Pagination style', + render: (style) => style, + valign: 'top', + }, + ]} + items={[ + { + entries: '0', + rows: ( + + Use{' '} + + EuiEmptyPrompt + {' '} + in place of table + + ), + style: 'N/A', + }, + { + entries: 'Less than 50', + rows: 'Show 10, but allow All', + style: Numbered, + }, + { + entries: '51 - 100', + rows: '10, 20, All', + style: ( + + Numbered + + ), + }, + { + entries: '101 - 200', + rows: '10, 20, 50', + style: ( + + + Numbered + {' '} + or{' '} + + Compressed + + + ), + }, + { + entries: 'More than 200', + rows: '20, 50, 100', + style: ( + + + Numbered + {' '} + or{' '} + + Indeterminate + + + ), + }, + { + entries: 'Unknown', + rows: ( + + Depends on what you expect the total entries to be + + ), + style: ( + + Indeterminate + + ), + }, + ]} + /> + + + + +

+ If the total results are unknown, you can make a best guess based on the + context of that specific table, whether there’s{' '} + most likely going to be tens or thousands of results. + From there you can decide to show 10 rows per page or 20 by default. +

+

+ + The complexity of the data will also contribute to this equation, + which is why the table above is just a sample. + +

+
+ + + + + + + + +

Preserve the user-customized state of pagination

+

+ When providing pagination, customizable display options, and data + filters, always save the user’s state in some form. This is especially + important if your data includes links that navigate a user away from the + current view. There’s nothing more frustrating for users than going back + to find their filters and pagination have been reset. +

+

+ Below is a working example that utilizes localStorage{' '} + to save the table’s state. +

+
+ + + + } + source={[ + { + type: GuideSectionTypes.JS, + code: source, + }, + ]} + /> + + + + + + + + + + + + +); diff --git a/src-docs/src/views/pagination/paginated_table.js b/src-docs/src/views/pagination/paginated_table.js new file mode 100644 index 00000000000..408dc416603 --- /dev/null +++ b/src-docs/src/views/pagination/paginated_table.js @@ -0,0 +1,128 @@ +import React, { useState } from 'react'; +import { fake } from 'faker'; + +import { + EuiBasicTable, + EuiLink, + EuiSpacer, + EuiHorizontalRule, + EuiText, +} from '../../../../src'; +import { formatDate } from '../../../../src/services'; + +const PAGE_INDEX_KEY = 'paginationGuide_currentPage'; +const PAGE_COUNT_KEY = 'paginationGuide_pageCount'; + +const raw_data = []; + +for (let i = 1; i < 25; i++) { + const name = fake('{{name.lastName}}, {{name.firstName}}'); + const suffix = fake('{{name.suffix}}'); + raw_data.push({ + name: { + formatted: `${name} ${suffix}`, + raw: name, + }, + location: ( + + {`${fake('{{address.city}}')}, `} + + {fake('{{address.country}}')} + + + ), + date: fake('{{date.past}}'), + amount: fake('${{commerce.price}}'), + }); +} + +export default () => { + const [pageIndex, setPageIndex] = useState( + Number(localStorage.getItem(PAGE_INDEX_KEY) || 0) + ); + const [pageSize, setPageSize] = useState( + Number(localStorage.getItem(PAGE_COUNT_KEY) || 10) + ); + + const onTableChange = ({ page = {} }) => { + const { index: pageIndex, size: pageSize } = page; + + setPageIndex(pageIndex); + setPageSize(pageSize); + localStorage.setItem(PAGE_INDEX_KEY, String(pageIndex)); + localStorage.setItem(PAGE_COUNT_KEY, String(pageSize)); + }; + + const totalItemCount = raw_data.length; + const startIndex = pageIndex * pageSize; + const pageOfItems = + pageSize === 'all' + ? raw_data + : raw_data.slice( + startIndex, + Math.min(startIndex + pageSize, totalItemCount) + ); + + const columns = [ + { + field: 'name', + name: 'Name', + truncateText: true, + render: (Name) => Name.formatted, + }, + { + field: 'location', + name: 'Location', + render: (location) => location, + }, + { + field: 'date', + name: 'Date', + dataType: 'date', + render: (date) => formatDate(date, 'dobLong'), + }, + { + field: 'amount', + name: 'Amount', + dataType: 'number', + width: '100px', + }, + ]; + + const pagination = { + pageIndex, + pageSize, + totalItemCount, + pageSizeOptions: [10, 'all'], + }; + + const resultsCount = + pageSize === 'all' ? ( + All + ) : ( + <> + + {pageSize * pageIndex + 1}- + {Math.min(pageSize * pageIndex + pageSize, totalItemCount)} + {' '} + of {totalItemCount} + + ); + + return ( +
+ + Showing {resultsCount} Users + + + + +
+ ); +}; diff --git a/src-docs/src/views/pagination/pagination_example.js b/src-docs/src/views/pagination/pagination_example.js index bd8d8213dab..69d0cedd296 100644 --- a/src-docs/src/views/pagination/pagination_example.js +++ b/src-docs/src/views/pagination/pagination_example.js @@ -8,9 +8,11 @@ import { EuiPagination, EuiText, EuiCallOut, + EuiTablePagination, } from '../../../../src/components'; -import { paginationConfig } from './playground'; +import Guidelines from './guidelines'; +import { paginationConfig, tablePaginationConfig } from './playground'; import ManyPages from './many_pages'; const manyPagesSource = require('!!raw-loader!./many_pages'); @@ -74,8 +76,12 @@ const indeterminateSnippet = ` goToPage(activePage)} />`; +import TablePagination from './table_pagination'; +const tablePaginationSource = require('!!raw-loader!./table_pagination'); + export const PaginationExample = { title: 'Pagination', + guidelines: , intro: (

@@ -216,6 +222,31 @@ export const PaginationExample = { demo: , props: { EuiPagination }, }, + { + title: 'Table pagination', + source: [ + { + type: GuideSectionTypes.TSX, + code: tablePaginationSource, + }, + ], + text: ( +

+ You can use EuiTablePagination to create a + combination "Rows per page" and pagination set, commonly + used with{' '} + + tables + + . If you pass {"'all'"} in as one of the{' '} + itemsPerPageOptions, it will create a "Show + all" option and hide the pagination. +

+ ), + demo: , + props: { EuiTablePagination }, + playground: tablePaginationConfig, + }, { title: 'Customizable pagination', source: [ @@ -226,7 +257,7 @@ export const PaginationExample = { ], text: (

- You can use{' '} + Or you can use{' '} EuiFlexGroup {' '} @@ -234,8 +265,7 @@ export const PaginationExample = { EuiContextMenu {' '} - to set up this pagination layout, commonly used with{' '} - tables. + to set up your own custom pagination layout.

), demo: , diff --git a/src-docs/src/views/pagination/playground.js b/src-docs/src/views/pagination/playground.js index feb74b33fcb..5e58199c02f 100644 --- a/src-docs/src/views/pagination/playground.js +++ b/src-docs/src/views/pagination/playground.js @@ -1,4 +1,4 @@ -import { EuiPagination } from '../../../../src/components/'; +import { EuiPagination, EuiTablePagination } from '../../../../src/components/'; import { propUtilityForPlayground, dummyFunction, @@ -36,3 +36,33 @@ export const paginationConfig = () => { }, }; }; + +export const tablePaginationConfig = () => { + const docgenInfo = Array.isArray(EuiTablePagination.__docgenInfo) + ? EuiTablePagination.__docgenInfo[0] + : EuiTablePagination.__docgenInfo; + const propsToUse = propUtilityForPlayground(docgenInfo.props); + + propsToUse.pageCount = { + ...propsToUse.pageCount, + value: 22, + }; + + return { + config: { + componentName: 'EuiTablePagination', + props: propsToUse, + scope: { + EuiTablePagination, + }, + imports: { + '@elastic/eui': { + named: ['EuiTablePagination'], + }, + }, + customProps: { + onPageClick: dummyFunction, + }, + }, + }; +}; diff --git a/src-docs/src/views/pagination/table_pagination.tsx b/src-docs/src/views/pagination/table_pagination.tsx new file mode 100644 index 00000000000..4ae75bd63fb --- /dev/null +++ b/src-docs/src/views/pagination/table_pagination.tsx @@ -0,0 +1,31 @@ +import React, { useState } from 'react'; + +import { EuiTablePagination } from '../../../../src'; + +export default () => { + const totalEntries = 1250; + const [activePage, setActivePage] = useState(0); + const [rowSize, setRowSize] = useState(50); + const [pageCount, setPageCount] = useState(Math.ceil(totalEntries / 50)); + + const goToPage = (pageNumber: number) => setActivePage(pageNumber); + const changeItemsPerPage = (pageSize: number | 'all') => { + const pageCount = + pageSize === 'all' ? 1 : Math.ceil(totalEntries / pageSize); + setPageCount(pageCount); + setRowSize(pageSize); + setActivePage(0); + }; + + return ( + + ); +}; diff --git a/src-docs/src/views/tables/data_store.js b/src-docs/src/views/tables/data_store.js index a9d1b73082a..43c4a3ba2b1 100644 --- a/src-docs/src/views/tables/data_store.js +++ b/src-docs/src/views/tables/data_store.js @@ -112,7 +112,7 @@ export const createDataStore = () => { let pageOfItems; - if (!pageIndex && !pageSize) { + if ((!pageIndex && !pageSize) || pageSize === 'all') { pageOfItems = items; } else { const startIndex = pageIndex * pageSize; 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 index 682805b7403..217516603d4 100644 --- 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 @@ -113,7 +113,10 @@ export const Table = () => { } items={users} columns={columns} - pagination={pagination} + pagination={{ + ...pagination, + pageSizeOptions: [10, 20, 'all'], + }} sorting={sorting} /> ); diff --git a/src-docs/src/views/tables/paginated/paginated.js b/src-docs/src/views/tables/paginated/paginated.js index b52dae09357..c514f791227 100644 --- a/src-docs/src/views/tables/paginated/paginated.js +++ b/src-docs/src/views/tables/paginated/paginated.js @@ -11,6 +11,8 @@ import { EuiFlexItem, EuiSpacer, EuiSwitch, + EuiHorizontalRule, + EuiText, } from '../../../../../src/components'; /* @@ -39,7 +41,7 @@ const store = createDataStore(); export const Table = () => { const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(5); + const [pageSize, setPageSize] = useState(10); const [showPerPageOptions, setShowPerPageOptions] = useState(true); const onTableChange = ({ page = {} }) => { @@ -132,10 +134,22 @@ export const Table = () => { pageIndex, pageSize, totalItemCount, - pageSizeOptions: [3, 5, 8], - hidePerPageOptions: !showPerPageOptions, + pageSizeOptions: [10, 'all'], + showPerPageOptions, }; + const resultsCount = + pageSize === 'all' ? ( + All + ) : ( + <> + + {pageSize * pageIndex + 1}-{pageSize * pageIndex + pageSize} + {' '} + of {totalItemCount} + + ); + return (
{ label={ Hide per page options with{' '} - pagination.hidePerPageOptions = true + pagination.showPerPageOptions = false } onChange={togglePerPageOptions} /> + + Showing {resultsCount} Users + + + , intro: (

@@ -167,5 +168,4 @@ export const TourExample = { }, }, ], - guidelines: , }; diff --git a/src/components/basic_table/__snapshots__/basic_table.test.tsx.snap b/src/components/basic_table/__snapshots__/basic_table.test.tsx.snap index 64d8243600d..c0b432625a9 100644 --- a/src/components/basic_table/__snapshots__/basic_table.test.tsx.snap +++ b/src/components/basic_table/__snapshots__/basic_table.test.tsx.snap @@ -2371,6 +2371,118 @@ exports[`EuiBasicTable with pagination - 2nd page 1`] = `

`; +exports[`EuiBasicTable with pagination - show all 1`] = ` +
+
+ + + + + + + + + + + + + + + + + Name + + + + + + name1 + + + + + name2 + + + + +
+ + + +
+`; + exports[`EuiBasicTable with pagination 1`] = `
- - : - 2 + + + : + 2 + } closePopover={[Function]} @@ -1686,7 +1689,6 @@ exports[`EuiInMemoryTable with pagination 1`] = ` onChange={[Function]} pagination={ Object { - "hidePerPageOptions": undefined, "pageIndex": 0, "pageSize": 2, "pageSizeOptions": Array [ @@ -1694,6 +1696,7 @@ exports[`EuiInMemoryTable with pagination 1`] = ` 4, 6, ], + "showPerPageOptions": undefined, "totalItemCount": 3, } } @@ -1702,6 +1705,178 @@ exports[`EuiInMemoryTable with pagination 1`] = ` /> `; +exports[`EuiInMemoryTable with pagination and "show all" page size 1`] = ` +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + +
+
+ + + Name + + + description + + +
+
+ Name +
+
+ + name1 + +
+
+
+ Name +
+
+ + name2 + +
+
+
+ Name +
+
+ + name3 + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+`; + exports[`EuiInMemoryTable with pagination and default page size and index 1`] = ` +
+`; + +exports[`PaginationBar render - show all pageSize 1`] = ` +
+ +
`; diff --git a/src/components/basic_table/basic_table.test.tsx b/src/components/basic_table/basic_table.test.tsx index 9dd4e535efe..e379fb6c338 100644 --- a/src/components/basic_table/basic_table.test.tsx +++ b/src/components/basic_table/basic_table.test.tsx @@ -304,6 +304,32 @@ describe('EuiBasicTable', () => { expect(component).toMatchSnapshot(); }); + test('with pagination - show all', () => { + const props: EuiBasicTableProps = { + items: [ + { id: '1', name: 'name1' }, + { id: '2', name: 'name2' }, + ], + columns: [ + { + field: 'name', + name: 'Name', + description: 'description', + }, + ], + pagination: { + pageIndex: 0, + pageSize: 'all', + pageSizeOptions: [1, 5, 'all'], + totalItemCount: 2, + }, + onChange: () => {}, + }; + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + test('with pagination and error', () => { const props: EuiBasicTableProps = { items: [ @@ -349,7 +375,7 @@ describe('EuiBasicTable', () => { pageIndex: 0, pageSize: 3, totalItemCount: 5, - hidePerPageOptions: true, + showPerPageOptions: false, }, onChange: () => {}, }; diff --git a/src/components/basic_table/basic_table.tsx b/src/components/basic_table/basic_table.tsx index 9a69a992f08..efd47f674c1 100644 --- a/src/components/basic_table/basic_table.tsx +++ b/src/components/basic_table/basic_table.tsx @@ -165,7 +165,7 @@ export interface Criteria { */ page?: { index: number; - size: number; + size: number | 'all'; }; /** * If the shown items are sorted, this describes the sort criteria @@ -182,7 +182,7 @@ export interface CriteriaWithPagination extends Criteria { */ page: { index: number; - size: number; + size: number | 'all'; }; } @@ -459,7 +459,7 @@ export class EuiBasicTable extends Component< this.changeSelection([]); } - onPageSizeChange(size: number) { + onPageSizeChange(size: number | 'all') { this.clearSelection(); const currentCriteria = this.buildCriteria(this.props); const criteria: CriteriaWithPagination = { @@ -657,6 +657,14 @@ export class EuiBasicTable extends Component< renderTableCaption() { const { items, pagination, tableCaption } = this.props; + const itemCount = items.length; + const totalItemCount = pagination ? pagination.totalItemCount : itemCount; + const page = pagination ? pagination.pageIndex + 1 : 1; + const pageCount = + typeof pagination?.pageSize === 'number' + ? Math.ceil(pagination.totalItemCount / pagination.pageSize) + : 1; + let captionElement; if (tableCaption) { if (pagination) { @@ -664,13 +672,7 @@ export class EuiBasicTable extends Component< ); } else { @@ -683,14 +685,7 @@ export class EuiBasicTable extends Component< ); } else { @@ -698,13 +693,7 @@ export class EuiBasicTable extends Component< ); } @@ -713,9 +702,7 @@ export class EuiBasicTable extends Component< ); } @@ -961,10 +948,12 @@ export class EuiBasicTable extends Component< const rows = items.map((item: T, index: number) => { // if there's pagination the item's index must be adjusted to the where it is in the whole dataset - const tableItemIndex = hasPagination(this.props) - ? this.props.pagination.pageIndex * this.props.pagination.pageSize + - index - : index; + const tableItemIndex = + hasPagination(this.props) && + typeof this.props.pagination.pageSize === 'number' + ? this.props.pagination.pageIndex * this.props.pagination.pageSize + + index + : index; return this.renderItemRow(item, tableItemIndex); }); return {rows}; diff --git a/src/components/basic_table/in_memory_table.test.tsx b/src/components/basic_table/in_memory_table.test.tsx index a768338c2fa..70a8dc2641c 100644 --- a/src/components/basic_table/in_memory_table.test.tsx +++ b/src/components/basic_table/in_memory_table.test.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import { mount, shallow } from 'enzyme'; +import { mount, shallow, render } from 'enzyme'; import { requiredProps } from '../../test'; import { EuiInMemoryTable, EuiInMemoryTableProps } from './in_memory_table'; @@ -224,6 +224,31 @@ describe('EuiInMemoryTable', () => { expect(component).toMatchSnapshot(); }); + test('with pagination and "show all" page size', () => { + const props: EuiInMemoryTableProps = { + ...requiredProps, + items: [ + { id: '1', name: 'name1' }, + { id: '2', name: 'name2' }, + { id: '3', name: 'name3' }, + ], + columns: [ + { + field: 'name', + name: 'Name', + description: 'description', + }, + ], + pagination: { + initialPageSize: 'all', + pageSizeOptions: [1, 2, 3, 'all'], + }, + }; + const component = render(); + + expect(component).toMatchSnapshot(); + }); + test('with pagination, default page size and error', () => { const props: EuiInMemoryTableProps = { ...requiredProps, @@ -262,7 +287,7 @@ describe('EuiInMemoryTable', () => { }, ], pagination: { - hidePerPageOptions: true, + showPerPageOptions: false, }, }; const component = shallow(); diff --git a/src/components/basic_table/in_memory_table.tsx b/src/components/basic_table/in_memory_table.tsx index 548840a2e9a..49ca5c4c498 100644 --- a/src/components/basic_table/in_memory_table.tsx +++ b/src/components/basic_table/in_memory_table.tsx @@ -31,6 +31,7 @@ import { EuiSpacer } from '../spacer'; import { CommonProps } from '../common'; import { EuiSearchBarProps } from '../search_bar/search_bar'; import { SchemaType } from '../search_bar/search_box'; +import { EuiTablePaginationProps } from '../table'; interface onChangeArgument { query: Query | null; @@ -46,13 +47,12 @@ function isEuiSearchBarProps( export type Search = boolean | EuiSearchBarProps; -interface PaginationOptions { - pageSizeOptions?: number[]; - hidePerPageOptions?: boolean; +interface PaginationOptions extends EuiTablePaginationProps { + pageSizeOptions?: Array; initialPageIndex?: number; - initialPageSize?: number; + initialPageSize?: number | 'all'; pageIndex?: number; - pageSize?: number; + pageSize?: number | 'all'; } type Pagination = boolean | PaginationOptions; @@ -114,12 +114,12 @@ interface State { search?: Search; query: Query | null; pageIndex: number; - pageSize?: number; - pageSizeOptions?: number[]; + pageSize?: number | 'all'; + pageSizeOptions?: Array; sortName: ReactNode; sortDirection?: Direction; allowNeutralSort: boolean; - hidePerPageOptions: boolean | undefined; + showPerPageOptions: boolean | undefined; } const getQueryFromSearch = ( @@ -151,7 +151,7 @@ const getInitialPagination = (pagination: Pagination | undefined) => { const { pageSizeOptions = paginationBarDefaults.pageSizeOptions, - hidePerPageOptions, + showPerPageOptions, } = pagination as PaginationOptions; const defaultPageSize = pageSizeOptions @@ -168,7 +168,7 @@ const getInitialPagination = (pagination: Pagination | undefined) => { : pagination.pageSize || pagination.initialPageSize || defaultPageSize; if ( - !hidePerPageOptions && + showPerPageOptions && initialPageSize && (!pageSizeOptions || !pageSizeOptions.includes(initialPageSize)) ) { @@ -181,7 +181,7 @@ const getInitialPagination = (pagination: Pagination | undefined) => { pageIndex: initialPageIndex, pageSize: initialPageSize, pageSizeOptions, - hidePerPageOptions, + showPerPageOptions, }; }; @@ -355,7 +355,7 @@ export class EuiInMemoryTable extends Component< pageIndex, pageSize, pageSizeOptions, - hidePerPageOptions, + showPerPageOptions, } = getInitialPagination(pagination); const { sortName, sortDirection } = getInitialSorting(columns, sorting); @@ -374,7 +374,7 @@ export class EuiInMemoryTable extends Component< sortName, sortDirection, allowNeutralSort: allowNeutralSort !== false, - hidePerPageOptions, + showPerPageOptions, }; this.tableRef = React.createRef(); @@ -389,7 +389,7 @@ export class EuiInMemoryTable extends Component< onTableChange = ({ page, sort }: Criteria) => { let { index: pageIndex, size: pageSize } = (page || {}) as { index: number; - size: number; + size: number | 'all'; }; // don't apply pagination changes that are otherwise controlled @@ -583,7 +583,7 @@ export class EuiInMemoryTable extends Component< : matchingItems; const visibleItems = - pageSize && this.props.pagination + typeof pageSize === 'number' && this.props.pagination ? (() => { const startIndex = pageIndex * pageSize; return sortedItems.slice( @@ -631,7 +631,7 @@ export class EuiInMemoryTable extends Component< pageSizeOptions, sortName, sortDirection, - hidePerPageOptions, + showPerPageOptions, } = this.state; const { items, totalItemCount } = this.getItems(); @@ -643,7 +643,7 @@ export class EuiInMemoryTable extends Component< pageSize: pageSize || 1, pageSizeOptions, totalItemCount, - hidePerPageOptions, + showPerPageOptions, }; // Data loaded from a server can have a default sort order which is meaningful to the diff --git a/src/components/basic_table/pagination_bar.test.tsx b/src/components/basic_table/pagination_bar.test.tsx index 76ca4a06673..3e39ae01e49 100644 --- a/src/components/basic_table/pagination_bar.test.tsx +++ b/src/components/basic_table/pagination_bar.test.tsx @@ -54,7 +54,25 @@ describe('PaginationBar', () => { pageIndex: 0, pageSize: 5, totalItemCount: 0, - hidePerPageOptions: true, + showPerPageOptions: false, + }, + onPageSizeChange: () => {}, + onPageChange: () => {}, + }; + + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + + test('render - show all pageSize', () => { + const props = { + ...requiredProps, + pagination: { + pageIndex: 0, + pageSize: 'all' as const, + pageSizeOptions: [1, 5, 'all' as const], + totalItemCount: 5, }, onPageSizeChange: () => {}, onPageChange: () => {}, diff --git a/src/components/basic_table/pagination_bar.tsx b/src/components/basic_table/pagination_bar.tsx index f4695ed3c93..5c4972e80da 100644 --- a/src/components/basic_table/pagination_bar.tsx +++ b/src/components/basic_table/pagination_bar.tsx @@ -20,21 +20,23 @@ export interface Pagination { */ pageIndex: number; /** - * The maximum number of items that can be shown in a single page + * The maximum number of items that can be shown in a single page. + * Pass `'all'` to display the selected "Show all" option and hide the pagination. */ - pageSize: number; + pageSize: number | 'all'; /** * The total number of items the page is "sliced" of */ totalItemCount: number; /** - * Configures the page size dropdown options + * Configures the page size dropdown options. + * Pass `'all'` as one of the options to create a "Show all" option. */ - pageSizeOptions?: number[]; + pageSizeOptions?: Array; /** * Hides the page size dropdown */ - hidePerPageOptions?: boolean; + showPerPageOptions?: boolean; } export interface PaginationBarProps { @@ -62,11 +64,14 @@ export const PaginationBar = ({ const pageSizeOptions = pagination.pageSizeOptions ? pagination.pageSizeOptions : defaults.pageSizeOptions; - const pageCount = Math.ceil(pagination.totalItemCount / pagination.pageSize); + const pageCount = + pagination.pageSize === 'all' + ? 1 + : Math.ceil(pagination.totalItemCount / pagination.pageSize); useEffect(() => { if (pageCount < pagination.pageIndex + 1) { - onPageChange(pageCount - 1); + onPageChange?.(pageCount - 1); } }, [pageCount, onPageChange, pagination]); @@ -75,7 +80,7 @@ export const PaginationBar = ({
( const interactiveCellId = useGeneratedHtmlId(); const ariaLabelledById = useGeneratedHtmlId(); + const ariaPage = pagination ? pagination.pageIndex + 1 : 1; + const ariaPageCount = + typeof pagination?.pageSize === 'number' + ? Math.ceil(rowCount / pagination.pageSize) + : 1; const ariaLabel = useEuiI18n( 'euiDataGrid.ariaLabel', '{label}; Page {page} of {pageCount}.', - { - label: rest['aria-label'], - page: pagination ? pagination.pageIndex + 1 : 0, - pageCount: pagination ? Math.ceil(rowCount / pagination.pageSize) : 0, - } + { label: rest['aria-label'], page: ariaPage, pageCount: ariaPageCount } ); - const ariaLabelledBy = useEuiI18n( 'euiDataGrid.ariaLabelledBy', 'Page {page} of {pageCount}.', - { - page: pagination ? pagination.pageIndex + 1 : 0, - pageCount: pagination ? Math.ceil(rowCount / pagination.pageSize) : 0, - } + { page: ariaPage, pageCount: ariaPageCount } ); // extract aria-label and/or aria-labelledby from `rest` @@ -413,11 +410,10 @@ export const EuiDataGrid = forwardRef( renderCellValue={renderCellValue} columns={columns} rowCount={ - inMemory.level === 'enhancements' - ? // if `inMemory.level === enhancements` then we can only be sure the pagination's pageSize is available in memory - pagination?.pageSize || rowCount - : // otherwise, all of the data is present and usable - rowCount + inMemory.level === 'enhancements' && // if `inMemory.level === enhancements` then we can only be sure the pagination's pageSize is available in memory + typeof pagination?.pageSize === 'number' // If pageSize is set to 'all' instead of a number, then all rows are being displayed + ? pagination?.pageSize || rowCount + : rowCount // otherwise, all of the data is present and usable } onCellRender={onCellRender} /> diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index 53f380f0e84..9a3f8ef8e19 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -766,18 +766,20 @@ export interface EuiDataGridPaginationProps { */ pageIndex: number; /** - * How many rows should initially be shown per page + * How many rows should initially be shown per page. + * Pass `'all'` to display the selected "Show all" option and hide the pagination. */ - pageSize: number; + pageSize: number | 'all'; /** * An array of page sizes the user can select from. - * Leave this prop undefined or use an empty array to hide "Rows per page" select button + * Pass `'all'` as one of the options to create a "Show all" option. + * Leave this prop undefined or use an empty array to hide "Rows per page" select button. */ - pageSizeOptions?: number[]; + pageSizeOptions?: Array; /** * A callback for when the user changes the page size selection */ - onChangeItemsPerPage: (itemsPerPage: number) => void; + onChangeItemsPerPage: (itemsPerPage: number | 'all') => void; /** * A callback for when the current page index changes */ diff --git a/src/components/datagrid/utils/data_grid_pagination.test.tsx b/src/components/datagrid/utils/data_grid_pagination.test.tsx index 1515ea44e75..6a312453dac 100644 --- a/src/components/datagrid/utils/data_grid_pagination.test.tsx +++ b/src/components/datagrid/utils/data_grid_pagination.test.tsx @@ -36,7 +36,6 @@ describe('EuiDataGridPaginationRenderer', () => { activePage={0} aria-controls="data-grid-id" aria-label="Pagination for preceding grid" - hidePerPageOptions={false} itemsPerPage={25} itemsPerPageOptions={ Array [ @@ -46,6 +45,7 @@ describe('EuiDataGridPaginationRenderer', () => { onChangeItemsPerPage={[MockFunction]} onChangePage={[Function]} pageCount={4} + showPerPageOptions={true} />
`); @@ -63,7 +63,6 @@ describe('EuiDataGridPaginationRenderer', () => { activePage={0} aria-controls="data-grid-id" aria-label="Pagination for preceding grid: Test Grid" - hidePerPageOptions={false} itemsPerPage={25} itemsPerPageOptions={ Array [ @@ -73,6 +72,7 @@ describe('EuiDataGridPaginationRenderer', () => { onChangeItemsPerPage={[MockFunction]} onChangePage={[Function]} pageCount={4} + showPerPageOptions={true} />
`); @@ -90,12 +90,45 @@ describe('EuiDataGridPaginationRenderer', () => { activePage={0} aria-controls="data-grid-id" aria-label="Pagination for preceding grid" - hidePerPageOptions={true} itemsPerPage={25} itemsPerPageOptions={Array []} onChangeItemsPerPage={[MockFunction]} onChangePage={[Function]} pageCount={4} + showPerPageOptions={false} + /> +
+ `); + }); + + it('handles the "all" page size option', () => { + const component = shallow( + + ); + expect(component).toMatchInlineSnapshot(` +
+
`); diff --git a/src/components/datagrid/utils/data_grid_pagination.tsx b/src/components/datagrid/utils/data_grid_pagination.tsx index 2f1aa84bde5..3e4e960a2db 100644 --- a/src/components/datagrid/utils/data_grid_pagination.tsx +++ b/src/components/datagrid/utils/data_grid_pagination.tsx @@ -42,9 +42,17 @@ export const EuiDataGridPaginationRenderer = ({ [setFocusedCell, _onChangePage] ); - const pageCount = Math.ceil(rowCount / pageSize); + const pageCount = + typeof pageSize === 'number' ? Math.ceil(rowCount / pageSize) : 1; const minSizeOption = - pageSizeOptions && [...pageSizeOptions].sort((a, b) => a - b)[0]; + pageSizeOptions?.length && + [...pageSizeOptions].reduce((a, b) => { + // Account for 'all' strings + if (typeof b !== 'number') return a; + if (typeof a !== 'number') return b; + // Find the smallest number + return Math.min(a, b); + }); if (rowCount < (minSizeOption || pageSize)) { /** @@ -63,7 +71,7 @@ export const EuiDataGridPaginationRenderer = ({ 0) { diff --git a/src/components/datagrid/utils/grid_height_width.ts b/src/components/datagrid/utils/grid_height_width.ts index 24aad7108bf..e4d94460a63 100644 --- a/src/components/datagrid/utils/grid_height_width.ts +++ b/src/components/datagrid/utils/grid_height_width.ts @@ -10,6 +10,7 @@ import { useEffect, useState, useContext, MutableRefObject } from 'react'; import { IS_JEST_ENVIRONMENT } from '../../../test'; import { useUpdateEffect, useForceRender } from '../../../services'; import { useResizeObserver } from '../../observer/resize_observer'; +import { EuiTablePaginationProps } from '../../table/table_pagination'; import { EuiDataGridRowHeightsOptions } from '../data_grid_types'; import { RowHeightUtils } from './row_heights'; import { DataGridSortingContext } from './sorting'; @@ -162,7 +163,7 @@ export const useUnconstrainedHeight = ({ export const useVirtualizeContainerWidth = ( virtualizeContainer: HTMLDivElement | null, gridWidth: number, - pageSize: number | undefined + pageSize: EuiTablePaginationProps['itemsPerPage'] ) => { const [virtualizeContainerWidth, setVirtualizeContainerWidth] = useState(0); useResizeObserver(virtualizeContainer); diff --git a/src/components/datagrid/utils/ref.ts b/src/components/datagrid/utils/ref.ts index 88aebb8eb8d..34f3ee0ebfa 100644 --- a/src/components/datagrid/utils/ref.ts +++ b/src/components/datagrid/utils/ref.ts @@ -135,7 +135,7 @@ export const useSortPageCheck = ( : rowIndex; // Account for pagination - if (pagination) { + if (pagination && pagination.pageSize !== 'all') { const pageIndex = Math.floor(visibleRowIndex / pagination.pageSize); // If the targeted row is on a different page than the current page, // we should automatically navigate the user to the correct page diff --git a/src/components/datagrid/utils/row_count.ts b/src/components/datagrid/utils/row_count.ts index 81bf03050b2..c32bc6062e2 100644 --- a/src/components/datagrid/utils/row_count.ts +++ b/src/components/datagrid/utils/row_count.ts @@ -15,11 +15,15 @@ export const computeVisibleRows = ({ pagination: EuiDataGridProps['pagination']; rowCount: EuiDataGridProps['rowCount']; }): EuiDataGridVisibleRows => { - const startRow = pagination ? pagination.pageIndex * pagination.pageSize : 0; + const startRow = + pagination && pagination.pageSize !== 'all' + ? pagination.pageIndex * pagination.pageSize + : 0; - let endRow = pagination - ? (pagination.pageIndex + 1) * pagination.pageSize - : rowCount; + let endRow = + pagination && pagination.pageSize !== 'all' + ? (pagination.pageIndex + 1) * pagination.pageSize + : rowCount; endRow = Math.min(endRow, rowCount); const visibleRowCount = endRow - startRow; diff --git a/src/components/image/_image.scss b/src/components/image/_image.scss index 3895964b2cd..94489b5397b 100644 --- a/src/components/image/_image.scss +++ b/src/components/image/_image.scss @@ -15,6 +15,7 @@ // Required for common usage of nesting within EuiText .euiImage__img { margin-bottom: 0; + max-width: 100%; } &.euiImage--hasShadow { diff --git a/src/components/pagination/pagination.tsx b/src/components/pagination/pagination.tsx index 5604444aa15..2f1e8aeeb0a 100644 --- a/src/components/pagination/pagination.tsx +++ b/src/components/pagination/pagination.tsx @@ -25,7 +25,7 @@ export type SafeClickHandler = (e: MouseEvent, pageIndex: number) => void; export interface EuiPaginationProps { /** * The total number of pages. - * Pass `0` if total count in unknown. + * Pass `0` if total count is unknown. */ pageCount?: number; diff --git a/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap b/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap index 5ce87f3bc71..ad3d34bcb8e 100644 --- a/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap +++ b/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap @@ -2,7 +2,7 @@ exports[`EuiTablePagination is rendered 1`] = `
`; + +exports[`EuiTablePagination renders a "show all" itemsPerPage option 1`] = ` +
+
+
+
+ +
+
+
+
+
+`; diff --git a/src/components/table/table_pagination/table_pagination.test.tsx b/src/components/table/table_pagination/table_pagination.test.tsx index d8f1fe3bd51..3326f98d8c5 100644 --- a/src/components/table/table_pagination/table_pagination.test.tsx +++ b/src/components/table/table_pagination/table_pagination.test.tsx @@ -31,7 +31,20 @@ describe('EuiTablePagination', () => { + ); + + expect(component).toMatchSnapshot(); + }); + + test('renders a "show all" itemsPerPage option', () => { + const component = render( + ); diff --git a/src/components/table/table_pagination/table_pagination.tsx b/src/components/table/table_pagination/table_pagination.tsx index a02712e397c..c14398dbba7 100644 --- a/src/components/table/table_pagination/table_pagination.tsx +++ b/src/components/table/table_pagination/table_pagination.tsx @@ -6,137 +6,161 @@ * Side Public License, v 1. */ -import React, { Component } from 'react'; +import React, { + FunctionComponent, + useState, + useMemo, + useCallback, +} from 'react'; import { EuiButtonEmpty } from '../../button'; import { EuiContextMenuItem, EuiContextMenuPanel } from '../../context_menu'; import { EuiFlexGroup, EuiFlexItem } from '../../flex'; -import { EuiPagination } from '../../pagination'; +import { EuiPagination, EuiPaginationProps } from '../../pagination'; import { EuiPopover } from '../../popover'; import { EuiI18n } from '../../i18n'; -export type PageChangeHandler = (pageIndex: number) => void; -export type ItemsPerPageChangeHandler = (pageSize: number) => void; +export type PageChangeHandler = EuiPaginationProps['onPageClick']; +export type ItemsPerPageChangeHandler = (pageSize: number | 'all') => void; -export interface EuiTablePaginationProps { - activePage?: number; - hidePerPageOptions?: boolean; - itemsPerPage?: number; - itemsPerPageOptions?: number[]; +export interface EuiTablePaginationProps + extends Omit { + /** + * Option to completely hide the "Rows per page" selector. + */ + showPerPageOptions?: boolean; + /** + * Current selection for "Rows per page". + * Pass `'all'` to display the selected "Show all" option and hide the pagination. + */ + itemsPerPage?: number | 'all'; + /** + * Custom array of options for "Rows per page". + * Pass `'all'` as one of the options to create a "Show all" option. + */ + itemsPerPageOptions?: Array; + /** + * Click handler that passes back selected `pageSize` number + */ onChangeItemsPerPage?: ItemsPerPageChangeHandler; onChangePage?: PageChangeHandler; - pageCount?: number; /** - * id of the table being controlled + * Requires the `id` of the table being controlled */ 'aria-controls'?: string; 'aria-label'?: string; } -interface State { - isPopoverOpen: boolean; -} - -export class EuiTablePagination extends Component< - EuiTablePaginationProps, - State -> { - state = { - isPopoverOpen: false, - }; +export const EuiTablePagination: FunctionComponent = ({ + activePage, + itemsPerPage = 50, + itemsPerPageOptions = [10, 20, 50, 100], + showPerPageOptions = true, + onChangeItemsPerPage = () => {}, + onChangePage, + pageCount, + ...rest +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); - onButtonClick = () => { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - }); - }; + const togglePopover = useCallback(() => { + setIsPopoverOpen((isOpen) => !isOpen); + }, []); - closePopover = () => { - this.setState({ - isPopoverOpen: false, - }); - }; + const closePopover = useCallback(() => { + setIsPopoverOpen(false); + }, []); - render() { - const { - activePage, - itemsPerPage = 50, - itemsPerPageOptions = [10, 20, 50, 100], - hidePerPageOptions = false, - onChangeItemsPerPage = () => {}, - onChangePage, - pageCount, - ...rest - } = this.props; - - const button = ( - + const button = ( + + {itemsPerPage === 'all' ? ( - : {itemsPerPage} - - ); + ) : ( + <> + + : {itemsPerPage} + + )} + + ); - const items = itemsPerPageOptions.map((itemsPerPageOption) => ( - { - this.closePopover(); - onChangeItemsPerPage(itemsPerPageOption); - }} - data-test-subj={`tablePagination-${itemsPerPageOption}-rows`} - > - - - )); + const items = useMemo( + () => + itemsPerPageOptions.map((itemsPerPageOption) => ( + { + closePopover(); + onChangeItemsPerPage(itemsPerPageOption); + }} + data-test-subj={`tablePagination-${itemsPerPageOption}-rows`} + > + {itemsPerPageOption === 'all' ? ( + + ) : ( + + )} + + )), + [itemsPerPageOptions, itemsPerPage, onChangeItemsPerPage, closePopover] + ); - const itemsPerPagePopover = ( - - - - ); + const itemsPerPagePopover = ( + + + + ); - return ( - - - {hidePerPageOptions ? null : itemsPerPagePopover} - + return ( + + + {showPerPageOptions && itemsPerPagePopover} + - + + {itemsPerPage !== 'all' && ( - - - ); - } -} + )} + + + ); +};