Add PolyGerrit UI for showing service user details

This change adds another screen, that lists the account details of
a service user. This screen can be accessed by clicking on the username
in the service user list or by using the URI
/x/serviceuser/user/${account_id}.

The page currently does not allow to edit the service user's account
details. This will be added in a future change.

Change-Id: I6e7e9feeb0d1778e87688ccfaccb631d76045870
diff --git a/src/main/resources/static/gr-serviceuser-detail.html b/src/main/resources/static/gr-serviceuser-detail.html
new file mode 100644
index 0000000..0eaf7bb
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-detail.html
@@ -0,0 +1,117 @@
+<!--
+@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="./gr-serviceuser-ssh-panel.html">
+
+<dom-module id="gr-serviceuser-detail">
+  <template>
+    <style include="shared-styles"></style>
+    <style include="gr-subpage-styles"></style>
+    <style include="gr-form-styles"></style>
+    <style>
+      div.serviceuser-detail {
+        margin: 2em auto;
+        max-width: 50em;
+      }
+
+      h1#Title {
+        margin-bottom: 1em;
+      }
+
+      span#gr_serviceuser_activity {
+        border-radius: 1em;
+        width: 10em;
+        padding: 0.3em;
+        font-weight: bold;
+        text-align: center;
+      }
+
+      span.Active {
+        background-color: #9fcc6b;
+      }
+
+      span.Inactive {
+        background-color: #f7a1ad;
+      }
+    </style>
+    <div class="serviceuser-detail">
+      <main class="gr-form-styles read-only">
+        <div id="loading"
+             class$="[[_computeLoadingClass(_loading)]]">
+          Loading...
+        </div>
+        <div id="loadedContent"
+             class$="[[_computeLoadingClass(_loading)]]">
+          <h1 id="Title">Service User "[[_serviceUser.name]]"</h1>
+          <div id="form">
+            <fieldset>
+              <fieldset>
+                <h2 id="accountState">Account State</h2>
+                <section>
+                  <span class="title">Current State</span>
+                  <span id="gr_serviceuser_activity"
+                        class$="value [[_active(_serviceUser)]]">
+                    [[_active(_serviceUser)]]
+                  </span>
+                </section>
+              </fieldset>
+              <fieldset>
+                <h2 id="userDataHeader">User Data</h2>
+                <section>
+                  <span class="title">Username</span>
+                  <span class="value">[[_serviceUser.username]]</span>
+                </section>
+                <section>
+                  <span class="title">Full Name</span>
+                  <span class="value">[[_serviceUser.name]]</span>
+                </section>
+                <section>
+                  <span class="title">Email Address</span>
+                  <span class="value">[[_serviceUser.email]]</span>
+                </section>
+                <section>
+                  <span class="title">Owner Group</span>
+                  <span class="value">[[_getOwnerGroup(_serviceUser)]]</span>
+                </section>
+              </fieldset>
+              <fieldset>
+                <h2 id="creationHeader">Creation</h2>
+                <section>
+                  <span class="title">Created By</span>
+                  <span class="value">[[_getCreator(_serviceUser)]]</span>
+                </section>
+                <section>
+                  <span class="title">Created At</span>
+                  <span class="value">[[_serviceUser.created_at]]</span>
+                </section>
+              </fieldset>
+              <fieldset>
+                <h2 id="credentialsHeader">Credentials</h2>
+                <fieldset>
+                  <h3 id="SSHKeys">SSH keys</h3>
+                  <gr-serviceuser-ssh-panel id="sshEditor"></gr-serviceuser-ssh-panel>
+                </fieldset>
+              </fieldset>
+            </fieldset>
+          </div>
+        </div>
+      </main>
+    </div>
+  </template>
+  <script src="gr-serviceuser-detail.js"></script>
+</dom-module>
diff --git a/src/main/resources/static/gr-serviceuser-detail.js b/src/main/resources/static/gr-serviceuser-detail.js
new file mode 100644
index 0000000..15da6af
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-detail.js
@@ -0,0 +1,102 @@
+// 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() {
+  'use strict';
+
+  const NOT_FOUND_MESSAGE = 'Not Found';
+
+  Polymer({
+    is: 'gr-serviceuser-detail',
+    _legacyUndefinedCheck: true,
+
+    properties: {
+      _restApi: Object,
+      _serviceUserId: String,
+      _serviceUser: Object,
+      _loading: {
+        type: Boolean,
+        value: true,
+      },
+    },
+
+    behaviors: [
+      Gerrit.ListViewBehavior,
+    ],
+
+    attached() {
+      this._extractUserId();
+
+      if (!this._serviceUserId) { return; }
+
+      Promise.resolve(this._getServiceUser()).then(() => {
+        this.$.sshEditor.loadData(this._restApi, this._serviceUser);
+
+        this.fire('title-change', {title: this._serviceUser.name});
+        this._loading = false;
+      });
+    },
+
+    _computeLoadingClass(loading) {
+      return loading ? 'loading' : '';
+    },
+
+    _extractUserId() {
+      this._serviceUserId = this.baseURI.split('/').pop();
+    },
+
+    _getServiceUser() {
+      this._restApi = this.plugin.restApi(
+          '/config/server/serviceuser~serviceusers/');
+      return this._restApi.get(this._serviceUserId)
+          .then(serviceUser => {
+            if (!serviceUser) {
+              this._serviceUser = {};
+              return;
+            }
+            this._serviceUser = serviceUser;
+          });
+    },
+
+    _active(serviceUser) {
+      if (!serviceUser) {
+        return NOT_FOUND_MESSAGE;
+      }
+
+      return serviceUser.inactive === true ? 'Inactive' : 'Active';
+    },
+
+    _getCreator(serviceUser) {
+      if (!serviceUser || !serviceUser.created_by) {
+        return NOT_FOUND_MESSAGE;
+      }
+
+      if (serviceUser.created_by.username != undefined) {
+        return serviceUser.created_by.username;
+      }
+
+      if (serviceUser.created_by._account_id != -1) {
+        return serviceUser.created_by._account_id;
+      }
+
+      return NOT_FOUND_MESSAGE;
+    },
+
+    _getOwnerGroup(serviceUser) {
+      return serviceUser && serviceUser.owner
+        ? serviceUser.owner.name
+        : NOT_FOUND_MESSAGE;
+    },
+  });
+})();
diff --git a/src/main/resources/static/gr-serviceuser-list.html b/src/main/resources/static/gr-serviceuser-list.html
index 3ff745c..5557202 100644
--- a/src/main/resources/static/gr-serviceuser-list.html
+++ b/src/main/resources/static/gr-serviceuser-list.html
@@ -46,7 +46,9 @@
         <template is="dom-repeat"
                   items="[[_serviceUsers]]">
           <tr class="table">
