Merge "Add filter to projects page"
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
index d84085e..ce281cf 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
@@ -16,6 +16,7 @@
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
@@ -35,6 +36,13 @@
         border-collapse: collapse;
         width: 100%;
       }
+      #filterContainer {
+        margin: 1em;
+      }
+      #projectFilter {
+        font-size: 1em;
+        max-width: 25em;
+      }
       td {
         flex-shrink: 0;
         padding: .3em .5em;
@@ -71,6 +79,14 @@
         padding: 1em var(--default-horizontal-margin);
       }
     </style>
+    <div id="filterContainer">
+      <label>Filter:</label>
+      <input is="iron-input"
+          type="text"
+          id="projectFilter"
+          bind-value="{{_filter}}"
+          on-input="_onValueChange">
+    </div>
     <table id="projectList">
       <tr class="headerRow">
         <th class="name topHeader">Project Name</th>
@@ -99,10 +115,10 @@
     </table>
     <nav>
       <a id="prevArrow"
-          href$="[[_computeNavLink(_offset, -1, _projectsPerPage)]]"
+          href$="[[_computeNavLink(_offset, -1, _projectsPerPage, _filter)]]"
           hidden$="[[_hidePrevArrow(_offset)]]" hidden>&larr; Prev</a>
       <a id="nextArrow"
-          href$="[[_computeNavLink(_offset, 1, _projectsPerPage)]]"
+          href$="[[_computeNavLink(_offset, 1, _projectsPerPage, _filter)]]"
           hidden$="[[_hideNextArrow(_loading, _projects)]]" hidden>
         Next &rarr;</a>
     </nav>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
index 6e6a51b..9aacccf 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
@@ -14,6 +14,8 @@
 (function() {
   'use strict';
 
+  const REQUEST_DEBOUNCE_INTERVAL_MS = 200;
+
   Polymer({
     is: 'gr-admin-project-list',
 
@@ -51,6 +53,7 @@
         type: Boolean,
         value: true,
       },
+      _filter: String,
     },
 
     behaviors: [
@@ -63,16 +66,35 @@
       'previous-page': '_handlePreviousPage',
     },
 
+    _onValueChange(e) {
+      this.debounce('reload', () => {
+        if (e.target.value) {
+          return page.show('/admin/projects/q/filter:' +
+              this.encodeURL(e.target.value, false));
+        }
+        page.show('/admin/projects');
+      }, REQUEST_DEBOUNCE_INTERVAL_MS);
+    },
+
     _paramsChanged(value) {
       this._loading = true;
 
+      if (value) {
+        this._filter = value.filter || null;
+      }
+
       if (value && value.offset) {
         this._offset = value.offset;
       } else {
         this._offset = 0;
       }
 
-      return this.$.restAPI.getProjects(this._projectsPerPage, this._offset)
+      return this._getProjects(this._filter, this._projectsPerPage,
+          this._offset);
+    },
+
+    _getProjects(filter, projectsPerPage, offset) {
+      return this.$.restAPI.getProjects(filter, projectsPerPage, offset)
           .then(projects => {
             if (!projects) {
               this._projects = [];
@@ -106,11 +128,14 @@
       return webLinks.length ? webLinks : null;
     },
 
-    _computeNavLink(offset, direction, projectsPerPage) {
+    _computeNavLink(offset, direction, projectsPerPage, filter) {
       // Offset could be a string when passed from the router.
       offset = +(offset || 0);
       const newOffset = Math.max(0, offset + (projectsPerPage * direction));
       let href = this.getBaseUrl() + '/admin/projects';
+      if (filter) {
+        href += '/q/filter:' + filter;
+      }
       if (newOffset > 0) {
         href += ',' + newOffset;
       }
@@ -132,17 +157,5 @@
       }
       return loading || lastPage || !projects || !projects.length;
     },
-
-    _handleNextPage() {
-      if (this.$.nextArrow.hidden) { return; }
-      page.show(this._computeNavLink(
-          this._offset, 1, this._projectsPerPage));
-    },
-
-    _handlePreviousPage() {
-      if (this.$.prevArrow.hidden) { return; }
-      page.show(this._computeNavLink(
-          this._offset, -1, this._projectsPerPage));
-    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html
index f9e08cd..4b43b34 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html
@@ -52,8 +52,18 @@
   suite('gr-admin-project-list tests', () => {
     let element;
     let projects;
+    let sandbox;
     let value;
 
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
     suite('list with projects', () => {
       setup(done => {
         projects = _.times(26, projectGenerator);
@@ -64,7 +74,6 @@
           },
         });
 
-        element = fixture('basic');
         element._paramsChanged(value).then(() => { flush(done); });
       });
 
@@ -115,7 +124,6 @@
           },
         });
 
-        element = fixture('basic');
         element._paramsChanged(value).then(() => { flush(done); });
       });
 
@@ -135,5 +143,55 @@
         assert.equal(element._shownProjects.length, 25);
       });
     });
