From ee25c8391c07dc59dc7cb9b3ff8b1b30dcbd2fcd Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Tue, 18 Oct 2011 17:01:22 -0400
Subject: [PATCH] Added icons and polish. Save and load gitblit registrations.

---
 src/com/gitblit/wicket/GitBlitWebApp.properties |    3 
 src/com/gitblit/client/SettingsModel.java       |  110 +++++++++++++++
 src/com/gitblit/client/HeaderPanel.java         |   13 +
 src/com/gitblit/client/GitblitPanel.java        |  143 ++++++++++++++++++--
 resources/settings_16x16.png                    |    0 
 build.xml                                       |    3 
 src/com/gitblit/client/GitblitManager.java      |  131 +++++++++++++++++-
 src/com/gitblit/client/Utils.java               |    1 
 8 files changed, 373 insertions(+), 31 deletions(-)

diff --git a/build.xml b/build.xml
index 5ed87d5..92174d4 100644
--- a/build.xml
+++ b/build.xml
@@ -428,6 +428,9 @@
 		<genjar jarfile="manager.jar">
 			<resource file="${basedir}/src/com/gitblit/client/splash.png" />
 			<resource file="${basedir}/resources/gitblt-favicon.png" />
+			<resource file="${basedir}/resources/gitweb-favicon.png" />
+			<resource file="${basedir}/resources/user_16x16.png" />
+			<resource file="${basedir}/resources/settings_16x16.png" />
 			<resource file="${basedir}/resources/lock_go_16x16.png" />
 			<resource file="${basedir}/resources/lock_pull_16x16.png" />
 			<resource file="${basedir}/resources/shield_16x16.png" />
diff --git a/resources/settings_16x16.png b/resources/settings_16x16.png
new file mode 100644
index 0000000..1619e3d
--- /dev/null
+++ b/resources/settings_16x16.png
Binary files differ
diff --git a/src/com/gitblit/client/GitblitManager.java b/src/com/gitblit/client/GitblitManager.java
index f777fdb..47ff6ea 100644
--- a/src/com/gitblit/client/GitblitManager.java
+++ b/src/com/gitblit/client/GitblitManager.java
@@ -20,12 +20,18 @@
 import java.awt.EventQueue;
 import java.awt.Font;
 import java.awt.GridLayout;
+import java.awt.Point;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.KeyEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.File;
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
+import java.text.MessageFormat;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
 
 import javax.swing.ImageIcon;
 import javax.swing.JFrame;
@@ -41,11 +47,17 @@
 import javax.swing.KeyStroke;
 import javax.swing.UIManager;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.Base64;
