From 38688b1f17bb2d43a144e92b086768e3e2523d2a Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Tue, 01 Nov 2011 17:21:09 -0400
Subject: [PATCH] Refactored monolithic GitblitPanel into pieces. Revised feed generation.

---
 src/com/gitblit/client/SettingsPanel.java         |  275 ++++++
 src/com/gitblit/client/GitblitClient.java         |    2 
 src/com/gitblit/models/FeedModel.java             |    2 
 src/com/gitblit/client/RepositoriesPanel.java     |  454 ++++++++++
 src/com/gitblit/client/FeedsTableModel.java       |    6 
 src/com/gitblit/client/GitblitPanel.java          | 1108 ------------------------
 src/com/gitblit/client/FeedsPanel.java            |  210 ++++
 src/com/gitblit/SyndicationServlet.java           |   25 
 src/com/gitblit/client/Utils.java                 |   23 
 tests/com/gitblit/tests/SyndicationUtilsTest.java |   24 
 src/com/gitblit/utils/SyndicationUtils.java       |   24 
 src/com/gitblit/client/StatusPanel.java           |   73 +
 src/com/gitblit/client/UsersPanel.java            |  356 ++++++++
 src/com/gitblit/client/EditRepositoryDialog.java  |    1 
 src/com/gitblit/client/GitblitManager.java        |    2 
 15 files changed, 1,458 insertions(+), 1,127 deletions(-)

diff --git a/src/com/gitblit/SyndicationServlet.java b/src/com/gitblit/SyndicationServlet.java
index f1c474f..694b9cc 100644
--- a/src/com/gitblit/SyndicationServlet.java
+++ b/src/com/gitblit/SyndicationServlet.java
@@ -16,6 +16,7 @@
 package com.gitblit;
 
 import java.text.MessageFormat;
+import java.util.ArrayList;
 import java.util.List;
 
 import javax.servlet.http.HttpServlet;
@@ -26,10 +27,13 @@
 import org.slf4j.LoggerFactory;
 
 import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.SyndicatedEntryModel;
 import com.gitblit.utils.HttpUtils;
 import com.gitblit.utils.JGitUtils;
 import com.gitblit.utils.StringUtils;
 import com.gitblit.utils.SyndicationUtils;
+import com.sun.syndication.feed.synd.SyndContent;
+import com.sun.syndication.feed.synd.SyndContentImpl;
 
 /**
  * SyndicationServlet generates RSS 2.0 feeds and feed links.
@@ -139,9 +143,26 @@
 		Repository repository = GitBlit.self().getRepository(repositoryName);
 		RepositoryModel model = GitBlit.self().getRepositoryModel(repositoryName);
 		List<RevCommit> commits = JGitUtils.getRevLog(repository, objectId, 0, length);
+		List<SyndicatedEntryModel> entries = new ArrayList<SyndicatedEntryModel>();
+
+		String gitblitUrl = HttpUtils.getGitblitURL(request);
+		// convert RevCommit to SyndicatedEntryModel
+		for (RevCommit commit : commits) {
+			SyndicatedEntryModel entry = new SyndicatedEntryModel();
+			entry.title = commit.getShortMessage();
+			entry.author = commit.getAuthorIdent().getName();
+			entry.link = MessageFormat.format("{0}/commit/{1}/{2}", gitblitUrl,
+					StringUtils.encodeURL(model.name), commit.getName());
+			entry.published = commit.getCommitterIdent().getWhen();
+			entry.contentType = "text/plain";
+			entry.content = commit.getFullMessage();
+			entry.repository = model.name;
+			entry.branch = objectId;
+			entries.add(entry);
+		}
 		try {
-			SyndicationUtils.toRSS(HttpUtils.getGitblitURL(request), getTitle(model.name, objectId), model.description,
-					model.name, commits, response.getOutputStream());
+			SyndicationUtils.toRSS(gitblitUrl, getTitle(model.name, objectId), model.description,
+					model.name, entries, response.getOutputStream());
 		} catch (Exception e) {
 			logger.error("An error occurred during feed generation", e);
 		}
diff --git a/src/com/gitblit/client/EditRepositoryDialog.java b/src/com/gitblit/client/EditRepositoryDialog.java
index 55f5268..ef0f58a 100644
--- a/src/com/gitblit/client/EditRepositoryDialog.java
+++ b/src/com/gitblit/client/EditRepositoryDialog.java
@@ -246,6 +246,7 @@
 		getContentPane().setLayout(new BorderLayout(5, 5));
 		getContentPane().add(centerPanel, BorderLayout.CENTER);
 		pack();
+		nameField.requestFocus();
 	}
 
 	private JPanel newFieldPanel(String label, JComponent comp) {
diff --git a/src/com/gitblit/client/FeedsPanel.java b/src/com/gitblit/client/FeedsPanel.java
new file mode 100644
index 0000000..e2b8527
--- /dev/null
+++ b/src/com/gitblit/client/FeedsPanel.java
@@ -0,0 +1,210 @@
+/*
+ * 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.awt.BorderLayout;
+import java.awt.FlowLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.util.List;
+
+import javax.swing.JButton;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+
+import com.gitblit.Constants.RpcRequest;
+import com.gitblit.models.FeedModel;
+import com.gitblit.models.SyndicatedEntryModel;
+
+/**
+ * RSS Feeds Panel displays recent entries and launches the browser to view the
+ * commit. commitdiff, or tree of a commit.
+ * 
+ * @author James Moger
+ * 
+ */
+public abstract class FeedsPanel extends JPanel {
+
+	private static final long serialVersionUID = 1L;
+
+	private final GitblitClient gitblit;
+
+	private SyndicatedEntryTableModel tableModel;
+
+	private HeaderPanel header;
+
+	private JTable table;
+
+	public FeedsPanel(GitblitClient gitblit) {
+		super();
+		this.gitblit = gitblit;
+		initialize();
+	}
+
+	private void initialize() {
+		JButton refreshFeeds = new JButton(Translation.get("gb.refresh"));
+		refreshFeeds.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				refreshFeeds();
+			}
+		});
+
+		final JButton viewCommit = new JButton(Translation.get("gb.view"));
+		viewCommit.setEnabled(false);
+		viewCommit.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				viewCommit();
+			}
+		});
+
+		final JButton viewCommitDiff = new JButton(Translation.get("gb.commitdiff"));
+		viewCommitDiff.setEnabled(false);
+		viewCommitDiff.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				viewCommitDiff();
+			}
+		});
+
+		final JButton viewTree = new JButton(Translation.get("gb.tree"));
+		viewTree.setEnabled(false);
+		viewTree.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				viewTree();
+			}
+		});
+
+		JButton subscribeFeeds = new JButton(Translation.get("gb.subscribe") + "...");
+		subscribeFeeds.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				subscribeFeeds(gitblit.getAvailableFeeds());
+			}
+		});
+
+		JPanel controls = new JPanel(new FlowLayout(FlowLayout.CENTER, Utils.MARGIN, 0));
+		controls.add(refreshFeeds);
+		controls.add(subscribeFeeds);
+		controls.add(viewCommit);
+		controls.add(viewCommitDiff);
+		controls.add(viewTree);
+
+		NameRenderer nameRenderer = new NameRenderer();
+		tableModel = new SyndicatedEntryTableModel();
+		header = new HeaderPanel(Translation.get("gb.recentActivity"), "feed_16x16.png");
+		table = Utils.newTable(tableModel, Utils.DATE_FORMAT);
+		String name = table.getColumnName(SyndicatedEntryTableModel.Columns.Author.ordinal());
+		table.setRowHeight(nameRenderer.getFont().getSize() + 8);
+		table.getColumn(name).setCellRenderer(nameRenderer);
+		name = table.getColumnName(SyndicatedEntryTableModel.Columns.Repository.ordinal());
+		table.getColumn(name).setCellRenderer(nameRenderer);
+
+		name = table.getColumnName(SyndicatedEntryTableModel.Columns.Branch.ordinal());
+		table.getColumn(name).setCellRenderer(nameRenderer);
+
+		table.addMouseListener(new MouseAdapter() {
+			public void mouseClicked(MouseEvent e) {
+				if (e.getClickCount() == 2) {
+					if (e.isControlDown()) {
+						viewCommitDiff();
+					} else {
+						viewCommit();
+					}
+				}
+			}
+		});
+
+		table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+			@Override
+			public void valueChanged(ListSelectionEvent e) {
+				if (e.getValueIsAdjusting()) {
+					return;
+				}
+				boolean singleSelection = table.getSelectedRowCount() == 1;
+				viewCommit.setEnabled(singleSelection);
+				viewCommitDiff.setEnabled(singleSelection);
+				viewTree.setEnabled(singleSelection);
+			}
+		});
+
+		setLayout(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+		add(header, BorderLayout.NORTH);
+		add(new JScrollPane(table), BorderLayout.CENTER);
+		add(controls, BorderLayout.SOUTH);
+	}
+
+	@Override
+	public Insets getInsets() {
+		return Utils.INSETS;
+	}
+
+	protected void refreshFeeds() {
+		// TODO change request type here
+		GitblitWorker worker = new GitblitWorker(FeedsPanel.this, RpcRequest.LIST_USERS) {
+			@Override
+			protected Boolean doRequest() throws IOException {
+				gitblit.refreshSubscribedFeeds();
+				return true;
+			}
+
+			@Override
+			protected void onSuccess() {
+				updateTable(false);
+			}
+		};
+		worker.execute();
+	}
+
+	protected abstract void subscribeFeeds(List<FeedModel> feeds);
+
+	protected void updateTable(boolean pack) {
+		tableModel.entries.clear();
+		tableModel.entries.addAll(gitblit.getSyndicatedEntries());
+		tableModel.fireTableDataChanged();
+		header.setText(Translation.get("gb.recentActivity") + " ("
+				+ gitblit.getSyndicatedEntries().size() + ")");
+		if (pack) {
+			Utils.packColumns(table, Utils.MARGIN);
+		}
+	}
+
+	protected SyndicatedEntryModel getSelectedSyndicatedEntry() {
+		int viewRow = table.getSelectedRow();
+		int modelRow = table.convertRowIndexToModel(viewRow);
+		SyndicatedEntryModel entry = tableModel.get(modelRow);
+		return entry;
+	}
+
+	protected void viewCommit() {
+		SyndicatedEntryModel entry = getSelectedSyndicatedEntry();
+		Utils.browse(entry.link);
+	}
+
+	protected void viewCommitDiff() {
+		SyndicatedEntryModel entry = getSelectedSyndicatedEntry();
+		Utils.browse(entry.link.replace("/commit/", "/commitdiff/"));
+	}
+
+	protected void viewTree() {
+		SyndicatedEntryModel entry = getSelectedSyndicatedEntry();
+		Utils.browse(entry.link.replace("/commit/", "/tree/"));
+	}
+}
diff --git a/src/com/gitblit/client/FeedsTableModel.java b/src/com/gitblit/client/FeedsTableModel.java
index a628fc8..0979a4c 100644
--- a/src/com/gitblit/client/FeedsTableModel.java
+++ b/src/com/gitblit/client/FeedsTableModel.java
@@ -36,7 +36,7 @@
 	List<FeedModel> list;
 
 	enum Columns {
-		Subscribed, Repository, Branch, Max_Length;
+		Subscribed, Repository, Branch;
 
 		@Override
 		public String toString() {
@@ -87,8 +87,6 @@
 		switch (col) {
 		case Subscribed:
 			return Boolean.class;
-		case Max_Length:
-			return Integer.class;
 		}
 		return String.class;
 	}
@@ -112,8 +110,6 @@
 			return model.repository;
 		case Branch:
 			return model.branch;
-		case Max_Length:
-			return model.maxRetrieval;
 		case Subscribed:
 			return model.subscribed;
 		}
