From 0558d0b96c81f9b28e1bb7841fb5e953d4e02008 Mon Sep 17 00:00:00 2001
From: Esko Ikkala <esko.ikkala@aalto.fi>
Date: Fri, 15 Jun 2018 16:19:08 +0300
Subject: [PATCH] Render results with react-virtualized

---
 src/client/components/VirtualizedTable.js | 232 ++++++++++++++++++++++
 src/client/containers/MapApp.js           |  11 +-
 src/client/index.js                       |   4 +-
 src/client/styles/virtualized-table.css   |  34 ++++
 4 files changed, 278 insertions(+), 3 deletions(-)
 create mode 100644 src/client/components/VirtualizedTable.js
 create mode 100644 src/client/styles/virtualized-table.css

diff --git a/src/client/components/VirtualizedTable.js b/src/client/components/VirtualizedTable.js
new file mode 100644
index 00000000..293fc288
--- /dev/null
+++ b/src/client/components/VirtualizedTable.js
@@ -0,0 +1,232 @@
+import React from 'react';
+import Immutable from 'immutable';
+import PropTypes from 'prop-types';
+import {
+  AutoSizer,
+  Column,
+  Table,
+  SortDirection,
+  SortIndicator
+} from 'react-virtualized';
+import { withStyles } from '@material-ui/core/styles';
+
+const styles = theme => ({
+  root: {
+    width: '100%',
+    marginTop: theme.spacing.unit * 25,
+  },
+  headerRow: {
+    borderBottom: '1px solid #e0e0e0'
+  },
+  evenRow: {
+    borderBottom: '1px solid #e0e0e0'
+  },
+  oddRow: {
+    borderBottom: '1px solid #e0e0e0',
+    //backgroundColor: '#fafafa'
+  },
+  headerColumn: {
+    textTransform: 'none'
+  },
+  noRows: {
+    position: 'absolute',
+    top: 0,
+    bottom: 0,
+    left: 0,
+    right: 0,
+    display: 'flex',
+    alignItems: 'center',
+    justifyContent: 'center',
+    fontSize: '1em',
+    color: '#bdbdbd',
+  }
+
+});
+
+class VirtualizedTable extends React.PureComponent {
+
+  constructor(props) {
+    super(props);
+
+    const sortBy = 'source';
+    const sortDirection = SortDirection.ASC;
+    const sortedList = this._sortList({sortBy, sortDirection});
+
+
+    this.state = {
+      disableHeader: false,
+      headerHeight: 30,
+      height: 500,
+      overscanRowCount: 10,
+      rowHeight: 40,
+      rowCount: this.props.list.size,
+      scrollToIndex: undefined,
+      sortBy,
+      sortDirection,
+      sortedList,
+      useDynamicRowHeight: false,
+    };
+
+    this._getRowHeight = this._getRowHeight.bind(this);
+    this._headerRenderer = this._headerRenderer.bind(this);
+    this._noRowsRenderer = this._noRowsRenderer.bind(this);
+    this._onRowCountChange = this._onRowCountChange.bind(this);
+    this._onScrollToRowChange = this._onScrollToRowChange.bind(this);
+    this._rowClassName = this._rowClassName.bind(this);
+    this._sort = this._sort.bind(this);
+  }
+
+  render() {
+    const {
+      disableHeader,
+      headerHeight,
+      height,
+      overscanRowCount,
+      rowHeight,
+      rowCount,
+      scrollToIndex,
+      sortBy,
+      sortDirection,
+      sortedList,
+      useDynamicRowHeight,
+    } = this.state;
+
+    const { classes } = this.props;
+
+    const rowGetter = ({index}) => this._getDatum(sortedList, index);
+
+    return (
+      <div>
+        <AutoSizer>
+          {({width}) => (
+            <Table
+              disableHeader={disableHeader}
+              headerClassName={classes.headerColumn}
+              headerHeight={headerHeight}
+              height={height}
+              noRowsRenderer={this._noRowsRenderer}
+              overscanRowCount={overscanRowCount}
+              rowClassName={this._rowClassName}
+              rowHeight={useDynamicRowHeight ? this._getRowHeight : rowHeight}
+              rowGetter={rowGetter}
+              rowCount={rowCount}
+              scrollToIndex={scrollToIndex}
+              sort={this._sort}
+              sortBy={sortBy}
+              sortDirection={sortDirection}
+              width={width}
+              style={classes}
+            >
+              <Column
+                label="Label"
+                cellDataGetter={({rowData}) => rowData.label}
+                dataKey="label"
+                width={150}
+              />
+              <Column
+                label="Type"
+                cellDataGetter={({rowData}) => rowData.typeLabel}
+                dataKey="typeLabel"
+                width={150}
+              />
+              <Column
+                label="Area"
+                cellDataGetter={({rowData}) => rowData.broaderAreaLabel}
+                dataKey="broaderAreaLabel"
+                width={150}
+              />
+              <Column
+                label="Source"
+                cellDataGetter={({rowData}) => rowData.source}
+                dataKey="source"
+                width={150}
+              />
+            </Table>
+          )}
+        </AutoSizer>
+      </div>
+    );
+  }
+
+  _getDatum(list, index) {
+    return list.get(index % list.size);
+  }
+
+  _getRowHeight({index}) {
+    const list = this.props.list;
+
+    return this._getDatum(list, index).size;
+  }
+
+  _headerRenderer({dataKey, sortBy, sortDirection}) {
+    return (
+      <div>
+        Full Name
+        {sortBy === dataKey && <SortIndicator sortDirection={sortDirection} />}
+      </div>
+    );
+  }
+
+  _noRowsRenderer() {
+    const { classes } = this.props;
+    return <div className={classes.noRows}>No rows</div>;
+  }
+
+  _onRowCountChange(event) {
+    const rowCount = parseInt(event.target.value, 10) || 0;
+
+    this.setState({rowCount});
+  }
+
+  _onScrollToRowChange(event) {
+    const {rowCount} = this.state;
+    let scrollToIndex = Math.min(
+      rowCount - 1,
+      parseInt(event.target.value, 10),
+    );
+
+    if (isNaN(scrollToIndex)) {
+      scrollToIndex = undefined;
+    }
+
+    this.setState({scrollToIndex});
+  }
+
+  _rowClassName({index}) {
+    const { classes } = this.props;
+    if (index < 0) {
+      return classes.headerRow;
+    } else {
+      return index % 2 === 0 ? classes.evenRow : classes.oddRow;
+    }
+  }
+
+  _sort({sortBy, sortDirection}) {
+    const sortedList = this._sortList({sortBy, sortDirection});
+
+    this.setState({sortBy, sortDirection, sortedList});
+  }
+
+  _sortList({sortBy, sortDirection}) {
+    const list = this.props.list;
+
+    return list
+      .sortBy(item => item[sortBy])
+      .update(
+        list => (sortDirection === SortDirection.DESC ? list.reverse() : list),
+      );
+  }
+
+  _updateUseDynamicRowHeight(value) {
+    this.setState({
+      useDynamicRowHeight: value,
+    });
+  }
+}
+
+VirtualizedTable.propTypes = {
+  list: PropTypes.instanceOf(Immutable.List).isRequired,
+  classes: PropTypes.object.isRequired,
+};
+
+export default withStyles(styles)(VirtualizedTable);
diff --git a/src/client/containers/MapApp.js b/src/client/containers/MapApp.js
index f107a062..9fe68e92 100644
--- a/src/client/containers/MapApp.js
+++ b/src/client/containers/MapApp.js
@@ -17,7 +17,9 @@ import Tab from '@material-ui/core/Tab';
 import IntegrationAutosuggest from '../components/IntegrationAutosuggest';
 import LeafletMap from '../components/map/LeafletMap';
 import Message from '../components/Message';
