]> source.dussan.org Git - gitblit.git/commitdiff
New permissions UI for EditUser and EditTeam (issue 36)
authorJames Moger <james.moger@gitblit.com>
Thu, 18 Oct 2012 21:25:07 +0000 (17:25 -0400)
committerJames Moger <james.moger@gitblit.com>
Sat, 20 Oct 2012 02:47:33 +0000 (22:47 -0400)
13 files changed:
resources/gitblit.css
src/com/gitblit/Constants.java
src/com/gitblit/models/RepositoryAccessPermission.java [new file with mode: 0644]
src/com/gitblit/models/TeamModel.java
src/com/gitblit/models/UserModel.java
src/com/gitblit/wicket/GitBlitWebApp.properties
src/com/gitblit/wicket/pages/BasePage.java
src/com/gitblit/wicket/pages/EditTeamPage.html
src/com/gitblit/wicket/pages/EditTeamPage.java
src/com/gitblit/wicket/pages/EditUserPage.html
src/com/gitblit/wicket/pages/EditUserPage.java
src/com/gitblit/wicket/panels/RepositoryPermissionsPanel.html [new file with mode: 0644]
src/com/gitblit/wicket/panels/RepositoryPermissionsPanel.java [new file with mode: 0644]

index 1d17dc84e8508ff4cd57d91470a1dc804fe60d0c..4d7e3ab18b02ccaee9933981a9829ba333f47ff9 100644 (file)
@@ -180,6 +180,15 @@ navbar div>ul .menu-dropdown li a:hover,.nav .menu-dropdown li a:hover,.navbar d
        vertical-align: middle;\r
 }\r
 \r