diff --git a/src/com/gitblit/client/GitblitClient.java b/src/com/gitblit/client/GitblitClient.java
index 74e0e08..a432c13 100644
--- a/src/com/gitblit/client/GitblitClient.java
+++ b/src/com/gitblit/client/GitblitClient.java
@@ -290,7 +290,7 @@
 	public boolean createRepository(RepositoryModel repository, List<String> permittedUsers)
 			throws IOException {
 		boolean success = true;
-		success &= RpcUtils.createRepository(repository, url, account, password);
+		success &= RpcUtils.createRepository(repository, url, account, password);		
 		if (permittedUsers.size() > 0) {
 			// if new repository has named members, set them
 			success &= RpcUtils.setRepositoryMembers(repository, permittedUsers, url, account,
diff --git a/src/com/gitblit/client/GitblitManager.java b/src/com/gitblit/client/GitblitManager.java
index d9c2dcc..f4ca7c5 100644
--- a/src/com/gitblit/client/GitblitManager.java
+++ b/src/com/gitblit/client/GitblitManager.java
@@ -56,12 +56,12 @@
 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.GitBlitException.ForbiddenException;
 import com.gitblit.models.FeedModel;
+import com.gitblit.utils.Base64;
 import com.gitblit.utils.StringUtils;
 
 /**
diff --git a/src/com/gitblit/client/GitblitPanel.java b/src/com/gitblit/client/GitblitPanel.java
index 64e85ea..6c65328 100644
--- a/src/com/gitblit/client/GitblitPanel.java
+++ b/src/com/gitblit/client/GitblitPanel.java
@@ -16,52 +16,18 @@
 package com.gitblit.client;
 
 import java.awt.BorderLayout;
-import java.awt.Color;
 import java.awt.Component;
-import java.awt.Desktop;
-import java.awt.Dimension;
-import java.awt.FlowLayout;
-import java.awt.GridLayout;
 import java.awt.Insets;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.awt.event.KeyAdapter;
-import java.awt.event.KeyEvent;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
 import java.io.IOException;
-import java.net.URI;
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
-import javax.swing.ImageIcon;
-import javax.swing.JButton;
-import javax.swing.JLabel;
-import javax.swing.JOptionPane;
 import javax.swing.JPanel;
-import javax.swing.JScrollPane;
 import javax.swing.JTabbedPane;
-import javax.swing.JTable;
-import javax.swing.JTextField;
-import javax.swing.RowFilter;
-import javax.swing.SwingConstants;
-import javax.swing.event.ListSelectionEvent;
-import javax.swing.event.ListSelectionListener;
-import javax.swing.table.DefaultTableCellRenderer;
-import javax.swing.table.TableCellRenderer;
-import javax.swing.table.TableRowSorter;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
 
-import com.gitblit.Constants.RpcRequest;
 import com.gitblit.client.ClosableTabComponent.CloseTabListener;
 import com.gitblit.models.FeedModel;
-import com.gitblit.models.RepositoryModel;
-import com.gitblit.models.SettingModel;
-import com.gitblit.models.SyndicatedEntryModel;
-import com.gitblit.models.UserModel;
-import com.gitblit.utils.StringUtils;
 
 /**
  * GitblitPanel performs the login, all business logic, and contains all widgets
@@ -74,53 +40,21 @@
 
 	private static final long serialVersionUID = 1L;
 
-	private final int margin = 5;
-
-	private final Insets insets = new Insets(margin, margin, margin, margin);
-
 	private final RegistrationsDialog.RegistrationListener listener;
 
 	private GitblitClient gitblit;
 
 	private JTabbedPane tabs;
 
-	private JTable repositoriesTable;
+	private RepositoriesPanel repositoriesPanel;
 
-	private RepositoriesTableModel repositoriesModel;
+	private FeedsPanel feedsPanel;
 
-	private JTable usersTable;
+	private UsersPanel usersPanel;
 
-	private UsersTableModel usersModel;
-
-	private JTable settingsTable;
-
-	private SettingsTableModel settingsModel;
-
-	private JButton createRepository;
-
-	private JButton delRepository;
-
-	private TableRowSorter<RepositoriesTableModel> defaultRepositoriesSorter;
-
-	private TableRowSorter<UsersTableModel> defaultUsersSorter;
-
-	private TableRowSorter<SettingsTableModel> defaultSettingsSorter;
-
-	private JButton editRepository;
-
-	private HeaderPanel repositoriesHeader;
-
-	private HeaderPanel usersHeader;
-
-	private HeaderPanel settingsHeader;
+	private SettingsPanel settingsPanel;
 
 	private StatusPanel statusPanel;
-
-	private SyndicatedEntryTableModel syndicationModel;
-
-	private HeaderPanel feedsHeader;
-
-	private JTable syndicationEntriesTable;
 
 	public GitblitPanel(GitblitRegistration reg, RegistrationsDialog.RegistrationListener listener) {
 		this.gitblit = new GitblitClient(reg);
@@ -132,510 +66,74 @@
 		tabs.addTab(Translation.get("gb.users"), createUsersPanel());
 		tabs.addTab(Translation.get("gb.settings"), createSettingsPanel());
 		tabs.addTab(Translation.get("gb.status"), createStatusPanel());
+		tabs.addChangeListener(new ChangeListener() {
+			public void stateChanged(ChangeEvent e) {
+				tabs.getSelectedComponent().requestFocus();				
+			}
+		});		
 
 		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() {
-			public void actionPerformed(ActionEvent e) {
-				RepositoryModel model = getSelectedRepositories().get(0);
-				String u = MessageFormat.format("{0}/summary/{1}", gitblit.url,
-						StringUtils.encodeURL(model.name));
-				try {
-					Desktop.getDesktop().browse(new URI(u));
-				} catch (Exception x) {
-					x.printStackTrace();
-				}
-			}
-		});
-
-		JButton refreshRepositories = new JButton(Translation.get("gb.refresh"));
-		refreshRepositories.addActionListener(new ActionListener() {
-			public void actionPerformed(ActionEvent e) {
-				refreshRepositories();
-			}
-		});
-
-		createRepository = new JButton(Translation.get("gb.create"));
-		createRepository.addActionListener(new ActionListener() {
-			public void actionPerformed(ActionEvent e) {
-				createRepository();
-			}
-		});
-
-		editRepository = new JButton(Translation.get("gb.edit"));
-		editRepository.setEnabled(false);
-		editRepository.addActionListener(new ActionListener() {
-			public void actionPerformed(ActionEvent e) {
-				editRepository(getSelectedRepositories().get(0));
-			}
-		});
-
-		delRepository = new JButton(Translation.get("gb.delete"));
-		delRepository.setEnabled(false);
-		delRepository.addActionListener(new ActionListener() {
-			public void actionPerformed(ActionEvent e) {
-				deleteRepositories(getSelectedRepositories());
-			}
-		});
-
-		final JButton subscribeRepository = new JButton(Translation.get("gb.subscribe") + "...");
-		subscribeRepository.setEnabled(false);
-		subscribeRepository.addActionListener(new ActionListener() {
-			public void actionPerformed(ActionEvent e) {
-				List<FeedModel> feeds = gitblit.getAvailableFeeds(getSelectedRepositories().get(0));
-				subscribeFeeds(feeds);
-			}
-		});
-
-		SubscribedRepositoryRenderer nameRenderer = new SubscribedRepositoryRenderer(gitblit);
-		IndicatorsRenderer typeRenderer = new IndicatorsRenderer();
-
-		DefaultTableCellRenderer sizeRenderer = new DefaultTableCellRenderer();
-		sizeRenderer.setHorizontalAlignment(SwingConstants.RIGHT);
-		sizeRenderer.setForeground(new Color(0, 0x80, 0));
-
-		DefaultTableCellRenderer ownerRenderer = new DefaultTableCellRenderer();
-		ownerRenderer.setForeground(Color.gray);
-		ownerRenderer.setHorizontalAlignment(SwingConstants.CENTER);
-
-		repositoriesModel = new RepositoriesTableModel();
-		defaultRepositoriesSorter = new TableRowSorter<RepositoriesTableModel>(repositoriesModel);
-		repositoriesTable = Utils.newTable(repositoriesModel, Utils.DATE_FORMAT);
-		repositoriesTable.setRowHeight(nameRenderer.getFont().getSize() + 8);
-		repositoriesTable.setRowSorter(defaultRepositoriesSorter);
-		repositoriesTable.getRowSorter().toggleSortOrder(
-				RepositoriesTableModel.Columns.Name.ordinal());
-
-		setRepositoryRenderer(RepositoriesTableModel.Columns.Name, nameRenderer, -1);
-		setRepositoryRenderer(RepositoriesTableModel.Columns.Indicators, typeRenderer, 100);
-		setRepositoryRenderer(RepositoriesTableModel.Columns.Owner, ownerRenderer, -1);
-		setRepositoryRenderer(RepositoriesTableModel.Columns.Size, sizeRenderer, 60);
-
-		repositoriesTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
-			@Override
-			public void valueChanged(ListSelectionEvent e) {
-				if (e.getValueIsAdjusting()) {
-					return;
-				}
-				boolean singleSelection = repositoriesTable.getSelectedRowCount() == 1;
-				boolean selected = repositoriesTable.getSelectedRow() > -1;
-				browseRepository.setEnabled(singleSelection);
-				delRepository.setEnabled(selected);
-				subscribeRepository.setEnabled(singleSelection);
-				if (selected) {
-					int viewRow = repositoriesTable.getSelectedRow();
-					int modelRow = repositoriesTable.convertRowIndexToModel(viewRow);
-					RepositoryModel model = ((RepositoriesTableModel) repositoriesTable.getModel()).list
-							.get(modelRow);
-					editRepository.setEnabled(singleSelection
-							&& (gitblit.allowManagement() || gitblit.isOwner(model)));
-				} else {
-					editRepository.setEnabled(false);
-				}
-			}
-		});
-
-		repositoriesTable.addMouseListener(new MouseAdapter() {
-			public void mouseClicked(MouseEvent e) {
-				if (e.getClickCount() == 2 && gitblit.allowManagement()) {
-					editRepository(getSelectedRepositories().get(0));
-				}
-			}
-		});
-
-		final JTextField repositoryFilter = new JTextField();
-		repositoryFilter.addActionListener(new ActionListener() {
-			public void actionPerformed(ActionEvent e) {
-				filterRepositories(repositoryFilter.getText());
-			}
-		});
-		repositoryFilter.addKeyListener(new KeyAdapter() {
-			public void keyReleased(KeyEvent e) {
-				filterRepositories(repositoryFilter.getText());
-			}
-		});
-
-		JPanel repositoryFilterPanel = new JPanel(new BorderLayout(margin, margin));
-		repositoryFilterPanel.add(new JLabel(Translation.get("gb.filter")), BorderLayout.WEST);
-		repositoryFilterPanel.add(repositoryFilter, BorderLayout.CENTER);
-
-		JPanel repositoryTablePanel = new JPanel(new BorderLayout(margin, margin));
-		repositoryTablePanel.add(repositoryFilterPanel, BorderLayout.NORTH);
-		repositoryTablePanel.add(new JScrollPane(repositoriesTable), BorderLayout.CENTER);
-
-		JPanel repositoryControls = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0));
-		repositoryControls.add(refreshRepositories);
-		repositoryControls.add(browseRepository);
-		repositoryControls.add(createRepository);
-		repositoryControls.add(editRepository);
-		repositoryControls.add(delRepository);
-		repositoryControls.add(subscribeRepository);
-
-		JPanel repositoriesPanel = new JPanel(new BorderLayout(margin, margin)) {
+		repositoriesPanel = new RepositoriesPanel(gitblit) {
 
 			private static final long serialVersionUID = 1L;
 
-			public Insets getInsets() {
-				return insets;
+			@Override
+			protected void subscribeFeeds(List<FeedModel> feeds) {
+				GitblitPanel.this.subscribeFeeds(feeds);
 			}
-		};
-		repositoriesHeader = new HeaderPanel(Translation.get("gb.repositories"),
-				"gitweb-favicon.png");
-		repositoriesPanel.add(repositoriesHeader, BorderLayout.NORTH);
-		repositoriesPanel.add(repositoryTablePanel, BorderLayout.CENTER);
-		repositoriesPanel.add(repositoryControls, BorderLayout.SOUTH);
 
+			@Override
+			protected void updateUsersTable() {
+				usersPanel.updateTable(false);
+			}
+
+		};
 		return repositoriesPanel;
 	}
 
-	private void setRepositoryRenderer(RepositoriesTableModel.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 createFeedsPanel() {
-		JButton refreshFeeds = new JButton(Translation.get("gb.refresh"));
-		refreshFeeds.addActionListener(new ActionListener() {
-			public void actionPerformed(ActionEvent e) {
-				refreshFeeds();
-			}
-		});
-
-		final JButton viewCommit = new JButton(Translation.get("gb.view"));
-		viewCommit.setEnabled(false);
-		viewCommit.addActionListener(new ActionListener() {
-			public void actionPerformed(ActionEvent e) {
-				viewCommit();
-			}
-		});
-
-		final JButton viewCommitDiff = new JButton(Translation.get("gb.commitdiff"));
-		viewCommitDiff.setEnabled(false);
-		viewCommitDiff.addActionListener(new ActionListener() {
-			public void actionPerformed(ActionEvent e) {
-				viewCommitDiff();
-			}
-		});
-
-		final JButton viewTree = new JButton(Translation.get("gb.tree"));
-		viewTree.setEnabled(false);
-		viewTree.addActionListener(new ActionListener() {
-			public void actionPerformed(ActionEvent e) {
-				viewTree();
-			}
-		});
-
-		JButton subscribeFeeds = new JButton(Translation.get("gb.subscribe") + "...");
-		subscribeFeeds.addActionListener(new ActionListener() {
-			public void actionPerformed(ActionEvent e) {
-				subscribeFeeds(gitblit.getAvailableFeeds());
-			}
-		});
-
-		JPanel controls = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0));
-		controls.add(refreshFeeds);
-		controls.add(subscribeFeeds);
-		controls.add(viewCommit);
-		controls.add(viewCommitDiff);
-		controls.add(viewTree);
-
-		NameRenderer nameRenderer = new NameRenderer();
-		syndicationModel = new SyndicatedEntryTableModel();
-		feedsHeader = new HeaderPanel(Translation.get("gb.recentActivity"), "feed_16x16.png");
-		syndicationEntriesTable = Utils.newTable(syndicationModel, Utils.DATE_FORMAT);
-		String name = syndicationEntriesTable
-				.getColumnName(SyndicatedEntryTableModel.Columns.Author.ordinal());
-		syndicationEntriesTable.setRowHeight(nameRenderer.getFont().getSize() + 8);
-		syndicationEntriesTable.getColumn(name).setCellRenderer(nameRenderer);
-		name = syndicationEntriesTable.getColumnName(SyndicatedEntryTableModel.Columns.Repository
-				.ordinal());
-		syndicationEntriesTable.getColumn(name).setCellRenderer(nameRenderer);
-
-		name = syndicationEntriesTable.getColumnName(SyndicatedEntryTableModel.Columns.Branch
-				.ordinal());
-		syndicationEntriesTable.getColumn(name).setCellRenderer(nameRenderer);
-
-		syndicationEntriesTable.addMouseListener(new MouseAdapter() {
-			public void mouseClicked(MouseEvent e) {
-				if (e.getClickCount() == 2) {
-					if (e.isControlDown()) {
-						viewCommitDiff();
-					} else {
-						viewCommit();
-					}
-				}
-			}
-		});
-
-		syndicationEntriesTable.getSelectionModel().addListSelectionListener(
-				new ListSelectionListener() {
-					@Override
-					public void valueChanged(ListSelectionEvent e) {
-						if (e.getValueIsAdjusting()) {
-							return;
-						}
-						boolean singleSelection = syndicationEntriesTable.getSelectedRowCount() == 1;
-						viewCommit.setEnabled(singleSelection);
-						viewCommitDiff.setEnabled(singleSelection);
-						viewTree.setEnabled(singleSelection);
-					}
-				});
-
-		JPanel panel = new JPanel(new BorderLayout(5, 5)) {
-
+		feedsPanel = new FeedsPanel(gitblit) {
 			private static final long serialVersionUID = 1L;
 
 			@Override
-			public Insets getInsets() {
-				return insets;
+			protected void subscribeFeeds(List<FeedModel> feeds) {
+				GitblitPanel.this.subscribeFeeds(feeds);
 			}
 		};
-		panel.add(feedsHeader, BorderLayout.NORTH);
-		panel.add(new JScrollPane(syndicationEntriesTable), BorderLayout.CENTER);
-		panel.add(controls, BorderLayout.SOUTH);
-		return panel;
+		return feedsPanel;
 	}
 
 	private JPanel createUsersPanel() {
-		JButton refreshUsers = new JButton(Translation.get("gb.refresh"));
-		refreshUsers.addActionListener(new ActionListener() {
-			public void actionPerformed(ActionEvent e) {
-				refreshUsers();
-			}
-		});
-
-		JButton createUser = new JButton(Translation.get("gb.create"));
-		createUser.addActionListener(new ActionListener() {
-			public void actionPerformed(ActionEvent e) {
-				createUser();
-			}
-		});
-
-		final JButton editUser = new JButton(Translation.get("gb.edit"));
-		editUser.setEnabled(false);
-		editUser.addActionListener(new ActionListener() {
-			public void actionPerformed(ActionEvent e) {
-				editUser(getSelectedUsers().get(0));
-			}
-		});
-
-		final JButton delUser = new JButton(Translation.get("gb.delete"));
-		delUser.setEnabled(false);
-		delUser.addActionListener(new ActionListener() {
-			public void actionPerformed(ActionEvent e) {
-				deleteUsers(getSelectedUsers());
-			}
-		});
-
-		NameRenderer nameRenderer = new NameRenderer();
-		usersModel = new UsersTableModel();
-		defaultUsersSorter = new TableRowSorter<UsersTableModel>(usersModel);
-		usersTable = Utils.newTable(usersModel, Utils.DATE_FORMAT);
-		String name = usersTable.getColumnName(UsersTableModel.Columns.Name.ordinal());
-		usersTable.setRowHeight(nameRenderer.getFont().getSize() + 8);
-		usersTable.getColumn(name).setCellRenderer(nameRenderer);
-		usersTable.setRowSorter(defaultUsersSorter);
-		usersTable.getRowSorter().toggleSortOrder(UsersTableModel.Columns.Name.ordinal());
-		usersTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
-
-			@Override
-			public void valueChanged(ListSelectionEvent e) {
-				if (e.getValueIsAdjusting()) {
-					return;
-				}
-				boolean selected = usersTable.getSelectedRow() > -1;
-				boolean singleSelection = usersTable.getSelectedRows().length == 1;
-				editUser.setEnabled(singleSelection && selected);
-				delUser.setEnabled(selected);
-			}
-		});
-
-		usersTable.addMouseListener(new MouseAdapter() {
-			public void mouseClicked(MouseEvent e) {
-				if (e.getClickCount() == 2) {
-					editUser(getSelectedUsers().get(0));
-				}
-			}
-		});
-
-		final JTextField userFilter = new JTextField();
-		userFilter.addActionListener(new ActionListener() {
-			public void actionPerformed(ActionEvent e) {
-				filterUsers(userFilter.getText());
-			}
-		});
-		userFilter.addKeyListener(new KeyAdapter() {
-			public void keyReleased(KeyEvent e) {
-				filterUsers(userFilter.getText());
-			}
-		});
-
-		JPanel userFilterPanel = new JPanel(new BorderLayout(margin, margin));
-		userFilterPanel.add(new JLabel(Translation.get("gb.filter")), BorderLayout.WEST);
-		userFilterPanel.add(userFilter, BorderLayout.CENTER);
-
-		JPanel userTablePanel = new JPanel(new BorderLayout(margin, margin));
-		userTablePanel.add(userFilterPanel, BorderLayout.NORTH);
-		userTablePanel.add(new JScrollPane(usersTable), BorderLayout.CENTER);
-
-		JPanel userControls = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0));
-		userControls.add(refreshUsers);
-		userControls.add(createUser);
-		userControls.add(editUser);
-		userControls.add(delUser);
-
-		JPanel usersPanel = new JPanel(new BorderLayout(margin, margin)) {
-
-			private static final long serialVersionUID = 1L;
-
-			public Insets getInsets() {
-				return insets;
-			}
-		};
-		usersHeader = new HeaderPanel(Translation.get("gb.users"), "user_16x16.png");
-		usersPanel.add(usersHeader, BorderLayout.NORTH);
-		usersPanel.add(userTablePanel, BorderLayout.CENTER);
-		usersPanel.add(userControls, BorderLayout.SOUTH);
-
+		usersPanel = new UsersPanel(gitblit);
 		return usersPanel;
 	}
 
 	private JPanel createSettingsPanel() {
-		JButton refreshSettings = new JButton(Translation.get("gb.refresh"));
-		refreshSettings.addActionListener(new ActionListener() {
-			public void actionPerformed(ActionEvent e) {
-				refreshSettings();
-			}
-		});
-
-		final JButton editSetting = new JButton(Translation.get("gb.edit"));
-		editSetting.addActionListener(new ActionListener() {
-			public void actionPerformed(ActionEvent e) {
-				int viewRow = settingsTable.getSelectedRow();
-				int modelRow = settingsTable.convertRowIndexToModel(viewRow);
-				String key = settingsModel.keys.get(modelRow);
-				SettingModel setting = settingsModel.settings.get(key);
-				editSetting(setting);
-			}
-		});
-
-		NameRenderer nameRenderer = new NameRenderer();
-		final SettingPanel settingPanel = new SettingPanel();
-		settingsModel = new SettingsTableModel();
-		defaultSettingsSorter = new TableRowSorter<SettingsTableModel>(settingsModel);
-		settingsTable = Utils.newTable(settingsModel, Utils.DATE_FORMAT);
-		settingsTable.setDefaultRenderer(SettingModel.class, new SettingCellRenderer());
-		String name = settingsTable.getColumnName(UsersTableModel.Columns.Name.ordinal());
-		settingsTable.setRowHeight(nameRenderer.getFont().getSize() + 8);
-		settingsTable.getColumn(name).setCellRenderer(nameRenderer);
-		settingsTable.setRowSorter(defaultSettingsSorter);
-		settingsTable.getRowSorter().toggleSortOrder(SettingsTableModel.Columns.Name.ordinal());
-		settingsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
-
-			@Override
-			public void valueChanged(ListSelectionEvent e) {
-				if (e.getValueIsAdjusting()) {
-					return;
-				}
-				boolean singleSelection = settingsTable.getSelectedRows().length == 1;
-				editSetting.setEnabled(singleSelection);
-				if (singleSelection) {
-					int viewRow = settingsTable.getSelectedRow();
-					int modelRow = settingsTable.convertRowIndexToModel(viewRow);
-					SettingModel setting = settingsModel.get(modelRow);
-					settingPanel.setSetting(setting);
-				} else {
-					settingPanel.clear();
-				}
-			}
-		});
-
-		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);
-		settingsTablePanel.add(settingPanel, BorderLayout.SOUTH);
-
-		JPanel settingsControls = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0));
-		settingsControls.add(refreshSettings);
-		settingsControls.add(editSetting);
-
-		JPanel settingsPanel = new JPanel(new BorderLayout(margin, margin)) {
-
-			private static final long serialVersionUID = 1L;
-
-			public Insets getInsets() {
-				return insets;
-			}
-		};
-		settingsHeader = new HeaderPanel(Translation.get("gb.settings"), "settings_16x16.png");
-		settingsPanel.add(settingsHeader, BorderLayout.NORTH);
-		settingsPanel.add(settingsTablePanel, BorderLayout.CENTER);
-		settingsPanel.add(settingsControls, BorderLayout.SOUTH);
-
+		settingsPanel = new SettingsPanel(gitblit);
 		return settingsPanel;
 	}
 
 	private JPanel createStatusPanel() {
-		JButton refreshStatus = new JButton(Translation.get("gb.refresh"));
-		refreshStatus.addActionListener(new ActionListener() {
-			public void actionPerformed(ActionEvent e) {
-				refreshStatus();
-			}
-		});
-
-		JPanel controls = new JPanel();
-		controls.add(refreshStatus);
-
-		JPanel panel = new JPanel(new BorderLayout());
-		statusPanel = new StatusPanel();
-		panel.add(statusPanel, BorderLayout.CENTER);
-		panel.add(controls, BorderLayout.SOUTH);
-		return panel;
+		statusPanel = new StatusPanel(gitblit);
+		return statusPanel;
 	}
 
 	public void login() throws IOException {
 		gitblit.login();
 
-		updateRepositoriesTable();
-		Utils.packColumns(repositoriesTable, 5);
-
-		updateFeedsTable();
-		Utils.packColumns(syndicationEntriesTable, 5);
+		repositoriesPanel.updateTable(true);
+		feedsPanel.updateTable(true);
 
 		if (gitblit.allowManagement()) {
-			updateUsersTable();
+			usersPanel.updateTable(false);
 		} else {
 			// user does not have administrator privileges
 			// hide admin repository buttons
-			createRepository.setVisible(false);
-			editRepository.setVisible(false);
-			delRepository.setVisible(false);
+			repositoriesPanel.disableManagement();
 
 			while (tabs.getTabCount() > 2) {
 				// remove all management/administration tabs
@@ -644,9 +142,8 @@
 		}
 
 		if (gitblit.allowAdministration()) {
-			updateSettingsTable();
-			updateStatusPanel();
-			Utils.packColumns(settingsTable, 5);
+			settingsPanel.updateTable(true);
+			statusPanel.updateTable(false);
 		} else {
 			// remove the settings tab
 			String[] titles = { Translation.get("gb.settings"), Translation.get("gb.status") };
@@ -661,281 +158,14 @@
 		}
 	}
 
-	private void updateRepositoriesTable() {
-		repositoriesModel.list.clear();
-		repositoriesModel.list.addAll(gitblit.getRepositories());
-		repositoriesModel.fireTableDataChanged();
-		repositoriesHeader.setText(Translation.get("gb.repositories") + " ("
-				+ gitblit.getRepositories().size() + ")");
-	}
-
-	private void updateFeedsTable() {
-		syndicationModel.entries.clear();
-		syndicationModel.entries.addAll(gitblit.getSyndicatedEntries());
-		syndicationModel.fireTableDataChanged();
-		feedsHeader.setText(Translation.get("gb.recentActivity") + " ("
-				+ gitblit.getSyndicatedEntries().size() + ")");
-	}
-
-	private void updateUsersTable() {
-		usersModel.list.clear();
-		usersModel.list.addAll(gitblit.getUsers());
-		usersModel.fireTableDataChanged();
-		usersHeader.setText(Translation.get("gb.users") + " (" + gitblit.getUsers().size() + ")");
-	}
-
-	private void updateSettingsTable() {
-		settingsModel.setSettings(gitblit.getSettings());
-		settingsModel.fireTableDataChanged();
-		settingsHeader.setText(Translation.get("gb.settings"));
-	}
-
-	private void updateStatusPanel() {
-		statusPanel.setStatus(gitblit.url, gitblit.getStatus());
-	}
-
-	private void filterRepositories(final String fragment) {
-		if (StringUtils.isEmpty(fragment)) {
-			repositoriesTable.setRowSorter(defaultRepositoriesSorter);
-			return;
-		}
-		RowFilter<RepositoriesTableModel, Object> containsFilter = new RowFilter<RepositoriesTableModel, Object>() {
-			public boolean include(Entry<? extends RepositoriesTableModel, ? 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<RepositoriesTableModel> sorter = new TableRowSorter<RepositoriesTableModel>(
-				repositoriesModel);
-		sorter.setRowFilter(containsFilter);
-		repositoriesTable.setRowSorter(sorter);
-	}
-
-	private void filterUsers(final String fragment) {
-		if (StringUtils.isEmpty(fragment)) {
-			usersTable.setRowSorter(defaultUsersSorter);
-			return;
-		}
-		RowFilter<UsersTableModel, Object> containsFilter = new RowFilter<UsersTableModel, Object>() {
-			public boolean include(Entry<? extends UsersTableModel, ? 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<UsersTableModel> sorter = new TableRowSorter<UsersTableModel>(usersModel);
-		sorter.setRowFilter(containsFilter);
-		usersTable.setRowSorter(sorter);
-	}
-
-	private void filterSettings(final String fragment) {
-		if (StringUtils.isEmpty(fragment)) {
-			settingsTable.setRowSorter(defaultSettingsSorter);
-			return;
-		}
-		RowFilter<SettingsTableModel, Object> containsFilter = new RowFilter<SettingsTableModel, Object>() {
-			public boolean include(Entry<? extends SettingsTableModel, ? 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<SettingsTableModel> sorter = new TableRowSorter<SettingsTableModel>(
-				settingsModel);
-		sorter.setRowFilter(containsFilter);
-		settingsTable.setRowSorter(sorter);
-	}
-
-	private List<RepositoryModel> getSelectedRepositories() {
-		List<RepositoryModel> repositories = new ArrayList<RepositoryModel>();
-		for (int viewRow : repositoriesTable.getSelectedRows()) {
-			int modelRow = repositoriesTable.convertRowIndexToModel(viewRow);
-			RepositoryModel model = repositoriesModel.list.get(modelRow);
-			repositories.add(model);
-		}
-		return repositories;
-	}
-
-	private List<UserModel> getSelectedUsers() {
-		List<UserModel> users = new ArrayList<UserModel>();
-		for (int viewRow : usersTable.getSelectedRows()) {
-			int modelRow = usersTable.convertRowIndexToModel(viewRow);
-			UserModel model = usersModel.list.get(modelRow);
-			users.add(model);
-		}
-		return users;
-	}
-
 	@Override
 	public Insets getInsets() {
-		return insets;
+		return Utils.INSETS;
 	}
 
 	@Override
 	public void closeTab(Component c) {
 		gitblit = null;
-	}
-
-	protected void refreshRepositories() {
-		GitblitWorker worker = new GitblitWorker(GitblitPanel.this, RpcRequest.LIST_REPOSITORIES) {
-			@Override
-			protected Boolean doRequest() throws IOException {
-				gitblit.refreshRepositories();
-				return true;
-			}
-
-			@Override
-			protected void onSuccess() {
-				updateRepositoriesTable();
-			}
-		};
-		worker.execute();
-	}
-
-	/**
-	 * Displays the create repository dialog and fires a SwingWorker to update
-	 * the server, if appropriate.
-	 * 
-	 */
-	protected void createRepository() {
-		EditRepositoryDialog dialog = new EditRepositoryDialog();
-		dialog.setLocationRelativeTo(GitblitPanel.this);
-		dialog.setUsers(null, gitblit.getUsernames(), null);
-		dialog.setRepositories(gitblit.getRepositories());
-		dialog.setFederationSets(gitblit.getFederationSets(), null);
-		dialog.setVisible(true);
-		final RepositoryModel newRepository = dialog.getRepository();
-		final List<String> permittedUsers = dialog.getPermittedUsers();
-		if (newRepository == null) {
-			return;
-		}
-
-		GitblitWorker worker = new GitblitWorker(this, RpcRequest.CREATE_REPOSITORY) {
-
-			@Override
-			protected Boolean doRequest() throws IOException {
-				boolean success = gitblit.createRepository(newRepository, permittedUsers);
-				if (success) {
-					gitblit.refreshRepositories();
-					if (permittedUsers.size() > 0) {
-						gitblit.refreshUsers();
-					}
-				}
-				return success;
-			}
-
-			@Override
-			protected void onSuccess() {
-				updateRepositoriesTable();
-				updateUsersTable();
-			}
-
-			@Override
-			protected void onFailure() {
-				showFailure("Failed to execute request \"{0}\" for repository \"{1}\".",
-						getRequestType(), newRepository.name);
-			}
-		};
-		worker.execute();
-	}
-
-	/**
-	 * Displays the edit repository dialog and fires a SwingWorker to update the
-	 * server, if appropriate.
-	 * 
-	 * @param repository
-	 */
-	protected void editRepository(final RepositoryModel repository) {
-		EditRepositoryDialog dialog = new EditRepositoryDialog(repository);
-		dialog.setLocationRelativeTo(GitblitPanel.this);
-		List<String> usernames = gitblit.getUsernames();
-		List<String> members = gitblit.getPermittedUsernames(repository);
-		dialog.setUsers(repository.owner, usernames, members);
-		dialog.setRepositories(gitblit.getRepositories());
-		dialog.setFederationSets(gitblit.getFederationSets(), repository.federationSets);
-		dialog.setVisible(true);
-		final RepositoryModel revisedRepository = dialog.getRepository();
-		final List<String> permittedUsers = dialog.getPermittedUsers();
-		if (revisedRepository == null) {
-			return;
-		}
-
-		GitblitWorker worker = new GitblitWorker(this, RpcRequest.EDIT_REPOSITORY) {
-
-			@Override
-			protected Boolean doRequest() throws IOException {
-				boolean success = gitblit.updateRepository(repository.name, revisedRepository,
-						permittedUsers);
-				if (success) {
-					gitblit.refreshRepositories();
-					gitblit.refreshUsers();
-				}
-				return success;
-			}
-
-			@Override
-			protected void onSuccess() {
-				updateRepositoriesTable();
-				updateUsersTable();
-			}
-
-			@Override
-			protected void onFailure() {
-				showFailure("Failed to execute request \"{0}\" for repository \"{1}\".",
-						getRequestType(), repository.name);
-			}
-		};
-		worker.execute();
-	}
-
-	protected void deleteRepositories(final List<RepositoryModel> repositories) {
-		if (repositories == null || repositories.size() == 0) {
-			return;
-		}
-		StringBuilder message = new StringBuilder("Delete the following repositories?\n\n");
-		for (RepositoryModel repository : repositories) {
-			message.append(repository.name).append("\n");
-		}
-		int result = JOptionPane.showConfirmDialog(GitblitPanel.this, message.toString(),
-				"Delete Repositories?", JOptionPane.YES_NO_OPTION);
-		if (result == JOptionPane.YES_OPTION) {
-			GitblitWorker worker = new GitblitWorker(this, RpcRequest.DELETE_REPOSITORY) {
-				@Override
-				protected Boolean doRequest() throws IOException {
-					boolean success = true;
-					for (RepositoryModel repository : repositories) {
-						success &= gitblit.deleteRepository(repository);
-					}
-					if (success) {
-						gitblit.refreshRepositories();
-						gitblit.refreshUsers();
-					}
-					return success;
-				}
-
-				@Override
-				protected void onSuccess() {
-					updateRepositoriesTable();
-					updateUsersTable();
-				}
-
-				@Override
-				protected void onFailure() {
-					showFailure("Failed to delete specified repositories!");
-				}
-			};
-			worker.execute();
-		}
 	}
 
 	protected void subscribeFeeds(final List<FeedModel> feeds) {
@@ -948,274 +178,10 @@
 				gitblit.updateSubscribedFeeds(feeds);
 				listener.saveRegistration(gitblit.reg.name, gitblit.reg);
 				setVisible(false);
-				updateRepositoriesTable();
+				repositoriesPanel.updateTable(false);
 			}
 		};
 		dialog.setLocationRelativeTo(GitblitPanel.this);
 		dialog.setVisible(true);
-	}
-
-	protected void refreshFeeds() {
-		// TODO change request type here
-		GitblitWorker worker = new GitblitWorker(GitblitPanel.this, RpcRequest.LIST_USERS) {
-			@Override
-			protected Boolean doRequest() throws IOException {
-				gitblit.refreshSubscribedFeeds();
-				return true;
-			}
-
-			@Override
-			protected void onSuccess() {
-				updateFeedsTable();
-			}
-		};
-		worker.execute();
-	}
-
-	protected SyndicatedEntryModel getSelectedSyndicatedEntry() {
-		int viewRow = syndicationEntriesTable.getSelectedRow();
-		int modelRow = syndicationEntriesTable.convertRowIndexToModel(viewRow);
-		SyndicatedEntryModel entry = syndicationModel.get(modelRow);
-		return entry;
-	}
-
-	protected void viewCommit() {
-		SyndicatedEntryModel entry = getSelectedSyndicatedEntry();
-		browse(entry.link);
-	}
-
-	protected void viewCommitDiff() {
-		SyndicatedEntryModel entry = getSelectedSyndicatedEntry();
-		browse(entry.link.replace("/commit/", "/commitdiff/"));
-	}
-
-	protected void viewTree() {
-		SyndicatedEntryModel entry = getSelectedSyndicatedEntry();
-		browse(entry.link.replace("/commit/", "/tree/"));
-	}
-
-	protected void browse(String url) {
-		try {
-			Desktop.getDesktop().browse(new URI(url));
-		} catch (Exception x) {
-			x.printStackTrace();
-		}
-	}
-
-	protected void refreshUsers() {
-		GitblitWorker worker = new GitblitWorker(GitblitPanel.this, RpcRequest.LIST_USERS) {
-			@Override
-			protected Boolean doRequest() throws IOException {
-				gitblit.refreshUsers();
-				return true;
-			}
-
-			@Override
-			protected void onSuccess() {
-				updateUsersTable();
-			}
-		};
-		worker.execute();
-	}
-
-	/**
-	 * Displays the create user dialog and fires a SwingWorker to update the
-	 * server, if appropriate.
-	 * 
-	 */
-	protected void createUser() {
-		EditUserDialog dialog = new EditUserDialog(gitblit.getSettings());
-		dialog.setLocationRelativeTo(GitblitPanel.this);
-		dialog.setUsers(gitblit.getUsers());
-		dialog.setRepositories(gitblit.getRepositories(), null);
-		dialog.setVisible(true);
-		final UserModel newUser = dialog.getUser();
-		if (newUser == null) {
-			return;
-		}
-
-		GitblitWorker worker = new GitblitWorker(this, RpcRequest.CREATE_USER) {
-
-			@Override
-			protected Boolean doRequest() throws IOException {
-				boolean success = gitblit.createUser(newUser);
-				if (success) {
-					gitblit.refreshUsers();
-				}
-				return success;
-			}
-
-			@Override
-			protected void onSuccess() {
-				updateUsersTable();
-			}
-
-			@Override
-			protected void onFailure() {
-				showFailure("Failed to execute request \"{0}\" for user \"{1}\".",
-						getRequestType(), newUser.username);
-			}
-		};
-		worker.execute();
-	}
-
-	/**
-	 * Displays the edit user dialog and fires a SwingWorker to update the
-	 * server, if appropriate.
-	 * 
-	 * @param user
-	 */
-	protected void editUser(final UserModel user) {
-		EditUserDialog dialog = new EditUserDialog(user, gitblit.getSettings());
-		dialog.setLocationRelativeTo(GitblitPanel.this);
-		dialog.setUsers(gitblit.getUsers());
-		dialog.setRepositories(gitblit.getRepositories(), new ArrayList<String>(user.repositories));
-		dialog.setVisible(true);
-		final UserModel revisedUser = dialog.getUser();
-		if (revisedUser == null) {
-			return;
-		}
-
-		GitblitWorker worker = new GitblitWorker(this, RpcRequest.EDIT_USER) {
-			@Override
-			protected Boolean doRequest() throws IOException {
-				boolean success = gitblit.updateUser(user.username, revisedUser);
-				if (success) {
-					gitblit.refreshUsers();
-				}
-				return success;
-			}
-
-			@Override
-			protected void onSuccess() {
-				updateUsersTable();
-			}
-
-			@Override
-			protected void onFailure() {
-				showFailure("Failed to execute request \"{0}\" for user \"{1}\".",
-						getRequestType(), user.username);
-			}
-		};
-		worker.execute();
-	}
-
-	protected void deleteUsers(final List<UserModel> users) {
-		if (users == null || users.size() == 0) {
-			return;
-		}
-		StringBuilder message = new StringBuilder("Delete the following users?\n\n");
-		for (UserModel user : users) {
-			message.append(user.username).append("\n");
-		}
-		int result = JOptionPane.showConfirmDialog(GitblitPanel.this, message.toString(),
-				"Delete Users?", JOptionPane.YES_NO_OPTION);
-		if (result == JOptionPane.YES_OPTION) {
-			GitblitWorker worker = new GitblitWorker(this, RpcRequest.DELETE_USER) {
-				@Override
-				protected Boolean doRequest() throws IOException {
-					boolean success = true;
-					for (UserModel user : users) {
-						success &= gitblit.deleteUser(user);
-					}
-					if (success) {
-						gitblit.refreshUsers();
-					}
-					return success;
-				}
-
-				@Override
-				protected void onSuccess() {
-					updateUsersTable();
-				}
-
-				@Override
-				protected void onFailure() {
-					showFailure("Failed to delete specified users!");
-				}
-			};
-			worker.execute();
-		}
-	}
-
-	protected void refreshSettings() {
-		GitblitWorker worker = new GitblitWorker(GitblitPanel.this, RpcRequest.LIST_SETTINGS) {
-			@Override
-			protected Boolean doRequest() throws IOException {
-				gitblit.refreshSettings();
-				return true;
-			}
-
-			@Override
-			protected void onSuccess() {
-				updateSettingsTable();
-			}
-		};
-		worker.execute();
-	}
-
-	protected void refreshStatus() {
-		GitblitWorker worker = new GitblitWorker(GitblitPanel.this, RpcRequest.LIST_STATUS) {
-			@Override
-			protected Boolean doRequest() throws IOException {
-				gitblit.refreshStatus();
-				return true;
-			}
-
-			@Override
-			protected void onSuccess() {
-				updateStatusPanel();
-			}
-		};
-		worker.execute();
-	}
-
-	protected void editSetting(final SettingModel settingModel) {
-		final JTextField textField = new JTextField(settingModel.currentValue);
-		JPanel editPanel = new JPanel(new GridLayout(0, 1));
-		editPanel.add(new JLabel("New Value"));
-		editPanel.add(textField);
-
-		JPanel settingPanel = new JPanel(new BorderLayout());
-		settingPanel.add(new SettingPanel(settingModel), BorderLayout.CENTER);
-		settingPanel.add(editPanel, BorderLayout.SOUTH);
-		settingPanel.setPreferredSize(new Dimension(800, 200));
-
-		String[] options;
-		if (settingModel.currentValue.equals(settingModel.defaultValue)) {
-			options = new String[] { Translation.get("gb.cancel"), Translation.get("gb.save") };
-		} else {
-			options = new String[] { Translation.get("gb.cancel"),
-					Translation.get("gb.setDefault"), Translation.get("gb.save") };
-		}
-		String defaultOption = options[0];
-		int selection = JOptionPane.showOptionDialog(GitblitPanel.this, settingPanel,
-				settingModel.name, JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE,
-				new ImageIcon(getClass().getResource("/settings_16x16.png")), options,
-				defaultOption);
-		if (selection <= 0) {
-			return;
-		}
-		if (options[selection].equals(Translation.get("gb.setDefault"))) {
-			textField.setText(settingModel.defaultValue);
-		}
-		final Map<String, String> newSettings = new HashMap<String, String>();
-		newSettings.put(settingModel.name, textField.getText().trim());
-		GitblitWorker worker = new GitblitWorker(GitblitPanel.this, RpcRequest.EDIT_SETTINGS) {
-			@Override
-			protected Boolean doRequest() throws IOException {
-				boolean success = gitblit.updateSettings(newSettings);
-				if (success) {
-					gitblit.refreshSettings();
-				}
-				return success;
-			}
-
-			@Override
-			protected void onSuccess() {
-				updateSettingsTable();
-			}
-		};
-		worker.execute();
 	}
 }
