Merge changes I0df46558,Ib93db1e0,I6e7e9fee,Ibcbfac31,I63c2a798 into stable-3.0
* changes:
Add PolyGerrit UI for creating a service user
Modify the detail-screen to allow editing the service user
Add PolyGerrit UI for showing service user details
Add PolyGerrit UI to list service users
Add eslint config
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..3a9c4b9
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,79 @@
+{
+ "extends": ["eslint:recommended", "google"],
+ "parserOptions": {
+ "ecmaVersion": 8
+ },
+ "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", "always-multiline"],
+ "eol-last": "off",
+ "indent": "off",
+ "indent-legacy": ["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 }],
+ "max-len": [
+ "error",
+ 80,
+ 2,
+ {"ignoreComments": true}
+ ],
+ "new-cap": ["error", { "capIsNewExceptions": ["Polymer"] }],
+ "no-console": "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"],
+ "prefer-arrow-callback": "error",
+ "prefer-const": "error",
+ "prefer-spread": "error",
+ "quote-props": ["error", "consistent-as-needed"],
+ "require-jsdoc": "off",
+ "semi": [2, "always"],
+ "template-curly-spacing": "error",
+ "valid-jsdoc": "off"
+ },
+ "plugins": [
+ "html"
+ ],
+ "settings": {
+ "html/report-bad-indent": "error"
+ }
+ }
diff --git a/BUILD b/BUILD
index 06e79d5..36d967f 100644
--- a/BUILD
+++ b/BUILD
@@ -2,11 +2,12 @@
gerrit_plugin(
name = "serviceuser",
- srcs = glob(["src/main/java/**/*.java"]),
+ srcs = glob(["src/main/java/com/googlesource/gerrit/plugins/serviceuser/**/*.java"]),
manifest_entries = [
"Gerrit-PluginName: serviceuser",
"Gerrit-Module: com.googlesource.gerrit.plugins.serviceuser.Module",
+ "Gerrit-HttpModule: com.googlesource.gerrit.plugins.serviceuser.HttpModule",
"Gerrit-SshModule: com.googlesource.gerrit.plugins.serviceuser.SshModule",
],
- resources = glob(["src/main/**/*"]),
+ resources = glob(["src/main/resources/**/*"]),
)
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/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/HttpModule.java
new file mode 100644
index 0000000..a99987b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/HttpModule.java
@@ -0,0 +1,28 @@
+// 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.
+
+package com.googlesource.gerrit.plugins.serviceuser;
+
+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.AbstractModule;
+
+public class HttpModule extends AbstractModule {
+ @Override
+ protected void configure() {
+ DynamicSet.bind(binder(), WebUiPlugin.class)
+ .toInstance(new JavaScriptPlugin("gr-serviceuser.html"));
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/Module.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/Module.java
index ca6f8f0..56d14e1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/Module.java
@@ -24,7 +24,6 @@
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.RestApiModule;
-import com.google.gerrit.extensions.webui.TopMenu;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.inject.AbstractModule;
import com.google.inject.assistedinject.FactoryModuleBuilder;
@@ -71,5 +70,6 @@
delete(SERVICE_USER_KIND, "owner").to(PutOwner.class);
}
});
+ install(new HttpModule());
}
}
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-detail.html b/src/main/resources/static/gr-serviceuser-detail.html
new file mode 100644
index 0000000..c65ed43
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-detail.html
@@ -0,0 +1,151 @@
+<!--
+@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">
+<link rel="import" href="./gr-serviceuser-http-password.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;
+ }
+
+ p#ownerChangeWarning {
+ margin-top: 1em;
+ 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>
+ <gr-button id="statusToggleButton" on-tap="_toggleStatus" disabled="[[_loading]]">
+ [[_statusButtonText]]</gr-button>
+ </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">
+ <input id="serviceUserFullNameInput" bind-value="{{_newFullName}}" is="iron-input" type="text"
+ disabled="[[_changingPrefs]]" placeholder$="[[_serviceUser.name]]"
+ on-keyup="_computePrefsChanged">
+ </span>
+ </section>
+ <section>
+ <span class="title">Email Address</span>
+ <input id="serviceUserEmailInput" bind-value="{{_newEmail}}" is="iron-input" type="text"
+ disabled="[[_changingPrefs]]" placeholder="[[_serviceUser.email]]" on-keyup="_computePrefsChanged"
+ hidden$="[[!_allowEmail]]">
+ <span class="value" hidden$="[[_allowEmail]]">[[_serviceUser.email]]</span>
+ </section>
+ <section>
+ <span class="title">Owner Group</span>
+ <gr-autocomplete id="serviceUserOwnerInput" text="{{_getOwnerGroup(_serviceUser)}}"
+ value="{{_newOwner}}" query="[[_query]]" disabled="[[_changingPrefs]]"
+ on-commit="_computePrefsChanged" on-keyup="_computePrefsChanged" hidden$="[[!_allowOwner]]">
+ [[_getOwnerGroup(_serviceUser)]]
+ </gr-autocomplete>
+ <span class="value" hidden$="[[_allowOwner]]">[[_getOwnerGroup(_serviceUser)]]</span>
+ </section>
+ <p id="ownerChangeWarning" class="style-scope gr-settings-view" hidden$="[[!_newOwner]]">
+ [[_ownerChangeWarning]]
+ </p>
+ <gr-button id="savePrefs" on-tap="_handleSavePreferences" disabled="[[!_prefsChanged]]">
+ Save changes
+ </gr-button>
+ </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>
+ <fieldset>
+ <h2 id="credentialsHeader">Credentials</h2>
+ </fieldset>
+ <fieldset hidden$="[[!_allowHttpPassword]]">∏
+ <h3 id="HTTPCredentials">HTTP Credentials</h3>
+ <fieldset>
+ <gr-serviceuser-http-password id="httpPass">
+ </gr-http-password>
+ </fieldset>
+ </fieldset>
+ <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..ed9a77a
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-detail.js
@@ -0,0 +1,343 @@
+// 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,
+ },
+ _statusButtonText: {
+ type: String,
+ value: 'Activate',
+ },
+ _prefsChanged: {
+ type: Boolean,
+ value: false,
+ },
+ _changingPrefs: {
+ type: Boolean,
+ value: false,
+ },
+ _isAdmin: {
+ type: Boolean,
+ value: false,
+ },
+ _allowEmail: {
+ type: Boolean,
+ value: false,
+ },
+ _allowOwner: {
+ type: Boolean,
+ value: false,
+ },
+ _allowHttpPassword: {
+ type: Boolean,
+ value: false,
+ },
+ _newFullName: String,
+ _newEmail: String,
+ _availableOwners: Array,
+ _newOwner: String,
+ _ownerChangeWarning: String,
+ _query: {
+ type: Function,
+ value() {
+ return this._getGroupSuggestions.bind(this);
+ },
+ },
+ },
+
+ behaviors: [
+ Gerrit.ListViewBehavior,
+ ],
+
+ attached() {
+ this._extractUserId();
+ this._loadServiceUser();
+ },
+
+ _loadServiceUser() {
+ if (!this._serviceUserId) { return; }
+
+ const promises = [];
+
+ promises.push(this._getPluginConfig());
+ promises.push(this._getServiceUser());
+
+ Promise.all(promises).then(() => {
+ this.$.sshEditor.loadData(this._restApi, this._serviceUser);
+ this.$.httpPass.loadData(this._restApi, this._serviceUser);
+
+ this.fire('title-change', {title: this._serviceUser.name});
+ this._computeStatusButtonText();
+ this._loading = false;
+ });
+ },
+
+ _computeLoadingClass(loading) {
+ return loading ? 'loading' : '';
+ },
+
+ _extractUserId() {
+ this._serviceUserId = this.baseURI.split('/').pop();
+ },
+
+ _getPermissions() {
+ return this.plugin.restApi('/accounts/self/capabilities/').get('')
+ .then(capabilities => {
+ this._isAdmin = capabilities && capabilities.administrateServer;
+ });
+ },
+
+ _getPluginConfig() {
+ return Promise.resolve(this._getPermissions()).then(() => {
+ this.plugin.restApi('/config/server/serviceuser~config/').get('')
+ .then(config => {
+ if (!config) {
+ return;
+ }
+ this._allowEmail = config.allow_email || this._isAdmin;
+ this._allowOwner = config.allow_owner || this._isAdmin;
+ this._allowHttpPassword = config.allow_http_password
+ || this._isAdmin;
+ });
+ });
+ },
+
+ _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';
+ },
+
+ _computeStatusButtonText() {
+ if (!this._serviceUser) {
+ return;
+ }
+
+ this._statusButtonText = this._serviceUser.inactive === true
+ ? 'Activate'
+ : 'Deactivate';
+ },
+
+ _toggleStatus() {
+ if (this._serviceUser.inactive === true) {
+ this._restApi.put(`${this._serviceUser._account_id}/active`)
+ .then(() => {
+ this._loadServiceUser();
+ });
+ } else {
+ this._restApi.delete(`${this._serviceUser._account_id}/active`)
+ .then(() => {
+ this._loadServiceUser();
+ });
+ }
+ },
+
+ _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;
+ },
+
+ _isEmailValid(email) {
+ if (!email) {
+ return false;
+ }
+ return email.includes('@');
+ },
+
+ _getGroupSuggestions(input) {
+ let query;
+ if (!input || input === this._getOwnerGroup(this._serviceUser)) {
+ query = '';
+ } else {
+ query = `?suggest=${input}`;
+ }
+
+ return this.plugin.restApi('/a/groups/').get(query)
+ .then(response => {
+ const groups = [];
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ groups.push({
+ name: key,
+ value: decodeURIComponent(response[key].id),
+ });
+ }
+ this._availableOwners = groups;
+ return groups;
+ });
+ },
+
+ _isOwnerValid(owner) {
+ if (!owner) {
+ return false;
+ }
+
+ return this._getOwnerName(owner);
+ },
+
+ _isNewOwner() {
+ return this._getOwnerName(this._newOwner)
+ === this._getOwnerGroup(this._serviceUser);
+ },
+
+ _getOwnerName(id) {
+ return this._availableOwners.find(o => { return o.value === id; }).name;
+ },
+
+ _computeOwnerWarning() {
+ let message = 'If ';
+ message += this._getOwnerGroup(this._serviceUser) != NOT_FOUND_MESSAGE
+ ? 'the owner group is changed' : 'an owner group is set';
+ message += ' only members of the ';
+ message += this._getOwnerGroup(this._serviceUser) != NOT_FOUND_MESSAGE
+ ? 'new ' : '';
+ message += 'owner group can see and administrate the service user.';
+ message += this._getOwnerGroup(this._serviceUser) != NOT_FOUND_MESSAGE
+ ? '' : ' The creator of the service user can no'
+ + ' longer see and administrate the service user if she/he'
+ + ' is not member of the owner group.';
+ this._ownerChangeWarning = message;
+ },
+
+ _computePrefsChanged() {
+ if (this.loading || this._changingPrefs) {
+ return;
+ }
+
+ if (!this._newOwner && !this._newEmail && !this._newFullName) {
+ this._prefsChanged = false;
+ return;
+ }
+
+ if (this._newEmail && !this._isEmailValid(this._newEmail)) {
+ this._prefsChanged = false;
+ return;
+ }
+
+ if (this._newOwner
+ && (this._isNewOwner() || !this._isOwnerValid(this._newOwner))) {
+ this._prefsChanged = false;
+ return;
+ }
+
+ if (this._newOwner) {
+ this._computeOwnerWarning();
+ }
+
+ this._prefsChanged = true;
+ },
+
+ _applyNewFullName() {
+ return this._restApi
+ .put(`${this._serviceUser._account_id}/name`,
+ {name: this._newFullName})
+ .then(() => {
+ this.$.serviceUserFullNameInput.value = '';
+ });
+ },
+
+ _applyNewEmail(email) {
+ if (!this._isEmailValid(email)) {
+ return;
+ }
+ return this._restApi
+ .put(`${this._serviceUser._account_id}/email`, {email})
+ .then(() => {
+ this.$.serviceUserEmailInput.value = '';
+ });
+ },
+
+ _applyNewOwner(owner) {
+ if (this._isNewOwner() || !this._isOwnerValid(this._newOwner)) {
+ return;
+ }
+ return this._restApi
+ .put(`${this._serviceUser._account_id}/owner`, {group: owner})
+ .then(() => {
+ this.$.serviceUserOwnerInput.text = this._getOwnerGroup(
+ _serviceUser);
+ });
+ },
+
+ _handleSavePreferences() {
+ const promises = [];
+ this._changingPrefs = true;
+
+ if (this._newFullName) {
+ promises.push(this._applyNewFullName());
+ }
+
+ if (this._newEmail) {
+ promises.push(this._applyNewEmail(this._newEmail));
+ }
+
+ if (this._newOwner) {
+ promises.push(this._applyNewOwner(this._newOwner));
+ }
+
+ Promise.all(promises).then(() => {
+ this._changingPrefs = false;
+ this._prefsChanged = false;
+ this._ownerChangeWarning = '';
+ this._loadServiceUser();
+ });
+ },
+ });
+})();
diff --git a/src/main/resources/static/gr-serviceuser-http-password.html b/src/main/resources/static/gr-serviceuser-http-password.html
new file mode 100644
index 0000000..f2f5b3e
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-http-password.html
@@ -0,0 +1,81 @@
+<!--
+@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-http-password">
+ <template>
+ <style include="shared-styles">
+ .password {
+ font-family: var(--monospace-font-family);
+ }
+
+ #generatedPasswordOverlay {
+ padding: 2em;
+ width: 50em;
+ }
+
+ #generatedPasswordDisplay {
+ margin: 1em 0;
+ }
+
+ #generatedPasswordDisplay .value {
+ font-family: var(--monospace-font-family);
+ }
+
+ #passwordWarning {
+ font-style: italic;
+ text-align: center;
+ }
+
+ .closeButton {
+ bottom: 2em;
+ position: absolute;
+ right: 2em;
+ }
+ </style>
+ <style include="gr-form-styles"></style>
+ <div class="gr-form-styles">
+ <div>
+ <section>
+ <span class="title">Username</span>
+ <span class="value">[[_serviceUser.username]]</span>
+ </section>
+ <gr-button id="generateButton"
+ on-tap="_handleGenerateTap">Generate new password</gr-button>
+ <gr-button id="deleteButton"
+ on-tap="_handleDelete">Delete password</gr-button>
+ </div>
+ </div>
+ <gr-overlay id="generatedPasswordOverlay"
+ on-iron-overlay-closed="_generatedPasswordOverlayClosed"
+ with-backdrop>
+ <div class="gr-form-styles">
+ <section id="generatedPasswordDisplay">
+ <span class="title">New Password:</span>
+ <span class="value">[[_generatedPassword]]</span>
+ </section>
+ <section id="passwordWarning">
+ This password will not be displayed again.<br>
+ If you lose it, you will need to generate a new one.
+ </section>
+ <gr-button link
+ class="closeButton"
+ on-tap="_closeOverlay">Close</gr-button>
+ </div>
+ </gr-overlay>
+ </template>
+ <script src="gr-serviceuser-http-password.js"></script>
+</dom-module>
diff --git a/src/main/resources/static/gr-serviceuser-http-password.js b/src/main/resources/static/gr-serviceuser-http-password.js
new file mode 100644
index 0000000..87e1543
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-http-password.js
@@ -0,0 +1,60 @@
+/**
+ * @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.
+ */
+
+(function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-serviceuser-http-password',
+ _legacyUndefinedCheck: true,
+
+ properties: {
+ _restApi: Object,
+ _serviceUser: Object,
+ _generatedPassword: String,
+ _passwordUrl: String,
+ },
+
+ loadData(restApi, serviceUser) {
+ this._restApi = restApi;
+ this._serviceUser = serviceUser;
+ },
+
+ _handleGenerateTap() {
+ this._generatedPassword = 'Generating...';
+ this.$.generatedPasswordOverlay.open();
+ this._restApi
+ .put(`${this._serviceUser._account_id}/password.http`,
+ {generate: true})
+ .then(newPassword => {
+ this._generatedPassword = newPassword;
+ });
+ },
+
+ _closeOverlay() {
+ this.$.generatedPasswordOverlay.close();
+ },
+
+ _generatedPasswordOverlayClosed() {
+ this._generatedPassword = '';
+ },
+
+ _handleDelete() {
+ this._restApi.delete(`${this._serviceUser._account_id}/password.http`);
+ },
+ });
+})();
diff --git a/src/main/resources/static/gr-serviceuser-list.html b/src/main/resources/static/gr-serviceuser-list.html
new file mode 100644
index 0000000..052e9f4
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-list.html
@@ -0,0 +1,84 @@
+<!--
+@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-list">
+ <template>
+ <style include="shared-styles"></style>
+ <style include="gr-table-styles"></style>
+ <style>
+ .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">
+ <th class="name topHeader">Username</th>
+ <th class="fullName topHeader">Full Name</th>
+ <th class="email topHeader">Email</th>
+ <th class="owner topHeader">Owner</th>
+ <th class="createdBy topHeader">Created By</th>
+ <th class="createdAt topHeader">Created At</th>
+ <th class="accountState topHeader">Account State</th>
+ </tr>
+ <tr id="loading"
+ class$="loadingMsg [[computeLoadingClass(_loading)]]">
+ <td>Loading...</td>
+ </tr>
+ <tbody class$="[[computeLoadingClass(_loading)]]">
+ <template is="dom-repeat"
+ items="[[_serviceUsers]]">
+ <tr class="table">
+ <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>
+ <td class="createdBy">[[_getCreator(item)]]</td>
+ <td class="createdAt">[[item.created_at]]</td>
+ <td class="accountState">[[_active(item)]]</td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ </template>
+ <script src="gr-serviceuser-list.js"></script>
+</dom-module>
diff --git a/src/main/resources/static/gr-serviceuser-list.js b/src/main/resources/static/gr-serviceuser-list.js
new file mode 100644
index 0000000..8f35dd4
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-list.js
@@ -0,0 +1,95 @@
+// 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-list',
+ _legacyUndefinedCheck: true,
+
+ properties: {
+ _serviceUsers: Array,
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ },
+
+ behaviors: [
+ Gerrit.ListViewBehavior,
+ ],
+
+ attached() {
+ this.fire('title-change', {title: 'Service Users'});
+ this._getServiceUsers();
+ },
+
+ _getServiceUsers() {
+ return this.plugin.restApi('/config/server/serviceuser~serviceusers/')
+ .get('')
+ .then(serviceUsers => {
+ if (!serviceUsers) {
+ this._serviceUsers = [];
+ return;
+ }
+ this._serviceUsers = Object.keys(serviceUsers)
+ .map(key => {
+ const serviceUser = serviceUsers[key];
+ serviceUser.username = key;
+ return serviceUser;
+ });
+ this._loading = false;
+ });
+ },
+
+ _active(item) {
+ if (!item) {
+ return NOT_FOUND_MESSAGE;
+ }
+
+ return item.inactive === true ? 'Inactive' : 'Active';
+ },
+
+ _getCreator(item) {
+ if (!item || !item.created_by) {
+ return NOT_FOUND_MESSAGE;
+ }
+
+ if (item.created_by.username != undefined) {
+ return item.created_by.username;
+ }
+
+ if (item.created_by._account_id != -1) {
+ return item.created_by._account_id;
+ }
+
+ return NOT_FOUND_MESSAGE;
+ },
+
+ _getOwnerGroup(item) {
+ return item && item.owner ? item.owner.name : NOT_FOUND_MESSAGE;
+ },
+
+ _computeServiceUserUrl(id) {
+ return `${this.plugin.screenUrl()}/user/${id}`;
+ },
+
+ _createNewServiceUser() {
+ page.show(this.plugin.screenUrl() + '/create');
+ },
+ });
+})();
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..b7ed896
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-ssh-panel.html
@@ -0,0 +1,132 @@
+<!--
+@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>
+ <th></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>
+ <td>
+ <gr-button link
+ data-index$="[[index]]"
+ on-tap="_handleDeleteKey">Delete</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>
+ <fieldset>
+ <section>
+ <span class="title">New SSH key</span>
+ <span class="value">
+ <iron-autogrow-textarea id="newKey"
+ autocomplete="on"
+ bind-value="{{_newKey}}"
+ placeholder="New SSH Key">
+ </iron-autogrow-textarea>
+ </span>
+ </section>
+ <gr-button id="addButton"
+ link
+ disabled$="[[_computeAddButtonDisabled(_newKey)]]"
+ on-tap="_handleAddKey">
+ Add new SSH key
+ </gr-button>
+ </fieldset>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ </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..59f307f
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-ssh-panel.js
@@ -0,0 +1,103 @@
+/**
+ * @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';
+
+ const JSON_PREFIX = ')]}\'';
+
+ Polymer({
+ is: 'gr-serviceuser-ssh-panel',
+ _legacyUndefinedCheck: true,
+
+ properties: {
+ _restApi: Object,
+ _serviceUser: Object,
+ _keys: Array,
+ /** @type {?} */
+ _keyToView: Object,
+ _newKey: {
+ type: String,
+ value: '',
+ },
+ _keysToRemove: {
+ type: Array,
+ value() { return []; },
+ },
+ },
+
+ 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();
+ },
+
+ _handleDeleteKey(e) {
+ const el = Polymer.dom(e).localTarget;
+ const index = parseInt(el.getAttribute('data-index'), 10);
+ this.push('_keysToRemove', this._keys[index]);
+
+ const promises = this._keysToRemove.map(key => {
+ this._restApi.delete(`${this._serviceUser._account_id}/sshkeys/${key.seq}`);
+ });
+
+ return Promise.all(promises).then(() => {
+ this.splice('_keys', index, 1);
+ this._keysToRemove = [];
+ });
+ },
+
+ _handleAddKey() {
+ this.$.addButton.disabled = true;
+ this.$.newKey.disabled = true;
+ return this._restApi.post(`${this._serviceUser._account_id}/sshkeys`,
+ this._newKey.trim(), 'plain/text')
+ .then(key => {
+ this.push('_keys', key);
+ }).catch(() => {
+ this.$.addButton.disabled = false;
+ this.$.newKey.disabled = false;
+ });
+ },
+
+ _computeAddButtonDisabled(newKey) {
+ return !newKey.length;
+ },
+ });
+})();
diff --git a/src/main/resources/static/gr-serviceuser.html b/src/main/resources/static/gr-serviceuser.html
new file mode 100644
index 0000000..92714a2
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser.html
@@ -0,0 +1,46 @@
+<!--
+@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-list.html">
+<link rel="import"
+ href="./gr-serviceuser-detail.html">
+<link rel="import"
+ href="./gr-serviceuser-create.html">
+
+<dom-module id="gr-serviceuser">
+ <script>
+ Gerrit.install(plugin => {
+ plugin.restApi('/accounts/self/capabilities/').get('')
+ .then(capabilities => {
+ if (capabilities
+ && (capabilities.administrateServer
+ || capabilities['serviceuser-createServiceUser'])) {
+ plugin.screen('list', 'gr-serviceuser-list');
+ plugin.screen('user', 'gr-serviceuser-detail');
+ plugin.screen('create', 'gr-serviceuser-create');
+ }
+ plugin.admin()
+ .addMenuLink(
+ 'Service Users',
+ '/x/serviceuser/list',
+ 'serviceuser-createServiceUser');
+ });
+ });
+ </script>
+</dom-module>