Add basic PolyGerrit UI

This basic variant of the UI shows the CRDs and provides links. But it
does not yet color them red if cycles are detected.

Depends-On: I783a00df00d386292ac458e925079219bb6392c7
Change-Id: I019d3b35de2933abecb5caef0613a02904c8eb68
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/.eslintignore
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..6d9ae7c
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,166 @@
+{
+  "extends": ["eslint:recommended", "google"],
+  "parserOptions": {
+    "ecmaVersion": 8,
+    "sourceType": "module"
+  },
+  "env": {
+    "browser": true,
+    "es6": true
+  },
+  "globals": {
+    "__dirname": false,
+    "app": false,
+    "page": false,
+    "Polymer": false,
+    "process": false,
+    "require": false,
+    "Gerrit": false,
+    "Promise": false,
+    "assert": false,
+    "test": false,
+    "flushAsynchronousOperations": false
+  },
+  "rules": {
+    "arrow-parens": ["error", "as-needed"],
+    "block-spacing": ["error", "always"],
+    "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
+    "camelcase": "off",
+    "comma-dangle": ["error", {
+      "arrays": "always-multiline",
+      "objects": "always-multiline",
+      "imports": "always-multiline",
+      "exports": "always-multiline",
+      "functions": "never"
+    }],
+    "eol-last": "off",
+    "indent": ["error", 2, {
+      "MemberExpression": 2,
+      "FunctionDeclaration": {"body": 1, "parameters": 2},
+      "FunctionExpression": {"body": 1, "parameters": 2},
+      "CallExpression": {"arguments": 2 },
+      "ArrayExpression": 1,
+      "ObjectExpression": 1,
+      "SwitchCase": 1
+    }],
+    "keyword-spacing": ["error", { "after": true, "before": true }],
+    "lines-between-class-members": ["error", "always"],
+    "max-len": [
+      "error",
+      80,
+      2,
+      {
+        "ignoreComments": true,
+        "ignorePattern": "^import .*;$"
+      }
+    ],
+    "new-cap": ["error", { "capIsNewExceptions": ["Polymer", "LegacyElementMixin", "GestureEventListeners", "LegacyDataMixin"] }],
+    "no-console": "off",
+    "no-multiple-empty-lines": [ "error", { "max": 1 } ],
+    "no-prototype-builtins": "off",
+    "no-redeclare": "off",
+    "no-restricted-syntax": [
+      "error",
+      {
+        "selector": "ExpressionStatement > CallExpression > MemberExpression[object.name='test'][property.name='only']",
+        "message": "Remove test.only."
+      },
+      {
+        "selector": "ExpressionStatement > CallExpression > MemberExpression[object.name='suite'][property.name='only']",
+        "message": "Remove suite.only."
+      }
+    ],
+    "no-undef": "off",
+    "no-useless-escape": "off",
+    "no-var": "error",
+    "object-shorthand": ["error", "always"],
+    "padding-line-between-statements": [
+      "error",
+      {
+        "blankLine": "always",
+        "prev": "class",
+        "next": "*"
+      },
+      {
+        "blankLine": "always",
+        "prev": "*",
+        "next": "class"
+      }
+    ],
+    "prefer-arrow-callback": "error",
+    "prefer-const": "error",
+    "prefer-spread": "error",
+    "quote-props": ["error", "consistent-as-needed"],
+    "semi": [2, "always"],
+    "template-curly-spacing": "error",
+    "valid-jsdoc": "off",
+
+    "require-jsdoc": 0,
+    "valid-jsdoc": 0,
+    "jsdoc/check-alignment": 2,
+    "jsdoc/check-examples": 0,
+    "jsdoc/check-indentation": 0,
+    "jsdoc/check-param-names": 0,
+    "jsdoc/check-syntax": 0,
+    "jsdoc/check-tag-names": 0,
+    "jsdoc/check-types": 0,
+    "jsdoc/implements-on-classes": 2,
+    "jsdoc/match-description": 0,
+    "jsdoc/newline-after-description": 2,
+    "jsdoc/no-types": 0,
+    "jsdoc/no-undefined-types": 0,
+    "jsdoc/require-description": 0,
+    "jsdoc/require-description-complete-sentence": 0,
+    "jsdoc/require-example": 0,
+    "jsdoc/require-hyphen-before-param-description": 0,
+    "jsdoc/require-jsdoc": 0,
+    "jsdoc/require-param": 0,
+    "jsdoc/require-param-description": 0,
+    "jsdoc/require-param-name": 2,
+    "jsdoc/require-param-type": 2,
+    "jsdoc/require-returns": 0,
+    "jsdoc/require-returns-check": 0,
+    "jsdoc/require-returns-description": 0,
+    "jsdoc/require-returns-type": 2,
+    "jsdoc/valid-types": 2,
+    "jsdoc/require-file-overview": ["error", {
+      "tags": {
+        "license": {
+          "mustExist": true,
+          "preventDuplicates": true
+        }
+      }
+    }],
+    "import/named": 2,
+    "import/no-unresolved": 2,
+    "import/no-self-import": 2,
+    // The no-cycle rule is slow, because it doesn't cache dependencies.
+    // Disable it.
+    "import/no-cycle": 0,
+    "import/no-useless-path-segments": 2,
+    "import/no-unused-modules": 2,
+    "import/no-default-export": 2
+  },
+  "plugins": [
+    "html",
+    "jsdoc",
+    "import"
+  ],
+  "settings": {
+    "html/report-bad-indent": "error"
+  },
+  "overrides": [
+    {
+      "files": ["*_html.js", "*-styles.js", "externs.js"],
+      "rules": {
+        "max-len": "off"
+      }
+    },
+    {
+      "files": ["*.html"],
+      "rules": {
+        "jsdoc/require-file-overview": "off"
+      }
+    }
+  ]
+}
diff --git a/BUILD b/BUILD
index 5368f19..5f1c689 100644
--- a/BUILD
+++ b/BUILD
@@ -1,11 +1,15 @@
 load("@rules_java//java:defs.bzl", "java_library")