\ No newline at end of file
diff --git a/src/com/gitblit/client/RepositoriesPanel.java b/src/com/gitblit/client/RepositoriesPanel.java
new file mode 100644
index 0000000..1efc876
--- /dev/null
+++ b/src/com/gitblit/client/RepositoriesPanel.java
@@ -0,0 +1,454 @@
+/*
+ * 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.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.FlowLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.RowFilter;
+import javax.swing.SwingConstants;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.DefaultTableCellRenderer;
+import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableRowSorter;
+
+import com.gitblit.Constants.RpcRequest;
+import com.gitblit.models.FeedModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * RSS Feeds Panel displays recent entries and launches the browser to view the
+ * commit. commitdiff, or tree of a commit.
+ * 
+ * @author James Moger
+ * 
+ */
+public abstract class RepositoriesPanel extends JPanel {
+
+	private static final long serialVersionUID = 1L;
+
+	private final GitblitClient gitblit;
+
+	private HeaderPanel header;
+
+	private JTable table;
+
+	private RepositoriesTableModel tableModel;
+
+	private TableRowSorter<RepositoriesTableModel> defaultSorter;
+
+	private JButton createRepository;
+
+	private JButton editRepository;
+
+	private JButton delRepository;
+
+	private JTextField filterTextfield;
+
+	public RepositoriesPanel(GitblitClient gitblit) {
+		super();
+		this.gitblit = gitblit;
+		initialize();
+	}
+
+	private void initialize() {
+		final JButton browseRepository = new JButton(Translation.get("gb.browse"));
+		browseRepository.setEnabled(false);
+		browseRepository.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				RepositoryModel model = getSelectedRepositories().get(0);
+				String u = MessageFormat.format("{0}/summary/{1}", gitblit.url,
+						StringUtils.encodeURL(model.name));
+				Utils.browse(u);
+			}
+		});
+
+		JButton refreshRepositories = new JButton(Translation.get("gb.refresh"));
+		refreshRepositories.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				refreshRepositories();
+			}
+		});
+
+		createRepository = new JButton(Translation.get("gb.create"));
+		createRepository.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				createRepository();
+			}
+		});
+
+		editRepository = new JButton(Translation.get("gb.edit"));
+		editRepository.setEnabled(false);
+		editRepository.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				editRepository(getSelectedRepositories().get(0));
+			}
+		});
+
+		delRepository = new JButton(Translation.get("gb.delete"));
+		delRepository.setEnabled(false);
+		delRepository.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				deleteRepositories(getSelectedRepositories());
+			}
+		});
+
+		final JButton subscribeRepository = new JButton(Translation.get("gb.subscribe") + "...");
+		subscribeRepository.setEnabled(false);
+		subscribeRepository.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				List<FeedModel> feeds = gitblit.getAvailableFeeds(getSelectedRepositories().get(0));
+				subscribeFeeds(feeds);
+			}
+		});
+
+		SubscribedRepositoryRenderer nameRenderer = new SubscribedRepositoryRenderer(gitblit);
+		IndicatorsRenderer typeRenderer = new IndicatorsRenderer();
+
+		DefaultTableCellRenderer sizeRenderer = new DefaultTableCellRenderer();
+		sizeRenderer.setHorizontalAlignment(SwingConstants.RIGHT);
+		sizeRenderer.setForeground(new Color(0, 0x80, 0));
+
+		DefaultTableCellRenderer ownerRenderer = new DefaultTableCellRenderer();
+		ownerRenderer.setForeground(Color.gray);
+		ownerRenderer.setHorizontalAlignment(SwingConstants.CENTER);
+
+		tableModel = new RepositoriesTableModel();
+		defaultSorter = new TableRowSorter<RepositoriesTableModel>(tableModel);
+		table = Utils.newTable(tableModel, Utils.DATE_FORMAT);
+		table.setRowHeight(nameRenderer.getFont().getSize() + 8);
+		table.setRowSorter(defaultSorter);
+		table.getRowSorter().toggleSortOrder(RepositoriesTableModel.Columns.Name.ordinal());
+
+		setRepositoryRenderer(RepositoriesTableModel.Columns.Name, nameRenderer, -1);
+		setRepositoryRenderer(RepositoriesTableModel.Columns.Indicators, typeRenderer, 100);
+		setRepositoryRenderer(RepositoriesTableModel.Columns.Owner, ownerRenderer, -1);
+		setRepositoryRenderer(RepositoriesTableModel.Columns.Size, sizeRenderer, 60);
+
+		table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+			@Override
+			public void valueChanged(ListSelectionEvent e) {
+				if (e.getValueIsAdjusting()) {
+					return;
+				}
+				boolean singleSelection = table.getSelectedRowCount() == 1;
+				boolean selected = table.getSelectedRow() > -1;
+				browseRepository.setEnabled(singleSelection);
+				delRepository.setEnabled(selected);
+				subscribeRepository.setEnabled(singleSelection);
+				if (selected) {
+					int viewRow = table.getSelectedRow();
+					int modelRow = table.convertRowIndexToModel(viewRow);
+					RepositoryModel model = ((RepositoriesTableModel) table.getModel()).list
+							.get(modelRow);
+					editRepository.setEnabled(singleSelection
+							&& (gitblit.allowManagement() || gitblit.isOwner(model)));
+				} else {
+					editRepository.setEnabled(false);
+				}
+			}
+		});
+
+		table.addMouseListener(new MouseAdapter() {
+			public void mouseClicked(MouseEvent e) {
+				if (e.getClickCount() == 2 && gitblit.allowManagement()) {
+					editRepository(getSelectedRepositories().get(0));
+				}
+			}
+		});
+
+		filterTextfield = new JTextField();
+		filterTextfield.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				filterRepositories(filterTextfield.getText());
+			}
+		});
+		filterTextfield.addKeyListener(new KeyAdapter() {
+			public void keyReleased(KeyEvent e) {
+				filterRepositories(filterTextfield.getText());
+			}
+		});
+
+		JPanel repositoryFilterPanel = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+		repositoryFilterPanel.add(new JLabel(Translation.get("gb.filter")), BorderLayout.WEST);
+		repositoryFilterPanel.add(filterTextfield, BorderLayout.CENTER);
+
+		JPanel repositoryTablePanel = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+		repositoryTablePanel.add(repositoryFilterPanel, BorderLayout.NORTH);
+		repositoryTablePanel.add(new JScrollPane(table), BorderLayout.CENTER);
+
+		JPanel repositoryControls = new JPanel(new FlowLayout(FlowLayout.CENTER, Utils.MARGIN, 0));
+		repositoryControls.add(refreshRepositories);
+		repositoryControls.add(browseRepository);
+		repositoryControls.add(createRepository);
+		repositoryControls.add(editRepository);
+		repositoryControls.add(delRepository);
+		repositoryControls.add(subscribeRepository);
+
+		setLayout(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+		header = new HeaderPanel(Translation.get("gb.repositories"), "gitweb-favicon.png");
+		add(header, BorderLayout.NORTH);
+		add(repositoryTablePanel, BorderLayout.CENTER);
+		add(repositoryControls, BorderLayout.SOUTH);
+	}
+
+	@Override
+	public void requestFocus() {
+		filterTextfield.requestFocus();
+	}
+
+	@Override
+	public Insets getInsets() {
+		return Utils.INSETS;
+	}
+
+	private void setRepositoryRenderer(RepositoriesTableModel.Columns col,
+			TableCellRenderer renderer, int maxWidth) {
+		String name = table.getColumnName(col.ordinal());
+		table.getColumn(name).setCellRenderer(renderer);
+		if (maxWidth > 0) {
+			table.getColumn(name).setMinWidth(maxWidth);
+			table.getColumn(name).setMaxWidth(maxWidth);
+		}
+	}
+
+	protected abstract void subscribeFeeds(List<FeedModel> feeds);
+
+	protected abstract void updateUsersTable();
+
+	protected void disableManagement() {
+		createRepository.setVisible(false);
+		editRepository.setVisible(false);
+		delRepository.setVisible(false);
+	}
+
+	protected void updateTable(boolean pack) {
+		tableModel.list.clear();
+		tableModel.list.addAll(gitblit.getRepositories());
+		tableModel.fireTableDataChanged();
+		header.setText(Translation.get("gb.repositories") + " (" + gitblit.getRepositories().size()
+				+ ")");
+		if (pack) {
+			Utils.packColumns(table, Utils.MARGIN);
+		}
+	}
+
+	private void filterRepositories(final String fragment) {
+		if (StringUtils.isEmpty(fragment)) {
+			table.setRowSorter(defaultSorter);
+			return;
+		}
+		RowFilter<RepositoriesTableModel, Object> containsFilter = new RowFilter<RepositoriesTableModel, Object>() {
+			public boolean include(Entry<? extends RepositoriesTableModel, ? 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<RepositoriesTableModel> sorter = new TableRowSorter<RepositoriesTableModel>(
+				tableModel);
+		sorter.setRowFilter(containsFilter);
+		table.setRowSorter(sorter);
+	}
+
+	private List<RepositoryModel> getSelectedRepositories() {
+		List<RepositoryModel> repositories = new ArrayList<RepositoryModel>();
+		for (int viewRow : table.getSelectedRows()) {
+			int modelRow = table.convertRowIndexToModel(viewRow);
+			RepositoryModel model = tableModel.list.get(modelRow);
+			repositories.add(model);
+		}
+		return repositories;
+	}
+
+	protected void refreshRepositories() {
+		GitblitWorker worker = new GitblitWorker(RepositoriesPanel.this,
+				RpcRequest.LIST_REPOSITORIES) {
+			@Override
+			protected Boolean doRequest() throws IOException {
+				gitblit.refreshRepositories();
+				return true;
+			}
+
+			@Override
+			protected void onSuccess() {
+				updateTable(false);
+			}
+		};
+		worker.execute();
+	}
+
+	/**
+	 * Displays the create repository dialog and fires a SwingWorker to update
+	 * the server, if appropriate.
+	 * 
+	 */
+	protected void createRepository() {
+		EditRepositoryDialog dialog = new EditRepositoryDialog();
+		dialog.setLocationRelativeTo(RepositoriesPanel.this);
+		dialog.setUsers(null, gitblit.getUsernames(), null);
+		dialog.setRepositories(gitblit.getRepositories());
+		dialog.setFederationSets(gitblit.getFederationSets(), null);
+		dialog.setVisible(true);
+		final RepositoryModel newRepository = dialog.getRepository();
+		final List<String> permittedUsers = dialog.getPermittedUsers();
+		if (newRepository == null) {
+			return;
+		}
+
+		GitblitWorker worker = new GitblitWorker(this, RpcRequest.CREATE_REPOSITORY) {
+
+			@Override
+			protected Boolean doRequest() throws IOException {
+				boolean success = gitblit.createRepository(newRepository, permittedUsers);
+				if (success) {
+					gitblit.refreshRepositories();
+					if (permittedUsers.size() > 0) {
+						gitblit.refreshUsers();
+					}
+				}
+				return success;
+			}
+
+			@Override
+			protected void onSuccess() {
+				updateTable(false);
+				updateUsersTable();
+			}
+
+			@Override
+			protected void onFailure() {
+				showFailure("Failed to execute request \"{0}\" for repository \"{1}\".",
+						getRequestType(), newRepository.name);
+			}
+		};
+		worker.execute();
+	}
+
+	/**
+	 * Displays the edit repository dialog and fires a SwingWorker to update the
+	 * server, if appropriate.
+	 * 
+	 * @param repository
+	 */
+	protected void editRepository(final RepositoryModel repository) {
+		EditRepositoryDialog dialog = new EditRepositoryDialog(repository);
+		dialog.setLocationRelativeTo(RepositoriesPanel.this);
+		List<String> usernames = gitblit.getUsernames();
+		List<String> members = gitblit.getPermittedUsernames(repository);
+		dialog.setUsers(repository.owner, usernames, members);
+		dialog.setRepositories(gitblit.getRepositories());
+		dialog.setFederationSets(gitblit.getFederationSets(), repository.federationSets);
+		dialog.setVisible(true);
+		final RepositoryModel revisedRepository = dialog.getRepository();
+		final List<String> permittedUsers = dialog.getPermittedUsers();
+		if (revisedRepository == null) {
+			return;
+		}
+
+		GitblitWorker worker = new GitblitWorker(this, RpcRequest.EDIT_REPOSITORY) {
+
+			@Override
+			protected Boolean doRequest() throws IOException {
+				boolean success = gitblit.updateRepository(repository.name, revisedRepository,
+						permittedUsers);
+				if (success) {
+					gitblit.refreshRepositories();
+					gitblit.refreshUsers();
+				}
+				return success;
+			}
+
+			@Override
+			protected void onSuccess() {
+				updateTable(false);
+				updateUsersTable();
+			}
+
+			@Override
+			protected void onFailure() {
+				showFailure("Failed to execute request \"{0}\" for repository \"{1}\".",
+						getRequestType(), repository.name);
+			}
+		};
+		worker.execute();
+	}
+
+	protected void deleteRepositories(final List<RepositoryModel> repositories) {
+		if (repositories == null || repositories.size() == 0) {
+			return;
+		}
+		StringBuilder message = new StringBuilder("Delete the following repositories?\n\n");
+		for (RepositoryModel repository : repositories) {
+			message.append(repository.name).append("\n");
+		}
+		int result = JOptionPane.showConfirmDialog(RepositoriesPanel.this, message.toString(),
+				"Delete Repositories?", JOptionPane.YES_NO_OPTION);
+		if (result == JOptionPane.YES_OPTION) {
+			GitblitWorker worker = new GitblitWorker(this, RpcRequest.DELETE_REPOSITORY) {
+				@Override
+				protected Boolean doRequest() throws IOException {
+					boolean success = true;
+					for (RepositoryModel repository : repositories) {
+						success &= gitblit.deleteRepository(repository);
+					}
+					if (success) {
+						gitblit.refreshRepositories();
+						gitblit.refreshUsers();
+					}
+					return success;
+				}
+
+				@Override
+				protected void onSuccess() {
+					updateTable(false);
+					updateUsersTable();
+				}
+
+				@Override
+				protected void onFailure() {
+					showFailure("Failed to delete specified repositories!");
+				}
+			};
+			worker.execute();
+		}
+	}
+
+}
diff --git a/src/com/gitblit/client/SettingsPanel.java b/src/com/gitblit/client/SettingsPanel.java
new file mode 100644
index 0000000..9591947
--- /dev/null
+++ b/src/com/gitblit/client/SettingsPanel.java
@@ -0,0 +1,275 @@
+/*
+ * 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.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.RowFilter;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.TableRowSorter;
+
+import com.gitblit.Constants.RpcRequest;
+import com.gitblit.models.SettingModel;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Settings panel displays a list of server settings and their associated
+ * metadata. This panel also allows editing of a setting.
+ * 
+ * @author James Moger
+ * 
+ */
+public class SettingsPanel extends JPanel {
+
+	private static final long serialVersionUID = 1L;
+
+	private final GitblitClient gitblit;
+
+	private HeaderPanel header;
+
+	private JTable table;
+
+	private SettingsTableModel tableModel;
+
+	private TableRowSorter<SettingsTableModel> defaultSorter;
+
+	private JTextField filterTextfield;
+
+	public SettingsPanel(GitblitClient gitblit) {
+		super();
+		this.gitblit = gitblit;
+		initialize();
+	}
+
+	private void initialize() {
+		JButton refreshSettings = new JButton(Translation.get("gb.refresh"));
+		refreshSettings.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				refreshSettings();
+			}
+		});
+
+		final JButton editSetting = new JButton(Translation.get("gb.edit"));
+		editSetting.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				int viewRow = table.getSelectedRow();
+				int modelRow = table.convertRowIndexToModel(viewRow);
+				String key = tableModel.keys.get(modelRow);
+				SettingModel setting = tableModel.settings.get(key);
+				editSetting(setting);
+			}
+		});
+
+		NameRenderer nameRenderer = new NameRenderer();
+		final SettingPanel settingPanel = new SettingPanel();
+		tableModel = new SettingsTableModel();
+		defaultSorter = new TableRowSorter<SettingsTableModel>(tableModel);
+		table = Utils.newTable(tableModel, Utils.DATE_FORMAT);
+		table.setDefaultRenderer(SettingModel.class, new SettingCellRenderer());
+		String name = table.getColumnName(UsersTableModel.Columns.Name.ordinal());
+		table.setRowHeight(nameRenderer.getFont().getSize() + 8);
+		table.getColumn(name).setCellRenderer(nameRenderer);
+		table.setRowSorter(defaultSorter);
+		table.getRowSorter().toggleSortOrder(SettingsTableModel.Columns.Name.ordinal());
+		table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+
+			@Override
+			public void valueChanged(ListSelectionEvent e) {
+				if (e.getValueIsAdjusting()) {
+					return;
+				}
+				boolean singleSelection = table.getSelectedRows().length == 1;
+				editSetting.setEnabled(singleSelection);
+				if (singleSelection) {
+					int viewRow = table.getSelectedRow();
+					int modelRow = table.convertRowIndexToModel(viewRow);
+					SettingModel setting = tableModel.get(modelRow);
+					settingPanel.setSetting(setting);
+				} else {
+					settingPanel.clear();
+				}
+			}
+		});
+		table.addMouseListener(new MouseAdapter() {
+			public void mouseClicked(MouseEvent e) {
+				if (e.getClickCount() == 2) {
+					int viewRow = table.getSelectedRow();
+					int modelRow = table.convertRowIndexToModel(viewRow);
+					SettingModel setting = tableModel.get(modelRow);
+					editSetting(setting);
+				}
+			}
+		});
+
+		filterTextfield = new JTextField();
+		filterTextfield.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				filterSettings(filterTextfield.getText());
+			}
+		});
+		filterTextfield.addKeyListener(new KeyAdapter() {
+			public void keyReleased(KeyEvent e) {
+				filterSettings(filterTextfield.getText());
+			}
+		});
+
+		JPanel settingFilterPanel = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+		settingFilterPanel.add(new JLabel(Translation.get("gb.filter")), BorderLayout.WEST);
+		settingFilterPanel.add(filterTextfield, BorderLayout.CENTER);
+
+		JPanel settingsTablePanel = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+		settingsTablePanel.add(settingFilterPanel, BorderLayout.NORTH);
+		settingsTablePanel.add(new JScrollPane(table), BorderLayout.CENTER);
+		settingsTablePanel.add(settingPanel, BorderLayout.SOUTH);
+
+		JPanel settingsControls = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0));
+		settingsControls.add(refreshSettings);
+		settingsControls.add(editSetting);
+
+		setLayout(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+		header = new HeaderPanel(Translation.get("gb.settings"), "settings_16x16.png");
+		add(header, BorderLayout.NORTH);
+		add(settingsTablePanel, BorderLayout.CENTER);
+		add(settingsControls, BorderLayout.SOUTH);
+	}
+	
+	@Override
+	public void requestFocus() {
+		filterTextfield.requestFocus();
+	}
+
+	@Override
+	public Insets getInsets() {
+		return Utils.INSETS;
+	}
+
+	protected void updateTable(boolean pack) {
+		tableModel.setSettings(gitblit.getSettings());
+		tableModel.fireTableDataChanged();
+		header.setText(Translation.get("gb.settings"));
+		if (pack) {
+			Utils.packColumns(table, Utils.MARGIN);
+		}
+	}
+
+	private void filterSettings(final String fragment) {
+		if (StringUtils.isEmpty(fragment)) {
+			table.setRowSorter(defaultSorter);
+			return;
+		}
+		RowFilter<SettingsTableModel, Object> containsFilter = new RowFilter<SettingsTableModel, Object>() {
+			public boolean include(Entry<? extends SettingsTableModel, ? 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<SettingsTableModel> sorter = new TableRowSorter<SettingsTableModel>(
+				tableModel);
+		sorter.setRowFilter(containsFilter);
+		table.setRowSorter(sorter);
+	}
+
+	protected void refreshSettings() {
+		GitblitWorker worker = new GitblitWorker(SettingsPanel.this, RpcRequest.LIST_SETTINGS) {
+			@Override
+			protected Boolean doRequest() throws IOException {
+				gitblit.refreshSettings();
+				return true;
+			}
+
+			@Override
+			protected void onSuccess() {
+				updateTable(false);
+			}
+		};
+		worker.execute();
+	}
+
+	protected void editSetting(final SettingModel settingModel) {
+		final JTextField textField = new JTextField(settingModel.currentValue);
+		JPanel editPanel = new JPanel(new GridLayout(0, 1));
+		editPanel.add(new JLabel("New Value"));
+		editPanel.add(textField);
+
+		JPanel settingPanel = new JPanel(new BorderLayout());
+		settingPanel.add(new SettingPanel(settingModel), BorderLayout.CENTER);
+		settingPanel.add(editPanel, BorderLayout.SOUTH);
+		settingPanel.setPreferredSize(new Dimension(800, 200));
+
+		String[] options;
+		if (settingModel.currentValue.equals(settingModel.defaultValue)) {
+			options = new String[] { Translation.get("gb.cancel"), Translation.get("gb.save") };
+		} else {
+			options = new String[] { Translation.get("gb.cancel"),
+					Translation.get("gb.setDefault"), Translation.get("gb.save") };
+		}
+		String defaultOption = options[0];
+		int selection = JOptionPane.showOptionDialog(SettingsPanel.this, settingPanel,
+				settingModel.name, JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE,
+				new ImageIcon(getClass().getResource("/settings_16x16.png")), options,
+				defaultOption);
+		if (selection <= 0) {
+			return;
+		}
+		if (options[selection].equals(Translation.get("gb.setDefault"))) {
+			textField.setText(settingModel.defaultValue);
+		}
+		final Map<String, String> newSettings = new HashMap<String, String>();
+		newSettings.put(settingModel.name, textField.getText().trim());
+		GitblitWorker worker = new GitblitWorker(SettingsPanel.this, RpcRequest.EDIT_SETTINGS) {
+			@Override
+			protected Boolean doRequest() throws IOException {
+				boolean success = gitblit.updateSettings(newSettings);
+				if (success) {
+					gitblit.refreshSettings();
+				}
+				return success;
+			}
+
+			@Override
+			protected void onSuccess() {
+				updateTable(false);
+			}
+		};
+		worker.execute();
+	}
+}
diff --git a/src/com/gitblit/client/StatusPanel.java b/src/com/gitblit/client/StatusPanel.java
index 797ae9b..669aa47 100644
--- a/src/com/gitblit/client/StatusPanel.java
+++ b/src/com/gitblit/client/StatusPanel.java
@@ -21,13 +21,18 @@
 import java.awt.Font;
 import java.awt.GridLayout;
 import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.IOException;
 
+import javax.swing.JButton;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
 import javax.swing.JTable;
 
 import com.gitblit.Constants;
+import com.gitblit.Constants.RpcRequest;
 import com.gitblit.models.ServerStatus;
 import com.gitblit.utils.ByteFormat;
 import com.gitblit.utils.TimeUtils;
@@ -40,29 +45,32 @@
 public class StatusPanel extends JPanel {
 
 	private static final long serialVersionUID = 1L;
-	private final Insets insets = new Insets(5, 5, 5, 5);
+	private final GitblitClient gitblit;
 	private JLabel bootDate;
 	private JLabel url;
 	private JLabel servletContainer;
 	private JLabel heapMaximum;
 	private JLabel heapAllocated;
 	private JLabel heapUsed;
-	private PropertiesTableModel model;
-	private HeaderPanel headerPanel;
+	private PropertiesTableModel tableModel;
+	private HeaderPanel header;
 	private JLabel version;
 	private JLabel releaseDate;
 
-	public StatusPanel() {
+	public StatusPanel(GitblitClient gitblit) {
 		super();
+		this.gitblit = gitblit;
 		initialize();
 	}
 
-	public StatusPanel(String url, ServerStatus status) {
-		this();
-		setStatus(url, status);
-	}
-
 	private void initialize() {
+		JButton refreshStatus = new JButton(Translation.get("gb.refresh"));
+		refreshStatus.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				refreshStatus();
+			}
+		});
+
 		version = new JLabel();
 		releaseDate = new JLabel();
 		bootDate = new JLabel();
@@ -73,13 +81,13 @@
 		heapAllocated = new JLabel();
 		heapUsed = new JLabel();
 
-		JPanel fieldsPanel = new JPanel(new GridLayout(0, 1, 0, 5)) {
+		JPanel fieldsPanel = new JPanel(new GridLayout(0, 1, 0, Utils.MARGIN)) {
 
 			private static final long serialVersionUID = 1L;
 
 			@Override
 			public Insets getInsets() {
-				return insets;
+				return Utils.INSETS;
 			}
 		};
 		fieldsPanel.add(createFieldPanel("gb.version", version));
@@ -91,8 +99,8 @@
 		fieldsPanel.add(createFieldPanel("gb.heapAllocated", heapAllocated));
 		fieldsPanel.add(createFieldPanel("gb.heapMaximum", heapMaximum));
 
-		model = new PropertiesTableModel();
-		JTable propertiesTable = Utils.newTable(model, Utils.DATE_FORMAT);
+		tableModel = new PropertiesTableModel();
+		JTable propertiesTable = Utils.newTable(tableModel, Utils.DATE_FORMAT);
 		String name = propertiesTable.getColumnName(PropertiesTableModel.Columns.Name.ordinal());
 		NameRenderer nameRenderer = new NameRenderer();
 		propertiesTable.setRowHeight(nameRenderer.getFont().getSize() + 8);
@@ -102,14 +110,18 @@
 		centerPanel.add(fieldsPanel, BorderLayout.NORTH);
 		centerPanel.add(new JScrollPane(propertiesTable), BorderLayout.CENTER);
 
-		headerPanel = new HeaderPanel(Translation.get("gb.status"), "health_16x16.png");
+		JPanel controls = new JPanel();
+		controls.add(refreshStatus);
+
+		header = new HeaderPanel(Translation.get("gb.status"), "health_16x16.png");
 		setLayout(new BorderLayout());
-		add(headerPanel, BorderLayout.NORTH);
+		add(header, BorderLayout.NORTH);
 		add(centerPanel, BorderLayout.CENTER);
+		add(controls, BorderLayout.SOUTH);
 	}
 
 	private JPanel createFieldPanel(String key, JLabel valueLabel) {
-		JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0));
+		JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, Utils.MARGIN, 0));
 		JLabel textLabel = new JLabel(Translation.get(key));
 		textLabel.setFont(textLabel.getFont().deriveFont(Font.BOLD));
 		textLabel.setPreferredSize(new Dimension(120, 10));