+div.odd {\r
+       \r
+}\r
+\r
+div.even {\r
+       background-color: whiteSmoke;\r
+       vertical-align: middle;\r
+}\r
+\r
 div.page_footer {\r
        clear: both;\r
        height: 17px;\r
index ed48bd277b34cdf3fbfae9b7379842db4836166d..0e68355ea1073cfce95f5b5374fe9959951bc60b 100644 (file)
@@ -320,6 +320,8 @@ public class Constants {
        public static enum AccessPermission {\r
                NONE("N"), VIEW("V"), CLONE("R"), PUSH("RW"), CREATE("RWC"), DELETE("RWD"), REWIND("RW+");\r
                \r
+               public static final AccessPermission [] NEWPERMISSIONS = { VIEW, CLONE, PUSH, CREATE, DELETE, REWIND };\r
+               \r
                public static AccessPermission LEGACY = REWIND;\r
                \r
                public final String code;\r
diff --git a/src/com/gitblit/models/RepositoryAccessPermission.java b/src/com/gitblit/models/RepositoryAccessPermission.java
new file mode 100644 (file)
index 0000000..06f5c05
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2012 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.models;
+
+import java.io.Serializable;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Represents a Repository-AccessPermission tuple.
+ * 
+ * @author James Moger
+ */
+public class RepositoryAccessPermission implements Serializable, Comparable<RepositoryAccessPermission> {
+
+       private static final long serialVersionUID = 1L;
+
+       public String repository;
+       public AccessPermission permission;
+
+       public RepositoryAccessPermission() {
+       }
+       
+       public RepositoryAccessPermission(String repository, AccessPermission permission) {
+               this.repository = repository;
+               this.permission = permission;
+       }
+       
+       @Override
+       public int compareTo(RepositoryAccessPermission p) {
+               return StringUtils.compareRepositoryNames(repository, p.repository);
+       }
+       
+       @Override
+       public String toString() {
+               return permission.asRole(repository);
+       }
+}
\ No newline at end of file
index 149c7659236d8d9cc1e57b8907e070e60fceeb2e..95e6ef4e6d1cc3194c7852df79d375402ce7daee 100644 (file)
@@ -18,6 +18,7 @@ package com.gitblit.models;
 import java.io.Serializable;\r
 import java.util.ArrayList;\r
 import java.util.Collection;\r
+import java.util.Collections;\r
 import java.util.HashMap;\r
 import java.util.HashSet;\r
 import java.util.List;\r
@@ -85,6 +86,21 @@ public class TeamModel implements Serializable, Comparable<TeamModel> {
        public void removeRepository(String name) {\r
                removeRepositoryPermission(name);\r
        }\r
+\r
+       \r
+       /**\r
+        * Returns a list of repository permissions for this team.\r
+        * \r
+        * @return the team's list of permissions\r
+        */\r
+       public List<RepositoryAccessPermission> getRepositoryPermissions() {\r
+               List<RepositoryAccessPermission> list = new ArrayList<RepositoryAccessPermission>();\r
+               for (Map.Entry<String, AccessPermission> entry : permissions.entrySet()) {\r
+                       list.add(new RepositoryAccessPermission(entry.getKey(), entry.getValue()));\r
+               }\r
+               Collections.sort(list);\r
+               return list;\r
+       }\r
        \r
        /**\r
         * Returns true if the team has any type of specified access permission for\r
index f14c1ae88cc1ad087df42c6b2f6845c62188332e..38a7aaedefab7d5e4d9aa9550063bd303283df64 100644 (file)
@@ -17,8 +17,11 @@ package com.gitblit.models;
 \r
 import java.io.Serializable;\r
 import java.security.Principal;\r
+import java.util.ArrayList;\r
+import java.util.Collections;\r
 import java.util.HashMap;\r
 import java.util.HashSet;\r
+import java.util.List;\r
 import java.util.Map;\r
 import java.util.Set;\r
 \r
@@ -124,6 +127,21 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
                removeRepositoryPermission(name);\r
        }\r
        \r
+       /**\r
+        * Returns a list of repository permissions for this user exclusive of\r
+        * permissions inherited from team memberships.\r
+        * \r
+        * @return the user's list of permissions\r
+        */\r
+       public List<RepositoryAccessPermission> getRepositoryPermissions() {\r
+               List<RepositoryAccessPermission> list = new ArrayList<RepositoryAccessPermission>();\r
+               for (Map.Entry<String, AccessPermission> entry : permissions.entrySet()) {\r
+                       list.add(new RepositoryAccessPermission(entry.getKey(), entry.getValue()));\r
+               }\r
+               Collections.sort(list);\r
+               return list;\r
+       }\r
+       \r
        /**\r
         * Returns true if the user has any type of specified access permission for\r
         * this repository.\r
index f6d60dca1d896628392a3fc143188acbbfac7660..eb7d77257b347ac6667fb5aeec9a364b12dcf4d2 100644 (file)
@@ -345,4 +345,12 @@ gb.verifyCommitter = verify committer
 gb.verifyCommitterDescription = require committer identity to match pushing Gitblt user account (all merges require "--no-ff" to enforce committer identity)\r
 gb.repositoryPermissions = repository permissions\r
 gb.userPermissions = user permissions\r
-gb.teamPermissions = team permissions
\ No newline at end of file
+gb.teamPermissions = team permissions\r
+gb.add = add\r
+gb.noPermission = NO ACCESS\r
+gb.viewPermission = {0} (view)\r
+gb.clonePermission = {0} (clone)\r
+gb.pushPermission = {0} (push)\r
+gb.createPermission = {0} (push, ref creation)\r
+gb.deletePermission = {0} (push, ref creation+deletion)\r
+gb.rewindPermission = {0} (push, ref creation+deletion+rewind)
\ No newline at end of file
index 4d3761147f8bfe1904a071e44cbd3ae94d569d55..48a872a814be39c7f07e1238f90693290040c25e 100644 (file)
@@ -15,6 +15,7 @@
  */\r
 package com.gitblit.wicket.pages;\r
 \r
+import java.text.MessageFormat;\r
 import java.util.ArrayList;\r
 import java.util.Calendar;\r
 import java.util.Collections;\r
@@ -52,6 +53,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;\r
 \r
 import com.gitblit.Constants;\r
+import com.gitblit.Constants.AccessPermission;\r
 import com.gitblit.Constants.AccessRestrictionType;\r
 import com.gitblit.Constants.FederationStrategy;\r
 import com.gitblit.GitBlit;\r
@@ -203,6 +205,36 @@ public abstract class BasePage extends WebPage {
                return map;\r
        }\r
        \r
+       protected Map<AccessPermission, String> getAccessPermissions() {\r
+               Map<AccessPermission, String> map = new LinkedHashMap<AccessPermission, String>();\r
+               for (AccessPermission type : AccessPermission.values()) {\r
+                       switch (type) {\r
+                       case NONE:\r
+                               map.put(type, MessageFormat.format(getString("gb.noPermission"), type.code));\r
+                               break;\r
+                       case VIEW:\r
+                               map.put(type, MessageFormat.format(getString("gb.viewPermission"), type.code));\r
+                               break;\r
+                       case CLONE:\r
+                               map.put(type, MessageFormat.format(getString("gb.clonePermission"), type.code));\r
+                               break;\r
+                       case PUSH:\r
+                               map.put(type, MessageFormat.format(getString("gb.pushPermission"), type.code));\r
+                               break;\r
+                       case CREATE:\r
+                               map.put(type, MessageFormat.format(getString("gb.createPermission"), type.code));\r
+                               break;\r
+                       case DELETE:\r
+                               map.put(type, MessageFormat.format(getString("gb.deletePermission"), type.code));\r
+                               break;\r
+                       case REWIND:\r
+                               map.put(type, MessageFormat.format(getString("gb.rewindPermission"), type.code));\r
+                               break;\r
+                       }\r
+               }\r
+               return map;\r
+       }\r
+       \r
        protected Map<FederationStrategy, String> getFederationTypes() {\r
                Map<FederationStrategy, String> map = new LinkedHashMap<FederationStrategy, String>();\r
                for (FederationStrategy type : FederationStrategy.values()) {\r
index 30d2a94c53544a5639d6b593e75a6a9600802f80..fb0819f936b9fe64044afe56ad67668ed2704029 100644 (file)
@@ -19,7 +19,7 @@
                                <tr><td colspan="2" style="padding-top:15px;"><h3><wicket:message key="gb.accessPermissions"></wicket:message> &nbsp;<small><wicket:message key="gb.accessPermissionsForTeamDescription"></wicket:message></small></h3></td></tr>       \r
                                <tr><th style="vertical-align: top;"><wicket:message key="gb.teamMembers"></wicket:message></th><td style="padding:2px;"><span wicket:id="users"></span></td></tr>\r
                                <tr><td colspan="2"><hr></hr></td></tr>\r
-                               <tr><th style="vertical-align: top;"><wicket:message key="gb.restrictedRepositories"></wicket:message></th><td style="padding:2px;"><span wicket:id="repositories"></span></td></tr>\r
+                               <tr><th style="vertical-align: top;"><wicket:message key="gb.repositoryPermissions"></wicket:message></th><td style="padding:2px;"><span wicket:id="repositories"></span></td></tr>\r
                                <tr><td colspan="2" style="padding-top:10px;"><h3><wicket:message key="gb.hookScripts"></wicket:message> &nbsp;<small><wicket:message key="gb.hookScriptsDescription"></wicket:message></small></h3></td></tr>  \r
                                <tr><th style="vertical-align: top;"><wicket:message key="gb.preReceiveScripts"></wicket:message><p></p><span wicket:id="inheritedPreReceive"></span></th><td style="padding:2px;"><span wicket:id="preReceiveScripts"></span></td></tr>\r
                                <tr><th style="vertical-align: top;"><wicket:message key="gb.postReceiveScripts"></wicket:message><p></p><span wicket:id="inheritedPostReceive"></span></th><td style="padding:2px;"><span wicket:id="postReceiveScripts"></span></td></tr>\r
index 9cbccb5933fc2aff889aabb652aa30332fd84ef5..05c91215ec3e2d1a703a58950c19719f9c2d6268 100644 (file)
@@ -40,12 +40,14 @@ import com.gitblit.Constants.AccessRestrictionType;
 import com.gitblit.GitBlit;\r
 import com.gitblit.GitBlitException;\r
 import com.gitblit.models.RepositoryModel;\r
+import com.gitblit.models.RepositoryAccessPermission;\r
 import com.gitblit.models.TeamModel;\r
 import com.gitblit.utils.StringUtils;\r
 import com.gitblit.wicket.RequiresAdminRole;\r
 import com.gitblit.wicket.StringChoiceRenderer;\r
 import com.gitblit.wicket.WicketUtils;\r
 import com.gitblit.wicket.panels.BulletListPanel;\r
+import com.gitblit.wicket.panels.RepositoryPermissionsPanel;\r
 \r
 @RequiresAdminRole\r
 public class EditTeamPage extends RootSubPage {\r
@@ -59,6 +61,7 @@ public class EditTeamPage extends RootSubPage {
                super();\r
                isCreate = true;\r
                setupPage(new TeamModel(""));\r
+               setStatelessHint(false);\r
        }\r
 \r
        public EditTeamPage(PageParameters params) {\r
@@ -68,6 +71,7 @@ public class EditTeamPage extends RootSubPage {
                String name = WicketUtils.getTeamname(params);\r
                TeamModel model = GitBlit.self().getTeamModel(name);\r
                setupPage(model);\r
+               setStatelessHint(false);\r
        }\r
 \r
        protected void setupPage(final TeamModel teamModel) {\r
@@ -94,11 +98,7 @@ public class EditTeamPage extends RootSubPage {
                List<String> postReceiveScripts = new ArrayList<String>();\r
 \r
                final String oldName = teamModel.name;\r
-\r
-               // repositories palette\r
-               final Palette<String> repositories = new Palette<String>("repositories",\r
-                               new ListModel<String>(new ArrayList<String>(teamModel.repositories)),\r
-                               new CollectionModel<String>(repos), new StringChoiceRenderer(), 10, false);\r
+               final List<RepositoryAccessPermission> permissions = teamModel.getRepositoryPermissions();\r
 \r
                // users palette\r
                final Palette<String> users = new Palette<String>("users", new ListModel<String>(\r
@@ -146,17 +146,10 @@ public class EditTeamPage extends RootSubPage {
                                                return;\r
                                        }\r
                                }\r
-                               Iterator<String> selectedRepositories = repositories.getSelectedChoices();\r
-                               List<String> repos = new ArrayList<String>();\r
-                               while (selectedRepositories.hasNext()) {\r
-                                       repos.add(selectedRepositories.next().toLowerCase());\r
-                               }\r
-                               if (repos.size() == 0) {\r
-                                       error(getString("gb.teamMustSpecifyRepository"));\r
-                                       return;\r
+                               // update team permissions\r
+                               for (RepositoryAccessPermission repositoryPermission : permissions) {\r
+                                       teamModel.setRepositoryPermission(repositoryPermission.repository, repositoryPermission.permission);\r
                                }\r
-                               teamModel.repositories.clear();\r
-                               teamModel.repositories.addAll(repos);\r
 \r
                                Iterator<String> selectedUsers = users.getSelectedChoices();\r
                                List<String> members = new ArrayList<String>();\r
@@ -231,7 +224,7 @@ public class EditTeamPage extends RootSubPage {
                                : StringUtils.flattenStrings(teamModel.mailingLists, " "));\r
                form.add(new TextField<String>("mailingLists", mailingLists));\r
 \r
-               form.add(repositories);\r
+               form.add(new RepositoryPermissionsPanel("repositories", permissions, getAccessPermissions()));\r
                form.add(preReceivePalette);\r
                form.add(new BulletListPanel("inheritedPreReceive", "inherited", GitBlit.self()\r
                                .getPreReceiveScriptsInherited(null)));\r
index 9f178df8bc0cbcf205c9f7962ac30787cc911e11..f993d46e0d503cc80178ff098953dcc07fc4ba14 100644 (file)
                                <tr><td colspan="2" style="padding-top:15px;"><h3><wicket:message key="gb.accessPermissions"></wicket:message> &nbsp;<small><wicket:message key="gb.accessPermissionsForUserDescription"></wicket:message></small></h3></td></tr>       \r
                                <tr><th style="vertical-align: top;"><wicket:message key="gb.teamMemberships"></wicket:message></th><td style="padding:2px;"><span wicket:id="teams"></span></td></tr>\r
                                <tr><td colspan="2"><hr></hr></td></tr>\r
-                               <tr><th style="vertical-align: top;"><wicket:message key="gb.restrictedRepositories"></wicket:message></th><td style="padding:2px;"><span wicket:id="repositories"></span></td></tr>\r
+                               <tr><th style="vertical-align: top;"><wicket:message key="gb.repositoryPermissions"></wicket:message></th>\r
+                                       <td style="padding:2px;">\r
+                                               <div wicket:id="repositories"></div>\r
+                                       </td>\r
+                               </tr>\r
                                <tr><td colspan='2'><div class="form-actions"><input class="btn btn-primary" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="9" /> &nbsp; <input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="10" /></div></td></tr>\r
                        </tbody>\r
                </table>\r
-       </form> \r
+       </form>\r
 </body>\r
+\r
 </wicket:extend>\r
 </html>
\ No newline at end of file
index 49515fb97275a47018c472200fb979052ab2ecfe..6e3535437a41249abd29b567c5fba31f798dafce 100644 (file)
@@ -39,18 +39,20 @@ import com.gitblit.GitBlit;
 import com.gitblit.GitBlitException;\r
 import com.gitblit.Keys;\r
 import com.gitblit.models.RepositoryModel;\r
+import com.gitblit.models.RepositoryAccessPermission;\r
 import com.gitblit.models.TeamModel;\r
 import com.gitblit.models.UserModel;\r
 import com.gitblit.utils.StringUtils;\r
 import com.gitblit.wicket.RequiresAdminRole;\r
 import com.gitblit.wicket.StringChoiceRenderer;\r
 import com.gitblit.wicket.WicketUtils;\r
+import com.gitblit.wicket.panels.RepositoryPermissionsPanel;\r
 \r
 @RequiresAdminRole\r
 public class EditUserPage extends RootSubPage {\r
 \r
        private final boolean isCreate;\r
-\r
+       \r
        public EditUserPage() {\r
                // create constructor\r
                super();\r
@@ -60,6 +62,7 @@ public class EditUserPage extends RootSubPage {
                }\r
                isCreate = true;\r
                setupPage(new UserModel(""));\r
+               setStatelessHint(false);\r
        }\r
 \r
        public EditUserPage(PageParameters params) {\r
@@ -69,6 +72,7 @@ public class EditUserPage extends RootSubPage {
                String name = WicketUtils.getUsername(params);\r
                UserModel model = GitBlit.self().getUserModel(name);\r
                setupPage(model);\r
+               setStatelessHint(false);\r
        }\r
 \r
        protected void setupPage(final UserModel userModel) {\r
@@ -96,16 +100,15 @@ public class EditUserPage extends RootSubPage {
                Collections.sort(userTeams);\r
                \r
                final String oldName = userModel.username;\r
-               final Palette<String> repositories = new Palette<String>("repositories",\r
-                               new ListModel<String>(new ArrayList<String>(userModel.repositories)),\r
-                               new CollectionModel<String>(repos), new StringChoiceRenderer(), 10, false);\r
+               final List<RepositoryAccessPermission> permissions = userModel.getRepositoryPermissions();\r
+\r
                final Palette<String> teams = new Palette<String>("teams", new ListModel<String>(\r
                                new ArrayList<String>(userTeams)), new CollectionModel<String>(GitBlit.self()\r
                                .getAllTeamnames()), new StringChoiceRenderer(), 10, false);\r
                Form<UserModel> form = new Form<UserModel>("editForm", model) {\r
 \r
                        private static final long serialVersionUID = 1L;\r
-\r
+                       \r
                        /*\r
                         * (non-Javadoc)\r
                         * \r
@@ -167,13 +170,10 @@ public class EditUserPage extends RootSubPage {
                                        }\r
                                }\r
 \r
-                               Iterator<String> selectedRepositories = repositories.getSelectedChoices();\r
-                               List<String> repos = new ArrayList<String>();\r
-                               while (selectedRepositories.hasNext()) {\r
-                                       repos.add(selectedRepositories.next().toLowerCase());\r
+                               // update user permissions\r
+                               for (RepositoryAccessPermission repositoryPermission : permissions) {\r
+                                       userModel.setRepositoryPermission(repositoryPermission.repository, repositoryPermission.permission);\r
                                }\r
-                               userModel.repositories.clear();\r
-                               userModel.repositories.addAll(repos);\r
 \r
                                Iterator<String> selectedTeams = teams.getSelectedChoices();\r
                                userModel.teams.clear();\r
@@ -234,7 +234,7 @@ public class EditUserPage extends RootSubPage {
                form.add(new CheckBox("canFork"));\r
                form.add(new CheckBox("canCreate"));\r
                form.add(new CheckBox("excludeFromFederation"));\r
-               form.add(repositories);\r
+               form.add(new RepositoryPermissionsPanel("repositories", permissions, getAccessPermissions()));\r
                form.add(teams.setEnabled(editTeams));\r
 \r
                form.add(new Button("save"));\r
diff --git a/src/com/gitblit/wicket/panels/RepositoryPermissionsPanel.html b/src/com/gitblit/wicket/panels/RepositoryPermissionsPanel.html
new file mode 100644 (file)
index 0000000..1c7e44e
--- /dev/null
@@ -0,0 +1,24 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\r
+<html xmlns="http://www.w3.org/1999/xhtml"  \r
+      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"  \r
+      xml:lang="en"  \r
+      lang="en"> \r
+\r
+<body>\r
+<wicket:panel>\r
+\r
+       <div wicket:id="permissionRow">\r
+               <div style="padding-top:10px" class="row-fluid">\r
+                       <span class="span8" wicket:id="repository"></span> <select class="input-medium" wicket:id="permission"></select>\r
+               </div>\r
+       </div>\r
+\r
+       <div style="padding-top:15px;" class="row-fluid">\r
+               <form class="well form-inline" wicket:id="addPermissionForm">\r
+                       <select class="input-large" wicket:id="repository"></select> <select class="input-medium" wicket:id="permission"></select> <input class="btn btn-success" type="submit" value="Add" wicket:message="value:gb.add" wicket:id="addPermissionButton"/>\r
+               </form>\r
+       </div>  \r
+       \r
+</wicket:panel>\r
+</body>\r
+</html>
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/panels/RepositoryPermissionsPanel.java b/src/com/gitblit/wicket/panels/RepositoryPermissionsPanel.java
new file mode 100644 (file)
index 0000000..3d967d3
--- /dev/null
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2012 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.wicket.panels;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
+import org.apache.wicket.ajax.markup.html.form.AjaxButton;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.form.DropDownChoice;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.IChoiceRenderer;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.OddEvenItem;
+import org.apache.wicket.markup.repeater.RefreshingView;
+import org.apache.wicket.markup.repeater.util.ModelIteratorAdapter;
+import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.IModel;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.GitBlit;
+import com.gitblit.models.RepositoryAccessPermission;
+import com.gitblit.utils.DeepCopier;
+
+/**
+ * Allows user to manipulate repository access permissions.
+ * 
+ * @author James Moger
+ *
+ */
+public class RepositoryPermissionsPanel extends BasePanel {
+
+       private static final long serialVersionUID = 1L;
+
+       public RepositoryPermissionsPanel(String wicketId, final List<RepositoryAccessPermission> permissions, final Map<AccessPermission, String> translations) {
+               super(wicketId);
+               
+               // update existing permissions repeater
+               RefreshingView<RepositoryAccessPermission> dataView = new RefreshingView<RepositoryAccessPermission>("permissionRow") {
+                       private static final long serialVersionUID = 1L;
+               
+                       @Override
+            protected Iterator<IModel<RepositoryAccessPermission>> getItemModels() {
+                // the iterator returns RepositoryPermission objects, but we need it to
+                // return models
+                return new ModelIteratorAdapter<RepositoryAccessPermission>(permissions.iterator()) {
+                    @Override
+                    protected IModel<RepositoryAccessPermission> model(RepositoryAccessPermission permission) {
+                        return new CompoundPropertyModel<RepositoryAccessPermission>(permission);
+                    }
+                };
+            }
+
+            @Override
+            protected Item<RepositoryAccessPermission> newItem(String id, int index, IModel<RepositoryAccessPermission> model) {
+                // this item sets markup class attribute to either 'odd' or
+                // 'even' for decoration
+                return new OddEvenItem<RepositoryAccessPermission>(id, index, model);
+            }
+            
+                       public void populateItem(final Item<RepositoryAccessPermission> item) {
+                               final RepositoryAccessPermission entry = item.getModelObject();
+                               item.add(new Label("repository", entry.repository));
+
+                               // use ajax to get immediate update of permission level change
+                               // otherwise we can lose it if they change levels and then add
+                               // a new repository permission
+                               final DropDownChoice<AccessPermission> permissionChoice = new DropDownChoice<AccessPermission>(
+                                               "permission", Arrays.asList(AccessPermission.values()), new AccessPermissionRenderer(translations));
+                               permissionChoice.add(new AjaxFormComponentUpdatingBehavior("onchange") {
+                          
+                                       private static final long serialVersionUID = 1L;
+
+                                       protected void onUpdate(AjaxRequestTarget target) {
+                               target.addComponent(permissionChoice);
+                           }
+                       });
+
+                               item.add(permissionChoice);
+                       }
+               };
+               add(dataView);
+               setOutputMarkupId(true);
+
+               // filter out repositories we already have permissions for
+               final List<String> repositories = GitBlit.self().getRepositoryList();
+               for (RepositoryAccessPermission rp : permissions) {
+                       repositories.remove(rp.repository);
+               }
+
+               // add new permission form
+               IModel<RepositoryAccessPermission> addPermissionModel = new CompoundPropertyModel<RepositoryAccessPermission>(new RepositoryAccessPermission());
+               Form<RepositoryAccessPermission> addPermissionForm = new Form<RepositoryAccessPermission>("addPermissionForm", addPermissionModel);
+               addPermissionForm.add(new DropDownChoice<String>("repository", repositories));
+               addPermissionForm.add(new DropDownChoice<AccessPermission>("permission", Arrays
+                               .asList(AccessPermission.NEWPERMISSIONS), new AccessPermissionRenderer(translations)));
+               AjaxButton button = new AjaxButton("addPermissionButton", addPermissionForm) {
+
+                       private static final long serialVersionUID = 1L;
+
+                       @Override
+                       protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
+                               // add permission to our list
+                               RepositoryAccessPermission rp = (RepositoryAccessPermission) form.getModel().getObject();
+                               permissions.add(DeepCopier.copy(rp));
+                               
+                               // remove repository from available choices
+                               repositories.remove(rp.repository);
+                               
+                               // force the panel to refresh
+                               target.addComponent(RepositoryPermissionsPanel.this);
+                       }
+               };
+               addPermissionForm.add(button);
+               
+               // only show add permission form if we have a repository choice
+               add(addPermissionForm.setVisible(repositories.size() > 0));
+       }
+       
+       private class AccessPermissionRenderer implements IChoiceRenderer<AccessPermission> {
+
+               private static final long serialVersionUID = 1L;
+
+               private final Map<AccessPermission, String> map;
+
+               public AccessPermissionRenderer(Map<AccessPermission, String> map) {
+                       this.map = map;
+               }
+
+               @Override
+               public String getDisplayValue(AccessPermission type) {
+                       return map.get(type);
+               }
+
+               @Override
+               public String getIdValue(AccessPermission type, int index) {
+                       return Integer.toString(index);
+               }
+       }
+}