-import ResultTable from '../components/result-table/ResultTable';
+//import ResultTable from '../components/result-table/ResultTable';
+import VirtualizedTable from '../components/VirtualizedTable';
+import Immutable from 'immutable';
 
 
 import {
@@ -119,6 +121,11 @@ let MapApp = (props) => {
   const anchor = 'left';
 
   //console.log(props.search.results)
+  let resultList = [];
+  if (props.search.results.length > 0) {
+    resultList = Immutable.List(props.search.results);
+  }
+
 
   const drawer = (
     <Drawer
@@ -156,7 +163,7 @@ let MapApp = (props) => {
         clearResults={props.clearResults}
       />
       {props.search.results.length > 0 &&
-        <ResultTable data={props.search.results} />
+        <VirtualizedTable list={resultList} />
       }
     </Drawer>
   );
diff --git a/src/client/index.js b/src/client/index.js
index fe9e0301..3bd1f2de 100644
--- a/src/client/index.js
+++ b/src/client/index.js
@@ -6,11 +6,13 @@ import { Provider } from 'react-redux';
 import reducer from './reducers';
 import rootEpic from './epics';
 import ReduxToastr from 'react-redux-toastr';
-import 'react-redux-toastr/lib/css/react-redux-toastr.min.css';
 import { bindActionCreators } from 'redux';
 import { actions as toastrActions } from 'react-redux-toastr';
 import App from './components/App';
 
+import 'react-redux-toastr/lib/css/react-redux-toastr.min.css';
+import 'react-virtualized/styles.css';
+
 const store = createStore(
   reducer,
   // applyMiddleware() tells createStore() how to handle middleware
diff --git a/src/client/styles/virtualized-table.css b/src/client/styles/virtualized-table.css
new file mode 100644
index 00000000..0819af2c
--- /dev/null
+++ b/src/client/styles/virtualized-table.css
@@ -0,0 +1,34 @@
+.Table {
+  width: 100%;
+  margin-top: 15px;
+}
+.headerRow,
+.evenRow,
+.oddRow {
+  border-bottom: 1px solid #e0e0e0;
+}
+.oddRow {
+  background-color: #fafafa;
+}
+.headerColumn {
+  text-transform: none;
+}
+.exampleColumn {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+
+.noRows {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 1em;
+  color: #bdbdbd;
+}
-- 
GitLab