@@ -120,23 +132,40 @@
 
 	@Override
 	public Insets getInsets() {
-		return insets;
+		return Utils.INSETS;
 	}
 
-	public void setStatus(String url, ServerStatus status) {
-		headerPanel.setText(Translation.get("gb.status"));
+	protected void refreshStatus() {
+		GitblitWorker worker = new GitblitWorker(StatusPanel.this, RpcRequest.LIST_STATUS) {
+			@Override
+			protected Boolean doRequest() throws IOException {
+				gitblit.refreshStatus();
+				return true;
+			}
+
+			@Override
+			protected void onSuccess() {
+				updateTable(false);
+			}
+		};
+		worker.execute();
+	}
+
+	protected void updateTable(boolean pack) {
+		ServerStatus status = gitblit.getStatus();
+		header.setText(Translation.get("gb.status"));
 		version.setText(Constants.NAME + (status.isGO ? " GO v" : " WAR v") + status.version);
 		releaseDate.setText(status.releaseDate);
 		bootDate.setText(status.bootDate.toString() + " (" + TimeUtils.timeAgo(status.bootDate)
 				+ ")");
-		this.url.setText(url);
+		url.setText(gitblit.url);
 		servletContainer.setText(status.servletContainer);
 		ByteFormat byteFormat = new ByteFormat();
 		heapMaximum.setText(byteFormat.format(status.heapMaximum));
 		heapAllocated.setText(byteFormat.format(status.heapAllocated));
 		heapUsed.setText(byteFormat.format(status.heapAllocated - status.heapFree) + " ("
 				+ byteFormat.format(status.heapFree) + " " + Translation.get("gb.free") + ")");
-		model.setProperties(status.systemProperties);
-		model.fireTableDataChanged();
+		tableModel.setProperties(status.systemProperties);
+		tableModel.fireTableDataChanged();
 	}
 }
