Allow to see/add/delete SSH keys from service user screen

Change-Id: I59173bf474e3cb0022b21cb6372cebda266ec482
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/pom.xml b/pom.xml
index b6cc2d9..06d8d1b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -111,6 +111,19 @@
       <version>2.5.1</version>
       <scope>provided</scope>
     </dependency>
+
+    <dependency>
+      <groupId>gwtexpui</groupId>
+      <artifactId>gwtexpui</artifactId>
+      <version>1.3.4</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>gwtexpui</groupId>
+      <artifactId>gwtexpui</artifactId>
+      <version>1.3.4</version>
+      <classifier>sources</classifier>
+    </dependency>
   </dependencies>
 
   <repositories>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserForm.gwt.xml b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserForm.gwt.xml
index 01f86d2..9d921cb 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserForm.gwt.xml
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserForm.gwt.xml
@@ -21,6 +21,8 @@
   <inherits name="com.google.gerrit.Plugin"/>
   <inherits name="com.google.gwt.http.HTTP"/>
   <inherits name="com.google.gwt.json.JSON"/>
+  <inherits name='com.google.gwtexpui.clippy.Clippy'/>
+  <inherits name='com.google.gwtexpui.globalkey.GlobalKey'/>
   <!-- Using GWT built-in themes adds a number of static          -->
   <!-- resources to the plugin. No theme inherits lines were      -->
   <!-- added in order to make this plugin as simple as possible   -->
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/CreateServiceUserScreen.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/CreateServiceUserScreen.java
index 5242c60..faacb3c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/CreateServiceUserScreen.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/CreateServiceUserScreen.java
@@ -28,7 +28,6 @@
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.DialogBox;
-import com.google.gwt.user.client.ui.DisclosurePanel;
 import com.google.gwt.user.client.ui.HTML;
 import com.google.gwt.user.client.ui.HorizontalPanel;
 import com.google.gwt.user.client.ui.Label;
@@ -85,27 +84,7 @@
 
     Panel sshKeyPanel = new VerticalPanel();
     sshKeyPanel.add(new Label("Public SSH Key:"));
