From 0349f8a1c8702481713744888eb07b6a3091aad7 Mon Sep 17 00:00:00 2001 From: Dave Borowitz Date: Mon, 3 Aug 2015 10:28:32 -0700 Subject: [PATCH] Add settings screen for editing GPG public keys The UI is almost identical to the UI for editing SSH keys (although implemented with UiBinder). Change-Id: Ic6cf4dc9d7f71b00efea00a86498660d113ebb2b --- .../com/google/gerrit/common/PageLinks.java | 1 + .../com/google/gerrit/client/rpc/Natives.java | 14 + .../com/google/gerrit/client/Dispatcher.java | 6 + .../com/google/gerrit/client/GerritCss.java | 1 + .../gerrit/client/account/AccountApi.java | 41 +++ .../client/account/AccountConstants.java | 19 +- .../account/AccountConstants.properties | 19 +- .../gerrit/client/account/GpgKeyInfo.java | 28 ++ .../client/account/MyGpgKeysScreen.java | 283 ++++++++++++++++++ .../client/account/MyGpgKeysScreen.ui.xml | 94 ++++++ .../gerrit/client/account/SettingsScreen.java | 3 + .../java/com/google/gerrit/client/gerrit.css | 4 + 12 files changed, 499 insertions(+), 14 deletions(-) create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/account/GpgKeyInfo.java create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.ui.xml diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java index c80d86751d..ff2121d133 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java @@ -26,6 +26,7 @@ public class PageLinks { public static final String SETTINGS = "/settings/"; public static final String SETTINGS_PREFERENCES = "/settings/preferences"; public static final String SETTINGS_SSHKEYS = "/settings/ssh-keys"; + public static final String SETTINGS_GPGKEYS = "/settings/gpg-keys"; public static final String SETTINGS_HTTP_PASSWORD = "/settings/http-password"; public static final String SETTINGS_WEBIDENT = "/settings/web-identities"; public static final String SETTINGS_MYGROUPS = "/settings/group-memberships"; diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java index 70878887b7..dcd96da20b 100644 --- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java +++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java @@ -91,6 +91,20 @@ public class Natives { return arr; } + public static JsArrayString arrayOf(Iterable elements) { + JsArrayString arr = JavaScriptObject.createArray().cast(); + for (String elem : elements) { + arr.push(elem); + } + return arr; + } + + public static JsArrayString arrayOf(String element) { + JsArrayString arr = JavaScriptObject.createArray().cast(); + arr.push(element); + return arr; + } + private Natives() { } } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java index e7381f842a..946888df14 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java @@ -29,6 +29,7 @@ import static com.google.gerrit.common.PageLinks.SETTINGS; import static com.google.gerrit.common.PageLinks.SETTINGS_AGREEMENTS; import static com.google.gerrit.common.PageLinks.SETTINGS_CONTACT; import static com.google.gerrit.common.PageLinks.SETTINGS_EXTENSION; +import static com.google.gerrit.common.PageLinks.SETTINGS_GPGKEYS; import static com.google.gerrit.common.PageLinks.SETTINGS_HTTP_PASSWORD; import static com.google.gerrit.common.PageLinks.SETTINGS_MYGROUPS; import static com.google.gerrit.common.PageLinks.SETTINGS_NEW_AGREEMENT; @@ -40,6 +41,7 @@ import static com.google.gerrit.common.PageLinks.toChangeQuery; import com.google.gerrit.client.account.MyAgreementsScreen; import com.google.gerrit.client.account.MyContactInformationScreen; +import com.google.gerrit.client.account.MyGpgKeysScreen; import com.google.gerrit.client.account.MyGroupsScreen; import com.google.gerrit.client.account.MyIdentitiesScreen; import com.google.gerrit.client.account.MyPasswordScreen; @@ -536,6 +538,10 @@ public class Dispatcher { return new MySshKeysScreen(); } + if (matchExact(SETTINGS_GPGKEYS, token)) { + return new MyGpgKeysScreen(); + } + if (matchExact(SETTINGS_WEBIDENT, token)) { return new MyIdentitiesScreen(); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java index c6eb2de2ef..c2a76379ab 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java @@ -184,6 +184,7 @@ public interface GerritCss extends CssResource { String sshHostKeyPanelKnownHostEntry(); String sshKeyPanelEncodedKey(); String sshKeyPanelInvalid(); + String sshKeyTable(); String stringListPanelButtons(); String topMostCell(); String topmenu(); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java index a796f945bd..367644f5b2 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java @@ -17,10 +17,13 @@ package com.google.gerrit.client.account; import com.google.gerrit.client.VoidResult; import com.google.gerrit.client.info.AccountInfo; import com.google.gerrit.client.rpc.CallbackGroup; +import com.google.gerrit.client.rpc.NativeMap; import com.google.gerrit.client.rpc.NativeString; +import com.google.gerrit.client.rpc.Natives; import com.google.gerrit.client.rpc.RestApi; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArray; +import com.google.gwt.core.client.JsArrayString; import com.google.gwt.user.client.rpc.AsyncCallback; import java.util.Set; @@ -147,4 +150,42 @@ public class AccountApi { protected UsernameInput() { } } + + public static void addGpgKey(String account, String armored, + AsyncCallback> cb) { + new RestApi("/accounts/") + .id(account) + .view("gpgkeys") + .post(GpgKeysInput.add(armored), cb); + } + + public static void removeGpgKeys(String account, + Iterable fingerprints, AsyncCallback> cb) { + new RestApi("/accounts/") + .id(account) + .view("gpgkeys") + .post(GpgKeysInput.remove(fingerprints), cb); + } + + private static class GpgKeysInput extends JavaScriptObject { + static GpgKeysInput add(String key) { + return createAdd(Natives.arrayOf(key)); + } + + static GpgKeysInput remove(Iterable fingerprints) { + return createRemove(Natives.arrayOf(fingerprints)); + } + + private static native GpgKeysInput createAdd(JsArrayString keys) /*-{ + return {'add': keys}; + }-*/; + + private static native GpgKeysInput createRemove( + JsArrayString fingerprints) /*-{ + return {'remove': fingerprints}; + }-*/; + + protected GpgKeysInput() { + } + } } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java index 4c3cc29a0d..6234f02e6b 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java @@ -50,14 +50,15 @@ public interface AccountConstants extends Constants { String myMenuReset(); String tabAccountSummary(); - String tabPreferences(); - String tabWatchedProjects(); - String tabContactInformation(); - String tabSshKeys(); - String tabHttpAccess(); - String tabWebIdentities(); - String tabMyGroups(); String tabAgreements(); + String tabContactInformation(); + String tabGpgKeys(); + String tabHttpAccess(); + String tabMyGroups(); + String tabPreferences(); + String tabSshKeys(); + String tabWatchedProjects(); + String tabWebIdentities(); String buttonShowAddSshKey(); String buttonCloseAddSshKey(); @@ -94,6 +95,10 @@ public interface AccountConstants extends Constants { String sshHostKeyFingerprint(); String sshHostKeyKnownHostEntry(); + String gpgKeyId(); + String gpgKeyFingerprint(); + String gpgKeyUserIds(); + String webIdStatus(); String webIdEmail(); String webIdIdentity(); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties index 36cb765f49..eee7a60199 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties @@ -36,14 +36,15 @@ changeScreenOldUi = Old Screen changeScreenNewUi = New Screen tabAccountSummary = Profile -tabPreferences = Preferences -tabWatchedProjects = Watched Projects -tabContactInformation = Contact Information -tabSshKeys = SSH Public Keys -tabHttpAccess = HTTP Password -tabWebIdentities = Identities -tabMyGroups = Groups tabAgreements = Agreements +tabContactInformation = Contact Information +tabGpgKeys = GPG Public Keys +tabHttpAccess = HTTP Password +tabMyGroups = Groups +tabPreferences = Preferences +tabSshKeys = SSH Public Keys +tabWatchedProjects = Watched Projects +tabWebIdentities = Identities buttonShowAddSshKey = Add Key ... buttonCloseAddSshKey = Close @@ -73,6 +74,10 @@ sshHostKeyTitle = Server Host Key sshHostKeyFingerprint = Fingerprint: sshHostKeyKnownHostEntry = Entry for ~/.ssh/known_hosts: +gpgKeyId = ID +gpgKeyFingerprint = Fingerprint +gpgKeyUserIds = User IDs + webIdStatus = Status webIdEmail = Email Address webIdIdentity = Identity diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/GpgKeyInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/GpgKeyInfo.java new file mode 100644 index 0000000000..d1bb42689c --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/GpgKeyInfo.java @@ -0,0 +1,28 @@ +// Copyright (C) 2015 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.google.gerrit.client.account; + +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.JsArrayString; + +public class GpgKeyInfo extends JavaScriptObject { + public final native String id() /*-{ return this.id; }-*/; + public final native String fingerprint() /*-{ return this.fingerprint; }-*/; + public final native JsArrayString userIds() /*-{ return this.user_ids; }-*/; + public final native String key() /*-{ return this.key; }-*/; + + protected GpgKeyInfo() { + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java new file mode 100644 index 0000000000..6d88e384be --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java @@ -0,0 +1,283 @@ +// Copyright (C) 2015 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.google.gerrit.client.account; + +import com.google.gerrit.client.Gerrit; +import com.google.gerrit.client.rpc.GerritCallback; +import com.google.gerrit.client.rpc.NativeMap; +import com.google.gerrit.client.rpc.Natives; +import com.google.gerrit.client.ui.FancyFlexTable; +import com.google.gwt.core.client.GWT; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.logical.shared.ValueChangeEvent; +import com.google.gwt.event.logical.shared.ValueChangeHandler; +import com.google.gwt.http.client.Response; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.rpc.StatusCodeException; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.CheckBox; +import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter; +import com.google.gwt.user.client.ui.HTMLPanel; +import com.google.gwt.user.client.ui.InlineLabel; +import com.google.gwt.user.client.ui.Label; +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.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class MyGpgKeysScreen extends SettingsScreen { + interface Binder extends UiBinder {} + private static final Binder uiBinder = GWT.create(Binder.class); + + @UiField(provided = true) GpgKeyTable keys; + @UiField Button deleteKey; + @UiField Button addKey; + + @UiField VerticalPanel addKeyBlock; + @UiField NpTextArea keyText; + + @UiField VerticalPanel errorPanel; + @UiField Label errorText; + + @UiField Button clearButton; + @UiField Button addButton; + @UiField Button closeButton; + + @Override + protected void onInitUI() { + super.onInitUI(); + keys = new GpgKeyTable(); + add(uiBinder.createAndBindUi(this)); + keys.updateDeleteButton(); + } + + @Override + protected void onLoad() { + super.onLoad(); + refreshKeys(); + } + + @UiHandler("deleteKey") + void onDeleteKey(@SuppressWarnings("unused") ClickEvent e) { + keys.deleteChecked(); + } + + @UiHandler("addKey") + void onAddKey(@SuppressWarnings("unused") ClickEvent e) { + showAddKeyBlock(true); + } + + @UiHandler("clearButton") + void onClearButton(@SuppressWarnings("unused") ClickEvent e) { + keyText.setText(""); + keyText.setFocus(true); + errorPanel.setVisible(false); + } + + @UiHandler("closeButton") + void onCloseButton(@SuppressWarnings("unused") ClickEvent e) { + showAddKeyBlock(false); + } + + @UiHandler("addButton") + void onAddButton(@SuppressWarnings("unused") ClickEvent e) { + doAddKey(); + } + + private void refreshKeys() { + AccountApi.self().view("gpgkeys").get(NativeMap.copyKeysIntoChildren("id", + new GerritCallback>() { + @Override + public void onSuccess(NativeMap result) { + List list = Natives.asList(result.values()); + // TODO(dborowitz): Sort on something more meaningful, like + // created date? + Collections.sort(list, new Comparator() { + @Override + public int compare(GpgKeyInfo a, GpgKeyInfo b) { + return a.id().compareTo(b.id()); + } + }); + keys.clear(); + keyText.setText(""); + errorPanel.setVisible(false); + addButton.setEnabled(true); + if (!list.isEmpty()) { + keys.setVisible(true); + for (GpgKeyInfo k : list) { + keys.addOneKey(k); + } + showKeyTable(true); + showAddKeyBlock(false); + } else { + keys.setVisible(false); + showAddKeyBlock(true); + showKeyTable(false); + } + + display(); + } + })); + } + + private void showAddKeyBlock(boolean show) { + addKey.setVisible(!show); + addKeyBlock.setVisible(show); + } + + private void showKeyTable(boolean show) { + keys.setVisible(show); + deleteKey.setVisible(show); + addKey.setVisible(show); + } + + private void doAddKey() { + if (keyText.getText().isEmpty()) { + return; + } + addButton.setEnabled(false); + keyText.setEnabled(false); + AccountApi.addGpgKey("self", keyText.getText(), + new AsyncCallback>() { + @Override + public void onSuccess(NativeMap result) { + keyText.setEnabled(true); + refreshKeys(); + } + + @Override + public void onFailure(Throwable caught) { + keyText.setEnabled(true); + addButton.setEnabled(true); + if (caught instanceof StatusCodeException) { + StatusCodeException sce = (StatusCodeException) caught; + if (sce.getStatusCode() == Response.SC_CONFLICT + || sce.getStatusCode() == Response.SC_BAD_REQUEST) { + errorText.setText(sce.getEncodedResponse()); + } else { + errorText.setText(sce.getMessage()); + } + } else { + errorText.setText( + "Unexpected error saving key: " + caught.getMessage()); + } + errorPanel.setVisible(true); + } + }); + } + + private class GpgKeyTable extends FancyFlexTable { + private final ValueChangeHandler updateDeleteHandler; + + GpgKeyTable() { + table.setWidth(""); + table.setText(0, 1, Util.C.gpgKeyId()); + table.setText(0, 2, Util.C.gpgKeyFingerprint()); + table.setText(0, 3, Util.C.gpgKeyUserIds()); + + FlexCellFormatter fmt = table.getFlexCellFormatter(); + fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().iconHeader()); + fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader()); + fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader()); + fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader()); + + updateDeleteHandler = new ValueChangeHandler() { + @Override + public void onValueChange(ValueChangeEvent event) { + updateDeleteButton(); + } + }; + } + + private void addOneKey(GpgKeyInfo k) { + int row = table.getRowCount(); + table.insertRow(row); + applyDataRowStyle(row); + + CheckBox sel = new CheckBox(); + sel.addValueChangeHandler(updateDeleteHandler); + table.setWidget(row, 0, sel); + table.setWidget(row, 1, new CopyableLabel(k.id())); + table.setText(row, 2, k.fingerprint()); + + VerticalPanel userIds = new VerticalPanel(); + for (int i = 0; i < k.userIds().length(); i++) { + userIds.add(new InlineLabel(k.userIds().get(i))); + } + table.setWidget(row, 3, userIds); + + FlexCellFormatter fmt = table.getFlexCellFormatter(); + fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().iconCell()); + fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell()); + fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell()); + fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell()); + + setRowItem(row, k); + } + + private void updateDeleteButton() { + for (int row = 1; row < table.getRowCount(); row++) { + if (isChecked(row)) { + deleteKey.setEnabled(true); + return; + } + } + deleteKey.setEnabled(false); + } + + private void deleteChecked() { + deleteKey.setEnabled(false); + List toDelete = new ArrayList<>(table.getRowCount()); + for (int row = 1; row < table.getRowCount(); row++) { + if (isChecked(row)) { + toDelete.add(getRowItem(row).fingerprint()); + } + } + AccountApi.removeGpgKeys("self", toDelete, + new GerritCallback>() { + @Override + public void onSuccess(NativeMap result) { + refreshKeys(); + } + + @Override + public void onFailure(Throwable caught) { + deleteKey.setEnabled(true); + super.onFailure(caught); + } + }); + } + + private boolean isChecked(int row) { + return ((CheckBox) table.getWidget(row, 0)).getValue(); + } + + private void clear() { + while (table.getRowCount() > 1) { + table.removeRow(1); + } + for (int i = table.getRowCount() - 1; i >= 1; i++) { + table.removeRow(i); + } + } + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.ui.xml new file mode 100644 index 0000000000..dc7373606b --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.ui.xml @@ -0,0 +1,94 @@ + + + + + + + .errorHeader { + font-weight: bold; + } + .errorText { + white-space: pre-wrap; + padding-bottom: 6px; + } + + + + + + +
Delete
+
+ +
Add Key ...
+
+
+ + Add GPG Public Key + + How to generate a GPG key + +
    +
  1. + From the Terminal or Git Bash, run gpg --gen-key and + follow the prompts to create the key. +
  2. +
  3. + Use the default kind. Use the default (or higher) keysize. Choose + any value for your expiration. +
  4. +
  5. + The user ID should contain one of your registered email addresses. +
  6. +
  7. Setting a passphrase is strongly recommended.
  8. +
  9. Note the ID of your new key.
  10. +
  11. + To export your key, run the following and paste the full output + into the text box: +
    + gpg --export -a <key ID> +
  12. +
+
+
+ + + Error adding GPG key: + + + + +
Clear
+
+ +
Add
+
+ +
Close
+
+
+
+
+
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java index ac140ff8af..2f3a819c3b 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java @@ -45,6 +45,9 @@ public abstract class SettingsScreen extends MenuScreen { if (Gerrit.info().auth().isHttpPasswordSettingsEnabled()) { linkByGerrit(Util.C.tabHttpAccess(), PageLinks.SETTINGS_HTTP_PASSWORD); } + if (Gerrit.info().receive().enableSignedPush()) { + linkByGerrit(Util.C.tabGpgKeys(), PageLinks.SETTINGS_GPGKEYS); + } linkByGerrit(Util.C.tabWebIdentities(), PageLinks.SETTINGS_WEBIDENT); linkByGerrit(Util.C.tabMyGroups(), PageLinks.SETTINGS_MYGROUPS); if (Gerrit.info().auth().useContributorAgreements()) { diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css index c26c43774b..0914efda63 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css @@ -1074,6 +1074,10 @@ a:hover.downloadLink { width: 100%; } +.sshKeyTable td.dataCell, .sshKeyTable td.iconCell { + vertical-align: top; +} + .createProjectPanel { margin-bottom: 10px; background-color: trimColor;