diff --git a/src/com/gitblit/client/UsersPanel.java b/src/com/gitblit/client/UsersPanel.java
new file mode 100644
index 0000000..5d31774
--- /dev/null
+++ b/src/com/gitblit/client/UsersPanel.java
@@ -0,0 +1,356 @@
+/*
+ * 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.awt.BorderLayout;
+import java.awt.FlowLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.RowFilter;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.TableRowSorter;
+
+import com.gitblit.Constants.RpcRequest;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Users panel displays a list of user accounts and allows management of those
+ * accounts.
+ * 
+ * @author James Moger
+ * 
+ */
+public class UsersPanel extends JPanel {
+
+	private static final long serialVersionUID = 1L;
+
+	private final GitblitClient gitblit;
+
+	private HeaderPanel header;
+
+	private JTable table;
+
+	private UsersTableModel tableModel;
+
+	private TableRowSorter<UsersTableModel> defaultSorter;
+
+	private JTextField filterTextfield;
+
+	public UsersPanel(GitblitClient gitblit) {
+		super();
+		this.gitblit = gitblit;
+		initialize();
+	}
+
+	private void initialize() {
+		JButton refreshUsers = new JButton(Translation.get("gb.refresh"));
+		refreshUsers.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				refreshUsers();
+			}
+		});
+
+		JButton createUser = new JButton(Translation.get("gb.create"));
+		createUser.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				createUser();
+			}
+		});
+
+		final JButton editUser = new JButton(Translation.get("gb.edit"));
+		editUser.setEnabled(false);
+		editUser.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				editUser(getSelectedUsers().get(0));
+			}
+		});
+
+		final JButton delUser = new JButton(Translation.get("gb.delete"));
+		delUser.setEnabled(false);
+		delUser.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				deleteUsers(getSelectedUsers());
+			}
+		});
+
+		NameRenderer nameRenderer = new NameRenderer();
+		tableModel = new UsersTableModel();
+		defaultSorter = new TableRowSorter<UsersTableModel>(tableModel);
+		table = Utils.newTable(tableModel, Utils.DATE_FORMAT);
+		String name = table.getColumnName(UsersTableModel.Columns.Name.ordinal());
+		table.setRowHeight(nameRenderer.getFont().getSize() + 8);
+		table.getColumn(name).setCellRenderer(nameRenderer);
+		table.setRowSorter(defaultSorter);
+		table.getRowSorter().toggleSortOrder(UsersTableModel.Columns.Name.ordinal());
+		table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+
+			@Override
+			public void valueChanged(ListSelectionEvent e) {
+				if (e.getValueIsAdjusting()) {
+					return;
+				}
+				boolean selected = table.getSelectedRow() > -1;
+				boolean singleSelection = table.getSelectedRows().length == 1;
+				editUser.setEnabled(singleSelection && selected);
+				delUser.setEnabled(selected);
+			}
+		});
+
+		table.addMouseListener(new MouseAdapter() {
+			public void mouseClicked(MouseEvent e) {
+				if (e.getClickCount() == 2) {
+					editUser(getSelectedUsers().get(0));
+				}
+			}
+		});
+
+		filterTextfield = new JTextField();
+		filterTextfield.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				filterUsers(filterTextfield.getText());
+			}
+		});
+		filterTextfield.addKeyListener(new KeyAdapter() {
+			public void keyReleased(KeyEvent e) {
+				filterUsers(filterTextfield.getText());
+			}
+		});
+
+		JPanel userFilterPanel = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+		userFilterPanel.add(new JLabel(Translation.get("gb.filter")), BorderLayout.WEST);
+		userFilterPanel.add(filterTextfield, BorderLayout.CENTER);
+
+		JPanel userTablePanel = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+		userTablePanel.add(userFilterPanel, BorderLayout.NORTH);
+		userTablePanel.add(new JScrollPane(table), BorderLayout.CENTER);
+
+		JPanel userControls = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0));
+		userControls.add(refreshUsers);
+		userControls.add(createUser);
+		userControls.add(editUser);
+		userControls.add(delUser);
+
+		setLayout(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+		header = new HeaderPanel(Translation.get("gb.users"), "user_16x16.png");
+		add(header, BorderLayout.NORTH);
+		add(userTablePanel, BorderLayout.CENTER);
+		add(userControls, BorderLayout.SOUTH);
+	}
+	
+	@Override
+	public void requestFocus() {
+		filterTextfield.requestFocus();
+	}
+
+	@Override
+	public Insets getInsets() {
+		return Utils.INSETS;
+	}
+
+	protected void updateTable(boolean pack) {
+		tableModel.list.clear();
+		tableModel.list.addAll(gitblit.getUsers());
+		tableModel.fireTableDataChanged();
+		header.setText(Translation.get("gb.users") + " (" + gitblit.getUsers().size() + ")");
+		if (pack) {
+			Utils.packColumns(table, Utils.MARGIN);
+		}
+	}
+
+	private void filterUsers(final String fragment) {
+		if (StringUtils.isEmpty(fragment)) {
+			table.setRowSorter(defaultSorter);
+			return;
+		}
+		RowFilter<UsersTableModel, Object> containsFilter = new RowFilter<UsersTableModel, Object>() {
+			public boolean include(Entry<? extends UsersTableModel, ? 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<UsersTableModel> sorter = new TableRowSorter<UsersTableModel>(tableModel);
+		sorter.setRowFilter(containsFilter);
+		table.setRowSorter(sorter);
+	}
+
+	private List<UserModel> getSelectedUsers() {
+		List<UserModel> users = new ArrayList<UserModel>();
+		for (int viewRow : table.getSelectedRows()) {
+			int modelRow = table.convertRowIndexToModel(viewRow);
+			UserModel model = tableModel.list.get(modelRow);
+			users.add(model);
+		}
+		return users;
+	}
+
+	protected void refreshUsers() {
+		GitblitWorker worker = new GitblitWorker(UsersPanel.this, RpcRequest.LIST_USERS) {
+			@Override
+			protected Boolean doRequest() throws IOException {
+				gitblit.refreshUsers();
+				return true;
+			}
+
+			@Override
+			protected void onSuccess() {
+				updateTable(false);
+			}
+		};
+		worker.execute();
+	}
+
+	/**
+	 * Displays the create user dialog and fires a SwingWorker to update the
+	 * server, if appropriate.
+	 * 
+	 */
+	protected void createUser() {
+		EditUserDialog dialog = new EditUserDialog(gitblit.getSettings());
+		dialog.setLocationRelativeTo(UsersPanel.this);
+		dialog.setUsers(gitblit.getUsers());
+		dialog.setRepositories(gitblit.getRepositories(), null);
+		dialog.setVisible(true);
+		final UserModel newUser = dialog.getUser();
+		if (newUser == null) {
+			return;
+		}
+
+		GitblitWorker worker = new GitblitWorker(this, RpcRequest.CREATE_USER) {
+
+			@Override
+			protected Boolean doRequest() throws IOException {
+				boolean success = gitblit.createUser(newUser);
+				if (success) {
+					gitblit.refreshUsers();
+				}
+				return success;
+			}
+
+			@Override
+			protected void onSuccess() {
+				updateTable(false);
+			}
+
+			@Override
+			protected void onFailure() {
+				showFailure("Failed to execute request \"{0}\" for user \"{1}\".",
+						getRequestType(), newUser.username);
+			}
+		};
+		worker.execute();
+	}
+
+	/**
+	 * Displays the edit user dialog and fires a SwingWorker to update the
+	 * server, if appropriate.
+	 * 
+	 * @param user
+	 */
+	protected void editUser(final UserModel user) {
+		EditUserDialog dialog = new EditUserDialog(user, gitblit.getSettings());
+		dialog.setLocationRelativeTo(UsersPanel.this);
+		dialog.setUsers(gitblit.getUsers());
+		dialog.setRepositories(gitblit.getRepositories(), new ArrayList<String>(user.repositories));
+		dialog.setVisible(true);
+		final UserModel revisedUser = dialog.getUser();
+		if (revisedUser == null) {
+			return;
+		}
+
+		GitblitWorker worker = new GitblitWorker(this, RpcRequest.EDIT_USER) {
+			@Override
+			protected Boolean doRequest() throws IOException {
+				boolean success = gitblit.updateUser(user.username, revisedUser);
+				if (success) {
+					gitblit.refreshUsers();
+				}
+				return success;
+			}
+
+			@Override
+			protected void onSuccess() {
+				updateTable(false);
+			}
+
+			@Override
+			protected void onFailure() {
+				showFailure("Failed to execute request \"{0}\" for user \"{1}\".",
+						getRequestType(), user.username);
+			}
+		};
+		worker.execute();
+	}
+
+	protected void deleteUsers(final List<UserModel> users) {
+		if (users == null || users.size() == 0) {
+			return;
+		}
+		StringBuilder message = new StringBuilder("Delete the following users?\n\n");
+		for (UserModel user : users) {
+			message.append(user.username).append("\n");
+		}
+		int result = JOptionPane.showConfirmDialog(UsersPanel.this, message.toString(),
+				"Delete Users?", JOptionPane.YES_NO_OPTION);
+		if (result == JOptionPane.YES_OPTION) {
+			GitblitWorker worker = new GitblitWorker(this, RpcRequest.DELETE_USER) {
+				@Override
+				protected Boolean doRequest() throws IOException {
+					boolean success = true;
+					for (UserModel user : users) {
+						success &= gitblit.deleteUser(user);
+					}
+					if (success) {
+						gitblit.refreshUsers();
+					}
+					return success;
+				}
+
+				@Override
+				protected void onSuccess() {
+					updateTable(false);
+				}
+
+				@Override
+				protected void onFailure() {
+					showFailure("Failed to delete specified users!");
+				}
+			};
+			worker.execute();
+		}
+	}
+}
diff --git a/src/com/gitblit/client/Utils.java b/src/com/gitblit/client/Utils.java
index 1f44c32..538e6be 100644
--- a/src/com/gitblit/client/Utils.java
+++ b/src/com/gitblit/client/Utils.java
@@ -17,10 +17,13 @@
 
 import java.awt.Color;
 import java.awt.Component;
+import java.awt.Desktop;
 import java.awt.Dimension;
 import java.awt.Font;
+import java.awt.Insets;
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import java.net.URI;
 import java.text.MessageFormat;
 import java.util.Date;
 
@@ -36,9 +39,13 @@
 import com.gitblit.Constants.RpcRequest;
 
 public class Utils {
-	
+
+	public final static int MARGIN = 5;
+
+	public final static Insets INSETS = new Insets(MARGIN, MARGIN, MARGIN, MARGIN);
+
 	public final static String TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm";
-	
+
 	public final static String DATE_FORMAT = "yyyy-MM-dd";
 
 	public static JTable newTable(TableModel model, String datePattern) {
@@ -48,7 +55,8 @@
 		table.getTableHeader().setReorderingAllowed(false);
 		table.setGridColor(new Color(0xd9d9d9));
 		table.setBackground(Color.white);
-		table.setDefaultRenderer(Date.class, new DateCellRenderer(datePattern, Color.orange.darker()));
+		table.setDefaultRenderer(Date.class,
+				new DateCellRenderer(datePattern, Color.orange.darker()));
 		return table;
 	}
 
@@ -131,4 +139,13 @@
 		// Set the width
 		col.setPreferredWidth(width);
 	}
+
+	public static void browse(String url) {
+		try {
+			Desktop.getDesktop().browse(new URI(url));
+		} catch (Exception x) {
+			showException(null, x);
+		}
+	}
+
 }