-    DisclosurePanel dp = new DisclosurePanel("How to generate an SSH Key");
-    StringBuilder b = new StringBuilder();
-    b.append("<ol>")
-        .append("<li>From the Terminal or Git Bash, run <em>ssh-keygen</em></li>")
-        .append("<li>")
-            .append("Enter a path for the key, e.g. <em>id_rsa</em>. If you are generating the key<br />")
-            .append("on your local system take care to not overwrite your own SSH key.")
-        .append("</li>")
-        .append("<li>")
-            .append("Enter a passphrase only if the service where you intend to use this<br />")
-            .append("service user is able to deal with passphrases, otherwise leave it blank.<br />")
-            .append("Remember this passphrase, as you will need it to unlock the key.")
-        .append("</li>")
-        .append("<li>")
-            .append("Open <em>id_rsa.pub</em> and copy &amp; paste the contents into the box below.<br />")
-            .append("Note that <em>id_rsa.pub</em> is your public key and can be shared,<br />")
-            .append("while <em>id_rsa</em> is your private key and should be kept secret.")
-        .append("</li>")
-     .append("</ol>");
-    dp.add(new HTML(b.toString()));
-    sshKeyPanel.add(dp);
+    sshKeyPanel.add(new SshKeyHelpPanel());
     sshKeyTxt = new TextArea();
     sshKeyTxt.addKeyPressHandler(new KeyPressHandler() {
       @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserScreen.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserScreen.java
index 534063c..cd02659 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserScreen.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserScreen.java
@@ -56,6 +56,8 @@
     t.addRow("Created By", info.created_by());
     t.addRow("Created At", info.created_at());
     add(t);
+
+    add(new SshPanel(info.username()));
   }
 
   private static class MyTable extends FlexTable {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshKeyHelpPanel.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshKeyHelpPanel.java
new file mode 100644
index 0000000..134b110
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshKeyHelpPanel.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2014 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.client;
+
+import com.google.gwt.user.client.ui.DisclosurePanel;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTML;
+
+public class SshKeyHelpPanel extends FlowPanel {
+
+  SshKeyHelpPanel() {
+    DisclosurePanel dp = new DisclosurePanel("How to generate an SSH Key");
+    StringBuilder b = new StringBuilder();
+    b.append("<ol>")
+        .append("<li>From the Terminal or Git Bash, run <em>ssh-keygen</em></li>")
+        .append("<li>")
+            .append("Enter a path for the key, e.g. <em>id_rsa</em>. If you are generating the key<br />")
+            .append("on your local system take care to not overwrite your own SSH key.")
+        .append("</li>")
+        .append("<li>")
+            .append("Enter a passphrase only if the service where you intend to use this<br />")
+            .append("service user is able to deal with passphrases, otherwise leave it blank.<br />")
+            .append("Remember this passphrase, as you will need it to unlock the key.")
+        .append("</li>")
+        .append("<li>")
+            .append("Open <em>id_rsa.pub</em> and copy &amp; paste the contents into the box below.<br />")
+            .append("Note that <em>id_rsa.pub</em> is your public key and can be shared,<br />")
+            .append("while <em>id_rsa</em> is your private key and should be kept secret.")
+        .append("</li>")
+     .append("</ol>");
+    dp.add(new HTML(b.toString()));
+    add(dp);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshKeyInfo.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshKeyInfo.java
new file mode 100644
index 0000000..238e548
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshKeyInfo.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2014 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.client;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class SshKeyInfo extends JavaScriptObject {
+  public final native int seq() /*-{ return this.seq || 0; }-*/;
+  public final native String sshPublicKey() /*-{ return this.ssh_public_key; }-*/;
+  public final native String encodedKey() /*-{ return this.encoded_key; }-*/;
+  public final native String algorithm() /*-{ return this.algorithm; }-*/;
+  public final native String comment() /*-{ return this.comment; }-*/;
+  public final native boolean isValid() /*-{ return this['valid'] ? true : false; }-*/;
+
+  protected SshKeyInfo() {
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshPanel.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshPanel.java
new file mode 100644
index 0000000..bad75b8
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshPanel.java
@@ -0,0 +1,364 @@
+// Copyright (C) 2014 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.client;
+
+import com.google.gerrit.plugin.client.Plugin;
+import com.google.gerrit.plugin.client.rpc.Natives;
+import com.google.gerrit.plugin.client.rpc.NoContent;
+import com.google.gerrit.plugin.client.rpc.RestApi;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HasHorizontalAlignment;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwtexpui.clippy.client.CopyableLabel;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+class SshPanel extends Composite {
+  private final String serviceUser;
+
+  private SshKeyTable keys;
+
+  private Button showAddKeyBlock;
+  private Panel addKeyBlock;
+  private Button closeAddKeyBlock;
+  private Button clearNew;
+  private Button addNew;
+  private NpTextArea addTxt;
+  private Button deleteKey;
+
+  private Panel serverKeys;
+
+  private int loadCount;
+
+  SshPanel(String serviceUser) {
+    this.serviceUser = serviceUser;
+
+    FlowPanel body = new FlowPanel();
+
+    showAddKeyBlock = new Button("Add Key ...");
+    showAddKeyBlock.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        showAddKeyBlock(true);
+      }
+    });
+
+    keys = new SshKeyTable();
+    body.add(keys);
+    {
+      final FlowPanel fp = new FlowPanel();
+      deleteKey = new Button("Delete");
+      deleteKey.setEnabled(false);
+      deleteKey.addClickHandler(new ClickHandler() {
+        @Override
+        public void onClick(final ClickEvent event) {
+          keys.deleteChecked();
+        }
+      });
+      fp.add(deleteKey);
+      fp.add(showAddKeyBlock);
+      body.add(fp);
+    }
+
+    addKeyBlock = new VerticalPanel();
+    addKeyBlock.setVisible(false);
+    addKeyBlock.setStyleName("serviceuser-addSshKeyPanel");
+    addKeyBlock.add(new Label("Add SSH Public Key"));
+    addKeyBlock.add(new SshKeyHelpPanel());
+
+    addTxt = new NpTextArea();
+    addTxt.setVisibleLines(12);
+    addTxt.setCharacterWidth(80);
+    addTxt.setSpellCheck(false);
+    addKeyBlock.add(addTxt);
+
+    final HorizontalPanel buttons = new HorizontalPanel();
+    addKeyBlock.add(buttons);
+
+    clearNew = new Button("Clear");
+    clearNew.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        addTxt.setText("");
+        addTxt.setFocus(true);
+      }
+    });
+    buttons.add(clearNew);
+
+    addNew = new Button("Add");
+    addNew.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        doAddNew();
+      }
+    });
+    buttons.add(addNew);
+
+    closeAddKeyBlock = new Button("Close");
+    closeAddKeyBlock.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        showAddKeyBlock(false);
+      }
+    });
+    buttons.add(closeAddKeyBlock);
+    buttons.setCellWidth(closeAddKeyBlock, "100%");
+    buttons.setCellHorizontalAlignment(closeAddKeyBlock,
+        HasHorizontalAlignment.ALIGN_RIGHT);
+
+    body.add(addKeyBlock);
+
+    serverKeys = new FlowPanel();
+    body.add(serverKeys);
+
+    initWidget(body);
+  }
+
+  void setKeyTableVisible(final boolean on) {
+    keys.setVisible(on);
+    deleteKey.setVisible(on);
+    closeAddKeyBlock.setVisible(on);
+  }
+
+  void doAddNew() {
+    final String txt = addTxt.getText();
+    if (txt != null && txt.length() > 0) {
+      new RestApi("config").id("server")
+          .view(Plugin.get().getPluginName(), "serviceusers").id(serviceUser)
+          .view("sshkeys").post(txt, new AsyncCallback<SshKeyInfo>() {
+        public void onSuccess(SshKeyInfo k) {
+          addTxt.setText("");
+          keys.addOneKey(k);
+          if (!keys.isVisible()) {
+            showAddKeyBlock(false);
+            setKeyTableVisible(true);
+            keys.updateDeleteButton();
+          }
+        }
+
+        @Override
+        public void onFailure(final Throwable caught) {
+          // never invoked
+        }
+      });
+    }
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    refreshSshKeys();
+  }
+
+  private void refreshSshKeys() {
+    new RestApi("config").id("server")
+        .view(Plugin.get().getPluginName(), "serviceusers").id(serviceUser)
+        .view("sshkeys").get(new AsyncCallback<JsArray<SshKeyInfo>>() {
+      @Override
+      public void onSuccess(JsArray<SshKeyInfo> result) {
+        keys.display(Natives.asList(result));
+        if (result.length() == 0 && keys.isVisible()) {
+          showAddKeyBlock(true);
+        }
+        if (++loadCount == 2) {
+          display();
+        }
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+        // never invoked
+      }
+    });
+  }
+
+  void display() {
+  }
+
+  private void showAddKeyBlock(boolean show) {
+    showAddKeyBlock.setVisible(!show);
+    addKeyBlock.setVisible(show);
+  }
+
+  private class SshKeyTable extends FlexTable {
+    private final Map<Integer, SshKeyInfo> sshKeyInfos;
+    private ValueChangeHandler<Boolean> updateDeleteHandler;
+
+    SshKeyTable() {
+      this.sshKeyInfos = new HashMap<Integer, SshKeyInfo>();
+      setStyleName("serviceuser-sshKeyTable");
+      setWidth("");
+      setText(0, 2, "Status");
+      setText(0, 3, "Algorithm");
+      setText(0, 4, "Key");
+      setText(0, 5, "Comment");
+
+      FlexCellFormatter fmt = getFlexCellFormatter();
+      fmt.addStyleName(0, 1, "iconHeader");
+      fmt.addStyleName(0, 2, "dataHeader");
+      fmt.addStyleName(0, 3, "dataHeader");
+      fmt.addStyleName(0, 4, "dataHeader");
+      fmt.addStyleName(0, 5, "dataHeader");
+
+      fmt.addStyleName(0, 1, "topMostCell");
+      fmt.addStyleName(0, 2, "topMostCell");
+      fmt.addStyleName(0, 3, "topMostCell");
+      fmt.addStyleName(0, 4, "topMostCell");
+      fmt.addStyleName(0, 5, "topMostCell");
+
+      updateDeleteHandler = new ValueChangeHandler<Boolean>() {
+        @Override
+        public void onValueChange(ValueChangeEvent<Boolean> event) {
+          updateDeleteButton();
+        }
+      };
+    }
+
+    void deleteChecked() {
+      final HashSet<Integer> sequenceNumbers = new HashSet<Integer>();
+      for (int row = 1; row < getRowCount(); row++) {
+        SshKeyInfo k = getRowItem(row);
+        if (k != null && ((CheckBox) getWidget(row, 1)).getValue()) {
+          sequenceNumbers.add(k.seq());
+        }
+      }
+      if (sequenceNumbers.isEmpty()) {
+        updateDeleteButton();
+      } else {
+        for (int seq : sequenceNumbers) {
+          new RestApi("config").id("server")
+              .view(Plugin.get().getPluginName(), "serviceusers").id(serviceUser)
+              .view("sshkeys").id(seq).delete(new AsyncCallback<NoContent>() {
+                public void onSuccess(NoContent result) {
+                  for (int row = 1; row < getRowCount();) {
+                    SshKeyInfo k = getRowItem(row);
+                    if (k != null && sequenceNumbers.contains(k.seq())) {
+                      removeRow(row);
+                    } else {
+                      row++;
+                    }
+                  }
+                  if (getRowCount() == 1) {
+                    display(Collections.<SshKeyInfo> emptyList());
+                  } else {
+                    updateDeleteButton();
+                  }
+                }
+
+                @Override
+                public void onFailure(Throwable caught) {
+                  // never invoked
+                }
+              });
+        }
+      }
+    }
+
+    void display(List<SshKeyInfo> result) {
+      if (result.isEmpty()) {
+        setKeyTableVisible(false);
+        showAddKeyBlock(true);
+      } else {
+        while (1 < getRowCount())
+          removeRow(getRowCount() - 1);
+        for (SshKeyInfo k : result) {
+          addOneKey(k);
+        }
+        setKeyTableVisible(true);
+        deleteKey.setEnabled(false);
+      }
+    }
+
+    void addOneKey(SshKeyInfo k) {
+      FlexCellFormatter fmt = getFlexCellFormatter();
+      int row = getRowCount();
+      insertRow(row);
+      getCellFormatter().addStyleName(row, 0, "iconCell");
+      getCellFormatter().addStyleName(row, 0, "leftMostCell");
+
+      CheckBox sel = new CheckBox();
+      sel.addValueChangeHandler(updateDeleteHandler);
+
+      setWidget(row, 1, sel);
+      if (k.isValid()) {
+        setText(row, 2, "");
+        fmt.removeStyleName(row, 2, "serviceuser-sshKeyPanelInvalid");
+      } else {
+        setText(row, 2, "Invalid Key");
+        fmt.addStyleName(row, 2, "serviceuser-sshKeyPanelInvalid");
+      }
+      setText(row, 3, k.algorithm());
+
+      CopyableLabel keyLabel = new CopyableLabel(k.sshPublicKey());
+      keyLabel.setPreviewText(elide(k.encodedKey(), 40));
+      setWidget(row, 4, keyLabel);
+
+      setText(row, 5, k.comment());
+
+      fmt.addStyleName(row, 1, "iconCell");
+      fmt.addStyleName(row, 4, "serviceuser-sshKeyPanelEncodedKey");
+      for (int c = 2; c <= 5; c++) {
+        fmt.addStyleName(row, c, "dataCell");
+      }
+
+      setRowItem(row, k);
+    }
+
+    void updateDeleteButton() {
+      boolean on = false;
+      for (int row = 1; row < getRowCount(); row++) {
+        CheckBox sel = (CheckBox) getWidget(row, 1);
+        if (sel.getValue()) {
+          on = true;
+          break;
+        }
+      }
+      deleteKey.setEnabled(on);
+    }
+
+    private SshKeyInfo getRowItem(int row) {
+      return sshKeyInfos.get(row);
+    }
+
+    private void setRowItem(int row, SshKeyInfo sshKeyInfo) {
+      sshKeyInfos.put(row, sshKeyInfo);
+    }
+  }
+
+  static String elide(String s, int len) {
+    if (s == null || s.length() < len || len <= 10) {
+      return s;
+    }
+    return s.substring(0, len - 10) + "..." + s.substring(s.length() - 10);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/public/serviceuser.css b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/public/serviceuser.css
index a094130..6959b1b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/public/serviceuser.css
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/public/serviceuser.css
@@ -6,20 +6,24 @@
   margin-left: 10px !important;
 }
 
