Merge "veyron/examples/pipetobrowser: Search, filter and sort for the Git Status plugin"
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