diff --git a/src/com/gitblit/models/FeedModel.java b/src/com/gitblit/models/FeedModel.java
index 67cff95..bed0a9c 100644
--- a/src/com/gitblit/models/FeedModel.java
+++ b/src/com/gitblit/models/FeedModel.java
@@ -68,7 +68,7 @@
 
 	@Override
 	public int compareTo(FeedModel o) {
-		int repositoryCompare = repository.compareTo(o.repository);
+		int repositoryCompare = StringUtils.compareRepositoryNames(repository, o.repository);
 		if (repositoryCompare == 0) {
 			// same repository
 			if (StringUtils.isEmpty(branch)) {
diff --git a/src/com/gitblit/utils/SyndicationUtils.java b/src/com/gitblit/utils/SyndicationUtils.java
index 2237774..6ea5f0e 100644
--- a/src/com/gitblit/utils/SyndicationUtils.java
+++ b/src/com/gitblit/utils/SyndicationUtils.java
@@ -24,8 +24,6 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import org.eclipse.jgit.revwalk.RevCommit;
-
 import com.gitblit.Constants;
 import com.gitblit.GitBlitException;
 import com.gitblit.models.SyndicatedEntryModel;
@@ -50,19 +48,20 @@
 public class SyndicationUtils {
 
 	/**
-	 * Outputs an RSS feed of the list of commits to the outputstream.
+	 * Outputs an RSS feed of the list of entries to the outputstream.
 	 * 
 	 * @param hostUrl
 	 * @param title
 	 * @param description
 	 * @param repository
-	 * @param commits
+	 * @param entryModels
 	 * @param os
 	 * @throws IOException
 	 * @throws FeedException
 	 */
 	public static void toRSS(String hostUrl, String title, String description, String repository,
-			List<RevCommit> commits, OutputStream os) throws IOException, FeedException {
+			List<SyndicatedEntryModel> entryModels, OutputStream os) throws IOException,
+			FeedException {
 
 		SyndFeed feed = new SyndFeedImpl();
 		feed.setFeedType("rss_2.0");
@@ -78,17 +77,16 @@
 		feed.setImage(image);
 
 		List<SyndEntry> entries = new ArrayList<SyndEntry>();
-		for (RevCommit commit : commits) {
+		for (SyndicatedEntryModel entryModel : entryModels) {
 			SyndEntry entry = new SyndEntryImpl();
-			entry.setTitle(commit.getShortMessage());
-			entry.setAuthor(commit.getAuthorIdent().getName());
-			entry.setLink(MessageFormat.format("{0}/commit/{1}/{2}", hostUrl,
-					StringUtils.encodeURL(repository), commit.getName()));
-			entry.setPublishedDate(commit.getCommitterIdent().getWhen());
+			entry.setTitle(entryModel.title);
+			entry.setAuthor(entryModel.author);
+			entry.setLink(entryModel.link);
+			entry.setPublishedDate(entryModel.published);
 
 			SyndContent content = new SyndContentImpl();
-			content.setType("text/plain");
-			content.setValue(commit.getFullMessage());
+			content.setType(entryModel.contentType);
+			content.setValue(entryModel.content);
 			entry.setDescription(content);
 			entries.add(entry);
 		}
diff --git a/tests/com/gitblit/tests/SyndicationUtilsTest.java b/tests/com/gitblit/tests/SyndicationUtilsTest.java
index 6084b99..9a977b2 100644
--- a/tests/com/gitblit/tests/SyndicationUtilsTest.java
+++ b/tests/com/gitblit/tests/SyndicationUtilsTest.java
@@ -16,28 +16,36 @@
 package com.gitblit.tests;
 
 import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 
 import junit.framework.TestCase;
 
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-
 import com.gitblit.models.SyndicatedEntryModel;
-import com.gitblit.utils.JGitUtils;
 import com.gitblit.utils.SyndicationUtils;
 
 public class SyndicationUtilsTest extends TestCase {
 
 	public void testSyndication() throws Exception {
-		Repository repository = GitBlitSuite.getHelloworldRepository();
-		List<RevCommit> commits = JGitUtils.getRevLog(repository, 1);
+		List<SyndicatedEntryModel> entries = new ArrayList<SyndicatedEntryModel>();
+		for (int i = 0; i < 10; i++) {
+			SyndicatedEntryModel entry = new SyndicatedEntryModel();
+			entry.title = "Title " + i;
+			entry.author = "Author " + i;
+			entry.link = "Link " + i;
+			entry.published = new Date();
+			entry.contentType = "text/plain";
+			entry.content = "Content " + i;
+			entry.repository = "Repository " + i;
+			entry.branch = "Branch " + i;
+			entries.add(entry);
+		}
 		ByteArrayOutputStream os = new ByteArrayOutputStream();
-		SyndicationUtils.toRSS("http://localhost", "Title", "Description", "Repository", commits,
+		SyndicationUtils.toRSS("http://localhost", "Title", "Description", "Repository", entries,
 				os);
 		String feed = os.toString();
 		os.close();
-		assertTrue(feed.length() > 100);
 		assertTrue(feed.indexOf("<title>Title</title>") > -1);
 		assertTrue(feed.indexOf("<description>Description</description>") > -1);
 	}

--
Gitblit v1.9.1