-.serviceuser-serviceUserTable {
+.serviceuser-serviceUserTable,
+.serviceuser-sshKeyTable {
   border-collapse: separate;
   border-spacing: 0;
 }
 
-.serviceuser-serviceUserTable .leftMostCell {
+.serviceuser-serviceUserTable .leftMostCell,
+.serviceuser-sshKeyTable .leftMostCell {
   border-left: 1px solid #EEE;
 }
 
-.serviceuser-serviceUserTable .topMostCell {
+.serviceuser-serviceUserTable .topMostCell,
+.serviceuser-sshKeyTable .topMostCell {
   border-top: 1px solid #EEE;
 }
 
-.serviceuser-serviceUserTable .dataHeader {
+.serviceuser-serviceUserTable .dataHeader,
+.serviceuser-sshKeyTable .dataHeader {
   border: 1px solid #FFF;
   padding: 2px 6px 1px;
   background-color: #EEE;
@@ -28,7 +32,14 @@
   color: textColor;
 }
 
-.serviceuser-serviceUserTable .dataCell {
+.serviceuser-sshKeyTable .iconHeader {
+  border-top: 1px solid #FFF;
+  border-bottom: 1px solid #FFF;
+  background-color: #EEE;
+}
+
+.serviceuser-serviceUserTable .dataCell,
+.serviceuser-sshKeyTable .dataCell {
   padding-left: 5px;
   padding-right: 5px;
   border-right: 1px solid #EEE;
@@ -37,3 +48,29 @@
   height: 20px;
 }
 
+.serviceuser-sshKeyTable .iconCell {
+  width: 1px;
+  padding: 0px;
+  vertical-align: middle;
+  border-bottom: 1px solid #EEE;
+}
+
+.serviceuser-sshKeyPanelEncodedKey {
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  overflow: hidden;
+  font-family: mono-font;
+  font-size: small;
+}
+
+.serviceuser-sshKeyPanelInvalid {
+  white-space: nowrap;
+  color: red;
+  font-weight: bold;
+}
+
+.serviceuser-addSshKeyPanel {
+  margin-top: 10px;
+  background-color: #EEE;
+  padding: 5px 5px 5px 5px;
+}