Merge branch 'stable-3.0'

* stable-3.0:
  PG: autocomplete reviewers
  Bazel: Fix standalone build mode
  Polygerrit UI
  Switch required bazel version to 0.29.1

Change-Id: I3f673674f8c3eef685ab104289c1887295300b6f
diff --git a/.bazelversion b/.bazelversion
index 8862dba..25939d3 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-1.0.0rc2
+0.29.1
diff --git a/BUILD b/BUILD
index a26e3b7..32b4c8d 100644
--- a/BUILD
+++ b/BUILD
@@ -6,6 +6,8 @@
     "PLUGIN_TEST_DEPS",
     "gerrit_plugin",
 )
+load("//tools/bzl:genrule2.bzl", "genrule2")
+load("//tools/bzl:js.bzl", "polygerrit_plugin")
 
 gerrit_plugin(
     name = "reviewers",
@@ -15,6 +17,28 @@
         "Gerrit-Module: com.googlesource.gerrit.plugins.reviewers.Module",
     ],
     resources = glob(["src/main/resources/**/*"]),
+    resource_jars = [":rv-reviewers-static"],
+)
+
+genrule2(
+    name = "rv-reviewers-static",
+    srcs = [":rv-reviewers"],
+    outs = ["rv-reviewers-static.jar"],
+    cmd = " && ".join([
+        "mkdir $$TMP/static",
+        "cp -r $(locations :rv-reviewers) $$TMP/static",
+        "cd $$TMP",
+        "zip -Drq $$ROOT/$@ -g .",
+    ]),
+)
+
+polygerrit_plugin(
+    name = "rv-reviewers",
+    srcs = glob([
+        "rv-reviewers/*.html",
+        "rv-reviewers/*.js",
+    ]),
+    app = "plugin.html",
 )
 
 junit_tests(
diff --git a/WORKSPACE b/WORKSPACE
index 4242552..190fd06 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -7,6 +7,32 @@
     #local_path = "/home/<user>/projects/bazlets",
 )
 
+# Polymer dependencies
+load(
+    "@com_googlesource_gerrit_bazlets//:gerrit_polymer.bzl",
+    "gerrit_polymer",
+)
+
+gerrit_polymer()
+
+# Load closure compiler with transitive dependencies
+load("@io_bazel_rules_closure//closure:defs.bzl", "closure_repositories")
+
+closure_repositories()
+
+# Load Gerrit npm_binary toolchain
+load("@com_googlesource_gerrit_bazlets//tools:js.bzl", "GERRIT", "npm_binary")
+
+npm_binary(
+    name = "polymer-bundler",
+    repository = GERRIT,
+)
+
+npm_binary(
+    name = "crisper",
+    repository = GERRIT,
+)
+
 # Release Plugin API
 #load(
 #    "@com_googlesource_gerrit_bazlets//:gerrit_api.bzl",