+import org.eclipse.jgit.util.FS;
+
 import com.gitblit.Constants;
 import com.gitblit.utils.StringUtils;
 
 /**
- * Sample RPC application.
+ * Gitblit Manager issues JSON RPC requests to a Gitblit server.
  * 
  * @author James Moger
  * 
@@ -54,10 +66,11 @@
 
 	private static final long serialVersionUID = 1L;
 	private JTabbedPane serverTabs;
+	private File configFile = new File(System.getProperty("user.home"), ".gitblit/config");
 	private GitblitRegistration localhost = new GitblitRegistration("default",
 			"https://localhost:8443", "admin", "admin".toCharArray());
 
-	private List<GitblitRegistration> registrations = new ArrayList<GitblitRegistration>();
+	private Map<String, GitblitRegistration> registrations = new LinkedHashMap<String, GitblitRegistration>();
 	private JMenu recentMenu;
 
 	private GitblitManager() {
@@ -67,10 +80,66 @@
 	private void initialize() {
 		setContentPane(getCenterPanel());
 		setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage());
-
 		setTitle("Gitblit Manager v" + Constants.VERSION + " (" + Constants.VERSION_DATE + ")");
 		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
-		setSize(800, 500);
+		addWindowListener(new WindowAdapter() {
+			@Override
+			public void windowClosing(WindowEvent event) {
+				saveSizeAndPosition();
+			}
+		});
+
+		setSizeAndPosition();
+		loadRegistrations();
+		rebuildRecentMenu();
+	}
+
+	private void setSizeAndPosition() {
+		String sz = null;
+		String pos = null;
+		try {
+			StoredConfig config = getConfig();
+			sz = config.getString("ui", null, "size");
+			pos = config.getString("ui", null, "position");
+		} catch (Throwable t) {
+			t.printStackTrace();
+		}
+
+		// try to restore saved window size
+		if (StringUtils.isEmpty(sz)) {
+			setSize(850, 500);
+		} else {
+			String[] chunks = sz.split("x");
+			int width = Integer.parseInt(chunks[0]);
+			int height = Integer.parseInt(chunks[1]);
+			setSize(width, height);
+		}
+
+		// try to restore saved window position
+		if (StringUtils.isEmpty(pos)) {
+			setLocationRelativeTo(null);
+		} else {
+			String[] chunks = pos.split(",");
+			int x = Integer.parseInt(chunks[0]);
+			int y = Integer.parseInt(chunks[1]);
+			setLocation(x, y);
+		}
+	}
+
+	private void saveSizeAndPosition() {
+		try {
+			// save window size and position
+			StoredConfig config = getConfig();
+			Dimension sz = GitblitManager.this.getSize();
+			config.setString("ui", null, "size",
+					MessageFormat.format("{0,number,0}x{1,number,0}", sz.width, sz.height));
+			Point pos = GitblitManager.this.getLocationOnScreen();
+			config.setString("ui", null, "position",
+					MessageFormat.format("{0,number,0},{1,number,0}", pos.x, pos.y));
+			config.save();
+		} catch (Throwable t) {
+			Utils.showException(GitblitManager.this, t);
+		}
 	}
 
 	public void setVisible(boolean value) {
@@ -80,10 +149,10 @@
 				loginPrompt(localhost);
 			} else if (registrations.size() == 1) {
 				// single registration prompt
-				loginPrompt(registrations.get(0));
+				GitblitRegistration reg = registrations.values().iterator().next();
+				loginPrompt(reg);
 			}
 			super.setVisible(value);
-			setLocationRelativeTo(null);
 		}
 	}
 
@@ -147,7 +216,7 @@
 		reg = new GitblitRegistration(nameField.getText(), url, accountField.getText(),
 				passwordField.getPassword());
 		boolean success = login(reg);
-		registrations.add(0, reg);
+		registrations.put(reg.name, reg);
 		rebuildRecentMenu();
 		return success;
 	}
@@ -161,6 +230,7 @@
 			serverTabs.setSelectedIndex(idx);
 			serverTabs.setTabComponentAt(idx, new ClosableTabComponent(reg.name, null, serverTabs,
 					panel));
+			saveRegistration(reg);
 			return true;
 		} catch (IOException e) {
 			JOptionPane.showMessageDialog(GitblitManager.this, e.getMessage(),
@@ -171,17 +241,54 @@
 
 	private void rebuildRecentMenu() {
 		recentMenu.removeAll();
-		for (final GitblitRegistration reg : registrations) {
-			JMenuItem item = new JMenuItem(reg.name);
+		ImageIcon icon = new ImageIcon(getClass().getResource("/gitblt-favicon.png"));
+		for (final GitblitRegistration reg : registrations.values()) {
+			JMenuItem item = new JMenuItem(reg.name, icon);
 			item.addActionListener(new ActionListener() {
 				public void actionPerformed(ActionEvent e) {
-					login(reg);
+					loginPrompt(reg);
 				}
 			});
 			recentMenu.add(item);
 		}
 	}
 
+	private void loadRegistrations() {
+		try {
+			StoredConfig config = getConfig();
+			Set<String> servers = config.getSubsections("servers");
+			for (String server : servers) {
+				String url = config.getString("servers", server, "url");
+				String account = config.getString("servers", server, "account");
+				char[] password = new String(Base64.decode(config.getString("servers", server,
+						"password"))).toCharArray();
+				GitblitRegistration reg = new GitblitRegistration(server, url, account, password);
+				registrations.put(reg.name, reg);
+			}
+		} catch (Throwable t) {
+			Utils.showException(GitblitManager.this, t);
+		}
+	}
+
+	private void saveRegistration(GitblitRegistration reg) {
+		try {
+			StoredConfig config = getConfig();
+			config.setString("servers", reg.name, "url", reg.url);
+			config.setString("servers", reg.name, "account", reg.account);
+			config.setString("servers", reg.name, "password",
+					Base64.encodeBytes(new String(reg.password).getBytes("UTF-8")));
+			config.save();
+		} catch (Throwable t) {
+			Utils.showException(GitblitManager.this, t);
+		}
+	}
+
+	private StoredConfig getConfig() throws IOException, ConfigInvalidException {
+		FileBasedConfig config = new FileBasedConfig(configFile, FS.detect());
+		config.load();
+		return config;
+	}
+
 	public static void main(String[] args) {
 		EventQueue.invokeLater(new Runnable() {
 			public void run() {
diff --git a/src/com/gitblit/client/GitblitPanel.java b/src/com/gitblit/client/GitblitPanel.java
index 5525e41..7bcaac5 100644
--- a/src/com/gitblit/client/GitblitPanel.java
+++ b/src/com/gitblit/client/GitblitPanel.java
@@ -80,6 +80,10 @@
 
 	private UsersModel usersModel;
 
+	private JTable settingsTable;
+
+	private SettingsModel settingsModel;
+
 	private JButton createRepository;
 
 	private JButton delRepository;
@@ -96,6 +100,8 @@
 
 	private TableRowSorter<UsersModel> defaultUsersSorter;
 
+	private TableRowSorter<SettingsModel> defaultSettingsSorter;
+
 	private JButton editRepository;
 
 	public GitblitPanel(GitblitRegistration reg) {
@@ -105,6 +111,17 @@
 	public GitblitPanel(String url, String account, char[] password) {
 		this.gitblit = new GitblitModel(url, account, password);
 
+		tabs = new JTabbedPane(JTabbedPane.BOTTOM);
+		tabs.addTab(Translation.get("gb.repositories"), createRepositoriesPanel());
+		tabs.addTab(Translation.get("gb.users"), createUsersPanel());
+		tabs.addTab(Translation.get("gb.federation"), new JPanel());
+		tabs.addTab(Translation.get("gb.settings"), createSettingsPanel());
+
+		setLayout(new BorderLayout());
+		add(tabs, BorderLayout.CENTER);
+	}
+
+	private JPanel createRepositoriesPanel() {
 		final JButton browseRepository = new JButton(Translation.get("gb.browse"));
 		browseRepository.setEnabled(false);
 		browseRepository.addActionListener(new ActionListener() {
@@ -168,10 +185,10 @@
 		repositoriesTable.setRowSorter(defaultRepositoriesSorter);
 		repositoriesTable.getRowSorter().toggleSortOrder(RepositoriesModel.Columns.Name.ordinal());
 
-		setRepositoryRenderer(RepositoriesModel.Columns.Name, nameRenderer);
-		setRepositoryRenderer(RepositoriesModel.Columns.Indicators, typeRenderer);
-		setRepositoryRenderer(RepositoriesModel.Columns.Owner, ownerRenderer);
-		setRepositoryRenderer(RepositoriesModel.Columns.Size, sizeRenderer);
+		setRepositoryRenderer(RepositoriesModel.Columns.Name, nameRenderer, -1);
+		setRepositoryRenderer(RepositoriesModel.Columns.Indicators, typeRenderer, 100);
+		setRepositoryRenderer(RepositoriesModel.Columns.Owner, ownerRenderer, -1);
+		setRepositoryRenderer(RepositoriesModel.Columns.Size, sizeRenderer, 60);
 
 		repositoriesTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
 			@Override
@@ -231,11 +248,25 @@
 				return insets;
 			}
 		};
-		repositoriesPanel.add(new HeaderPanel(Translation.get("gb.repositories")),
-				BorderLayout.NORTH);
+		repositoriesPanel.add(new HeaderPanel(Translation.get("gb.repositories"),
+				"gitweb-favicon.png"), BorderLayout.NORTH);
 		repositoriesPanel.add(repositoryTablePanel, BorderLayout.CENTER);
 		repositoriesPanel.add(repositoryControls, BorderLayout.SOUTH);
 
+		return repositoriesPanel;
+	}
+
+	private void setRepositoryRenderer(RepositoriesModel.Columns col, TableCellRenderer renderer,
+			int maxWidth) {
+		String name = repositoriesTable.getColumnName(col.ordinal());
+		repositoriesTable.getColumn(name).setCellRenderer(renderer);
+		if (maxWidth > 0) {
+			repositoriesTable.getColumn(name).setMinWidth(maxWidth);
+			repositoriesTable.getColumn(name).setMaxWidth(maxWidth);
+		}
+	}
+
+	private JPanel createUsersPanel() {
 		JButton refreshUsers = new JButton(Translation.get("gb.refresh"));
 		refreshUsers.addActionListener(new ActionListener() {
 			public void actionPerformed(ActionEvent e) {
@@ -322,22 +353,73 @@
 				return insets;
 			}
 		};
-		usersPanel.add(new HeaderPanel(Translation.get("gb.users")), BorderLayout.NORTH);
+		usersPanel.add(new HeaderPanel(Translation.get("gb.users"), "user_16x16.png"),
+				BorderLayout.NORTH);
 		usersPanel.add(userTablePanel, BorderLayout.CENTER);
 		usersPanel.add(userControls, BorderLayout.SOUTH);
 
-		tabs = new JTabbedPane(JTabbedPane.BOTTOM);
-		tabs.addTab(Translation.get("gb.repositories"), repositoriesPanel);
-		tabs.addTab(Translation.get("gb.users"), usersPanel);
-		tabs.addTab(Translation.get("gb.federation"), new JPanel());
-
-		setLayout(new BorderLayout());
-		add(tabs, BorderLayout.CENTER);
+		return usersPanel;
 	}
 
-	private void setRepositoryRenderer(RepositoriesModel.Columns col, TableCellRenderer renderer) {
-		String name = repositoriesTable.getColumnName(col.ordinal());
-		repositoriesTable.getColumn(name).setCellRenderer(renderer);
+	private JPanel createSettingsPanel() {
+		settingsModel = new SettingsModel();
+		defaultSettingsSorter = new TableRowSorter<SettingsModel>(settingsModel);
+		settingsTable = Utils.newTable(settingsModel);
+		String name = settingsTable.getColumnName(UsersModel.Columns.Name.ordinal());
+		settingsTable.setRowHeight(nameRenderer.getFont().getSize() + 8);
+		settingsTable.getColumn(name).setCellRenderer(nameRenderer);
+		settingsTable.setRowSorter(defaultSettingsSorter);
+		settingsTable.getRowSorter().toggleSortOrder(SettingsModel.Columns.Name.ordinal());
+		settingsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+
+			@Override
+			public void valueChanged(ListSelectionEvent e) {
+				if (e.getValueIsAdjusting()) {
+					return;
+				}
+				boolean selected = settingsTable.getSelectedRow() > -1;
+				boolean singleSelection = settingsTable.getSelectedRows().length == 1;
+				// TODO enable/disable setting buttons
+			}
+		});
+
+		final JTextField settingFilter = new JTextField();
+		settingFilter.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				filterSettings(settingFilter.getText());
+			}
+		});
+		settingFilter.addKeyListener(new KeyAdapter() {
+			public void keyReleased(KeyEvent e) {
+				filterSettings(settingFilter.getText());
+			}
+		});
+
+		JPanel settingFilterPanel = new JPanel(new BorderLayout(margin, margin));
+		settingFilterPanel.add(new JLabel(Translation.get("gb.filter")), BorderLayout.WEST);
+		settingFilterPanel.add(settingFilter, BorderLayout.CENTER);
+
+		JPanel settingsTablePanel = new JPanel(new BorderLayout(margin, margin));
+		settingsTablePanel.add(settingFilterPanel, BorderLayout.NORTH);
+		settingsTablePanel.add(new JScrollPane(settingsTable), BorderLayout.CENTER);
+
+		JPanel settingsControls = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0));
+		// TODO update setting?
+
+		JPanel settingsPanel = new JPanel(new BorderLayout(margin, margin)) {
+
+			private static final long serialVersionUID = 1L;
+
+			public Insets getInsets() {
+				return insets;
+			}
+		};
+		settingsPanel.add(new HeaderPanel(Translation.get("gb.settings"), "settings_16x16.png"),
+				BorderLayout.NORTH);
+		settingsPanel.add(settingsTablePanel, BorderLayout.CENTER);
+		settingsPanel.add(settingsControls, BorderLayout.SOUTH);
+
+		return settingsPanel;
 	}
 
 	public void login() throws IOException {
@@ -348,6 +430,7 @@
 
 		if (gitblit.allowAdmin()) {
 			updateUsersTable();
+			updateSettingsTable();
 		} else {
 			// user does not have administrator privileges
 			// hide admin repository buttons
@@ -372,6 +455,11 @@
 		usersModel.list.clear();
 		usersModel.list.addAll(gitblit.getUsers());
 		usersModel.fireTableDataChanged();
+	}
+
+	private void updateSettingsTable() {
+		settingsModel.setSettings(gitblit.getSettings());
+		settingsModel.fireTableDataChanged();
 	}
 
 	private void filterRepositories(final String fragment) {
@@ -413,6 +501,26 @@
 		TableRowSorter<UsersModel> sorter = new TableRowSorter<UsersModel>(usersModel);
 		sorter.setRowFilter(containsFilter);
 		usersTable.setRowSorter(sorter);
+	}
+
+	private void filterSettings(final String fragment) {
+		if (StringUtils.isEmpty(fragment)) {
+			settingsTable.setRowSorter(defaultSettingsSorter);
+			return;
+		}
+		RowFilter<SettingsModel, Object> containsFilter = new RowFilter<SettingsModel, Object>() {
+			public boolean include(Entry<? extends SettingsModel, ? extends Object> entry) {
+				for (int i = entry.getValueCount() - 1; i >= 0; i--) {
+					if (entry.getStringValue(i).toLowerCase().contains(fragment.toLowerCase())) {
+						return true;
+					}
+				}
+				return false;
+			}
+		};
+		TableRowSorter<SettingsModel> sorter = new TableRowSorter<SettingsModel>(settingsModel);
+		sorter.setRowFilter(containsFilter);
+		settingsTable.setRowSorter(sorter);
 	}
 
 	private List<RepositoryModel> getSelectedRepositories() {
@@ -572,6 +680,7 @@
 						success &= gitblit.deleteRepository(repository);
 					}
 					if (success) {
+						gitblit.refreshRepositories();
 						gitblit.refreshUsers();
 					}
 					return success;
diff --git a/src/com/gitblit/client/HeaderPanel.java b/src/com/gitblit/client/HeaderPanel.java
index b3953b3..0df2914 100644
--- a/src/com/gitblit/client/HeaderPanel.java
+++ b/src/com/gitblit/client/HeaderPanel.java
@@ -15,6 +15,7 @@
  */
 package com.gitblit.client;
 
