From 9dc8e02b1d8a3baadc6c8679634ad50a055f1254 Mon Sep 17 00:00:00 2001
From: Will Piers <willpiers@influxdb.com>
Date: Thu, 1 Dec 2016 16:31:21 -0800
Subject: [PATCH] Refactor HostsTable for fun and SPEED

---
 ui/src/hosts/components/HostsTable.js | 144 ++++++++++++++++----------
 1 file changed, 89 insertions(+), 55 deletions(-)

diff --git a/ui/src/hosts/components/HostsTable.js b/ui/src/hosts/components/HostsTable.js
index c93cfad89..c866952b6 100644
--- a/ui/src/hosts/components/HostsTable.js
+++ b/ui/src/hosts/components/HostsTable.js
@@ -19,24 +19,14 @@ const HostsTable = React.createClass({
   getInitialState() {
     return {
       searchTerm: '',
-      filteredHosts: this.props.hosts,
       sortDirection: null,
       sortKey: null,
     };
   },
 
-  componentWillReceiveProps(newProps) {
-    this.filterHosts(newProps.hosts, this.state.searchTerm);
-  },
-
-  filterHosts(allHosts, searchTerm) {
-    const hosts = allHosts.filter((h) => {
-      let apps = null;
-      if (h.apps) {
-        apps = h.apps.join(', ');
-      } else {
-        apps = '';
-      }
+  filter(allHosts, searchTerm) {
+    return allHosts.filter((h) => {
+      const apps = h.apps ? h.apps.join(', ') : '';
       // search each tag for the presence of the search term
       let tagResult = false;
       if (h.tags) {
@@ -52,17 +42,6 @@ const HostsTable = React.createClass({
         tagResult
       );
     });
-    this.setState({searchTerm, filteredHosts: hosts});
-  },
-
-  changeSort(key) {
-    // if we're using the key, reverse order; otherwise, set it with ascending
-    if (this.state.sortKey === key) {
-      const reverseDirection = (this.state.sortDirection === 'asc' ? 'desc' : 'asc');
-      this.setState({sortDirection: reverseDirection});
-    } else {
-      this.setState({sortKey: key, sortDirection: 'asc'});
-    }
   },
 
   sort(hosts, key, direction) {
@@ -76,6 +55,20 @@ const HostsTable = React.createClass({
     }
   },
 
+  updateSearchTerm(term) {
+    this.setState({searchTerm: term});
+  },
+
+  updateSort(key) {
+    // if we're using the key, reverse order; otherwise, set it with ascending
+    if (this.state.sortKey === key) {
+      const reverseDirection = (this.state.sortDirection === 'asc' ? 'desc' : 'asc');
+      this.setState({sortDirection: reverseDirection});
+    } else {
+      this.setState({sortKey: key, sortDirection: 'asc'});
+    }
+  },
+
   sortableClasses(key) {
     if (this.state.sortKey === key) {
       if (this.state.sortDirection === 'asc') {
@@ -87,52 +80,32 @@ const HostsTable = React.createClass({
   },
 
   render() {
-    const hosts = this.sort(this.state.filteredHosts, this.state.sortKey, this.state.sortDirection);
-    const hostCount = hosts.length;
-    const {source} = this.props;
+    const {searchTerm, sortKey, sortDirection} = this.state;
+    const {hosts, source} = this.props;
+    const sortedHosts = this.sort(this.filter(hosts, searchTerm), sortKey, sortDirection);
+    const hostCount = sortedHosts.length;
 
     return (
       <div className="panel panel-minimal">
         <div className="panel-heading u-flex u-ai-center u-jc-space-between">
           <h2 className="panel-title">{hostCount ? `${hostCount} Hosts` : ''}</h2>
-          <SearchBar onSearch={_.wrap(this.props.hosts, this.filterHosts)} />
+          <SearchBar onSearch={this.updateSearchTerm} />
         </div>
         <div className="panel-body">
           <table className="table v-center">
             <thead>
               <tr>
-                <th onClick={() => this.changeSort('name')} className={this.sortableClasses('name')}>Hostname</th>
+                <th onClick={() => this.updateSort('name')} className={this.sortableClasses('name')}>Hostname</th>
                 <th className="text-center">Status</th>
-                <th onClick={() => this.changeSort('cpu')} className={this.sortableClasses('cpu')}>CPU</th>
-                <th onClick={() => this.changeSort('load')} className={this.sortableClasses('load')}>Load</th>
+                <th onClick={() => this.updateSort('cpu')} className={this.sortableClasses('cpu')}>CPU</th>
+                <th onClick={() => this.updateSort('load')} className={this.sortableClasses('load')}>Load</th>
                 <th>Apps</th>
               </tr>
             </thead>
             <tbody>
               {
-                hosts.map(({name, cpu, load, apps = []}) => {
-                  return (
-                    <tr key={name}>
-                      <td className="monotype"><Link to={`/sources/${source.id}/hosts/${name}`}>{name}</Link></td>
-                      <td className="text-center"><div className="table-dot dot-success"></div></td>
-                      <td className="monotype">{isNaN(cpu) ? 'N/A' : `${cpu.toFixed(2)}%`}</td>
-                      <td className="monotype">{isNaN(load) ? 'N/A' : `${load.toFixed(2)}`}</td>
-                      <td className="monotype">
-                        {apps.map((app, index) => {
-                          return (
-                            <span key={app}>
-                              <Link
-                                style={{marginLeft: "2px"}}
-                                to={{pathname: `/sources/${source.id}/hosts/${name}`, query: {app}}}>
-                                {app}
-                              </Link>
-                              {index === apps.length - 1 ? null : ', '}
-                            </span>
-                          );
-                        })}
-                      </td>
-                    </tr>
-                  );
+                sortedHosts.map((h) => {
+                  return <HostRow key={h.name} host={h} source={source} />;
                 })
               }
             </tbody>
@@ -143,13 +116,74 @@ const HostsTable = React.createClass({
   },
 });
 
+const HostRow = React.createClass({
+  propTypes: {
+    source: PropTypes.shape({
+      id: PropTypes.string.isRequired,
+      name: PropTypes.string.isRequired,
+    }).isRequired,
+    host: PropTypes.shape({
+      name: PropTypes.string,
+      cpu: PropTypes.number,
+      load: PropTypes.number,
+      apps: PropTypes.arrayOf(PropTypes.string.isRequired),
+    }),
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return this.props.host !== nextProps.host;
+  },
+
+  render() {
+    const {host, source} = this.props;
+    const {name, cpu, load, apps = []} = host;
+    return (
+      <tr>
+        <td className="monotype"><Link to={`/sources/${source.id}/hosts/${name}`}>{name}</Link></td>
+        <td className="text-center"><div className="table-dot dot-success"></div></td>
+        <td className="monotype">{isNaN(cpu) ? 'N/A' : `${cpu.toFixed(2)}%`}</td>
+        <td className="monotype">{isNaN(load) ? 'N/A' : `${load.toFixed(2)}`}</td>
+        <td className="monotype">
+          {apps.map((app, index) => {
+            return (
+              <span key={app}>
+                <Link
+                  style={{marginLeft: "2px"}}
+                  to={{pathname: `/sources/${source.id}/hosts/${name}`, query: {app}}}>
+                  {app}
+                </Link>
+                {index === apps.length - 1 ? null : ', '}
+              </span>
+            );
+          })}
+        </td>
+      </tr>
+    );
+  },
+});
+
 const SearchBar = React.createClass({
   propTypes: {
     onSearch: PropTypes.func.isRequired,
   },
 
+  getInitialState() {
+    return {
+      searchTerm: '',
+    };
+  },
+
+  componentWillMount() {
+    const waitPeriod = 300;
+    this.handleSearch = _.debounce(this.handleSearch, waitPeriod);
+  },
+
+  handleSearch() {
+    this.props.onSearch(this.state.searchTerm);
+  },
+
   handleChange() {
-    this.props.onSearch(this.refs.searchInput.value);
+    this.setState({searchTerm: this.refs.searchInput.value}, this.handleSearch);
   },
 
   render() {