column.title.toLowerCase(),
+ isAscending: true,
+ }, {
+ name: 'dateCreated',
+ getValue: column => column.dateCreated.toLowerCase(),
+ isAscending: true,
+ }, {
+ name: 'magnitude',
+ getValue: column => column.magnitude.toLowerCase(),
+ isAscending: true,
+ }]}
+ />
+ );
+ }
+}
diff --git a/src-docs/src/views/table_plus/table_plus_example.js b/src-docs/src/views/table_plus/table_plus_example.js
new file mode 100644
index 000000000000..bf3ba6a0021d
--- /dev/null
+++ b/src-docs/src/views/table_plus/table_plus_example.js
@@ -0,0 +1,35 @@
+import React from 'react';
+
+import { renderToHtml } from '../../services';
+
+import {
+ GuideSectionTypes,
+} from '../../components';
+
+import {
+ EuiCode,
+} from '../../../../src/components';
+
+import TablePlus from './table_plus';
+const tablePlusSource = require('!!raw-loader!./table_plus');
+const tablePlusHtml = renderToHtml(TablePlus);
+
+export const TablePlusExample = {
+ title: 'TablePlus',
+ sections: [{
+ title: 'TablePlus',
+ source: [{
+ type: GuideSectionTypes.JS,
+ code: tablePlusSource,
+ }, {
+ type: GuideSectionTypes.HTML,
+ code: tablePlusHtml,
+ }],
+ text: (
+
+ Description needed: how to use the EuiTablePlus component.
+
+ ),
+ demo: ,
+ }],
+};
diff --git a/src/components/index.js b/src/components/index.js
index 5cde1aee0411..3fa4f021cec6 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -215,6 +215,10 @@ export {
TooltipTrigger
} from './tooltip';
+export {
+ EuiTablePlus,
+} from './table_plus';
+
export {
EuiTitle,
} from './title';
diff --git a/src/components/table_plus/__snapshots__/table_plus.test.js.snap b/src/components/table_plus/__snapshots__/table_plus.test.js.snap
new file mode 100644
index 000000000000..5966d4159c67
--- /dev/null
+++ b/src/components/table_plus/__snapshots__/table_plus.test.js.snap
@@ -0,0 +1,152 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiTablePlus is rendered 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/table_plus/index.js b/src/components/table_plus/index.js
new file mode 100644
index 000000000000..58cc6cbe41c1
--- /dev/null
+++ b/src/components/table_plus/index.js
@@ -0,0 +1,3 @@
+export {
+ EuiTablePlus,
+} from './table_plus';
diff --git a/src/components/table_plus/table_plus.js b/src/components/table_plus/table_plus.js
new file mode 100644
index 000000000000..86e202737001
--- /dev/null
+++ b/src/components/table_plus/table_plus.js
@@ -0,0 +1,331 @@
+import React, {
+ Component,
+} from 'react';
+import PropTypes from 'prop-types';
+
+import {
+ EuiButton,
+} from '../button';
+
+import {
+ EuiCheckbox,
+ EuiFieldSearch,
+} from '../form';
+
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+} from '../flex';
+
+import {
+ EuiSpacer,
+} from '../spacer';
+
+import {
+ EuiTable,
+ EuiTableBody,
+ EuiTableHeader,
+ EuiTableHeaderCell,
+ EuiTableHeaderCellCheckbox,
+ EuiTablePagination,
+ EuiTableRow,
+ EuiTableRowCell,
+ EuiTableRowCellCheckbox,
+} from '../table';
+
+import {
+ Pager,
+ SortableProperties,
+} from '../../services';
+
+export class EuiTablePlus extends Component {
+ static propTypes = {
+ id: PropTypes.string.isRequired,
+ className: PropTypes.string,
+ searchFilterer: PropTypes.func,
+ initialSortedColumn: PropTypes.string,
+ columns: PropTypes.array.isRequired,
+ rows: PropTypes.array.isRequired,
+ rowCellRenderer: PropTypes.func.isRequired,
+ sortablePropertiesConfig: PropTypes.array,
+ }
+
+ constructor(props) {
+ super(props);
+
+ const {
+ initialSortedColumn,
+ sortablePropertiesConfig,
+ rows,
+ } = props;
+
+ this.state = {
+ rowIdToSelectedMap: {},
+ sortedColumn: initialSortedColumn || props.columns[0].id,
+ rowsPerPage: 20,
+ filteredRows: rows
+ };
+
+ this.sortableProperties = sortablePropertiesConfig
+ ? new SortableProperties(sortablePropertiesConfig, this.state.sortedColumn)
+ : undefined;
+
+ this.pager = new Pager(rows.length, this.state.rowsPerPage);
+ this.state.firstRowIndex = this.pager.getFirstItemIndex();
+ this.state.lastRowIndex = this.pager.getLastItemIndex();
+ }
+
+ onChangeRowsPerPage = rowsPerPage => {
+ this.pager.setItemsPerPage(rowsPerPage);
+ this.setState({
+ rowsPerPage,
+ firstRowIndex: this.pager.getFirstItemIndex(),
+ lastRowIndex: this.pager.getLastItemIndex(),
+ });
+ }
+
+ onChangePage = pageIndex => {
+ this.pager.goToPageIndex(pageIndex);
+ this.setState({
+ firstRowIndex: this.pager.getFirstItemIndex(),
+ lastRowIndex: this.pager.getLastItemIndex(),
+ });
+ };
+
+ onSort = prop => {
+ this.sortableProperties.sortOn(prop);
+
+ this.setState({
+ sortedColumn: prop,
+ });
+ }
+
+ getVisibleRowIds = () => {
+ // If there are no rows.
+ if (this.state.firstRowIndex === -1) {
+ return [];
+ }
+
+ const { rows } = this.props;
+ const rowIds = [];
+
+ for (let rowIndex = this.state.firstRowIndex; rowIndex <= this.state.lastRowIndex; rowIndex++) {
+ const row = rows[rowIndex];
+ rowIds.push(row.id);
+ }
+
+ return rowIds;
+ }
+
+ toggleRow = rowId => {
+ this.setState(previousState => {
+ const newRowIdToSelectedMap = {
+ ...previousState.rowIdToSelectedMap,
+ [rowId]: !previousState.rowIdToSelectedMap[rowId],
+ };
+
+ return {
+ rowIdToSelectedMap: newRowIdToSelectedMap,
+ };
+ });
+ }
+
+ toggleAll = () => {
+ const rowIds = this.getVisibleRowIds();
+ const allSelected = this.areAllRowsSelected();
+ const newRowIdToSelectedMap = {};
+ rowIds.forEach(rowId => newRowIdToSelectedMap[rowId] = !allSelected);
+
+ this.setState({
+ rowIdToSelectedMap: newRowIdToSelectedMap,
+ });
+ }
+
+ isRowSelected = rowId => {
+ return this.state.rowIdToSelectedMap[rowId];
+ }
+
+ areAllRowsSelected = () => {
+ const rowIds = this.getVisibleRowIds();
+ const indexOfUnselectedRow = rowIds.findIndex(rowId => !this.isRowSelected(rowId));
+ return indexOfUnselectedRow === -1;
+ }
+
+ areAnyRowsSelected = () => {
+ return Object.keys(this.state.rowIdToSelectedMap).findIndex(id => {
+ return this.state.rowIdToSelectedMap[id];
+ }) !== -1;
+ }
+
+ onSearch = e => {
+ const filteredRows = this.props.rows.filter(row => this.props.searchFilterer(row, e.target.value));
+ this.pager.setTotalItems(filteredRows.length);
+ this.setState({
+ filteredRows,
+ firstRowIndex: this.pager.getFirstItemIndex(),
+ lastRowIndex: this.pager.getLastItemIndex(),
+ });
+ }
+
+ renderHeaderCells(columns) {
+ const customColumns = columns.map((column, columnIndex) => {
+ const {
+ id,
+ width,
+ isSortable,
+ content,
+ align,
+ ...rest
+ } = column;
+
+ return (
+
+ {typeof content === 'function' ? content(column, columnIndex) : content}
+
+ );
+ });
+
+ return [(
+
+
+
+ )].concat(customColumns);
+ }
+
+ renderRows(rows, columns, rowCellRenderer) {
+ const renderRow = row => {
+ const customCells = columns.map(column => {
+ const cell = row[column.id];
+ return rowCellRenderer(EuiTableRowCell, cell, column, row);
+ });
+
+ const cells = [(
+
+
+
+ )].concat(customCells);
+
+ return (
+
+ {cells}
+
+ );
+ };
+
+ const renderedRows = [];
+
+ // If we have rows.
+ if (this.state.firstRowIndex !== -1) {
+ for (let rowIndex = this.state.firstRowIndex; rowIndex <= this.state.lastRowIndex; rowIndex++) {
+ const item = rows[rowIndex];
+ renderedRows.push(renderRow(item));
+ }
+ }
+
+ return renderedRows;
+ }
+
+ render() {
+ const {
+ id, // eslint-disable-line no-unused-vars
+ className,
+ searchFilterer,
+ columns,
+ rows, // eslint-disable-line no-unused-vars
+ rowCellRenderer,
+ initialSortedColumn, // eslint-disable-line no-unused-vars
+ sortablePropertiesConfig, // eslint-disable-line no-unused-vars
+ ...rest
+ } = this.props;
+
+ let bulkActions;
+
+ if (this.areAnyRowsSelected() > 0) {
+ bulkActions = (
+
+ Delete selected
+
+ );
+ }
+
+ let search;
+
+ if (searchFilterer) {
+ search = (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {bulkActions}
+ {search}
+
+
+
+
+
+
+ {this.renderHeaderCells(columns)}
+
+
+
+ {this.renderRows(this.state.filteredRows, columns, rowCellRenderer)}
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/src/components/table_plus/table_plus.test.js b/src/components/table_plus/table_plus.test.js
new file mode 100644
index 000000000000..27d045d3b9af
--- /dev/null
+++ b/src/components/table_plus/table_plus.test.js
@@ -0,0 +1,22 @@
+import React from 'react';
+import { render } from 'enzyme';
+import { requiredProps } from '../../test';
+
+import { EuiTablePlus } from './table_plus';
+
+describe('EuiTablePlus', () => {
+ test('is rendered', () => {
+ const component = render(
+ {}}
+ />
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+});
diff --git a/src/services/paging/pager.js b/src/services/paging/pager.js
index 5342ff5ee3eb..28a312321dfd 100644
--- a/src/services/paging/pager.js
+++ b/src/services/paging/pager.js
@@ -58,8 +58,8 @@ export class Pager {
if (this.totalItems <= 0) {
this.totalPages = 0;
this.currentPageIndex = 0;
- this.firstItemIndex = 0;
- this.lastItemIndex = 0;
+ this.firstItemIndex = -1;
+ this.lastItemIndex = -1;
return;
}
diff --git a/src/services/paging/pager.test.js b/src/services/paging/pager.test.js
index 9613fed141de..8f8eff04a1d9 100644
--- a/src/services/paging/pager.test.js
+++ b/src/services/paging/pager.test.js
@@ -140,14 +140,14 @@ describe('Pager', () => {
describe('behavior', () => {
describe('when there are no items', () => {
- test('getFirstItemIndex defaults to 0', () => {
+ test('getFirstItemIndex defaults to -1', () => {
const pager = new Pager(0, 20);
- expect(pager.getFirstItemIndex()).toBe(0);
+ expect(pager.getFirstItemIndex()).toBe(-1);
});
- test('getLastItemIndex defaults to 0', () => {
+ test('getLastItemIndex defaults to -1', () => {
const pager = new Pager(0, 20);
- expect(pager.getLastItemIndex()).toBe(0);
+ expect(pager.getLastItemIndex()).toBe(-1);
});
});
});