+import java.awt.BasicStroke;
 import java.awt.Color;
 import java.awt.FlowLayout;
 import java.awt.GradientPaint;
@@ -24,8 +25,11 @@
 import java.awt.geom.Point2D;
 import java.awt.geom.Rectangle2D;
 
+import javax.swing.ImageIcon;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
+
+import com.gitblit.utils.StringUtils;
 
 public class HeaderPanel extends JPanel {
 
@@ -33,12 +37,15 @@
 
 	private Color lightColor = new Color(0, 0, 0x60);
 
-	public HeaderPanel(String text) {
+	public HeaderPanel(String text, String icon) {
 		super(new FlowLayout(FlowLayout.LEFT), true);
 		setOpaque(true);
 		setBackground(new Color(0, 0, 0x20));
 
 		JLabel label = new JLabel(text);
+		if (!StringUtils.isEmpty(icon)) {
+			label.setIcon(new ImageIcon(getClass().getResource("/" + icon)));
+		}
 		label.setForeground(Color.white);
 		label.setFont(label.getFont().deriveFont(14f));
 		add(label);
@@ -53,5 +60,9 @@
 				false);
 		g.setPaint(gradientPaint);
 		g.fill(new Rectangle2D.Double(0, 0, getWidth(), getHeight()));
+		g.setColor(new Color(0xff, 0x99, 0x00));
+		int stroke = 2;
+		g.setStroke(new BasicStroke(stroke));
+		g.drawLine(0, getHeight() - 1, getWidth(), getHeight() - 1);
 	}
 }
