veyron/examples/pipetobrowser: Search, filter and sort for the Git Status plugin

Switching the Git Status plugin to use the DataGrid component and added sort
and filtering logic.

Plus small changes to sort algorithm for vLog plugin and added ability to only
target the "more info" dialog from cell templates.

Change-Id: I166b157a0a937e17ee855b9e8531ea0f46eca3c7
diff --git a/examples/pipetobrowser/browser/libs/ui-components/data-grid/grid/component.css b/examples/pipetobrowser/browser/libs/ui-components/data-grid/grid/component.css
index 8a2cebe..34574cf 100644
--- a/examples/pipetobrowser/browser/libs/ui-components/data-grid/grid/component.css
+++ b/examples/pipetobrowser/browser/libs/ui-components/data-grid/grid/component.css
@@ -70,4 +70,16 @@
   font-size: 0.8em;
   color: #616161;
   float: right;
+}
+
+.info-column {
+  text-align: center;
+}
+
+[moreInfoOnly] {
+  display: none;
+}
+
+.more-dialog-content [moreInfoOnly] {
+  display: initial;
 }
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/libs/ui-components/data-grid/grid/component.html b/examples/pipetobrowser/browser/libs/ui-components/data-grid/grid/component.html
index 526e44e..3ff76c0 100644
--- a/examples/pipetobrowser/browser/libs/ui-components/data-grid/grid/component.html
+++ b/examples/pipetobrowser/browser/libs/ui-components/data-grid/grid/component.html
@@ -42,7 +42,7 @@
           <td is="p2b-grid-cell-renderer" data="{{ col.columnData }}" repeat="{{ col in columns }}" template>
             <template ref="{{ col.cellTemplateId }}" bind></template>
           </td>
-          <td>
+          <td class="info-column">
             <paper-icon-button on-tap="{{ showMoreInfo }}" class="more-icon" icon="more-vert" title="more info"></paper-icon-button
             >
           </td>
