Repository: gregnb/mui-datatables
Pull request #913 (2019-09-11 01:18:11)

Issue-857 Server Side Filters
Attempt to resolve https://github.com/gregnb/mui-datatables/issues/857 as well as any other requests around the difficulty of achieving server side filter functionality.

Handling filters on the server side is not an experience that has been optimized in this library. Some of the problems are:

- Lack of callbacks for the filter dialog, preventing different kinds of interactions with external APIs
- `filterList` in column options is connected to both the change in the fields in the filter dialog as well as the filter chip display, meaning that the display of applied filters cannot be stalled until a successful API call (they will be applied immediately regardless of any success/failure from an API call)
- Lack of ability to add custom buttons/functionality to the filter dialog

Here are the solutions offered in this PR:

- `onFilterDialogOpen` and `onFilterDialogClose` callbacks are added for opening and closing the filter dialog respectively
- `serverSideFilterList` option has been added to track the display of `filterList` as chips (array of filter arrays, as structured internally). When `serverSide` is set to `true`, filter chips will no longer populate as before, and will require `serverSideFilterList` to populate.
- `customFilterDialogFooter` option added to allow a custom component to be added to the bottom portion of the filter dialog for custom functionality, such as an "Apply Filters" button

The example `serverside-filters` was added to demonstrate the use of some of these features to create a serverside filtering experience.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/25643755/badge)](https://coveralls.io/builds/25643755)

Coverage decreased (-0.006%) to 76.402% when pulling **0cc2ddfa449216ac9b22f2df47926407d71d4c30 on enhancement/issue-857-server-side-filters** into **3032a7c22cdbbafbc53409d797a179215a40f6ad on master**.


Patch:
diff --git a/README.md b/README.md
index a99b7b7..4ae3c79 100644
--- a/README.md
+++ b/README.md
@@ -135,14 +135,16 @@ The component accepts the following props:
 |**`page`**|number||User provided starting page for pagination
 |**`count`**|number||User provided override for total number of rows
 |**`serverSide`**|boolean|false|Enable remote data source
+|**`serverSideFilterList`**|array|[]|Sets the filter list display when using serverSide: true
 |**`rowsSelected`**|array||User provided selected rows
 |**`rowsExpanded`**|array||User provided expanded rows
-|**`filterType `**|string||Choice of filtering view. `enum('checkbox', 'dropdown', 'multiselect', 'textField')`
-|**`textLabels `**|object||User provided labels to localize text
+|**`filterType`**|string||Choice of filtering view. `enum('checkbox', 'dropdown', 'multiselect', 'textField', 'custom')`
+|**`textLabels`**|object||User provided labels to localize text
 |**`pagination`**|boolean|true|Enable/disable pagination
 |**`selectableRows`**|string|'multiple'|Numbers of rows that can be selected. Options are "multiple", "single", "none".
 |**`selectableRowsOnClick`**|boolean|false|Enable/disable select toggle when row is clicked. When False, only checkbox will trigger this action.
-|**`isRowSelectable`**|function||Enable/disable selection on certain rows with custom function. Returns true if not provided. `function(dataIndex: number, selectedRows: object(lookup: {[dataindex]: boolean}, data: arrayOfObjects: {index: number, dataIndex: number})) => boolean`.
+|**`isRowSelectable`**|function||Enable/disable selection on certain rows with custom function. Returns true if not provided. `function(dataIndex: number, selectedRows: object(lookup: {dataindex: boolean}, data: arrayOfObjects: {index, dataIndex})) => boolean`.
+|**`isRowExpandable`**|function||Enable/disable expansion or collapse on certain expandable rows with custom function. Will be considered true if not provided. `function(dataIndex: number, expandedRows: object(lookup: {dataIndex: number}, data: arrayOfObjects: {index: number, dataIndex: number})) => boolean`.
 |**`selectableRowsHeader`**|boolean|true|Show/hide the select all/deselect all checkbox header for selectable rows
 |**`expandableRows`**|boolean|false|Enable/disable expandable rows
 |**`expandableRowsOnClick`**|boolean|false|Enable/disable expand trigger when row is clicked. When False, only expand icon will trigger this action.
@@ -155,6 +157,7 @@ The component accepts the following props:
 |**`customSort`**|function||Override default sorting with custom function. `function(data: array, colIndex: number, order: string) => array`
 |**`customSearch `**|function||Override default search with custom function. `customSearch(searchQuery: string, currentRow: array, columns: array) => boolean`
 |**`customSearchRender `**|function||Render a custom table search. `customSearchRender(searchText: string, handleSearch, hideSearch, options) => React Component`
+|**`customFilterDialogFooter `**|function||Add a custom footer to the filter dialog. `customFilterDialogFooter(filterList: array) => React Component`
 |**`elevation`**|number|4|Shadow depth applied to Paper component
 |**`caseSensitive `**|boolean|false|Enable/disable case sensitivity for search
 |**`responsive`**|string|'stacked'|Enable/disable responsive table views. Options: 'stacked', 'scrollMaxHeight' (limits height of table), 'scrollFullHeight' (table takes on as much height as needed to display all rows set in rowsPerPage)
@@ -166,6 +169,7 @@ The component accepts the following props:
 |**`sort`**|boolean|true|Enable/disable sort on all columns
 |**`filter`**|boolean|true|Show/hide filter icon from toolbar
 |**`search`**|boolean|true|Show/hide search icon from toolbar
+|**`searchOpen`**|boolean|false|Initially displays search bar  
 |**`searchText`**|string||Initial search text
 |**`searchPlaceholder`**|string||Search text placeholder. [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/customize-search/index.js)
 |**`print`**|boolean|true|Show/hide print	 icon from toolbar
@@ -174,6 +178,7 @@ The component accepts the following props:
 |**`onDownload`**|function||A callback function that triggers when the user downloads the CSV file. In the callback, you can control what is written to the CSV file. `function(buildHead: (columns) => string, buildBody: (data) => string, columns, data) => string`. Return `false` to cancel download of file.
 |**`viewColumns`**|boolean|true|Show/hide viewColumns icon from toolbar
 |**`onRowsSelect`**|function||Callback function that triggers when row(s) are selected. `function(currentRowsSelected: array, allRowsSelected: array) => void`
+|**`onRowsExpand`**|function||Callback function that triggers when row(s) are expanded. `function(currentRowsExpanded: array, allRowsExpanded: array) => void`
 |**`onRowsDelete`**|function||Callback function that triggers when row(s) are deleted. `function(rowsDeleted: object(lookup: {[dataIndex]: boolean}, data: arrayOfObjects: {index: number, dataIndex: number})) => void OR false` (Returning `false` prevents row deletion.)
 |**`onRowClick`**|function||Callback function that triggers when a row is clicked. `function(rowData: string[], rowMeta: { dataIndex: number, rowIndex: number }) => void`
 |**`onCellClick`**|function||Callback function that triggers when a cell is clicked. `function(colData: any, cellMeta: { colIndex: number, rowIndex: number, dataIndex: number }) => void`
@@ -181,8 +186,10 @@ The component accepts the following props:
 |**`onChangeRowsPerPage`**|function||Callback function that triggers when the number of rows per page has changed. `function(numberOfRows: number) => void`
 |**`onSearchChange`**|function||Callback function that triggers when the search text value has changed. `function(searchText: string) => void`
 |**`onSearchOpen`**|function||Callback function that triggers when the searchbox opens. `function() => void`
+|**`onFilterDialogOpen`**|function||Callback function that triggers when the filter dialog opens. `function() => void`
+|**`onFilterDialogClose`**|function||Callback function that triggers when the filter dialog closes. `function() => void`
+|**`onFilterChange`**|function||Callback function that triggers when filters have changed. `function(changedColumn: string, filterList: array, type: enum('checkbox', 'dropdown', 'multiselect', 'textField', 'custom', 'chip', 'reset')) => void`
 |**`onSearchClose`**|function||Callback function that triggers when the searchbox closes. `function() => void`
-|**`onFilterChange`**|function||Callback function that triggers when filters have changed. `function(changedColumn: string, filterList: array) => void`
 |**`onColumnSortChange`**|function||Callback function that triggers when a column has been sorted. `function(changedColumn: string, direction: string) => void`
 |**`onColumnViewChange`**|function||Callback function that triggers when a column view has been changed. `function(changedColumn: string, action: string) => void`
 |**`onTableChange`**|function||Callback function that triggers when table state has changed. `function(action: string, tableState: object) => void`
diff --git a/examples/examples.js b/examples/examples.js
index 59972ed..0a26199 100644
--- a/examples/examples.js
+++ b/examples/examples.js
@@ -22,6 +22,7 @@ import OnDownload from "./on-download";
 import OnTableInit from "./on-table-init";
 import ResizableColumns from "./resizable-columns";
 import SelectableRows from "./selectable-rows";
+import ServerSideFilters from "./serverside-filters";
 import ServerSideOptions from "./serverside-options";
 import ServerSidePagination from "./serverside-pagination";
 import Simple from "./simple";
@@ -57,6 +58,7 @@ export default {
     'OnTableInit': OnTableInit,
     'resizableColumns': ResizableColumns,
     'selectableRows': SelectableRows,
+    'ServerSide Filters': ServerSideFilters,
     'serverSide Options': ServerSideOptions,
     'serverSide Pagination': ServerSidePagination,
     'Simple': Simple,
diff --git a/examples/expandable-rows/index.js b/examples/expandable-rows/index.js
index ba67d66..4ef1dca 100644
--- a/examples/expandable-rows/index.js
+++ b/examples/expandable-rows/index.js
@@ -82,7 +82,12 @@ class Example extends React.Component {
       responsive: 'scrollMaxHeight',
       expandableRows: true,
       expandableRowsOnClick: true,
-      rowsExpanded: [0, 2, 3],
+      isRowExpandable: (dataIndex, expandedRows) => {
+        // Prevent expand/collapse of any row after the 5th (but allow those already expanded to be collapsed)
+        if (expandedRows.data.length > 4 && expandedRows.data.filter(d => d.dataIndex === dataIndex).length === 0) return false;
+        return true;
+      },
+      rowsExpanded: [0, 1],
       renderExpandableRow: (rowData, rowMeta) => {
         const colSpan = rowData.length + 1;
         return (
@@ -92,7 +97,8 @@ class Example extends React.Component {
             </TableCell>
           </TableRow>
         );
-      }
+      },
+      onRowsExpand: (curExpanded, allExpanded) => console.log(curExpanded, allExpanded)
     };
 
     return (
diff --git a/examples/serverside-filters/index.js b/examples/serverside-filters/index.js
new file mode 100644
index 0000000..6ebfaf7
--- /dev/null
+++ b/examples/serverside-filters/index.js
@@ -0,0 +1,196 @@
+import { Button, CircularProgress } from '@material-ui/core';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import MUIDataTable from '../../src';
+
+class Example extends React.Component {
+  state = {
+    serverSideFilterList: [],
+    filters: [[], [], [], [], []],
+    isLoading: false,
+    data: [
+      ['Gabby George', 'Business Analyst', 'Minneapolis', 30, '$100,000'],
+      ['Aiden Lloyd', 'Business Consultant', 'Dallas', 55, '$200,000'],
+      ['Jaden Collins', 'Attorney', 'Santa Ana', 27, '$500,000'],
+      ['Franky Rees', 'Business Analyst', 'St. Petersburg', 22, '$50,000'],
+      ['Aaren Rose', 'Business Consultant', 'Toledo', 28, '$75,000'],
+      ['Blake Duncan', 'Business Management Analyst', 'San Diego', 65, '$94,000'],
+      ['Frankie Parry', 'Agency Legal Counsel', 'Jacksonville', 71, '$210,000'],
+      ['Lane Wilson', 'Commercial Specialist', 'Chicago', 19, '$65,000'],
+      ['Robin Duncan', 'Business Analyst', 'Los Angeles', 20, '$77,000'],
+      ['Mel Brooks', 'Business Consultant', 'Oklahoma City', 37, '$135,000'],
+      ['Harper White', 'Attorney', 'Pittsburgh', 52, '$420,000'],
+      ['Kris Humphrey', 'Agency Legal Counsel', 'Laredo', 30, '$150,000'],
+      ['Frankie Long', 'Industrial Analyst', 'Austin', 31, '$170,000'],
+      ['Brynn Robbins', 'Business Analyst', 'Norfolk', 22, '$90,000'],
+      ['Justice Mann', 'Business Consultant', 'Chicago', 24, '$133,000'],
+      ['Addison Navarro', 'Business Management Analyst', 'New York', 50, '$295,000'],
+      ['Jesse Welch', 'Agency Legal Counsel', 'Seattle', 28, '$200,000'],
+      ['Eli Mejia', 'Commercial Specialist', 'Long Beach', 65, '$400,000'],
+      ['Gene Leblanc', 'Industrial Analyst', 'Hartford', 34, '$110,000'],
+      ['Danny Leon', 'Computer Scientist', 'Newark', 60, '$220,000'],
+      ['Lane Lee', 'Corporate Counselor', 'Cincinnati', 52, '$180,000'],
+      ['Jesse Hall', 'Business Analyst', 'Baltimore', 44, '$99,000'],
+      ['Danni Hudson', 'Agency Legal Counsel', 'Tampa', 37, '$90,000'],
+      ['Terry Macdonald', 'Commercial Specialist', 'Miami', 39, '$140,000'],
+      ['Justice Mccarthy', 'Attorney', 'Tucson', 26, '$330,000'],
+      ['Silver Carey', 'Computer Scientist', 'Memphis', 47, '$250,000'],
+      ['Franky Miles', 'Industrial Analyst', 'Buffalo', 49, '$190,000'],
+      ['Glen Nixon', 'Corporate Counselor', 'Arlington', 44, '$80,000'],
+      ['Gabby Strickland', 'Business Process Consultant', 'Scottsdale', 26, '$45,000'],
+      ['Mason Ray', 'Computer Scientist', 'San Francisco', 39, '$142,000'],
+    ]
+  };
+
+  // mock async function
+  xhrRequest = (url, filterList) => {
+    return new Promise(resolve => {
+      window.setTimeout(
+        () => {
+          const data = [
+            ['Gabby George', 'Business Analyst', 'Minneapolis', 30, '$100,000'],
+            ['Aiden Lloyd', 'Business Consultant', 'Dallas', 55, '$200,000'],
+            ['Jaden Collins', 'Attorney', 'Santa Ana', 27, '$500,000'],
+            ['Franky Rees', 'Business Analyst', 'St. Petersburg', 22, '$50,000'],
+            ['Aaren Rose', 'Business Consultant', 'Toledo', 28, '$75,000'],
+            ['Blake Duncan', 'Business Management Analyst', 'San Diego', 65, '$94,000'],
+            ['Frankie Parry', 'Agency Legal Counsel', 'Jacksonville', 71, '$210,000'],
+            ['Lane Wilson', 'Commercial Specialist', 'Chicago', 19, '$65,000'],
+            ['Robin Duncan', 'Business Analyst', 'Los Angeles', 20, '$77,000'],
+            ['Mel Brooks', 'Business Consultant', 'Oklahoma City', 37, '$135,000'],
+            ['Harper White', 'Attorney', 'Pittsburgh', 52, '$420,000'],
+            ['Kris Humphrey', 'Agency Legal Counsel', 'Laredo', 30, '$150,000'],
+            ['Frankie Long', 'Industrial Analyst', 'Austin', 31, '$170,000'],
+            ['Brynn Robbins', 'Business Analyst', 'Norfolk', 22, '$90,000'],
+            ['Justice Mann', 'Business Consultant', 'Chicago', 24, '$133,000'],
+            ['Addison Navarro', 'Business Management Analyst', 'New York', 50, '$295,000'],
+            ['Jesse Welch', 'Agency Legal Counsel', 'Seattle', 28, '$200,000'],
+            ['Eli Mejia', 'Commercial Specialist', 'Long Beach', 65, '$400,000'],
+            ['Gene Leblanc', 'Industrial Analyst', 'Hartford', 34, '$110,000'],
+            ['Danny Leon', 'Computer Scientist', 'Newark', 60, '$220,000'],
+            ['Lane Lee', 'Corporate Counselor', 'Cincinnati', 52, '$180,000'],
+            ['Jesse Hall', 'Business Analyst', 'Baltimore', 44, '$99,000'],
+            ['Danni Hudson', 'Agency Legal Counsel', 'Tampa', 37, '$90,000'],
+            ['Terry Macdonald', 'Commercial Specialist', 'Miami', 39, '$140,000'],
+            ['Justice Mccarthy', 'Attorney', 'Tucson', 26, '$330,000'],
+            ['Silver Carey', 'Computer Scientist', 'Memphis', 47, '$250,000'],
+            ['Franky Miles', 'Industrial Analyst', 'Buffalo', 49, '$190,000'],
+            ['Glen Nixon', 'Corporate Counselor', 'Arlington', 44, '$80,000'],
+            ['Gabby Strickland', 'Business Process Consultant', 'Scottsdale', 26, '$45,000'],
+            ['Mason Ray', 'Computer Scientist', 'San Francisco', 39, '$142,000'],
+          ];
+          const newData = [
+            ['Lane Wilson', 'Commercial Specialist', 'Chicago', 19, '$65,000'],
+            ['Justice Mann', 'Business Consultant', 'Chicago', 24, '$133,000']
+          ];
+
+          if (
+            !filterList[0].length
+            && !filterList[1].length
+            && !filterList[2].length
+            && !filterList[3].length
+            && !filterList[4].length
+          ) {
+            resolve({ data });
+          } else {
+            resolve({ data: newData });
+          }
+        },
+        2000
+      );
+    });
+  }
+
+  handleFilterSubmit = filterList => () => {
+    console.log('Submitting filters: ', filterList);
+
+    this.setState({ isLoading: true, filters: filterList });
+
+    // fake async request
+    this.xhrRequest(`/myApiServer?filters=${filterList}`, filterList).then(res => {
+      this.setState({ isLoading: false, data: res.data, serverSideFilterList: filterList });
+    });
+  };
+
+  render() {
+    const columns = [
+      {
+        name: 'Name',
+        options: {
+          filter: true,
+          filterList: this.state.filters[0],
+        },
+      },
+      {
+        label: 'Title',
+        name: 'Title',
+        options: {
+          filter: true,
+          filterList: this.state.filters[1],
+        },
+      },
+      {
+        name: 'Location',
+        options: {
+          filter: true,
+          filterList: this.state.filters[2],
+        },
+      },
+      {
+        name: 'Age',
+        options: {
+          filter: true,
+          filterList: this.state.filters[3],
+        },
+      },
+      {
+        name: 'Salary',
+        options: {
+          filter: true,
+          filterList: this.state.filters[4],
+        },
+      },
+    ];
+
+    const options = {
+      filter: true,
+      serverSideFilterList: this.state.serverSideFilterList,
+      filterType: 'dropdown',
+      responsive: 'scrollMaxHeight',
+      serverSide: true,
+      onFilterDialogOpen: () => {
+        console.log('filter dialog opened');
+      },
+      onFilterDialogClose: () => {
+        console.log('filter dialog closed');
+      },
+      onFilterChange: (column, filterList, type) => {
+        if (type === 'chip') {
+          console.log('updating filters via chip');
+          this.handleFilterSubmit(filterList)();
+        }
+      },
+      customFilterDialogFooter: filterList => {
+        return (
+          <div style={{ marginTop: '40px' }}>
+            <Button variant="contained" onClick={this.handleFilterSubmit(filterList)}>Apply Filters*</Button>
+            <p><em>*(Simulates selecting "Chicago" from "Location")</em></p>
+          </div>
+        );
+      }
+    };
+
+    return (
+      <React.Fragment>
+        {this.state.isLoading && (
+          <div style={{ position: 'absolute', top: '50%', left: '50%' }}>
+            <CircularProgress />
+          </div>
+        )}
+        <MUIDataTable title={'ACME Employee list'} data={this.state.data} columns={columns} options={options} />
+      </React.Fragment>
+    );
+  }
+}
+
+export default Example;
diff --git a/src/MUIDataTable.js b/src/MUIDataTable.js
index d0fe3d5..3255d67 100644
--- a/src/MUIDataTable.js
+++ b/src/MUIDataTable.js
@@ -142,14 +142,20 @@ class MUIDataTable extends React.Component {
       customFooter: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
       customSearchRender: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
       customRowRender: PropTypes.func,
+      customFilterDialogFooter: PropTypes.func,
       onRowClick: PropTypes.func,
+      onRowsExpand: PropTypes.func,
       onRowsSelect: PropTypes.func,
       resizableColumns: PropTypes.bool,
       selectableRows: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['none', 'single', 'multiple'])]),
       selectableRowsOnClick: PropTypes.bool,
       isRowSelectable: PropTypes.func,
+      isRowExpandable: PropTypes.func,
       selectableRowsHeader: PropTypes.bool,
       serverSide: PropTypes.bool,
+      onFilterChange: PropTypes.func,
+      onFilterDialogOpen: PropTypes.func,
+      onFilterDialogClose: PropTypes.func,
       onTableChange: PropTypes.func,
       onTableInit: PropTypes.func,
       caseSensitive: PropTypes.bool,
@@ -166,6 +172,7 @@ class MUIDataTable extends React.Component {
       customSort: PropTypes.func,
       customSearch: PropTypes.func,
       search: PropTypes.bool,
+      searchOpen: PropTypes.bool,
       searchText: PropTypes.string,
       searchPlaceholder: PropTypes.string,
       print: PropTypes.bool,
@@ -278,6 +285,7 @@ class MUIDataTable extends React.Component {
     filterType: 'dropdown',
     pagination: true,
     textLabels,
+    serverSideFilterList: [],
     expandableRows: false,
     expandableRowsOnClick: false,
     resizableColumns: false,
@@ -946,7 +954,7 @@ class MUIDataTable extends React.Component {
       () => {
         this.setTableAction('resetFilters');
         if (this.options.onFilterChange) {
-          this.options.onFilterChange(null, this.state.filterList);
+          this.options.onFilterChange(null, this.state.filterList, 'reset');
         }
       },
     );
@@ -962,6 +970,9 @@ class MUIDataTable extends React.Component {
           case 'checkbox':
             filterPos >= 0 ? filterList[index].splice(filterPos, 1) : filterList[index].push(value);
             break;
+          case 'chip':
+            filterPos >= 0 ? filterList[index].splice(filterPos, 1) : filterList[index].push(value);
+            break;
           case 'multiselect':
             filterList[index] = value === '' ? [] : value;
             break;
@@ -987,7 +998,7 @@ class MUIDataTable extends React.Component {
       () => {
         this.setTableAction('filterChange');
         if (this.options.onFilterChange) {
-          this.options.onFilterChange(column, this.state.filterList);
+          this.options.onFilterChange(column, this.state.filterList, type);
         }
       },
     );
@@ -1020,31 +1031,43 @@ class MUIDataTable extends React.Component {
 
   toggleExpandRow = row => {
     const { dataIndex } = row;
-    let expandedRows = [...this.state.expandedRows.data];
-    let rowPos = -1;
-
-    for (let cIndex = 0; cIndex < expandedRows.length; cIndex++) {
-      if (expandedRows[cIndex].dataIndex === dataIndex) {
-        rowPos = cIndex;
+    const { isRowExpandable } = this.options;
+    let { expandedRows } = this.state;
+    const expandedRowsData = [...expandedRows.data];
+    let shouldCollapseExpandedRow = false;
+    let hasRemovedRow = false;
+    let removedRow = [];
+
+    for (var cIndex = 0; cIndex < expandedRowsData.length; cIndex++) {
+      if (expandedRowsData[cIndex].dataIndex === dataIndex) {
+        shouldCollapseExpandedRow = true;
         break;
       }
     }
 
-    if (rowPos >= 0) {
-      expandedRows.splice(rowPos, 1);
+    if (shouldCollapseExpandedRow) {
+      if ((isRowExpandable && isRowExpandable(dataIndex, expandedRows) || !isRowExpandable)) {
+        removedRow = expandedRowsData.splice(cIndex, 1);
+        hasRemovedRow = true;
+      }
     } else {
-      expandedRows.push(row);
+      if (isRowExpandable && isRowExpandable(dataIndex, expandedRows)) expandedRowsData.push(row);
+      else if (!isRowExpandable) expandedRowsData.push(row);
     }
 
     this.setState(
       {
+        curExpandedRows: hasRemovedRow ? removedRow : [row],
         expandedRows: {
-          lookup: buildMap(expandedRows),
-          data: expandedRows,
+          lookup: buildMap(expandedRowsData),
+          data: expandedRowsData,
         },
       },
       () => {
         this.setTableAction('expandRow');
+        if (this.options.onRowsExpand) {
+          this.options.onRowsExpand(this.state.curExpandedRows, this.state.expandedRows.data);
+        }
       },
     );
   };
@@ -1223,6 +1246,7 @@ class MUIDataTable extends React.Component {
       previousSelectedRow,
       expandedRows,
       searchText,
+      serverSideFilterList,
     } = this.state;
 
     const rowCount = this.state.count || displayData.length;
@@ -1282,6 +1306,7 @@ class MUIDataTable extends React.Component {
         )}
         <TableFilterList
           options={this.options}
+          serverSideFilterList={this.props.options.serverSideFilterList || []}
           filterListRenderers={columns.map(c => {
             return c.customFilterListRender ? c.customFilterListRender : f => f;
           })}
diff --git a/src/components/Popover.js b/src/components/Popover.js
index a912316..f51d97c 100644
--- a/src/components/Popover.js
+++ b/src/components/Popover.js
@@ -78,7 +78,8 @@ class Popover extends React.Component {
           ref={el => this.popoverEl}
           anchorOrigin={anchorOriginSpecs}
           transformOrigin={transformOriginSpecs}
-          {...providedProps}>
+          {...providedProps}
+        >
           {content}
         </MuiPopover>
         {triggerEl}
diff --git a/src/components/TableBody.js b/src/components/TableBody.js
index efa91e9..a8ed200 100644
--- a/src/components/TableBody.js
+++ b/src/components/TableBody.js
@@ -29,6 +29,8 @@ class TableBody extends React.Component {
     filterList: PropTypes.array,
     /** Callback to execute when row is clicked */
     onRowClick: PropTypes.func,
+    /** Table rows expanded */
+    expandedRows: PropTypes.object,
     /** Table rows selected */
     selectedRows: PropTypes.object,
     /** Callback to trigger table row select */
@@ -100,6 +102,15 @@ class TableBody extends React.Component {
     }
   }
 
+  isRowExpandable(dataIndex) {
+    const { options, expandedRows } = this.props;
+    if (options.isRowExpandable) {
+      return options.isRowExpandable(dataIndex, expandedRows);
+    } else {
+      return true;
+    }
+  }
+
   handleRowSelect = (data, event) => {
     let shiftKey = event && event.nativeEvent ? event.nativeEvent.shiftKey : false;
     let shiftAdjacentRows = [];
@@ -177,7 +188,11 @@ class TableBody extends React.Component {
       this.handleRowSelect(selectRow, event);
     }
     // Check if we should trigger row expand when row is clicked anywhere
-    if (this.props.options.expandableRowsOnClick && this.props.options.expandableRows) {
+    if (
+      this.props.options.expandableRowsOnClick &&
+      this.props.options.expandableRows &&
+      this.isRowExpandable(data.dataIndex, this.props.expandedRows)
+    ) {
       const expandRow = { index: data.rowIndex, dataIndex: data.dataIndex };
       this.props.toggleExpandRow(expandRow);
     }
diff --git a/src/components/TableFilter.js b/src/components/TableFilter.js
index b0ed72e..7975360 100644
--- a/src/components/TableFilter.js
+++ b/src/components/TableFilter.js
@@ -266,7 +266,7 @@ class TableFilter extends React.Component {
   }
 
   render() {
-    const { classes, columns, options, onFilterReset } = this.props;
+    const { classes, columns, options, onFilterReset, customFooter, filterList } = this.props;
     const textLabels = options.textLabels.filter;
     const filterGridColumns = columns.filter(col => col.filter).length === 1 ? 1 : 2;
 
@@ -309,6 +309,7 @@ class TableFilter extends React.Component {
             }
           })}
         </GridList>
+        {customFooter ? customFooter(filterList) : ''}
       </div>
     );
   }
diff --git a/src/components/TableFilterList.js b/src/components/TableFilterList.js
index 6866a23..61d3c0b 100644
--- a/src/components/TableFilterList.js
+++ b/src/components/TableFilterList.js
@@ -35,30 +35,42 @@ class TableFilterList extends React.Component {
   };
 
   render() {
-    const { classes, filterList, filterUpdate, filterListRenderers, columnNames } = this.props;
+    const { classes, filterList, filterUpdate, filterListRenderers, columnNames, serverSideFilterList } = this.props;
+    const { serverSide } = this.props.options;
+
+    const customFilterChip = (item, index) => (
+      <Chip
+        label={filterListRenderers[index](item)}
+        key={index}
+        onDelete={filterUpdate.bind(null, index, [], columnNames[index].name, columnNames[index].filterType)}
+        className={classes.chip}
+      />
+    );
+
+    const filterChip = (index, data, colIndex) => (
+      <Chip
+        label={filterListRenderers[index](data)}
+        key={colIndex}
+        onDelete={filterUpdate.bind(null, index, data, columnNames[index].name, 'chip')}
+        className={classes.chip}
+      />
+    );
 
     return (
       <div className={classes.root}>
-        {filterList.map((item, index) => {
+        {serverSide ? serverSideFilterList.map((item, index) => {
+          if (columnNames[index].filterType === 'custom' && filterListRenderers[index](item)) {
+            return customFilterChip(item, index);
+          }
+
+          return item.map((data, colIndex) => filterChip(index, data, colIndex));
+        }) :
+        filterList.map((item, index) => {
           if (columnNames[index].filterType === 'custom' && filterListRenderers[index](item)) {
-            return (
-              <Chip
-                label={filterListRenderers[index](item)}
-                key={index}
-                onDelete={filterUpdate.bind(null, index, [], columnNames[index].name, columnNames[index].filterType)}
-                className={classes.chip}
-              />
-            );
+            return customFilterChip(item, index);
           }
 
-          return item.map((data, colIndex) => (
-            <Chip
-              label={filterListRenderers[index](data)}
-              key={colIndex}
-              onDelete={filterUpdate.bind(null, index, data, columnNames[index].name, 'checkbox')}
-              className={classes.chip}
-            />
-          ));
+          return item.map((data, colIndex) => filterChip(index, data, colIndex));
         })}
       </div>
     );
diff --git a/src/components/TableToolbar.js b/src/components/TableToolbar.js
index 1f264de..263a621 100644
--- a/src/components/TableToolbar.js
+++ b/src/components/TableToolbar.js
@@ -80,7 +80,7 @@ export const defaultToolbarStyles = theme => ({
 class TableToolbar extends React.Component {
   state = {
     iconActive: null,
-    showSearch: Boolean(this.props.searchText || this.props.options.searchText),
+    showSearch: Boolean(this.props.searchText || this.props.options.searchText || this.props.options.searchOpen),
     searchText: this.props.searchText || null,
   };
 
@@ -126,10 +126,26 @@ class TableToolbar extends React.Component {
   };
 
   setActiveIcon = iconName => {
-    this.setState(() => ({
+    this.setState(prevState => ({
       showSearch: this.isSearchShown(iconName),
       iconActive: iconName,
-    }));
+      prevIconActive: prevState.iconActive
+    }), () => {
+      const { iconActive, prevIconActive } = this.state;
+
+      if (iconActive === 'filter') {
+        this.props.setTableAction('onFilterDialogOpen');
+        if (this.props.options.onFilterDialogOpen) {
+          this.props.options.onFilterDialogOpen();
+        }
+      }
+      if (iconActive === undefined && prevIconActive === 'filter') {
+        this.props.setTableAction('onFilterDialogClose');
+        if (this.props.options.onFilterDialogClose) {
+          this.props.options.onFilterDialogClose();
+        }
+      }
+    });
   };
 
   isSearchShown = iconName => {
@@ -301,6 +317,7 @@ class TableToolbar extends React.Component {
               }
               content={
                 <TableFilter
+                  customFooter={options.customFilterDialogFooter}
                   columns={columns}
                   options={options}
                   filterList={filterList}


Test patch:
diff --git a/test/MUIDataTable.test.js b/test/MUIDataTable.test.js
index c6265ec..2a75338 100644
--- a/test/MUIDataTable.test.js
+++ b/test/MUIDataTable.test.js
@@ -606,6 +606,7 @@ describe('<MUIDataTable />', function() {
 
     const mountWrapper = mount(
       <TableFilterList
+        options={{ serverSide: false }}
         filterList={filterList}
         filterListRenderers={filterListRenderers}
         filterUpdate={() => true}
@@ -627,6 +628,7 @@ describe('<MUIDataTable />', function() {
 
     const mountWrapper = mount(
       <TableFilterList
+        options={{ serverSide: false }}
         filterList={filterList}
         filterListRenderers={filterListRenderers}
         filterUpdate={() => true}
@@ -638,6 +640,55 @@ describe('<MUIDataTable />', function() {
     assert.strictEqual(actualResult.prop('label'), 'Name: Joe James');
   });
 
+  it('should render filter Chip(s) when options.serverSide = true and serverSideFilterList is populated', () => {
+    const serverSideFilterList = [['Joe James'], [], [], [], []];
+    const filterListRenderers = [
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+    ];
+    const columnNames = columns.map(column => ({ name: column.name }));
+
+    const mountWrapper = mount(
+      <TableFilterList
+        options={{ serverSide: true }}
+        serverSideFilterList={serverSideFilterList}
+        filterListRenderers={filterListRenderers}
+        filterUpdate={() => true}
+        columnNames={columnNames}
+      />,
+    );
+
+    const actualResult = mountWrapper.find(Chip);
+    assert.strictEqual(actualResult.length, 1);
+  });
+
+  it('should not render filter Chip(s) when options.serverSide = true and serverSideFilterList is not populated', () => {
+    const filterListRenderers = [
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+    ];
+    const columnNames = columns.map(column => ({ name: column.name }));
+
+    const mountWrapper = mount(
+      <TableFilterList
+        options={{ serverSide: true }}
+        serverSideFilterList={[]}
+        filterListRenderers={filterListRenderers}
+        filterUpdate={() => true}
+        columnNames={columnNames}
+      />,
+    );
+
+    const actualResult = mountWrapper.find(Chip);
+    assert.strictEqual(actualResult.length, 0);
+  });
+
   it('should remove entry from filterList when calling filterUpdate method with type=dropdown and an empty array', () => {
     const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} />);
     const table = shallowWrapper.dive();
@@ -1109,6 +1160,32 @@ describe('<MUIDataTable />', function() {
     assert.deepEqual(JSON.stringify(newDisplayData), expectedResult);
   });
 
+  it('should call onRowsExpand when row is expanded or collapsed', () => {
+    const options = {
+      expandableRows: true,
+      renderExpandableRow: () => <tr><td>foo</td></tr>,
+      expandableRowsOnClick: true,
+      onRowsExpand: spy()
+    };
+    const mountWrapper = mount(<MUIDataTable columns={columns} data={data} options={options} />);
+
+    mountWrapper
+      .find('#MUIDataTableBodyRow-2')
+      .first()
+      .simulate('click');
+
+    assert.strictEqual(options.onRowsExpand.callCount, 1);
+    assert(options.onRowsExpand.calledWith([{ index: 2, dataIndex: 2 }], [{ index: 2, dataIndex: 2 }]));
+
+    mountWrapper
+      .find('#MUIDataTableBodyRow-2')
+      .first()
+      .simulate('click');
+
+    assert.strictEqual(options.onRowsExpand.callCount, 2);
+    assert(options.onRowsExpand.calledWith([{ index: 2, dataIndex: 2 }], []));
+  });
+
   it('should not remove selected data on selectRowDelete when type=cell when onRowsDelete returns false', () => {
     const options = {
       onRowsDelete: () => false,
diff --git a/test/MUIDataTableBody.test.js b/test/MUIDataTableBody.test.js
index 71a0423..c105eec 100644
--- a/test/MUIDataTableBody.test.js
+++ b/test/MUIDataTableBody.test.js
@@ -307,12 +307,47 @@ describe('<TableBody />', function() {
     assert.strictEqual(toggleExpandRow.callCount, 0);
   });
 
+  it('should not gather expanded row data when clicking row with expandableRowsOnClick=true when it is disabled with isRowExpandable via dataIndex.', () => {
+    let expandedRowData;
+    const options = {
+      expandableRows: true,
+      renderExpandableRow: () => <tr><td>foo</td></tr>,
+      expandableRowsOnClick: true,
+      isRowExpandable: dataIndex => dataIndex === 2 ? false : true,
+    };
+    const toggleExpandRow = spy((_, data) => expandedRowData = data);
+
+    const mountWrapper = mount(
+      <TableBody
+        data={displayData}
+        count={displayData.length}
+        columns={columns}
+        page={0}
+        rowsPerPage={10}
+        selectedRows={[]}
+        expandedRows={[]}
+        toggleExpandRow={toggleExpandRow}
+        options={options}
+        searchText={''}
+        filterList={[]}
+      />,
+    );
+
+    mountWrapper
+      .find('#MUIDataTableBodyRow-2')
+      .first()
+      .simulate('click');
+
+    assert.isUndefined(expandedRowData);
+    assert.strictEqual(toggleExpandRow.callCount, 0);
+  });
+
   it('should not gather selected row data when clicking row with selectableRowsOnClick=true when it is disabled with isRowSelectable via selectedRows.', () => {
     let selectedRowData;
     const options = {
       selectableRows: true,
       selectableRowsOnClick: true,
-      isRowSelectable: (index, selectedRows) => selectedRows.lookup[index] || selectedRows.data.length < 1,
+      isRowSelectable: (dataIndex, selectedRows) => selectedRows.lookup[dataIndex] || selectedRows.data.length < 1,
     };
     const selectRowUpdate = (_, data) => (selectedRowData = data);
     const toggleExpandRow = spy();
@@ -347,12 +382,12 @@ describe('<TableBody />', function() {
     assert.strictEqual(toggleExpandRow.callCount, 0);
   });
 
-  it('should gather selected row data when clicking row with selectableRowsOnClick=true when it is enabled with isRowSelectable via index.', () => {
+  it('should gather selected row data when clicking row with selectableRowsOnClick=true when it is enabled with isRowSelectable via dataIndex.', () => {
     let selectedRowData;
     const options = {
       selectableRows: true,
       selectableRowsOnClick: true,
-      isRowSelectable: (index, selectedRows) => selectedRows.lookup[index] || selectedRows.data.length < 1,
+      isRowSelectable: (dataIndex, selectedRows) => selectedRows.lookup[dataIndex] || selectedRows.data.length < 1,
     };
     const selectRowUpdate = (_, data) => (selectedRowData = data);
     const toggleExpandRow = spy();
@@ -387,6 +422,41 @@ describe('<TableBody />', function() {
     assert.strictEqual(toggleExpandRow.callCount, 0);
   });
 
+  it('should gather expanded row data when clicking row with expandableRowsOnClick=true when it is enabled with isRowExpandable via dataIndex.', () => {
+    let expandedRowData;
+    const options = {
+      expandableRows: true,
+      renderExpandableRow: () => <tr><td>foo</td></tr>,
+      expandableRowsOnClick: true,
+      isRowExpandable: dataIndex => dataIndex === 2 ? true : false,
+    };
+    const toggleExpandRow = spy(data => expandedRowData = data);
+
+    const mountWrapper = mount(
+      <TableBody
+        data={displayData}
+        count={displayData.length}
+        columns={columns}
+        page={0}
+        rowsPerPage={10}
+        selectedRows={[]}
+        expandedRows={[]}
+        toggleExpandRow={toggleExpandRow}
+        options={options}
+        searchText={''}
+        filterList={[]}
+      />,
+    );
+
+    mountWrapper
+      .find('#MUIDataTableBodyRow-2')
+      .first()
+      .simulate('click');
+
+    assert.isDefined(expandedRowData);
+    assert.strictEqual(toggleExpandRow.callCount, 1);
+  });
+
   it('should gather expanded row data when clicking row with expandableRows=true and expandableRowsOnClick=true.', () => {
     let expandedRowData;
     const options = { selectableRows: true, expandableRows: true, expandableRowsOnClick: true };
diff --git a/test/MUIDataTableFilter.test.js b/test/MUIDataTableFilter.test.js
index 7c2fcb2..c5b3cb6 100644
--- a/test/MUIDataTableFilter.test.js
+++ b/test/MUIDataTableFilter.test.js
@@ -186,6 +186,27 @@ describe('<TableFilter />', function() {
     assert.strictEqual(actualResult.length, 13);
   });
 
+  it('should render a filter dialog with custom footer when customFooter is provided', () => {
+    const CustomFooter = () => <div id="custom-footer">customFooter</div>;
+    const options = { textLabels };
+    const filterList = [[], [], [], []];
+    const onFilterUpdate = spy();
+
+    const shallowWrapper = shallow(
+      <TableFilter
+        customFooter={CustomFooter}
+        columns={columns}
+        onFilterUpdate={onFilterUpdate}
+        filterData={filterData}
+        filterList={filterList}
+        options={options}
+      />,
+    ).dive();
+
+    const actualResult = shallowWrapper.find('#custom-footer');
+    assert.strictEqual(actualResult.length, 1);
+  });
+
   it('should trigger onFilterUpdate prop callback when calling method handleCheckboxChange', () => {
     const options = { filterType: 'checkbox', textLabels };
     const filterList = [[], [], [], []];
diff --git a/test/MUIDataTableToolbar.test.js b/test/MUIDataTableToolbar.test.js
index 32405ed..9e5a76f 100644
--- a/test/MUIDataTableToolbar.test.js
+++ b/test/MUIDataTableToolbar.test.js
@@ -79,6 +79,16 @@ describe('<TableToolbar />', function() {
     assert.strictEqual(actualResult.props().options.searchText, 'searchText');
   });
 
+  it('should render a toolbar with search if option.searchOpen = true', () => {
+    const newOptions = { ...options, searchOpen: true };
+    const mountWrapper = mount(
+      <TableToolbar columns={columns} data={data} options={newOptions} setTableAction={setTableAction} />,
+    );
+    const actualResult = mountWrapper.find(TableSearch);
+    assert.strictEqual(actualResult.length, 1);
+    assert.strictEqual(actualResult.props().options.searchText, undefined);
+  });
+
   it('should render a toolbar with no search icon if option.search = false', () => {
     const newOptions = { ...options, search: false };
     const mountWrapper = mount(
@@ -182,6 +192,36 @@ describe('<TableToolbar />', function() {
     assert.strictEqual(actualResult.length, 0);
   });
 
+  it('should call onFilterDialogOpen when opening filters via toolbar', () => {
+    const onFilterDialogOpen = spy();
+    const newOptions = { ...options, onFilterDialogOpen };
+    const shallowWrapper = shallow(
+      <TableToolbar columns={columns} data={data} options={newOptions} setTableAction={setTableAction} />,
+    ).dive();
+    const instance = shallowWrapper.instance();
+
+    instance.setActiveIcon('filter');
+    shallowWrapper.update();
+
+    assert.strictEqual(onFilterDialogOpen.callCount, 1);
+  });
+
+  it('should call onFilterDialogClose when closing filters dialog', () => {
+    const onFilterDialogClose = spy();
+    const newOptions = { ...options, onFilterDialogClose };
+    const shallowWrapper = shallow(
+      <TableToolbar columns={columns} data={data} options={newOptions} setTableAction={setTableAction} />,
+    ).dive();
+    const instance = shallowWrapper.instance();
+
+    instance.setActiveIcon('filter');
+    shallowWrapper.update();
+    instance.setActiveIcon(undefined);
+    shallowWrapper.update();
+
+    assert.strictEqual(onFilterDialogClose.callCount, 1);
+  });
+
   it('should set icon when calling method setActiveIcon', () => {
     const shallowWrapper = shallow(
       <TableToolbar columns={columns} data={data} options={options} setTableAction={setTableAction} />,