diff --git a/src/com/gitblit/client/SettingsModel.java b/src/com/gitblit/client/SettingsModel.java
new file mode 100644
index 0000000..2c7bff8
--- /dev/null
+++ b/src/com/gitblit/client/SettingsModel.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * 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.gitblit.client;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.swing.table.AbstractTableModel;
+
+import com.gitblit.IStoredSettings;
+
+/**
+ * Table model of IStoredSettings.
+ * 
+ * @author James Moger
+ * 
+ */
+public class SettingsModel extends AbstractTableModel {
+
+	private static final long serialVersionUID = 1L;
+
+	IStoredSettings settings;
+
+	List<String> keys;
+
+	enum Columns {
+		Name, Value;
+
+		@Override
+		public String toString() {
+			return name().replace('_', ' ');
+		}
+	}
+
+	public SettingsModel() {
+		this(null);
+	}
+
+	public SettingsModel(IStoredSettings settings) {
+		setSettings(settings);
+	}
+
+	public void setSettings(IStoredSettings settings) {
+		this.settings = settings;
+		if (settings == null) {
+			keys = new ArrayList<String>();
+		} else {
+			keys = new ArrayList<String>(settings.getAllKeys(null));
+			Collections.sort(keys);
+		}
+	}
+
+	@Override
+	public int getRowCount() {
+		return keys.size();
+	}
+
+	@Override
+	public int getColumnCount() {
+		return Columns.values().length;
+	}
+
+	@Override
+	public String getColumnName(int column) {
+		Columns col = Columns.values()[column];
+		switch (col) {
+		case Name:
+			return Translation.get("gb.name");
+		}
+		return "";
+	}
+
+	/**
+	 * Returns <code>Object.class</code> regardless of <code>columnIndex</code>.
+	 * 
+	 * @param columnIndex
+	 *            the column being queried
+	 * @return the Object.class
+	 */
+	public Class<?> getColumnClass(int columnIndex) {
+		return String.class;
+	}
+
+	@Override
+	public Object getValueAt(int rowIndex, int columnIndex) {
+		String key = keys.get(rowIndex);
+		Columns col = Columns.values()[columnIndex];
+		switch (col) {
+		case Name:
+			return key;
+		case Value:
+			return settings.getString(key, "");
+		}
+		return null;
+	}
+}
diff --git a/src/com/gitblit/client/Utils.java b/src/com/gitblit/client/Utils.java
index 1ce6d7d..c837579 100644
--- a/src/com/gitblit/client/Utils.java
+++ b/src/com/gitblit/client/Utils.java
@@ -56,6 +56,7 @@
 
 	public static void showException(Component c, Throwable t) {
 		// TODO show the unexpected exception
+		t.printStackTrace();
 	}
 
 	public static void packColumns(JTable table, int margin) {
diff --git a/src/com/gitblit/wicket/GitBlitWebApp.properties b/src/com/gitblit/wicket/GitBlitWebApp.properties
index 9702e6c..a811260 100644
--- a/src/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -155,4 +155,5 @@
 gb.downloading = downloading
 gb.loading = loading
 gb.starting = starting
-gb.general = general
\ No newline at end of file
+gb.general = general
+gb.settings = settings
\ No newline at end of file

--
Gitblit v1.9.1