@@ -250,7 +250,7 @@
         var previousTableWidth = self.$.table.offsetWidth;
         onResizeHandler = function() {
           var newWidth = self.$.table.offsetWidth;
-          if (newWidth != previousTableWidth) {
+          if (newWidth != previousTableWidth && newWidth > 0) {
             self.adjustFlexWidths();
           }
           previousTableWidth = newWidth;
diff --git a/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/component.css b/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/component.css
index bc9c2b3..9b266fe 100644
--- a/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/component.css
+++ b/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/component.css
@@ -1,19 +1,28 @@
-tr.notstaged {
-  background-color: #FF9700;
+::shadow /deep/ .state-icon.notstaged {
+  fill: #f57c00;
 }
 
-tr.conflicted {
-  background-color: #DA4336;
+::shadow /deep/ .state-icon.staged {
+  fill: #689f38;
 }
 
-tr.untracked {
-  background-color: #FFEA3A;
+::shadow /deep/ .state-icon.conflicted {
+  fill: #e51c23;
 }
 
-tr.ignored {
-  background-color: #E0E0E0;
+::shadow /deep/ .state-icon.untracked {
+  fill: #bf360c;
 }
 
-tr.staged {
-  background-color: #8AC249;
-}
\ No newline at end of file
+::shadow /deep/ .state-icon.ignored {
+  fill: #bf360c;
+}
+
+::shadow /deep/ .action-icon {
+  fill: #91a7ff;
+}
+
+::shadow /deep/ .file-parent {
+  font-size: 0.8em;
+  color: rgba(0,0,0,.54);
+}
diff --git a/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/component.html b/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/component.html
index 2c517ba..05cd8d2 100644
--- a/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/component.html
+++ b/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/component.html
@@ -1,37 +1,68 @@
 <link rel="import" href="/libs/vendor/polymer/polymer/polymer.html">
+<link rel="import" href="/libs/vendor/polymer/core-icon/core-icon.html">
+<link rel="import" href="/libs/ui-components/data-grid/grid/component.html">
+<link rel="import" href="/libs/ui-components/data-grid/grid/column/component.html">
+<link rel="import" href="/libs/ui-components/data-grid/filter/select/component.html">
+<link rel="import" href="/libs/ui-components/data-grid/filter/select/item/component.html">
+<link rel="import" href="/libs/ui-components/data-grid/filter/toggle/component.html">
+<link rel="import" href="/libs/ui-components/data-grid/search/component.html">
 
 <polymer-element name="p2b-plugin-git-status">
   <template>
-    <link rel="stylesheet" href="component.css">
-    <table summary="Data Grid displaying status of modified file for the git repository.">
-      <thead>
-        <tr>
-          <th>Status</th>
-          <th>Action</th>
-          <th>File</th>
-          <th>Summary</th>
-        </tr>
-      </thead>
-      <tbody>
-        <template repeat="{{ item in statusItems }}">
-          <tr class="{{ item.fileState }}">
-            <td>{{ item.fileState }}</td>
-            <td>{{ item.fileAction }}</td>
-            <th scope="row">{{ item.filePath }}</th>
-            <td>{{ item.summary }}</td>
-          </tr>
-        </template>
-      </tbody>
-    </table>
 
+    <link rel="stylesheet" href="/libs/css/common-style.css">
+    <link rel="stylesheet" href="component.css">
+
+    <p2b-grid id="grid" defaultSortKey="state" defaultSortAscending dataSource="{{ dataSource }}" summary="Data Grid displaying status of modified file for the git repository.">
+
+      <!-- Search -->
+      <p2b-grid-search label="Search Logs"></p2b-grid-search>
+
+      <!-- State Filter (multiple allowed) -->
+      <p2b-grid-filter-select multiple key="state" label="Show state">
+        <p2b-grid-filter-select-item checked label="Staged" value="staged"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Not Staged" value="notstaged"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Conflicted" value="conflicted"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Untracked" value="untracked"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Ignored" value="ignored"></p2b-grid-filter-select-item>
+      </p2b-grid-filter-select>
+
+      <!-- Action Filter (multiple allowed) -->
+      <p2b-grid-filter-select multiple key="action" label="Show actions">
+        <p2b-grid-filter-select-item checked label="Added" value="added"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Deleted" value="deleted"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Modified" value="modified"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Renamed" value="renamed"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Copied" value="copied"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Unknown" value="unknown"></p2b-grid-filter-select-item>
+      </p2b-grid-filter-select>
+
+      <!-- Columns, sorting and cell templates -->
+      <p2b-grid-column label="State" key="state" sortable flex="2" priority="2" >
+        <template>
+          <core-icon class="state-icon {{ item.state }}" icon="{{ item.stateIcon }}" title="{{item.state}}"></core-icon>
+          <span moreInfoOnly style="vertical-align:middle">{{item.state}}</span>
+        </template>
+      </p2b-grid-column>
+      <p2b-grid-column label="Action" key="action" sortable flex="2" priority="3" >
+        <template>
+          <core-icon class="action-icon {{ item.action }}" icon="{{ item.actionIcon }}" title="{{item.action}}"></core-icon>
+          <span moreInfoOnly style="vertical-align:middle">{{item.action}}</span>
+        </template>
+      </p2b-grid-column>
+      <p2b-grid-column label="File" key="filename" sortable primary flex="8" minFlex="5" priority="1" >
+        <template>{{ item.filename }}
+          <div class="file-parent" title="folder: {{item.fileParent}}">{{ item.fileParent }}</div>
+        </template>
+      </p2b-grid-column>
+      <p2b-grid-column label="Summary" flex="7" minFlex="3" priority="4" >
+        <template>{{ item.summary }}</template>
+      </p2b-grid-column>
+
+    </p2b-grid>
   </template>
   <script>
     Polymer('p2b-plugin-git-status', {
-      /*
-       * List of git status items to display
-       * @type {object}
-       */
-      statusItems: []
     });
     </script>
 </polymer-element>
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/data-source.js b/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/data-source.js
new file mode 100644
index 0000000..3e6a271
--- /dev/null
+++ b/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/data-source.js
@@ -0,0 +1,51 @@
+/*
+ * Implement the data source which handles searching, filtering
+ * and sorting of the git status items
+ * @fileoverview
+ */
+
+import { gitStatusSort } from './sorter';
+import { gitStatusSearch } from './searcher';
+import { gitStatusFilter } from './filterer';
+
+export class gitStatusDataSource {
+  constructor(items) {
+
+    /*
+     * all items, unlimited buffer for now.
+     * @private
+     */
+    this.allItems = items;
+  }
+
+  /*
+   * Implements the fetch method expected by the grid components.
+   * handles searching, filtering and sorting of the data.
+   * search, sort and filters are provided by the grid control whenever they are
+   * changed by the user.
+   * DataSource is called automatically by the grid when user interacts with the component
+   * Grid does some batching of user actions and only calls fetch when needed.
+   * keys provided for sort and filters correspond to keys set in the markup
+   * when constructing the grid.
+   * @param {object} search search{key<string>} current search keyword
+   * @param {object} sort sort{key<string>, ascending<bool>} current sort key and direction
+   * @param {map} filters map{key<string>, values<Array>} Map of filter keys to currently selected filter values
+   * @return {Array<object>} Returns an array of filtered sorted results of the items.
+   */
+  fetch(search, sort, filters) {
+
+    var filteredSortedItems = this.allItems.
+      filter((item) => {
+        return gitStatusFilter(item, filters);
+      }).
+      filter((item) => {
+        return gitStatusSearch(item, search.keyword);
+      }).
+      sort((item1, item2) => {
+        return gitStatusSort(item1, item2, sort.key, sort.ascending);
+      });
+
+    return filteredSortedItems;
+
+  }
+}
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/filterer.js b/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/filterer.js
new file mode 100644
index 0000000..4976e66
--- /dev/null
+++ b/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/filterer.js
@@ -0,0 +1,43 @@
+/*
+ * Returns whether the given git status items matches the map of filters.
+ * @param {Object} item A single git status item as defined by parser.item
+ * @param {map} filters Map of keys to selected filter values as defined
+ * when constructing the filters in the grid components.
+ * e.g. filters:{'state':['staged'], 'action':['added','modified']}
+ * @return {boolean} Whether the item satisfies ALL of the given filters.
+ */
+export function gitStatusFilter(item, filters) {
+  if (Object.keys(filters).length === 0) {
+    return true;
+  }
+
+  for (var key in filters) {
+    var isMatch = applyFilter(item, key, filters[key]);
+    // we AND all the filters, short-circuit for early termination
+    if (!isMatch) {
+      return false;
+    }
+  }
+
+  // matches all filters
+  return true;
+};
+
+/*
+ * Returns whether the given git status item matches a single filter
+ * @param {Object} item A single git status item as defined by parser.item
+ * @param {string} key filter key e.g. 'state'
+ * @param {string} value filter value e.g. '['staged','untracked']
+ * @return {boolean} Whether the item satisfies then the given filter key value pair
+ * @private
+ */
+function applyFilter(item, key, value) {
+  switch (key) {
+    case 'state':
+    case 'action':
+      return value.indexOf(item[key]) >= 0;
+    default:
+      // ignore unknown filters
+      return true;
+  }
+}
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/parser.js b/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/parser.js
index cf0135d..b9e2c50 100644
--- a/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/parser.js
+++ b/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/parser.js
@@ -57,18 +57,18 @@
 
 /*
  * A structure representing the status of a git file.
- * @param {string} fileAction, one of added, deleted, renamed, copied, modified, unknown
- * @param {string} fileState, one staged, notstaged, conflicted, untracked, ignored
- * @param {string} filePath filename and path
+ * @param {string} action, one of added, deleted, renamed, copied, modified, unknown
+ * @param {string} state, one staged, notstaged, conflicted, untracked, ignored
+ * @param {string} file filename and path
  * @param {string} summary A summary text for what these states mean
  * @class
  * @private
  */
 class item {
-  constructor(fileAction, fileState, filePath, summary) {
-    this.fileAction = fileAction;
-    this.fileState = fileState;
-    this.filePath = filePath;
+  constructor(action, state, file, summary) {
+    this.action = action;
+    this.state = state;
+    this.file = file;
     this.summary = summary;
   }
 }
diff --git a/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/plugin.js b/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/plugin.js
index b9d6e55..86db6ce 100644
--- a/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/plugin.js
+++ b/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/plugin.js
@@ -7,8 +7,11 @@
  */
 import { View } from 'view';
 import { PipeViewer } from 'pipe-viewer';
-
+import { Logger } from 'logger'
 import { parse } from './parser';
+import { gitStatusDataSource } from './data-source';
+
+var log = new Logger('pipe-viewers/builtin/git/status');
 
 var streamUtil = require('event-stream');
 
@@ -18,27 +21,139 @@
   }
 
   play(stream) {
-    // TODO(aghassemi) let's have the plugin specify if they expect data in
-    // in binary or text so p2b can set the proper encoding for them rather
-    // than each plugin doing it like this.
-    // read data as UTF8
     stream.setEncoding('utf8');
 
     // split by new line
     stream = stream.pipe(streamUtil.split(/\r?\n/));
 
-    var statusItems = [];
-    var statusView = document.createElement('p2b-plugin-git-status');
-    statusView.statusItems = statusItems;
-
-    stream.on('data', (line) => {
-      if (line.trim().length > 0) {
-        statusItems.push( parse(line) );
+    // parse the git status items
+    stream = stream.pipe(streamUtil.map((line, cb) => {
+      if (line.trim() === '') {
+        // eliminate the item
+        cb();
+        return;
       }
+      var item;
+      try {
+        item = parse(line);
+      } catch(e) {
+        log.debug(e);
+      }
+      if (item) {
+        addAdditionalUIProperties(item);
+        cb(null, item);
+      } else {
+        // eliminate the item
+        cb();
+      }
+    }));
+
+    // we return a view promise instead of a view since we want to wait
+    // until all items arrive before showing the data.
+    var viewPromise = new Promise(function(resolve,reject) {
+      // write into an array when stream is done return the UI component
+      stream.pipe(streamUtil.writeArray((err, items) => {
+        if (err) {
+          reject(err);
+        } else {
+          var statusView = document.createElement('p2b-plugin-git-status');
+          statusView.dataSource = new gitStatusDataSource(items);
+          resolve(new View(statusView));
+        }
+      }));
     });
 
-    return new View(statusView);
+    return viewPromise;
   }
 }
 
+/*
+ * Adds additional UI specific properties to the item
+ * @private
+ */
+function addAdditionalUIProperties(item) {
+  addActionIconProperty(item);
+  addStateIconProperty(item);
+  addFileNameFileParentProperty(item);
+}
+
+/*
+ * Adds an icon property to the item specifying what icon to display
+ * based on state
+ * @private
+ */
+function addStateIconProperty(item) {
+  var iconName;
+  switch (item.state) {
+    case 'staged':
+      iconName = 'check-circle';
+      break;
+    case 'notstaged':
+      iconName = 'warning';
+      break;
+    case 'conflicted':
+      iconName = 'error';
+      break;
+    case 'untracked':
+      iconName = 'report';
+      break;
+    case 'ignored':
+      iconName = 'visibility-off';
+      break;
+  }
+
+  item.stateIcon = iconName;
+}
+
+/*
+ * Adds an icon property to the item specifying what icon to display
+ * based on action
+ * @private
+ */
+function addActionIconProperty(item) {
+  var iconName;
+  switch (item.action) {
+    case 'added':
+      iconName = 'add';
+      break;
+    case 'deleted':
+      iconName = 'clear';
+      break;
+    case 'modified':
+      iconName = 'translate';
+      break;
+    case 'renamed':
+      iconName = 'sync';
+      break;
+    case 'copied':
+      iconName = 'content-copy';
+      break;
+    case 'unknown':
+      iconName = 'remove';
+      break;
+  }
+
+  item.actionIcon = iconName;
+}
+
+/*
+ * Splits file into filename and fileParent
+ * @private
+ */
+function addFileNameFileParentProperty(item) {
+
+  var filename = item.file;
+  var fileParent = "./";
+
+  var slashIndex = item.file.lastIndexOf('/');
+
+  if (slashIndex > 0) {
+    filename = item.file.substr(slashIndex + 1);
+    fileParent = item.file.substring(0, slashIndex);
+  }
+
+  item.filename = filename;
+  item.fileParent = fileParent;
+}
+
 export default GitStatusPipeViewer;
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/searcher.js b/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/searcher.js
new file mode 100644
index 0000000..57e8a01
--- /dev/null
+++ b/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/searcher.js
@@ -0,0 +1,12 @@
+export function gitStatusSearch(item, keyword) {
+  if (!keyword) {
+    return true;
+  }
+
+  // we only search file
+  if (item.file.indexOf(keyword) >= 0) {
+    return true
+  }
+
+  return false;
+};
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/sorter.js b/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/sorter.js
new file mode 100644
index 0000000..9dec432
--- /dev/null
+++ b/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/sorter.js
@@ -0,0 +1,41 @@
+var stateSortPriority = {
+  'conflicted' : 1,
+  'untracked' : 2,
+  'notstaged' : 3,
+  'staged': 4,
+  'ignored': 5
+}
+
+var actionSortPriority = {
+  'added' : 1,
+  'deleted' : 2,
+  'modified' : 3,
+  'renamed': 4,
+  'copied': 5,
+  'unknown': 6
+}
+
+export function gitStatusSort(item1, item2, key, ascending) {
+  var first = item1[key];
+  var second = item2[key];
+  if (!ascending) {
+    first = item2[key];
+    second = item1[key];
+  }
+
+  if (key === 'state') {
+    first = stateSortPriority[first];
+    second = stateSortPriority[second];
+  }
+
+  if (key === 'action') {
+    first = actionSortPriority[first];
+    second = actionSortPriority[second];
+  }
+
+  if (typeof first === 'string') {
+    return first.localeCompare(second);
+  } else {
+    return first - second;
+  }
+};
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/pipe-viewers/builtin/vlog/component.html b/examples/pipetobrowser/browser/pipe-viewers/builtin/vlog/component.html
index 972c7cc..196036e 100644
--- a/examples/pipetobrowser/browser/pipe-viewers/builtin/vlog/component.html
+++ b/examples/pipetobrowser/browser/pipe-viewers/builtin/vlog/component.html
@@ -36,18 +36,19 @@
       <p2b-grid-filter-toggle key="autorefresh" label="Live Refresh" checked></p2b-grid-filter-toggle>
 
       <!-- Columns, sorting and cell templates -->
-      <p2b-grid-column label="Level" key="level" sortable flex="1" priority="2" >
+      <p2b-grid-column label="Level" key="level" sortable flex="2" priority="2" >
         <template>
           <core-icon class="level-icon {{ item.level }}" icon="{{ item.icon }}" title="{{item.level}}"></core-icon>
+          <span  moreInfoOnly style="vertical-align:middle">{{item.level}}</span>
         </template>
       </p2b-grid-column>
-      <p2b-grid-column label="File" key="file" sortable flex="3" minFlex="2" priority="4" >
+      <p2b-grid-column label="File" key="file" sortable flex="4" minFlex="2" priority="4" >
         <template>{{ item.file }}<span class="line-number">{{ item.fileLine }}</span></template>
       </p2b-grid-column>
-      <p2b-grid-column label="Message" key="message" primary flex="7" minFlex="5" priority="1" >
+      <p2b-grid-column label="Message" key="message" primary flex="8" minFlex="5" priority="1" >
         <template><div class="message-text">{{ item.message }}</div></template>
       </p2b-grid-column>
-      <p2b-grid-column label="Date" key="date" sortable flex="5" minFlex="3" priority="3">
+      <p2b-grid-column label="Date" key="date" sortable flex="6" minFlex="3" priority="3">
         <template><span class="smaller-text">{{ item.date }}</span></template>
       </p2b-grid-column>
       <p2b-grid-column label="Threadid" key="threadid" sortable flex="0" priority="5">
diff --git a/examples/pipetobrowser/browser/pipe-viewers/builtin/vlog/plugin.js b/examples/pipetobrowser/browser/pipe-viewers/builtin/vlog/plugin.js
index dfb2dbd..1891d5e 100644
--- a/examples/pipetobrowser/browser/pipe-viewers/builtin/vlog/plugin.js
+++ b/examples/pipetobrowser/browser/pipe-viewers/builtin/vlog/plugin.js
@@ -8,8 +8,9 @@
  */
 import { View } from 'view';
 import { PipeViewer } from 'pipe-viewer';
-import { vLogDataSource } from './data-source';
 import { Logger } from 'logger'
+import { vLogDataSource } from './data-source';
+
 var log = new Logger('pipe-viewers/builtin/vlog');
 
 var streamUtil = require('event-stream');
diff --git a/examples/pipetobrowser/browser/pipe-viewers/builtin/vlog/sorter.js b/examples/pipetobrowser/browser/pipe-viewers/builtin/vlog/sorter.js
index f2663f4..0e0a2db 100644
--- a/examples/pipetobrowser/browser/pipe-viewers/builtin/vlog/sorter.js
+++ b/examples/pipetobrowser/browser/pipe-viewers/builtin/vlog/sorter.js
@@ -1,14 +1,26 @@
-export function vLogSort(logItem1, logItem2, key, ascending) {
-  var first = logItem1;
-  var second = logItem2;
+var levelSortPriority = {
+  'fatal' : 1,
+  'error' : 2,
+  'warning' : 3,
+  'info': 4
+}
+
+export function vLogSort(item1, item2, key, ascending) {
+  var first = item1[key];
+  var second = item2[key];
   if (!ascending) {
-    first = logItem2;
-    second = logItem1;
+    first = item2[key];
+    second = item1[key];
   }
 
-  if (typeof first[key] === 'string') {
-    return first[key].localeCompare(second[key]);
+  if (key === 'level') {
+    first = levelSortPriority[first];
+    second = levelSortPriority[second];
+  }
+
+  if (typeof first === 'string') {
+    return first.localeCompare(second);
   } else {
-    return first[key] - second[key];
+    return first - second;
   }
 };
\ No newline at end of file