diff --git a/plugin.html b/plugin.html
new file mode 100644
index 0000000..29c10cb
--- /dev/null
+++ b/plugin.html
@@ -0,0 +1,27 @@
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<link rel="import" href="./rv-reviewers/rv-reviewers.html">
+
+<dom-module id="reviewers">
+  <script>
+    if (window.Polymer) {
+      Gerrit.install(plugin => {
+        plugin.registerCustomComponent('repo-command', 'rv-reviewers');
+      });
+    }
+  </script>
+</dom-module>
diff --git a/rv-reviewers/rv-edit-screen.html b/rv-reviewers/rv-edit-screen.html
new file mode 100644
index 0000000..ca32849
--- /dev/null
+++ b/rv-reviewers/rv-edit-screen.html
@@ -0,0 +1,76 @@
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<link rel="import" href="./rv-filter-section.html">
+
+<dom-module id="rv-edit-screen">
+  <template>
+    <style include="shared-styles"></style>
+    <style include="gr-menu-page-styles"></style>
+    <style include="gr-subpage-styles">
+      .bottomButtons {
+        display: flex;
+      }
+      #closeButton {
+        float: right;
+      }
+      #filterSections {
+          width: 100%;
+      }
+      header {
+        border-bottom: 1px solid var(--border-colo);
+        flex-shrink: 0;
+        font-weight: var(--font-weight-bold)
+      }
+    </style>
+    <div>
+      <header>Reviewers Config</header>
+      <table id="filterSections">
+        <tr>
+          <th>Filter Sections</th>
+        </tr>
+        <tr id="loading" class$="loadingMsg [[_computeLoadingClass(loading)]]">
+          <td>Loading...</td>
+        </tr>
+        <tbody class$="[[_computeLoadingClass(loading)]]">
+          <tr>
+            <template
+                is="dom-repeat"
+                items="[[_filterSections]]"
+                as="section">
+              <rv-filter-section
+                  filter="[[section.filter]]"
+                  reviewers="[[section.reviewers]]"
+                  editing="[[section.editing]]"
+                  reviewers-url="[[_getReviewersUrl(repoName)]]"
+                  plugin-rest-api="[[pluginRestApi]]"
+                  can-modify-config="[[canModifyConfig]]"
+                  on-reviewer-changed="_handleReviewerChanged"></rv-filter-section>
+            </template>
+          </tr>
+        </tbody>
+      </table>
+      <div class="bottomButtons">
+        <gr-button id="closeButton" on-tap="_handleCloseTap">Close</gr-button>
+        <gr-button
+            id="addFilterBtn"
+            on-tap="_handleCreateSection"
+            hidden="[[_computeAddFilterBtnHidden(canModifyConfig, _editingFilter)]]">Add New Filter</gr-button>
+      </div>
+    </div>
+  </template>
+  <script src="./rv-edit-screen.js"></script>
+</dom-module>
diff --git a/rv-reviewers/rv-edit-screen.js b/rv-reviewers/rv-edit-screen.js
new file mode 100644
index 0000000..f25f696
--- /dev/null
+++ b/rv-reviewers/rv-edit-screen.js
@@ -0,0 +1,69 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  Polymer({
+    is: 'rv-edit-screen',
+
+    properties: {
+      pluginRestApi: Object,
+      repoName: String,
+      loading: Boolean,
+      canModifyConfig: Boolean,
+      _editingFilter: {
+        type: Boolean,
+        value: false,
+      },
+      _filterSections: Array,
+    },
+
+    attached() {
+      this._loadFilterSections();
+    },
+
+    _loadFilterSections() {
+      this.pluginRestApi.get(this._getReviewersUrl(this.repoName))
+          .then(filterSections => {
+            this._filterSections = filterSections;
+          });
+    },
+
+    _computeAddFilterBtnHidden(canModifyConfig, editingFilter) {
+      return !canModifyConfig || editingFilter;
+    },
+
+    _computeLoadingClass(loading) {
+      return loading ? 'loading' : '';
+    },
+
+    _getReviewersUrl(repoName) {
+      return `/projects/${repoName}/reviewers`;
+    },
+
+    _handleCreateSection() {
+      const section = {filter: '', reviewers: [], editing: true};
+      this._editingFilter = true;
+      this.push('_filterSections', section);
+    },
+
+    _handleCloseTap(e) {
+      e.preventDefault();
+      this.fire('close', null, {bubbles: false});
+    },
+
+    _handleReviewerChanged(e) {
+      this._filterSections = e.detail.result;
+      this._editingFilter = false;
+    },
+  });
+})();
diff --git a/rv-reviewers/rv-filter-section.html b/rv-reviewers/rv-filter-section.html
new file mode 100644
index 0000000..104ecbd
--- /dev/null
+++ b/rv-reviewers/rv-filter-section.html
@@ -0,0 +1,97 @@
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<link rel="import" href="./rv-reviewer.html">
+
+<dom-module id="rv-filter-section">
+  <template>
+    <style include="shared-styles">
+      :host {
+        display: block;
+        margin-bottom: 1em;
+      }
+      fieldset {
+        border: 1px solid var(--border-color);
+      }
+      .name {
+        align-items: center;
+        display: flex;
+      }
+      .header {
+        align-items: center;
+        background: var(--table-header-background-color);
+        border-bottom: 1px dotted var(--border-color);
+        display: flex;
+        justify-content: space-between;
+        min-height: 3em;
+        padding: 0 .7em;
+      }
+      #addReviewer {
+        display: flex;
+      }
+      #editFilterInput {
+        width: 30vw;
+        max-width: 500px;
+        margin-left: 3px;
+      }
+      #mainContainer {
+        display: block;
+      }
+    </style>
+    <style include="gr-form-styles"></style>
+    <fieldset id="section"
+        class$="gr-form-styles">
+      <div id="mainContainer">
+        <div class="header">
+          <div class="name">
+            <h3>Filter:</h3>
+            <input
+                id="editFilterInput"
+                bind-value="{{filter}}"
+                is="iron-input"
+                type="text"
+                disabled="[[_computeFilterInputDisabled(canModifyConfig, _originalFilter)]]">
+            <gr-button
+                id="cancelBtn"
+                on-tap="_handleCancel"
+                hidden$="[[_computeCancelHidden(filter, _originalFilter)]]">Cancel</gr-button>
+          </div><!-- name -->
+        </div><!-- header -->
+        <div class="reviewers">
+          <template
+              is="dom-repeat"
+              items="{{reviewers}}">
+            <rv-reviewer
+                reviewer="{{item}}"
+                can-modify-config="[[canModifyConfig]]"
+                plugin-rest-api="[[pluginRestApi]]"
+                on-reviewer-deleted="_handleReviewerDeleted"
+                on-reviewer-added="_handleReviewerAdded">
+            </rv-reviewer>
+          </template>
+          <div id="addReviewer">
+            <gr-button
+                link
+                id="addBtn"
+                on-tap="_handleAddReviewer"
+                hidden="[[_computeAddBtnHidden(canModifyConfig, _editingReviewer)]]">Add Reviewer</gr-button>
+          </div><!-- addReviewer -->
+        </div><!-- reviewers -->
+      </div>
+    </fieldset>
+  </template>
+  <script src="./rv-filter-section.js"></script>
+</dom-module>
diff --git a/rv-reviewers/rv-filter-section.js b/rv-reviewers/rv-filter-section.js
new file mode 100644
index 0000000..0ca36dc
--- /dev/null
+++ b/rv-reviewers/rv-filter-section.js
@@ -0,0 +1,98 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  Polymer({
+    is: 'rv-filter-section',
+
+    properties: {
+      pluginRestApi: Object,
+      reviewers: Array,
+      filter: String,
+      canModifyConfig: Boolean,
+      _originalFilter: String,
+      _editingReviewer: {
+        type: Boolean,
+        value: false,
+      },
+      reviewersUrl: String,
+    },
+
+    attached() {
+      this._updateSection();
+    },
+
+    _updateSection() {
+      this._originalFilter = this.filter;
+    },
+
+    _computeEditing(filter, _originalFilter) {
+      if (_originalFilter === '') {
+        return true;
+      }
+      return filter === '';
+    },
+
+    _computeCancelHidden(filter, _originalFilter) {
+      return !this._computeEditing(filter, _originalFilter);
+    },
+
+    _computeAddBtnHidden(canModifyConfig, editingReviewer) {
+      return !(canModifyConfig && !editingReviewer);
+    },
+
+    _computeFilterInputDisabled(canModifyConfig, originalFilter) {
+      return !canModifyConfig || originalFilter !== '';
+    },
+
+    _handleCancel() {
+      this.remove();
+    },
+
+    _handleReviewerDeleted(e) {
+      if (e.detail.editing) {
+        this.reviewers.pop();
+        this._editingReviewer = false;
+      } else {
+        const index = e.model.index;
+        const deleted = this.reviewers[index];
+        this._putReviewer(deleted, 'DELETE');
+      }
+    },
+
+    _handleReviewerAdded(e) {
+      this._editingReviewer = false;
+      this._putReviewer(e.detail.reviewer, 'ADD').catch(err => {
+        this.fire('show-alert', {message: err});
+        throw err;
+      });
+    },
+
+    _putReviewer(reviewer, action) {
+      return this.pluginRestApi.put(this.reviewersUrl, {
+        action,
+        reviewer,
+        filter: this.filter,
+      }).then(result => {
+        const detail = {result};
+        this.dispatchEvent(
+            new CustomEvent('reviewer-changed', {detail, bubbles: true}));
+      });
+    },
+
+    _handleAddReviewer() {
+      this.push('reviewers', '');
+      this._editingReviewer = true;
+    },
+  });
+})();
diff --git a/rv-reviewers/rv-reviewer.html b/rv-reviewers/rv-reviewer.html
new file mode 100644
index 0000000..339eff7
--- /dev/null
+++ b/rv-reviewers/rv-reviewer.html
@@ -0,0 +1,80 @@
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<dom-module id="rv-reviewer">
+  <template>
+    <style include="shared-styles">
+      #editReviewerInput {
+        display: block;
+        width: 250px;
+      }
+      .reviewerRow {
+        align-items: center;
+        display: flex;
+      }
+      #reviewerHeader,
+      #editReviewerInput,
+      #deleteCancelBtn,
+      #addBtn,
+      #reviewerField {
+          margin-left: 3px;
+      }
+      #reviewerField {
+        width: 250px;
+        text-indent: 1px;
+        border: 1px solid var(--border-color);
+      }
+    </style>
+    <style include="gr-form-styles"></style>
+    <div class="reviewerRow">
+      <h4 id="reviewerHeader">Reviewer:</h4>
+      <template is="dom-if" if="[[_computeEditing(reviewer, _originalReviewer)]]">
+        <span class="value">
+            <!--
+              TODO:
+              Investigate wether we could reuse gr-account-list.
+              If the REST API returns AccountInfo instead of an account
+              identifier String we should be able to use gr-account-list(size=1)
+              for all reviewers, including those who are non-editable
+              (#reviewerField below) and allign the plugin with how accounts
+              are displayed in core Gerrit's UI.
+            -->
+            <gr-autocomplete
+                id="editReviewerInput"
+                text="{{reviewer}}"
+                value="{{_reviewerSearchId}}"
+                query="[[_queryReviewers]]"
+                placeholder="Name Or Email"
+                disabled="[[_computeReviewerDisabled(reviewer, _originalReviewer)]]">
+            </gr-autocomplete>
+        </span>
+      </template>
+      <template is="dom-if" if="[[!_computeEditing(reviewer, _originalReviewer)]]">
+        <td id="reviewerField">[[reviewer]]</td>
+      </template>
+      <gr-button
+          id="deleteCancelBtn"
+          on-tap="_handleDeleteCancel"
+          hidden$="[[_computeHideDeleteButton(canModifyConfig)]]"
+          >[[_computeDeleteCancel(reviewer, _originalReviewer)]]</gr-button>
+      <gr-button
+          id="addBtn"
+          on-tap="_handleAddReviewer"
+          hidden$="[[_computeHideAddButton(reviewer, _originalReviewer)]]">Add</gr-button>
+    </div> <!-- reviewerRow -->
+  </template>
+  <script src="./rv-reviewer.js"></script>
+</dom-module>
diff --git a/rv-reviewers/rv-reviewer.js b/rv-reviewers/rv-reviewer.js
new file mode 100644
index 0000000..a8da113
--- /dev/null
+++ b/rv-reviewers/rv-reviewer.js
@@ -0,0 +1,119 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  Polymer({
+    is: 'rv-reviewer',
+
+    properties: {
+      canModifyConfig: Boolean,
+      pluginRestAPi: Object,
+      reviewer: String,
+      _reviewerSearchId: String,
+      _queryReviewers: {
+        type: Function,
+        value() {
+          return this._getAccountSuggestions.bind(this);
+        },
+      },
+      _originalReviewer: String,
+      _deleted: Boolean,
+      _editing: {
+        type: Boolean,
+        computed: '_computeEditing(reviewer, _originalReviewer)',
+      },
+    },
+
+    attached() {
+      this._originalReviewer = this.reviewer;
+    },
+
+    _computeReviewerDisabled(reviewer, _originalReviewer) {
+      return !this._computeEditing(reviewer, _originalReviewer);
+    },
+
+    _computeEditing(reviewer, _originalReviewer) {
+      if (_originalReviewer === '') {
+        return true;
+      }
+      return reviewer === '';
+    },
+
+    _computeDeleteCancel(reviewer, _originalReviewer) {
+      return this._computeEditing(reviewer, _originalReviewer) ?
+      'Cancel' : 'Delete';
+    },
+
+    _computeHideAddButton(reviewer, _originalReviewer) {
+      return !(this._computeEditing(reviewer, _originalReviewer)
+      && this._reviewerSearchId);
+    },
+
+    _computeHideDeleteButton(canModifyConfig) {
+      return !canModifyConfig;
+    },
+
+    _getAccountSuggestions(input) {
+      if (input.length === 0) { return Promise.resolve([]); }
+      return this._getSuggestedAccounts(
+          input).then(accounts => {
+            const accountSuggestions = [];
+            let nameAndEmail;
+            let value;
+            if (!accounts) { return []; }
+            for (const key in accounts) {
+              if (!accounts.hasOwnProperty(key)) { continue; }
+              if (accounts[key].email) {
+                nameAndEmail = accounts[key].name +
+                  ' <' + accounts[key].email + '>';
+              } else {
+                nameAndEmail = accounts[key].name;
+              }
+              if (accounts[key].username) {
+                value = accounts[key].username;
+              } else if (accounts[key].email) {
+                value = accounts[key].email;
+              } else {
+                value = accounts[key]._account_id;
+              }
+              accountSuggestions.push({
+                name: nameAndEmail,
+                value,
+              });
+            }
+            return accountSuggestions;
+          });
+    },
+
+    _getSuggestedAccounts(input) {
+      const suggestUrl = `/accounts/?suggest&q=${input}`;
+      return this.pluginRestApi.get(suggestUrl);
+    },
+
+    _handleDeleteCancel() {
+      const detail = {editing: this._editing};
+      if (this._editing) {
+        this.remove();
+      }
+      this.dispatchEvent(
+          new CustomEvent('reviewer-deleted', {detail, bubbles: true}));
+    },
+
+    _handleAddReviewer() {
+      const detail = {reviewer: this._reviewerSearchId};
+      this._originalReviewer = this.reviewer;
+      this.dispatchEvent(
+          new CustomEvent('reviewer-added', {detail, bubbles: true}));
+    },
+  });
+})();
diff --git a/rv-reviewers/rv-reviewers.html b/rv-reviewers/rv-reviewers.html
new file mode 100644
index 0000000..dcf890d
--- /dev/null
+++ b/rv-reviewers/rv-reviewers.html
@@ -0,0 +1,40 @@
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<link rel="import" href="./rv-edit-screen.html">
+
+<dom-module id="rv-reviewers">
+  <template>
+    <style include="shared-styles">
+      #rvScreenOverlay {
+        width: 50em;
+      }
+    </style>
+    <gr-repo-command
+        title="Reviewers Config"
+        on-command-tap="_handleCommandTap">
+    </gr-repo-command>
+    <gr-overlay id="rvScreenOverlay" with-backdrop>
+      <rv-edit-screen
+          plugin-rest-api="[[pluginRestApi]]"
+          repo-name="[[repoName]]"
+          loading="[[_loading]]"
+          can-modify-config="[[_canModifyConfig]]"
+          on-close="_handleRvEditScreenClose"></rv-edit-screen>
+    </gr-overlay>
+  </template>
+  <script src="./rv-reviewers.js"></script>
+</dom-module>
diff --git a/rv-reviewers/rv-reviewers.js b/rv-reviewers/rv-reviewers.js
new file mode 100644
index 0000000..e40e03a
--- /dev/null
+++ b/rv-reviewers/rv-reviewers.js
@@ -0,0 +1,82 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  Polymer({
+    is: 'rv-reviewers',
+
+    properties: {
+      pluginRestApi: Object,
+      repoName: String,
+      _canModifyConfig: {
+        type: Boolean,
+        computed: '_computeCanModifyConfig(_isOwner, _hasModifyCapability)',
+      },
+      _loading: {
+        type: Boolean,
+        value: true,
+      },
+      _isOwner: {
+        type: Boolean,
+        value: false,
+      },
+      _hasModifyCapability: {
+        type: Boolean,
+        value: false,
+      },
+    },
+
+    attached() {
+      this.pluginRestApi = this.plugin.restApi();
+      this._setCanModifyConfig();
+    },
+
+    _handleCommandTap() {
+      this.$.rvScreenOverlay.open();
+    },
+
+    _handleRvEditScreenClose() {
+      this.$.rvScreenOverlay.close();
+    },
+
+    _setCanModifyConfig() {
+      const promises = [];
+      promises.push(this._getRepoAccess(this.repoName).then( access => {
+        if (access && access[this.repoName] && access[this.repoName].is_owner) {
+          this._isOwner = true;
+        }
+      }));
+      promises.push(this._getCapabilities().then(capabilities => {
+        if (capabilities['reviewers-modifyReviewersConfig']) {
+          this._hasModifyCapability = true;
+        }
+      }));
+      Promise.all(promises).then(() => {
+        this._loading = false;
+      });
+    },
+
+    _computeCanModifyConfig(isOwner, hasModifyCapability) {
+      return isOwner || hasModifyCapability;
+    },
+
+    _getRepoAccess(repoName) {
+      return this.pluginRestApi.get(
+          '/access/?project=' + encodeURIComponent(repoName));
+    },
+
+    _getCapabilities() {
+      return this.pluginRestApi.get('/accounts/self/capabilities');
+    },
+  });
+})();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/Module.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/Module.java
index 076040b..efc3614 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/reviewers/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/Module.java
@@ -25,6 +25,8 @@
 import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.extensions.webui.JavaScriptPlugin;
+import com.google.gerrit.extensions.webui.WebUiPlugin;
 import com.google.gerrit.server.change.ReviewerSuggestion;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
@@ -78,5 +80,13 @@
             }
           });
     }
+    install(
+        new AbstractModule() {
+          @Override
+          protected void configure() {
+            DynamicSet.bind(binder(), WebUiPlugin.class)
+                .toInstance(new JavaScriptPlugin("rv-reviewers.html"));
+          }
+        });
   }
 }
diff --git a/tools/bzl/genrule2.bzl b/tools/bzl/genrule2.bzl
new file mode 100644
index 0000000..61c4e18
--- /dev/null
+++ b/tools/bzl/genrule2.bzl
@@ -0,0 +1,3 @@
+load("@com_googlesource_gerrit_bazlets//tools:genrule2.bzl", _genrule2 = "genrule2")
+
+genrule2 = _genrule2
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
new file mode 100644
index 0000000..0f9e367
--- /dev/null
+++ b/tools/bzl/js.bzl
@@ -0,0 +1,3 @@
+load("@com_googlesource_gerrit_bazlets//tools:js.bzl", _polygerrit_plugin = "polygerrit_plugin")
+
+polygerrit_plugin = _polygerrit_plugin