+load("@npm_bazel_rollup//:index.bzl", "rollup_bundle")
 load("//tools/bzl:junit.bzl", "junit_tests")
+load("//tools/js:eslint.bzl", "eslint")
 load(
     "//tools/bzl:plugin.bzl",
     "PLUGIN_DEPS",
     "PLUGIN_TEST_DEPS",
     "gerrit_plugin",
 )
+load("//tools/bzl:genrule2.bzl", "genrule2")
+load("//tools/bzl:js.bzl", "polygerrit_plugin")
 
 gerrit_plugin(
     name = "zuul",
@@ -14,7 +18,9 @@
     manifest_entries = [
         "Gerrit-PluginName: zuul",
         "Gerrit-Module: com.googlesource.gerrit.plugins.zuul.Module",
+        "Gerrit-HttpModule: com.googlesource.gerrit.plugins.zuul.HttpModule",
     ],
+    resource_jars = [":gr-zuul-static"],
 )
 
 junit_tests(
@@ -36,3 +42,54 @@
     ],
 )
 
+genrule2(
+    name = "gr-zuul-static",
+    srcs = [":gr-zuul"],
+    outs = ["gr-zuul-static.jar"],
+    cmd = " && ".join([
+        "mkdir $$TMP/static",
+        "cp $(locations :gr-zuul) $$TMP/static",
+        "cd $$TMP",
+        "zip -Drq $$ROOT/$@ -g .",
+    ]),
+)
+
+polygerrit_plugin(
+    name = "gr-zuul",
+    app = "zuul-bundle.js",
+    plugin_name = "zuul",
+)
+
+rollup_bundle(
+    name = "zuul-bundle",
+    srcs = glob(["gr-zuul/*.js"]),
+    entry_point = "gr-zuul/plugin.js",
+    rollup_bin = "//tools/node_tools:rollup-bin",
+    sourcemap = "hidden",
+    format = "iife",
+    deps = [
+        "@tools_npm//rollup-plugin-node-resolve",
+    ],
+)
+
+# Define the eslinter for the plugin
+# The eslint macro creates 2 rules: lint_test and lint_bin
+eslint(
+    name = "lint",
+    srcs = glob([
+        "gr-zuul/**/*.js",
+    ]),
+    config = ".eslintrc.json",
+    data = [],
+    extensions = [
+        ".js",
+    ],
+    ignore = ".eslintignore",
+    plugins = [
+        "@npm//eslint-config-google",
+        "@npm//eslint-plugin-html",
+        "@npm//eslint-plugin-import",
+        "@npm//eslint-plugin-jsdoc",
+    ],
+)
+
diff --git a/gr-zuul/gr-zuul.js b/gr-zuul/gr-zuul.js
new file mode 100644
index 0000000..9bcd761
--- /dev/null
+++ b/gr-zuul/gr-zuul.js
@@ -0,0 +1,58 @@
+/**
+ * @license
+ * Copyright (C) 2020 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.
+ */
+
+import {htmlTemplate} from './gr-zuul_html.js';
+
+class GrZuul extends Polymer.Element {
+  /** @returns {string} name of the component */
+  static get is() { return 'gr-zuul'; }
+
+  /** @returns {?} template for this component */
+  static get template() { return htmlTemplate; }
+
+  static get properties() {
+    return {
+      change: {
+        type: Object,
+        observer: '_onChangeChanged',
+      },
+      _crd: {
+        type: Object,
+        value: {},
+      },
+      _crd_loaded: {
+        type: Boolean,
+        value: false,
+      },
+    };
+  }
+
+  _onChangeChanged() {
+    this._crd_loaded = false;
+    const url = '/changes/' + this.change.id + '/revisions/current/crd';
+    return this.plugin.restApi().send('GET', url).then(crd => {
+      this._crd = crd;
+      this._crd_loaded = true;
+    });
+  }
+
+  _computeDependencyUrl(changeId) {
+    return Gerrit.Nav.getUrlForSearchQuery(changeId);
+  }
+}
+
+customElements.define(GrZuul.is, GrZuul);
diff --git a/gr-zuul/gr-zuul_html.js b/gr-zuul/gr-zuul_html.js
new file mode 100644
index 0000000..a5328e2
--- /dev/null
+++ b/gr-zuul/gr-zuul_html.js
@@ -0,0 +1,88 @@
+/**
+ * @license
+ * Copyright (C) 2020 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.
+ */
+
+export const htmlTemplate = Polymer.html`
+    <style include="shared-styles">
+      section.related-changes-section {
+        margin-bottom: 1.4em; /* Same as line height for collapse purposes */
+        display: block;
+      }
+      div.foo {
+        margin-bottom: 1.4em; /* Same as line height for collapse purposes */
+      }
+      a {
+        display: block;
+      }
+      .changeContainer,
+      a {
+        max-width: 100%;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+      }
+      .changeContainer {
+        display: flex;
+      }
+      .changeContainer.thisChange:before {
+        content: '➔';
+        width: 1.2em;
+      }
+      h4,
+      section div {
+        display: flex;
+      }
+      h4:before,
+      section div:before {
+        content: ' ';
+        flex-shrink: 0;
+        width: 1.2em;
+      }
+    </style>
+    <template is="dom-if" if="[[_crd_loaded]]">
+      <template is="dom-if" if="[[_crd.depends_on.length]]">
+        <section class="related-changes-section">
+          <h4>Depends on</h4>
+          <template is="dom-repeat" items="[[_crd.depends_on]]">
+            <div class="changeContainer zuulDependencyContainer">
+              <a
+                href$="[[_computeDependencyUrl(item)]]"
+                title$="[[item]]"
+              >
+                [[item]]
+              </a>
+            </div>
+          </template>
+        </section>
+      </template>
+      <template is="dom-if" if="[[_crd.needed_by.length]]">
+        <section class="related-changes-section">
+          <h4>Needed by</h4>
+          <template is="dom-repeat" items="[[_crd.needed_by]]">
+            <div class="changeContainer zuulDependencyContainer">
+              <a
+                href$="[[_computeDependencyUrl(item)]]"
+                title$="[[item]]"
+              >
+                [[item]]
+              </a>
+            </div>
+          </template>
+        </section>
+      </template>
+    </template>
+`;
+
diff --git a/gr-zuul/plugin.js b/gr-zuul/plugin.js
new file mode 100644
index 0000000..475f053
--- /dev/null
+++ b/gr-zuul/plugin.js
@@ -0,0 +1,22 @@
+/**
+ * @license
+ * Copyright (C) 2020 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.
+ */
+import './gr-zuul.js';
+
+Gerrit.install(plugin => {
+  plugin.registerCustomComponent(
+      'related-changes-section', 'gr-zuul', {slot: 'bottom'});
+});
diff --git a/src/main/java/com/googlesource/gerrit/plugins/zuul/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/zuul/HttpModule.java
new file mode 100644
index 0000000..bc6db6b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/zuul/HttpModule.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2020 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.
+
+package com.googlesource.gerrit.plugins.zuul;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.webui.JavaScriptPlugin;
+import com.google.gerrit.extensions.webui.WebUiPlugin;
+import com.google.inject.servlet.ServletModule;
+
+public class HttpModule extends ServletModule {
+  @Override
+  protected void configureServlets() {
+    DynamicSet.bind(binder(), WebUiPlugin.class).toInstance(new JavaScriptPlugin("zuul.html"));
+  }
+}