-            <td class="name">[[item.username]]</td>
+            <td class="name">
+              <a href$="[[_computeServiceUserUrl(item._account_id)]]">[[item.username]]</a>
+            </td>
             <td class="fullName">[[item.name]]</td>
             <td class="email">[[item.email]]</td>
             <td class="owner">[[_getOwnerGroup(item)]]</td>
diff --git a/src/main/resources/static/gr-serviceuser-list.js b/src/main/resources/static/gr-serviceuser-list.js
index 1a6272a..b7ef602 100644
--- a/src/main/resources/static/gr-serviceuser-list.js
+++ b/src/main/resources/static/gr-serviceuser-list.js
@@ -83,5 +83,9 @@
     _getOwnerGroup(item) {
       return item && item.owner ? item.owner.name : NOT_FOUND_MESSAGE;
     },
+
+    _computeServiceUserUrl(id) {
+      return `${this.plugin.screenUrl()}/user/${id}`;
+    },
   });
 })();
diff --git a/src/main/resources/static/gr-serviceuser-ssh-panel.html b/src/main/resources/static/gr-serviceuser-ssh-panel.html
new file mode 100644
index 0000000..c63ab5c
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-ssh-panel.html
@@ -0,0 +1,107 @@
+<!--
+@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="gr-serviceuser-ssh-panel">
+  <template>
+    <style include="shared-styles"></style>
+    <style include="gr-form-styles">
+      .statusHeader {
+        width: 4em;
+      }
+
+      .keyHeader {
+        width: 7.5em;
+      }
+
+      #viewKeyOverlay {
+        padding: 2em;
+        width: 50em;
+      }
+
+      .publicKey {
+        font-family: var(--monospace-font-family);
+        overflow-x: scroll;
+        overflow-wrap: break-word;
+        width: 30em;
+      }
+
+      .closeButton {
+        bottom: 2em;
+        position: absolute;
+        right: 2em;
+      }
+
+      #existing {
+        margin-bottom: 1em;
+      }
+
+      #existing .commentColumn {
+        min-width: 27em;
+        width: auto;
+      }
+    </style>
+    <div class="gr-form-styles">
+      <fieldset id="existing">
+        <table>
+          <thead>
+            <tr>
+              <th class="commentColumn">Comment</th>
+              <th class="statusHeader">Status</th>
+              <th class="keyHeader">Public key</th>
+            </tr>
+          </thead>
+          <tbody>
+            <template is="dom-repeat"
+                      items="[[_keys]]"
+                      as="key">
+              <tr>
+                <td class="commentColumn">[[key.comment]]</td>
+                <td>[[_getStatusLabel(key.valid)]]</td>
+                <td>
+                  <gr-button link
+                             on-tap="_showKey"
+                             data-index$="[[index]]"
+                             link>Click to View</gr-button>
+                </td>
+              </tr>
+            </template>
+          </tbody>
+        </table>
+        <gr-overlay id="viewKeyOverlay"
+                    with-backdrop>
+          <fieldset>
+            <section>
+              <span class="title">Algorithm</span>
+              <span class="value">[[_keyToView.algorithm]]</span>
+            </section>
+            <section>
+              <span class="title">Public key</span>
+              <span class="value publicKey">[[_keyToView.encoded_key]]</span>
+            </section>
+            <section>
+              <span class="title">Comment</span>
+              <span class="value">[[_keyToView.comment]]</span>
+            </section>
+          </fieldset>
+          <gr-button class="closeButton"
+                     on-tap="_closeOverlay">Close</gr-button>
+        </gr-overlay>
+      </fieldset>
+    </div>
+  </template>
+  <script src="gr-serviceuser-ssh-panel.js"></script>
+</dom-module>
diff --git a/src/main/resources/static/gr-serviceuser-ssh-panel.js b/src/main/resources/static/gr-serviceuser-ssh-panel.js
new file mode 100644
index 0000000..4ee0b36
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-ssh-panel.js
@@ -0,0 +1,61 @@
+/**
+ * @license
+ * Copyright (C) 2016 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() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-serviceuser-ssh-panel',
+    _legacyUndefinedCheck: true,
+
+    properties: {
+      _restApi: Object,
+      _serviceUser: Object,
+      _keys: Array,
+      /** @type {?} */
+      _keyToView: Object,
+    },
+
+    loadData(restApi, serviceUser) {
+      this._restApi = restApi;
+      this._serviceUser = serviceUser;
+      return this._restApi.get(`${this._serviceUser._account_id}/sshkeys`)
+          .then(keys => {
+            if (!keys) {
+              this._keys = [];
+              return;
+            }
+            this._keys = keys;
+          });
+    },
+
+    _getStatusLabel(isValid) {
+      return isValid ? 'Valid' : 'Invalid';
+    },
+
+    _showKey(e) {
+      const el = Polymer.dom(e).localTarget;
+      const index = parseInt(el.getAttribute('data-index'), 10);
+      this._keyToView = this._keys[index];
+      this.$.viewKeyOverlay.open();
+    },
+
+    _closeOverlay() {
+      this.$.viewKeyOverlay.close();
+    },
+  });
+})();
diff --git a/src/main/resources/static/gr-serviceuser.html b/src/main/resources/static/gr-serviceuser.html
index 982ba43..deb277e 100644
--- a/src/main/resources/static/gr-serviceuser.html
+++ b/src/main/resources/static/gr-serviceuser.html
@@ -18,6 +18,8 @@
 
 <link rel="import"
       href="./gr-serviceuser-list.html">
+<link rel="import"
+      href="./gr-serviceuser-detail.html">
 
 <dom-module id="gr-serviceuser">
   <script>
@@ -28,6 +30,7 @@
                 && (capabilities.administrateServer
                     || capabilities['serviceuser-createServiceUser'])) {
               plugin.screen('list', 'gr-serviceuser-list');
+              plugin.screen('user', 'gr-serviceuser-detail');
             }
             plugin.admin()
               .addMenuLink(