+
+    suite('filter', () => {
+      test('_onValueChange', done => {
+        sandbox.stub(page, 'show', url => {
+          assert.equal(url, '/admin/projects/q/filter:test');
+          done();
+        });
+        const e = {target: {value: 'test'}};
+        element._onValueChange(e);
+      });
+
+      test('_paramsChanged', done => {
+        sandbox.stub(element.$.restAPI, 'getProjects', () => {
+          return Promise.resolve(projects);
+        });
+        const value = {
+          filter: 'test',
+          offset: 25,
+        };
+        element._paramsChanged(value).then(() => {
+          assert.isTrue(element.$.restAPI.getProjects.lastCall
+              .calledWithExactly('test', 25, 25));
+          done();
+        });
+      });
+
+      test('_computeNavLink', () => {
+        const offset = 25;
+        const projectsPerPage = 25;
+        const filter = 'test';
+
+        sandbox.stub(element, 'getBaseUrl', () => '');
+
+        assert.equal(
+            element._computeNavLink(offset, 1, projectsPerPage, filter),
+            '/admin/projects/q/filter:test,50');
+
+        assert.equal(
+            element._computeNavLink(offset, -1, projectsPerPage, filter),
+            '/admin/projects/q/filter:test');
+
+        assert.equal(
+            element._computeNavLink(offset, 1, projectsPerPage, null),
+            '/admin/projects,50');
+
+        assert.equal(
+            element._computeNavLink(offset, -1, projectsPerPage, null),
+            '/admin/projects');
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index d66b5f8..c6b3ccf 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -113,6 +113,22 @@
       app.params = {
         view: 'gr-admin-project-list',
         offset: data.params[1] || 0,
+        filter: null,
+      };
+    });
+
+    page('/admin/projects/q/filter::filter,:offset', loadUser, data => {
+      app.params = {
+        view: 'gr-admin-project-list',
+        offset: data.params.offset,
+        filter: data.params.filter,
+      };
+    });
+
+    page('/admin/projects/q/filter::filter', loadUser, data => {
+      app.params = {
+        view: 'gr-admin-project-list',
+        filter: data.params.filter || null,
       };
     });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index c404f25..c2352c5d 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -497,10 +497,12 @@
       });
     },
 
-    getProjects(projectsPerPage, opt_offset) {
+    getProjects(filter, projectsPerPage, opt_offset) {
       const offset = opt_offset || 0;
+      filter = filter ? '&m=' + filter : '';
+
       return this._fetchSharedCacheURL(
-          `/projects/?d&n=${projectsPerPage + 1}&S=${offset}`
+          `/projects/?d&n=${projectsPerPage + 1}&S=${offset}${filter}`
       );
     },
 
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 9b8820a..1257529 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -643,5 +643,20 @@
           'POST', '/changes/foo/revisions/bar/comments/01234/delete',
           {reason: 'removal reason'}));
     });
+
+    test('getProjects', () => {
+      sandbox.stub(element, '_fetchSharedCacheURL');
+      element.getProjects('test', 25);
+      assert.isTrue(element._fetchSharedCacheURL.lastCall
+          .calledWithExactly('/projects/?d&n=26&S=0&m=test'));
+
+      element.getProjects(null, 25);
+      assert.isTrue(element._fetchSharedCacheURL.lastCall
+          .calledWithExactly('/projects/?d&n=26&S=0'));
+
+      element.getProjects('test', 25, 25);
+      assert.isTrue(element._fetchSharedCacheURL.lastCall
+          .calledWithExactly('/projects/?d&n=26&S=25&m=test'));
+    });
   });
 </script>