Add PolyGerrit UI for creating a service user

Change-Id: I0df465587e29ae21ff6b475c11e80cb3434637f9
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetConfig.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetConfig.java
index e955f43..85da7b0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetConfig.java
@@ -14,6 +14,7 @@
 
 package com.googlesource.gerrit.plugins.serviceuser;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -61,6 +62,8 @@
   public ConfigInfo apply(ConfigResource rsrc) throws PermissionBackendException {
     PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName);
     ConfigInfo info = new ConfigInfo();
+    info.info = Strings.emptyToNull(cfg.getString("infoMessage"));
+    info.onSuccess = Strings.emptyToNull(cfg.getString("onSuccessMessage"));
     info.allowEmail = toBoolean(cfg.getBoolean("allowEmail", false));
     info.allowHttpPassword = toBoolean(cfg.getBoolean("allowHttpPassword", false));
     info.allowOwner = toBoolean(cfg.getBoolean("allowOwner", false));
@@ -92,6 +95,8 @@
   }
 
   public class ConfigInfo {
+    public String info;
+    public String onSuccess;
     public Boolean allowEmail;
     public Boolean allowHttpPassword;
     public Boolean allowOwner;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutConfig.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutConfig.java
index 8b45918..09a559e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutConfig.java
@@ -40,6 +40,8 @@
 @Singleton
 class PutConfig implements RestModifyView<ConfigResource, Input> {
   public static class Input {
+    public String info;
+    public String onSuccess;
     public Boolean allowEmail;
     public Boolean allowHttpPassword;
     public Boolean allowOwner;
@@ -71,6 +73,12 @@
       throws IOException, ConfigInvalidException, UnprocessableEntityException {
     FileBasedConfig cfg = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.DETECTED);
     cfg.load();
+    if (input.info != null) {
+      cfg.setString("plugin", pluginName, "infoMessage", Strings.emptyToNull(input.info));
+    }
+    if (input.onSuccess != null) {
+      cfg.setString("plugin", pluginName, "onSuccessMessage", Strings.emptyToNull(input.onSuccess));
+    }
     if (input.allowEmail != null) {
       setBoolean(cfg, "allowEmail", input.allowEmail);
     }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 0793bb6..f52421e 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -22,6 +22,16 @@
 	should be automatically added. Multiple groups can be specified by
 	having multiple `plugin.@PLUGIN@.group` entries.
 
+<a id="infoMessage">
+`plugin.@PLUGIN@.infoMessage`
+:	HTML formatted message that should be displayed on the service user
+	creation screen.
+
+<a id="onSuccessMessage">
+`plugin.@PLUGIN@.onSuccessMessage`
+:	Message that should be displayed after a service user was
+	successfully created.
+
 <a id="allowEmail">
 `plugin.@PLUGIN@.allowEmail`
 :	Whether it is allowed for service user owners to set email
diff --git a/src/main/resources/Documentation/rest-api-config.md b/src/main/resources/Documentation/rest-api-config.md
index 8497fcd..3999e60 100644
--- a/src/main/resources/Documentation/rest-api-config.md
+++ b/src/main/resources/Documentation/rest-api-config.md
@@ -704,6 +704,10 @@
 The `ConfigInfo` entity contains the configuration of the @PLUGIN@
 plugin.
 
+* _info_: HTML formatted message that should be displayed on the
+  service user creation screen.
+* _on\_success_: Message that should be displayed after a service user
+  was successfully created.
 * _allow\_email_: Whether it is allowed to provide an email address for
   a service user (not set if `false`).
 * _allow\_http\_password_: Whether it is allowed to generate an HTTP
@@ -727,6 +731,10 @@
 The `ConfigInput` entity contains updates for the configuration of the
 @PLUGIN@ plugin.
 
+* _info_: HTML formatted message that should be displayed on the
+  service user creation screen.
+* _on\_success_: Message that should be displayed after a service user
+  was successfully created.
 * _allow\_email_: Whether it is allowed to provide an email address for
   a service user (not set if `false`).
 * _allow\_http\_password_: Whether it is allowed to generate an HTTP
diff --git a/src/main/resources/static/gr-serviceuser-create.html b/src/main/resources/static/gr-serviceuser-create.html
new file mode 100644
index 0000000..b6b71fe
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-create.html
@@ -0,0 +1,78 @@
+<!--
+@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-create">
+  <template>
+    <style include="shared-styles"></style>
+    <style include="gr-subpage-styles"></style>
+    <style include="gr-form-styles"></style>
+    <style>
+      main {
+        margin: 2em auto;
+        max-width: 50em;
+      }
+    </style>
+    <main class="gr-form-styles read-only">
+      <div class="topHeader">
+        <h2>Create Service User</h2>
+      </div>
+      <fieldset id="infoMessage"
+           hidden$="[[!_infoMessageEnabled]]">
+      </fieldset>
+      <fieldset>
+        <section>
+          <span class="title">Username</span>
+          <span class="value">
+            <input id="serviceUserNameInput"
+                   bind-value="{{_newUsername}}"
+                   is="iron-input"
+                   type="text"
+                   on-keyup="_validateData">
+          </span>
+        </section>
+        <section hidden$="[[!_emailEnabled]]">
+          <span class="title">Email</span>
+          <span class="value">
+            <input id="serviceUserEmailInput"
+                   bind-value="{{_newEmail}}"
+                   is="iron-input"
+                   type="text"
+                   on-keyup="_validateData">
+          </span>
+        </section>
+      </fieldset>
+      <fieldset>
+        <section>
+          <span class="title">Public SSH key</span>
+          <span class="value">
+            <iron-autogrow-textarea id="newKey"
+                                    bind-value="{{_newKey}}"
+                                    placeholder="New SSH Key"
+                                    on-keyup="_validateData">
+            </iron-autogrow-textarea>
+          </span>
+        </section>
+      </fieldset>
+      <gr-button id="createButton"
+                 on-tap="_handleCreateServiceUser"
+                 disabled="[[!_enableButton]]">
+        Create
+      </gr-button>
+    </main>
+  </template>
+  <script src="gr-serviceuser-create.js"></script>
+</dom-module>
diff --git a/src/main/resources/static/gr-serviceuser-create.js b/src/main/resources/static/gr-serviceuser-create.js
new file mode 100644
index 0000000..bc04de5
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-create.js
@@ -0,0 +1,142 @@
+/**
+ * @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-create',
+    _legacyUndefinedCheck: true,
+
+    properties: {
+      _infoMessageEnabled: {
+        type: Boolean,
+        value: false,
+      },
+      _infoMessage: String,
+      _successMessageEnabled: {
+        type: Boolean,
+        value: false,
+      },
+      _successMessage: String,
+      _newUsername: String,
+      _emailEnabled: {
+        type: Boolean,
+        value: false,
+      },
+      _newEmail: String,
+      _newKey: String,
+      _dataValid: {
+        type: Boolean,
+        value: false,
+      },
+      _isAdding: {
+        type: Boolean,
+        value: false,
+      },
+      _enableButton: {
+        type: Boolean,
+        value: false,
+      },
+    },
+
+    attached() {
+      this._getConfig();
+    },
+
+    _getConfig() {
+      return this.plugin.restApi('/config/server/serviceuser~config/').get('')
+          .then(config => {
+            if (!config) {
+              return;
+            }
+
+            if (config.info && config.info != '') {
+              this._infoMessageEnabled = true;
+              this._infoMessage = config.info;
+              this.$.infoMessage.innerHTML = this._infoMessage;
+            }
+
+            if (config.on_success && config.on_success != '') {
+              this._successMessageEnabled = true;
+              this._successMessage = config.on_success;
+            }
+
+            this._emailEnabled = config.allow_email;
+          });
+    },
+
+    _validateData() {
+      this._dataValid = this._validateName(this._newUsername)
+        && this._validateEmail(this._newEmail)
+        && this._validateKey(this._newKey);
+      this._computeButtonEnabled();
+    },
+
+    _validateName(username) {
+      if (username && username.trim().length > 0) {
+        return true;
+      }
+
+      return false;
+    },
+
+    _validateEmail(email) {
+      if (!email || email.trim().length == 0 || email.includes('@')) {
+        return true;
+      }
+
+      return false;
+    },
+
+    _validateKey(key) {
+      if (!key || !key.trim()) {
+        return false;
+      }
+
+      return true;
+    },
+
+    _computeButtonEnabled() {
+      this._enableButton = this._dataValid && !this._isAdding;
+    },
+
+    _handleCreateServiceUser() {
+      this._isAdding = true;
+      this._computeButtonEnabled();
+      const body = {
+        ssh_key: this._newKey.trim(),
+        email: this._newEmail ? this._newEmail.trim() : null,
+      };
+      return this.plugin.restApi('/config/server/serviceuser~serviceusers/')
+          .post(this._newUsername, body)
+          .then(response => {
+            if (this._successMessage) {
+              this.fire('show-alert', {message: this._successMessage});
+            }
+            page.show(
+                this.plugin.screenUrl()
+              + '/user/'
+              + response._account_id);
+          }).catch(response => {
+            this.fire('show-error', {message: response});
+            this._isAdding = false;
+            this._computeButtonEnabled();
+          });
+    },
+  });
+})();
diff --git a/src/main/resources/static/gr-serviceuser-list.html b/src/main/resources/static/gr-serviceuser-list.html
index 5557202..052e9f4 100644
--- a/src/main/resources/static/gr-serviceuser-list.html
+++ b/src/main/resources/static/gr-serviceuser-list.html
@@ -23,10 +23,30 @@
       .topHeader {
         padding: 8px;
       }
+
+      #topContainer {
+        align-items: center;
+        display: flex;
+        height: 3rem;
+        justify-content: space-between;
+        margin: 0 1em;
+      }
     </style>
     <div class="topHeader">
       <h2>Service Users</h2>
     </div>
+    <div id="topContainer">
+      <div></div>
+      <div id="createNewContainer"
+           class$="[[_computeCreateClass(createNew)]]">
+        <gr-button primary
+                   link
+                   id="createNew"
+                   on-tap="_createNewServiceUser">
+          Create New
+        </gr-button>
+      </div>
+    </div>
     <table id="list"
            class="genericList">
       <tr class="headerRow">
diff --git a/src/main/resources/static/gr-serviceuser-list.js b/src/main/resources/static/gr-serviceuser-list.js
index b7ef602..8f35dd4 100644
--- a/src/main/resources/static/gr-serviceuser-list.js
+++ b/src/main/resources/static/gr-serviceuser-list.js
@@ -87,5 +87,9 @@
     _computeServiceUserUrl(id) {
       return `${this.plugin.screenUrl()}/user/${id}`;
     },
+
+    _createNewServiceUser() {
+      page.show(this.plugin.screenUrl() + '/create');
+    },
   });
 })();
diff --git a/src/main/resources/static/gr-serviceuser.html b/src/main/resources/static/gr-serviceuser.html
index deb277e..92714a2 100644
--- a/src/main/resources/static/gr-serviceuser.html
+++ b/src/main/resources/static/gr-serviceuser.html
@@ -20,6 +20,8 @@
       href="./gr-serviceuser-list.html">
 <link rel="import"
       href="./gr-serviceuser-detail.html">
+<link rel="import"
+      href="./gr-serviceuser-create.html">
 
 <dom-module id="gr-serviceuser">
   <script>
@@ -31,6 +33,7 @@
                     || capabilities['serviceuser-createServiceUser'])) {
               plugin.screen('list', 'gr-serviceuser-list');
               plugin.screen('user', 'gr-serviceuser-detail');
+              plugin.screen('create', 'gr-serviceuser-create');
             }
             plugin.admin()
               .addMenuLink(