From 580814f7120e6fcf94cc4e3f066106fd8da62a2f Mon Sep 17 00:00:00 2001
From: Joseph Schorr <josephschorr@users.noreply.github.com>
Date: Thu, 12 May 2016 13:42:11 -0400
Subject: [PATCH 1/4] Move the credentials dialog to always be under the body

Fixes a CSS issue with display:table-cell and nesting
---
 static/js/directives/ui/credentials-dialog.js | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/static/js/directives/ui/credentials-dialog.js b/static/js/directives/ui/credentials-dialog.js
index c574a478e..3f83606b1 100644
--- a/static/js/directives/ui/credentials-dialog.js
+++ b/static/js/directives/ui/credentials-dialog.js
@@ -22,6 +22,10 @@ angular.module('quay').directive('credentialsDialog', function () {
       $scope.rkt = {};
       $scope.docker = {};
 
+      $scope.$on('$destroy', function() {
+        document.body.removeChild($element[0]);
+      });
+
       // Generate a unique ID for the dialog.
       if (!$rootScope.credentialsDialogCounter) {
         $rootScope.credentialsDialogCounter = 0;
@@ -36,6 +40,10 @@ angular.module('quay').directive('credentialsDialog', function () {
 
       $scope.show = function() {
         $element.find('.modal').modal({});
+
+        // Move the dialog to the body to prevent it from being affected
+        // by being placed inside other tables.
+        document.body.appendChild($element[0]);
       };
 
       $scope.$watch('credentials', function(credentials) {

From 2d4916c6eccf8b4b5b0e1323619c5877cdb48c3a Mon Sep 17 00:00:00 2001
From: Joseph Schorr <josephschorr@users.noreply.github.com>
Date: Thu, 12 May 2016 13:42:18 -0400
Subject: [PATCH 2/4] Make role group thinner

---
 static/css/directives/ui/role-group.css | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/static/css/directives/ui/role-group.css b/static/css/directives/ui/role-group.css
index 119a0db16..829209d33 100644
--- a/static/css/directives/ui/role-group.css
+++ b/static/css/directives/ui/role-group.css
@@ -2,11 +2,14 @@
   width: 90px;
   position: relative;
   text-align: left;
+
+  padding: 4px;
+  padding-left: 10px;
 }
 
 .new-role-group .btn .caret {
   position: absolute;
-  top: 15px;
+  top: 13px;
   right: 7px;
 }
 

From 2274d6ff84c0a5b12361d796a199988d64aee2ae Mon Sep 17 00:00:00 2001
From: Joseph Schorr <josephschorr@users.noreply.github.com>
Date: Thu, 12 May 2016 16:54:05 -0400
Subject: [PATCH 3/4] Fix error

---
 static/js/directives/ui/credentials-dialog.js | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/static/js/directives/ui/credentials-dialog.js b/static/js/directives/ui/credentials-dialog.js
index 3f83606b1..646197bac 100644
--- a/static/js/directives/ui/credentials-dialog.js
+++ b/static/js/directives/ui/credentials-dialog.js
@@ -23,7 +23,9 @@ angular.module('quay').directive('credentialsDialog', function () {
       $scope.docker = {};
 
       $scope.$on('$destroy', function() {
-        document.body.removeChild($element[0]);
+        if ($scope.inBody) {
+          document.body.removeChild($element[0]);
+        }
       });
 
       // Generate a unique ID for the dialog.
@@ -43,6 +45,7 @@ angular.module('quay').directive('credentialsDialog', function () {
 
         // Move the dialog to the body to prevent it from being affected
         // by being placed inside other tables.
+        $scope.inBody = true;
         document.body.appendChild($element[0]);
       };
 

From 4a543be7a7be5ffd2d3ec141c9f56b19dd50b47a Mon Sep 17 00:00:00 2001
From: Joseph Schorr <josephschorr@users.noreply.github.com>
Date: Thu, 12 May 2016 17:59:49 -0400
Subject: [PATCH 4/4] New create entity dialogs (team and robot)

Fixes https://github.com/coreos-inc/design/issues/230
---
 static/css/core-ui.css                        |   8 +-
 .../directives/ui/create-entity-dialog.css    |  53 +++++
 static/css/directives/ui/feedback-bar.css     |   1 +
 static/css/directives/ui/role-group.css       |   9 +
 static/directives/create-entity-dialog.html   |  98 +++++++++
 static/directives/create-robot-dialog.html    |   9 +
 static/directives/create-team-dialog.html     |   9 +
 static/directives/entity-search.html          |   8 +-
 static/directives/fetch-tag-dialog.html       |   1 +
 static/directives/header-bar.html             |  12 +-
 static/directives/repo-list-table.html        |   2 +-
 .../directives/repo-view/repo-panel-tags.html |   2 +-
 .../repository-permissions-table.html         |   4 +
 static/directives/robots-manager.html         |  10 +-
 static/directives/teams-manager.html          |  20 +-
 .../directives/repo-view/repo-panel-tags.js   |  22 +-
 .../js/directives/ui/create-entity-dialog.js  | 202 ++++++++++++++++++
 .../js/directives/ui/create-robot-dialog.js   |  43 ++++
 static/js/directives/ui/create-team-dialog.js |  44 ++++
 static/js/directives/ui/entity-search.js      |  38 +++-
 static/js/directives/ui/header-bar.js         |  38 ++--
 static/js/directives/ui/repo-list-table.js    |  20 +-
 .../ui/repository-permissions-table.js        |  58 ++---
 static/js/directives/ui/robots-manager.js     |  32 +--
 static/js/directives/ui/teams-manager.js      |  50 ++---
 static/js/pages/user-view.js                  |   1 +
 static/js/services/create-service.js          |  61 ------
 static/js/services/roles-service.js           |  41 +++-
 static/js/services/table-service.js           |   8 +
 static/js/services/ui-service.js              |   2 +-
 static/js/services/user-service.js            |  13 ++
 31 files changed, 687 insertions(+), 232 deletions(-)
 create mode 100644 static/css/directives/ui/create-entity-dialog.css
 create mode 100644 static/directives/create-entity-dialog.html
 create mode 100644 static/directives/create-robot-dialog.html
 create mode 100644 static/directives/create-team-dialog.html
 create mode 100644 static/js/directives/ui/create-entity-dialog.js
 create mode 100644 static/js/directives/ui/create-robot-dialog.js
 create mode 100644 static/js/directives/ui/create-team-dialog.js
 delete mode 100644 static/js/services/create-service.js

diff --git a/static/css/core-ui.css b/static/css/core-ui.css
index 86a3eed90..3352c7a49 100644
--- a/static/css/core-ui.css
+++ b/static/css/core-ui.css
@@ -1527,13 +1527,13 @@ a:focus {
   -webkit-filter: grayscale(100%);
 }
 
-
 .co-dialog .co-tab-content {
   padding: 16px;
   padding-bottom: 30px;
 }
 
 .co-dialog .co-tab-content h3 {
+  margin-top: 0px;
   margin-bottom: 0px;
 }
 
@@ -1574,3 +1574,9 @@ a:focus {
     height: auto;
   }
 }
+
+.co-modal-body-scrollable {
+  overflow-y: auto;
+  overflow-x: hidden;
+  max-height: 400px;
+}
\ No newline at end of file
diff --git a/static/css/directives/ui/create-entity-dialog.css b/static/css/directives/ui/create-entity-dialog.css
new file mode 100644
index 000000000..f7eb3fc9f
--- /dev/null
+++ b/static/css/directives/ui/create-entity-dialog.css
@@ -0,0 +1,53 @@
+.create-entity-dialog-element .modal-body {
+  min-height: 300px;
+}
+
+.create-entity-dialog-element form {
+  padding: 10px;
+}
+
+.create-entity-dialog-element .help-text {
+  color: #aaa;
+  margin-top: 10px;
+}
+
+.create-entity-dialog-element h4 .fa {
+  margin-left: 4px;
+  margin-right: 4px;
+}
+
+.create-entity-dialog-element label {
+  margin-top: 4px;
+}
+
+.create-entity-dialog-element .co-table {
+  margin-top: 20px;
+}
+
+.create-entity-dialog-element .fa-hdd-o {
+  margin-right: 4px;
+  vertical-align: middle;
+}
+
+.create-entity-dialog-element .co-filter-box {
+  display: block;
+  float: right;
+  margin-bottom: 20px;
+}
+
+.create-entity-dialog-element .co-filter-box .filter-message {
+  left: -180px;
+  top: 4px;
+}
+
+.create-entity-dialog-element .co-filter-box input {
+  width: 100%;
+  padding-top: 2px;
+  padding-bottom: 2px;
+  height: 28px;
+}
+
+.create-entity-dialog-element label .avatar {
+  vertical-align: text-bottom;
+  margin-left: 4px;
+}
\ No newline at end of file
diff --git a/static/css/directives/ui/feedback-bar.css b/static/css/directives/ui/feedback-bar.css
index b8fce1285..3f72cbc70 100644
--- a/static/css/directives/ui/feedback-bar.css
+++ b/static/css/directives/ui/feedback-bar.css
@@ -7,6 +7,7 @@
   overflow: hidden;
   text-align: center;
   height: 42px;
+  pointer-events: none;
 }
 
 @keyframes flow-down-up {
diff --git a/static/css/directives/ui/role-group.css b/static/css/directives/ui/role-group.css
index 829209d33..327a4665e 100644
--- a/static/css/directives/ui/role-group.css
+++ b/static/css/directives/ui/role-group.css
@@ -7,12 +7,21 @@
   padding-left: 10px;
 }
 
+.role-group.small .btn {
+  padding: 2px;
+  padding-left: 10px;
+}
+
 .new-role-group .btn .caret {
   position: absolute;
   top: 13px;
   right: 7px;
 }
 
+.role-group.small .btn .caret {
+  top: 11px;
+}
+
 .new-role-group .role-help-text {
   font-size: 12px;
   color: #ccc;
diff --git a/static/directives/create-entity-dialog.html b/static/directives/create-entity-dialog.html
new file mode 100644
index 000000000..eff7fa581
--- /dev/null
+++ b/static/directives/create-entity-dialog.html
@@ -0,0 +1,98 @@
+<div class="create-entity-dialog-element">
+  <div class="modal fade co-dialog wider">
+    <div class="modal-dialog">
+      <div class="modal-content">
+        <div class="modal-header">
+          <button type="button" class="close" ng-click="hide()" aria-hidden="true">&times;</button>
+          <h4 class="modal-title" ng-show="view == 'enterName' || view == 'creating'">
+            <i class="fa {{ entityIcon }}"></i>
+            Create {{ entityTitle }}
+          </h4>
+          <h4 class="modal-title" ng-show="view == 'addperms' || view == 'addingperms'">
+            Add permissions for <i class="fa {{ entityIcon }}"></i> {{ entity.name }}
+          </h4>
+        </div> <!-- /.model-header -->
+        <div class="modal-body" ng-show="view == 'creating' || view == 'addingperms'">
+          <div class="cor-loader"></div>
+        </div>
+        <div class="modal-body co-modal-body-scrollable" ng-show="view == 'addperms'">
+          <span class="co-filter-box">
+            <span class="filter-message" ng-if="options.filter">
+              Showing {{ orderedRepositories.entries.length }} of {{ repositories.length }} repositories
+            </span>
+            <input class="form-control" type="text" ng-model="options.filter" placeholder="Filter Repositories...">
+          </span>
+
+          <label>
+            Select repositories in
+            <span class="avatar" size="16" data="namespace.avatar"></span>
+            {{ info.namespace }}:
+          </label>
+
+          <table class="co-table" style="margin-bottom: 210px;">
+            <thead>
+              <td class="checkbox-col"></td>
+              <td ng-class="TableService.tablePredicateClass('name', options.predicate, options.reverse)">
+                <a ng-click="TableService.orderBy('name', options)">Repository Name</a>
+              </td>
+              <td>Permission</td>
+              <td ng-class="TableService.tablePredicateClass('last_modified_datetime', options.predicate, options.reverse)">
+                <a ng-click="TableService.orderBy('last_modified_datetime', options)">Last Updated</a>
+              </td>
+             </thead>
+
+             <tr class="co-checkable-row"
+                 ng-repeat="repo in orderedRepositories.visibleEntries"
+                 ng-class="checkedRepos.isChecked(repo, checkedRepos.checked) ? 'checked' : ''"
+                 bindonce>
+              <td>
+                <span class="cor-checkable-item" controller="checkedRepos" item="repo"></span>
+              </td>
+              <td>
+                <i class="fa fa-hdd-o"></i>
+                <span bo-text="repo.name"></span>
+              </td>
+              <td>
+                <span class="role-group small" current-role="repo.permission"
+                      roles="repoRolesOrNone"
+                      role-changed="setRole(role, repo)"></span>
+              </td>
+              <td>
+                 <span ng-if="repo.last_modified">
+                   {{ repo.last_modified * 1000 | amCalendar }}
+                 </span>
+                 <span class="empty" ng-if="!repo.last_modified">(Empty Repository)</span>
+              </td>
+            </tr>
+          </table>
+
+          <div class="empty" ng-if="!orderedRepositories.entries.length"
+               style="margin-top: 20px;">
+            <div class="empty-primary-msg">No matching repositories found.</div>
+            <div class="empty-secondary-msg">Try expanding your filtering terms.</div>
+          </div>
+        </div>
+        <div class="modal-body" ng-show="view == 'enterName'">
+          <form name="enterNameForm" ng-submit="createEntity()">
+            <label>Provide a name for your new {{ entityTitle }}:</label>
+            <input type="text" class="form-control" ng-model="entityName" ng-pattern="entityNameRegexObj" required>
+            <div class="help-text">
+              Choose a name to inform your teammates
+              about this {{ entityTitle }}. Must match {{ entityNameRegex }}.
+            </div>
+          </form>
+        </div> <!-- /.modal-body -->
+        <div class="modal-footer" ng-show="view == 'addperms'">
+          <button type="button" class="btn btn-primary" ng-click="addPermissions()"
+                  ng-show="checkedRepos.checked.length">Add permissions</button>
+          <button type="button" class="btn btn-default" ng-click="hide()">Close</button>
+        </div> <!-- /.footer-body -->
+        <div class="modal-footer" ng-show="view == 'enterName'">
+          <button type="button" class="btn btn-primary" ng-click="createEntity()"
+                  ng-disabled="enterNameForm.$invalid">Create {{ entityTitle }}</button>
+          <button type="button" class="btn btn-default" ng-click="hide()">Cancel</button>
+        </div> <!-- /.footer-body -->
+      </div>
+    </div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/static/directives/create-robot-dialog.html b/static/directives/create-robot-dialog.html
new file mode 100644
index 000000000..c631f7880
--- /dev/null
+++ b/static/directives/create-robot-dialog.html
@@ -0,0 +1,9 @@
+<div class="create-robot-dialog-element">
+  <div ng-if="info">
+    <div class="create-entity-dialog" info="info" entity-title="robot account"
+         entity-kind="user"
+         entity-icon="ci-robot" entity-name-regex="{{ ROBOT_PATTERN }}"
+         entity-create-requested="createRobot(name, callback)"
+         entity-create-completed="robotFinished(entity)"></div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/static/directives/create-team-dialog.html b/static/directives/create-team-dialog.html
new file mode 100644
index 000000000..0aafffa9f
--- /dev/null
+++ b/static/directives/create-team-dialog.html
@@ -0,0 +1,9 @@
+<div class="create-team-dialog-element">
+  <div ng-if="info">
+    <div class="create-entity-dialog" info="info" entity-title="team"
+         entity-kind="team"
+         entity-icon="fa-group" entity-name-regex="{{ TEAM_PATTERN }}"
+         entity-create-requested="createTeam(name, callback)"
+         entity-create-completed="teamFinished(entity)"></div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/static/directives/entity-search.html b/static/directives/entity-search.html
index dfb8b99db..6baa3db77 100644
--- a/static/directives/entity-search.html
+++ b/static/directives/entity-search.html
@@ -14,12 +14,12 @@
       </li>
 
       <li role="presentation" ng-show="includeTeams && isOrganization && !lazyLoading && isAdmin">
-        <a role="menuitem" class="new-action" tabindex="-1" ng-click="createTeam()">
+        <a role="menuitem" class="new-action" tabindex="-1" ng-click="askCreateTeam()">
           <i class="fa fa-group"></i> Create team
         </a>
       </li>
       <li role="presentation" ng-show="includeRobots && !lazyLoading && isAdmin">
-        <a role="menuitem" class="new-action" tabindex="-1" ng-click="createRobot()">
+        <a role="menuitem" class="new-action" tabindex="-1" ng-click="askCreateRobot()">
           <i class="fa ci-robot"></i>
           Create robot account
         </a>
@@ -70,4 +70,8 @@
       </li>
     </ul>
   </div>
+  <div class="create-team-dialog" info="createTeamInfo"
+       team-created="handleTeamCreated(team)"></div>
+  <div class="create-robot-dialog" info="createRobotInfo"
+       robot-created="handleRobotCreated(robot)"></div>
 </span>
diff --git a/static/directives/fetch-tag-dialog.html b/static/directives/fetch-tag-dialog.html
index b75c7c2ab..0cb45f2d1 100644
--- a/static/directives/fetch-tag-dialog.html
+++ b/static/directives/fetch-tag-dialog.html
@@ -38,6 +38,7 @@
               <td class="first-col">Pull Credentials:</td>
               <td>
                 <div class="entity-search" namespace="repository.namespace"
+                     for-repository="repository"
                      placeholder="'Choose Pull Credentials'"
                      allowed-entities="['robot']"
                      clear-value="clearCounter"
diff --git a/static/directives/header-bar.html b/static/directives/header-bar.html
index 1346bdbe9..74fe68c93 100644
--- a/static/directives/header-bar.html
+++ b/static/directives/header-bar.html
@@ -85,12 +85,12 @@
                     Namespace {{ getNamespace(currentPageContext) }}
                   </li>
                   <li ng-if="isOrganization(getNamespace(currentPageContext)) && canAdmin(getNamespace(currentPageContext))">
-                    <a ng-click="createTeam(currentPageContext)">
+                    <a ng-click="askCreateTeam(currentPageContext)">
                       <i class="fa fa-group"></i> New Team
                     </a>
                   </li>
                   <li ng-if="canAdmin(getNamespace(currentPageContext))">
-                    <a ng-click="createRobot(currentPageContext)">
+                    <a ng-click="askCreateRobot(currentPageContext)">
                       <i class="fa ci-robot"></i> New Robot Account
                     </a>
                   </li>
@@ -207,6 +207,14 @@
       </ul>
     </div>
 
+    <div class="create-robot-dialog" info="createRobotInfo"
+         robot-created="handleRobotCreated(robot, currentPageContext)">
+    </div>
+
+    <div class="create-team-dialog" info="createTeamInfo"
+         team-created="handleTeamCreated(team, currentPageContext)">
+    </div>
+
     <div class="dockerfile-build-dialog"
          show-now="showBuildDialogCounter"
          repository="currentPageContext.repository"
diff --git a/static/directives/repo-list-table.html b/static/directives/repo-list-table.html
index c79215a72..98d19af40 100644
--- a/static/directives/repo-list-table.html
+++ b/static/directives/repo-list-table.html
@@ -30,7 +30,7 @@
    </thead>
 
    <tbody>
-     <tr ng-repeat="repository in orderedRepositories">
+     <tr ng-repeat="repository in orderedRepositories.entries">
        <td class="repo-name-icon">
           <span class="avatar" size="24" data="getAvatarData(repository.namespace)"></span>
           <a href="/repository/{{ repository.namespace }}/{{ repository.name }}">
diff --git a/static/directives/repo-view/repo-panel-tags.html b/static/directives/repo-view/repo-panel-tags.html
index 4c5ddba14..387a2aff1 100644
--- a/static/directives/repo-view/repo-panel-tags.html
+++ b/static/directives/repo-view/repo-panel-tags.html
@@ -61,7 +61,7 @@
 
      <span class="co-filter-box">
       <span class="page-controls" total-count="tags.length" current-page="options.page" page-size="tagsPerPage"></span>
-      <input class="form-control" type="text" ng-model="options.tagFilter" placeholder="Filter Tags...">
+      <input class="form-control" type="text" ng-model="options.filter" placeholder="Filter Tags...">
      </span>
    </div>
 
diff --git a/static/directives/repository-permissions-table.html b/static/directives/repository-permissions-table.html
index eb0f114c7..a839617b1 100644
--- a/static/directives/repository-permissions-table.html
+++ b/static/directives/repository-permissions-table.html
@@ -106,6 +106,8 @@
         <tr class="add-row" ng-if-media="'(min-width: 768px)'">
           <td id="add-entity-permission" class="admin-search">
             <span class="entity-search" namespace="repository.namespace"
+                  for-repository="repository"
+                  skip-permissions="true"
                   placeholder="'Select a ' + (repository.is_organization ? 'team or ' : '') + 'user...'"
                   current-entity="addPermissionInfo.entity"></span>
           </td>
@@ -125,6 +127,8 @@
       <!-- Mobile add permissions -->
       <div class="mobile-add-row" ng-if-media="'(max-width: 767px)'">
         <span class="entity-search" namespace="repository.namespace"
+              for-repository="repository"
+              skip-permissions="true"
               placeholder="'Select a ' + (repository.is_organization ? 'team or ' : '') + 'user...'"
               current-entity="addPermissionInfo.entity"
               pull-right="true"></span>
diff --git a/static/directives/robots-manager.html b/static/directives/robots-manager.html
index d6f4ad755..22186560f 100644
--- a/static/directives/robots-manager.html
+++ b/static/directives/robots-manager.html
@@ -4,12 +4,9 @@
 
   <div ng-show="!loading">
     <div class="manager-header" header-title="Robot Accounts">
-      <span class="popup-input-button" pattern="ROBOT_PATTERN"
-            placeholder="'Robot Account Name'"
-            submitted="createRobot(value)"
-            ng-show="isEnabled">
-        <i class="fa fa-plus"></i> Create Robot Account
-      </span>
+      <button class="btn btn-primary" ng-click="askCreateRobot()" ng-show="isEnabled">
+        <i class="fa fa-plus" style="margin-right: 4px;"></i> Create Robot Account
+      </button>
     </div>
 
     <div class="section-description-header">
@@ -134,5 +131,6 @@
     </table>
   </div>
 
+  <div class="create-robot-dialog" info="createRobotInfo" robot-created="robotCreated()"></div>
   <div class="robot-credentials-dialog" info="robotDisplayInfo"></div>
 </div>
diff --git a/static/directives/teams-manager.html b/static/directives/teams-manager.html
index 72b459257..e17a938a8 100644
--- a/static/directives/teams-manager.html
+++ b/static/directives/teams-manager.html
@@ -13,23 +13,17 @@
         </button>
       </div>
     </div>
-
-    <span class="popup-input-button visible-xs" ng-if="!showingMembers"
-          pattern="TEAM_PATTERN" placeholder="'Team Name'"
-          submitted="createTeam(value)" ng-show="organization.is_admin">
-      <i class="fa fa-plus" style="margin-right: 6px;"></i> Create New Team
-    </span>
   </div>
 
   <!-- Teams List -->
   <div ng-show="!showingMembers">
     <div class="row" style="margin-left: 0px; margin-right: 0px;">
-      <span class="popup-input-button hidden-xs"
-            pattern="TEAM_PATTERN" placeholder="'Team Name'"
-            submitted="createTeam(value)" ng-show="organization.is_admin"
-            style="margin-bottom: 10px;">
-        <i class="fa fa-plus" style="margin-right: 6px;"></i> Create New Team
-      </span>
+      <button class="btn btn-primary hidden-xs"
+              ng-show="organization.is_admin"
+              style="margin-bottom: 10px; float: right;"
+              ng-click="askCreateTeam()">
+        <i class="fa fa-plus" style="margin-right: 4px;"></i> Create New Team
+      </button>
     </div>
 
     <div class="row hidden-xs">
@@ -155,6 +149,8 @@
     </table>
   </div>
 
+  <div class="create-team-dialog" info="createTeamInfo" team-created="handleTeamCreated(team)"></div>
+
   <!-- Remove member confirm -->
   <div class="cor-confirm-dialog"
      dialog-context="removeMemberInfo"
diff --git a/static/js/directives/repo-view/repo-panel-tags.js b/static/js/directives/repo-view/repo-panel-tags.js
index 6bbff1984..71568b11c 100644
--- a/static/js/directives/repo-view/repo-panel-tags.js
+++ b/static/js/directives/repo-view/repo-panel-tags.js
@@ -18,9 +18,7 @@ angular.module('quay').directive('repoPanelTags', function () {
 
       'getImages': '&getImages'
     },
-    controller: function($scope, $element, $filter, $location, ApiService, UIService, VulnerabilityService) {
-      var orderBy = $filter('orderBy');
-
+    controller: function($scope, $element, $filter, $location, ApiService, UIService, VulnerabilityService, TableService) {
       $scope.maxTrackCount = 5;
 
       $scope.checkedTags = UIService.createCheckStateController([], 'name');
@@ -45,32 +43,26 @@ angular.module('quay').directive('repoPanelTags', function () {
       var setTagState = function() {
         if (!$scope.repository || !$scope.selectedTags) { return; }
 
-        var tags = [];
+        // Build a list of all the tags, with extending information.
         var allTags = [];
-
-        // Build a list of tags and filtered tags.
         for (var tag in $scope.repository.tags) {
           if (!$scope.repository.tags.hasOwnProperty(tag)) { continue; }
 
           var tagData = $scope.repository.tags[tag];
           var tagInfo = $.extend(tagData, {
             'name': tag,
-            'last_modified_datetime': (new Date(tagData.last_modified || 0)).valueOf() * (-1)
+            'last_modified_datetime': TableService.getReversedTimestamp(tagData.last_modified)
           });
 
           allTags.push(tagInfo);
-
-          if (!$scope.options.tagFilter || tag.indexOf($scope.options.tagFilter) >= 0 ||
-              tagInfo.image_id.indexOf($scope.options.tagFilter) >= 0) {
-            tags.push(tagInfo);
-          }
         }
 
         // Sort the tags by the predicate and the reverse, and map the information.
         var imageIDs = [];
-        var ordered = orderBy(tags, $scope.options.predicate, $scope.options.reverse);
-        var checked = [];
+        var ordered = TableService.buildOrderedItems(allTags, $scope.options,
+            ['name'], ['last_modified_datetime', 'size']).entries;
 
+        var checked = [];
         var imageMap = {};
         var imageIndexMap = {};
         for (var i = 0; i < ordered.length; ++i) {
@@ -175,7 +167,7 @@ angular.module('quay').directive('repoPanelTags', function () {
 
       $scope.$watch('options.predicate', setTagState);
       $scope.$watch('options.reverse', setTagState);
-      $scope.$watch('options.tagFilter', setTagState);
+      $scope.$watch('options.filter', setTagState);
 
       $scope.$watch('options.page', function(page) {
         if (page != null && $scope.checkedTags) {
diff --git a/static/js/directives/ui/create-entity-dialog.js b/static/js/directives/ui/create-entity-dialog.js
new file mode 100644
index 000000000..781e07747
--- /dev/null
+++ b/static/js/directives/ui/create-entity-dialog.js
@@ -0,0 +1,202 @@
+/**
+ * An element which displays a create entity dialog.
+ */
+angular.module('quay').directive('createEntityDialog', function () {
+  var directiveDefinitionObject = {
+    priority: 0,
+    templateUrl: '/static/directives/create-entity-dialog.html',
+    replace: false,
+    transclude: true,
+    restrict: 'C',
+    scope: {
+      'info': '=info',
+
+      'entityKind': '@entityKind',
+      'entityTitle': '@entityTitle',
+      'entityIcon': '@entityIcon',
+      'entityNameRegex': '@entityNameRegex',
+
+      'entityCreateRequested': '&entityCreateRequested',
+      'entityCreateCompleted': '&entityCreateCompleted'
+    },
+
+    controller: function($scope, $element, ApiService, UIService, TableService, RolesService, UserService) {
+      $scope.TableService = TableService;
+
+      $scope.options = {
+        'predicate': 'last_modified_datetime',
+        'reverse': false,
+        'filter': ''
+      };
+
+      var handleRepoCheckChange = function() {
+        $scope.repositories.forEach(function(repo) {
+          if ($scope.checkedRepos.isChecked(repo)) {
+            if (repo['permission'] == 'none') {
+              repo['permission'] = 'read';
+            }
+          } else {
+            repo['permission'] = 'none';
+          }
+        });
+      };
+
+      $scope.$on('$destroy', function() {
+        if ($scope.inBody) {
+          document.body.removeChild($element[0]);
+        }
+      });
+
+      $scope.setRole = function(role, repo) {
+        repo['permission'] = role;
+
+        if (role == 'none') {
+          $scope.checkedRepos.uncheckItem(repo);
+        } else {
+          $scope.checkedRepos.checkItem(repo);
+        }
+      };
+
+      $scope.hide = function() {
+        $element.find('.modal').modal('hide');
+        if ($scope.entity) {
+          $scope.entityCreateCompleted({'entity': $scope.entity});
+          $scope.entity = null;
+        }
+      };
+
+      $scope.show = function() {
+        $scope.entityName = null;
+        $scope.entity = null;
+        $scope.creating = false;
+        $scope.view = 'enterName';
+        $scope.enterNameForm.$setPristine(true);
+
+        // Move the dialog to the body to prevent it from nesting if called
+        // from within another dialog.
+        $element.find('.modal').modal({});
+        $scope.inBody = true;
+        document.body.appendChild($element[0]);
+      };
+
+      var setRepoState = function() {
+        if (!$scope.repositories) {
+          return;
+        }
+
+        $scope.orderedRepositories = TableService.buildOrderedItems($scope.repositories, $scope.options,
+            ['name', 'permission'],
+            ['last_modified_datetime']);
+      };
+
+      var entityCreateCallback = function(entity) {
+        if (!entity || $scope.info.skip_permissions) {
+          $scope.entity = entity;
+          $scope.hide();
+          return;
+        }
+
+        // Load the repositories under the entity's namespace.
+        var params = {
+          'namespace': $scope.info.namespace,
+          'last_modified': true
+        };
+
+        ApiService.listRepos(null, params).then(function(resp) {
+          $scope.view = 'addperms';
+          $scope.entity = entity;
+
+          var repos = [];
+          resp['repositories'].forEach(function(repo) {
+            repos.push({
+              'namespace': repo.namespace,
+              'name': repo.name,
+              'last_modified': repo.last_modified,
+              'last_modified_datetime': TableService.getReversedTimestamp(repo.last_modified),
+              'permission': 'none'
+            });
+          });
+
+          if (repos.length == 0) {
+            $scope.hide();
+            return;
+          }
+
+          $scope.repositories = repos;
+          $scope.checkedRepos = UIService.createCheckStateController($scope.repositories, 'name');
+          $scope.checkedRepos.listen(handleRepoCheckChange);
+
+          if ($scope.info.repository) {
+            repos.forEach(function(repo) {
+              if (repo['namespace'] == $scope.info.repository.namespace &&
+                  repo['name'] == $scope.info.repository.name) {
+                $scope.checkedRepos.checkItem(repo);
+                $scope.options.filter = $scope.info.repository.name;
+              }
+            });
+          }
+
+          setRepoState();
+        }, ApiService.errorDisplay('Could not load repositories'));
+      };
+
+      $scope.addPermissions = function() {
+        $scope.view = 'addingperms';
+
+        var repos = $scope.checkedRepos.checked;
+        var counter = 0;
+
+        var addPerm = function() {
+          if (counter >= repos.length) {
+            $scope.hide();
+            return;
+          }
+
+          var repo = repos[counter];
+          RolesService.setRepositoryRole(repo, repo.permission, $scope.entityKind, $scope.entity.name,
+            function(status) {
+              if (status) {
+                counter++;
+                addPerm();
+              } else {
+                $scope.hide();
+              }
+            });
+        };
+
+        addPerm();
+      };
+
+      $scope.createEntity = function() {
+        $scope.view = 'creating';
+        $scope.entityCreateRequested({
+          'name': $scope.entityName,
+          'callback': entityCreateCallback
+        });
+      };
+
+      $scope.$watch('options.predicate', setRepoState);
+      $scope.$watch('options.reverse', setRepoState);
+      $scope.$watch('options.filter', setRepoState);
+
+      $scope.$watch('entityNameRegex', function(r) {
+        if (r) {
+          $scope.entityNameRegexObj = new RegExp(r);
+        }
+      });
+
+      $scope.$watch('info', function(info) {
+        if (!info || !info.namespace) {
+          $scope.hide();
+          return;
+        }
+
+        $scope.namespace = UserService.getNamespace(info.namespace);
+        if ($scope.namespace) {
+          $scope.show();
+        }
+      });
+    }
+  };
+  return directiveDefinitionObject;
+});
\ No newline at end of file
diff --git a/static/js/directives/ui/create-robot-dialog.js b/static/js/directives/ui/create-robot-dialog.js
new file mode 100644
index 000000000..59beecc90
--- /dev/null
+++ b/static/js/directives/ui/create-robot-dialog.js
@@ -0,0 +1,43 @@
+/**
+ * An element which displays a dialog for creating a robot account.
+ */
+angular.module('quay').directive('createRobotDialog', function () {
+  var directiveDefinitionObject = {
+    priority: 0,
+    templateUrl: '/static/directives/create-robot-dialog.html',
+    replace: false,
+    transclude: true,
+    restrict: 'C',
+    scope: {
+      'info': '=info',
+      'robotCreated': '&robotCreated'
+    },
+    controller: function($scope, $element, ApiService, UserService) {
+      $scope.ROBOT_PATTERN = ROBOT_PATTERN;
+
+      $scope.robotFinished = function(robot) {
+        $scope.robotCreated({'robot': robot});
+      };
+
+      $scope.createRobot = function(name, callback) {
+        var organization = $scope.info.namespace;
+        if (!UserService.isOrganization(organization)) {
+          organization = null;
+        }
+
+        var params = {
+          'robot_shortname': name
+        };
+
+        var errorDisplay = ApiService.errorDisplay('Cannot create robot account', function() {
+          callback(null);
+        });
+
+        ApiService.createRobot(organization, null, params).then(function(resp) {
+          callback(resp);
+        }, errorDisplay);
+      };
+    }
+  };
+  return directiveDefinitionObject;
+});
\ No newline at end of file
diff --git a/static/js/directives/ui/create-team-dialog.js b/static/js/directives/ui/create-team-dialog.js
new file mode 100644
index 000000000..4d63e57b8
--- /dev/null
+++ b/static/js/directives/ui/create-team-dialog.js
@@ -0,0 +1,44 @@
+/**
+ * An element which displays a dialog for creating a team.
+ */
+angular.module('quay').directive('createTeamDialog', function () {
+  var directiveDefinitionObject = {
+    priority: 0,
+    templateUrl: '/static/directives/create-team-dialog.html',
+    replace: false,
+    transclude: true,
+    restrict: 'C',
+    scope: {
+      'info': '=info',
+      'teamCreated': '&teamCreated'
+    },
+    controller: function($scope, $element, ApiService, UserService) {
+      $scope.TEAM_PATTERN = TEAM_PATTERN;
+
+      $scope.teamFinished = function(team) {
+        $scope.teamCreated({'team': team});
+      };
+
+      $scope.createTeam = function(name, callback) {
+        var data = {
+          'name': name,
+          'role': 'member'
+        };
+
+        var params = {
+          'orgname': $scope.info.namespace,
+          'teamname': name
+        };
+
+        var errorDisplay = ApiService.errorDisplay('Cannot create team', function() {
+          callback(null);
+        });
+
+        ApiService.updateOrganizationTeam(data, params).then(function(resp) {
+          callback(resp);
+        }, errorDisplay);
+      };
+    }
+  };
+  return directiveDefinitionObject;
+});
\ No newline at end of file
diff --git a/static/js/directives/ui/entity-search.js b/static/js/directives/ui/entity-search.js
index 565103ca0..874908848 100644
--- a/static/js/directives/ui/entity-search.js
+++ b/static/js/directives/ui/entity-search.js
@@ -18,6 +18,8 @@ angular.module('quay').directive('entitySearch', function () {
     scope: {
       'namespace': '=namespace',
       'placeholder': '=placeholder',
+      'forRepository': '=forRepository',
+      'skipPermissions': '=skipPermissions',
 
       // Default: ['user', 'team', 'robot']
       'allowedEntities': '=allowedEntities',
@@ -41,7 +43,7 @@ angular.module('quay').directive('entitySearch', function () {
       // True if the menu should pull right.
       'pullRight': '@pullRight'
     },
-    controller: function($rootScope, $scope, $element, Restangular, UserService, ApiService, UtilService, Config, CreateService) {
+    controller: function($rootScope, $scope, $element, Restangular, UserService, ApiService, UtilService, Config) {
       $scope.lazyLoading = true;
 
       $scope.teams = null;
@@ -55,6 +57,8 @@ angular.module('quay').directive('entitySearch', function () {
       $scope.includeOrgs = false;
 
       $scope.currentEntityInternal = $scope.currentEntity;
+      $scope.createRobotInfo = null;
+      $scope.createTeamInfo = null;
 
       $scope.Config = Config;
 
@@ -91,18 +95,30 @@ angular.module('quay').directive('entitySearch', function () {
         }
       };
 
-      $scope.createTeam = function() {
-        CreateService.askCreateTeam($scope.namespace, function(created) {
-          $scope.setEntity(created.name, 'team', false, created.avatar);
-          $scope.teams[created.name] = created;
-        });
+      $scope.askCreateTeam = function() {
+        $scope.createTeamInfo = {
+          'namespace': $scope.namespace,
+          'repository': $scope.forRepository,
+          'skip_permissions': $scope.skipPermissions
+        };
       };
 
-      $scope.createRobot = function() {
-        CreateService.askCreateRobot($scope.namespace, function(created) {
-          $scope.setEntity(created.name, 'user', true, created.avatar);
-          $scope.robots.push(created);
-        });
+      $scope.askCreateRobot = function() {
+        $scope.createRobotInfo = {
+          'namespace': $scope.namespace,
+          'repository': $scope.forRepository,
+          'skip_permissions': $scope.skipPermissions
+        };
+      };
+
+      $scope.handleTeamCreated = function(created) {
+        $scope.setEntity(created.name, 'team', false, created.avatar);
+        $scope.teams[created.name] = created;
+      };
+
+      $scope.handleRobotCreated = function(created) {
+        $scope.setEntity(created.name, 'user', true, created.avatar);
+        $scope.robots.push(created);
       };
 
       $scope.setEntity = function(name, kind, is_robot, avatar) {
diff --git a/static/js/directives/ui/header-bar.js b/static/js/directives/ui/header-bar.js
index 3617fdcdd..8b88d886f 100644
--- a/static/js/directives/ui/header-bar.js
+++ b/static/js/directives/ui/header-bar.js
@@ -13,7 +13,7 @@ angular.module('quay').directive('headerBar', function () {
     scope: {
     },
     controller: function($rootScope, $scope, $element, $location, $timeout, hotkeys, UserService,
-          PlanService, ApiService, NotificationService, Config, CreateService, Features,
+          PlanService, ApiService, NotificationService, Config, Features,
           DocumentationService, ExternalLoginService) {
 
       $scope.externalSigninUrl = ExternalLoginService.getSingleSigninUrl();
@@ -268,24 +268,36 @@ angular.module('quay').directive('headerBar', function () {
         $location.url('/repository/' + context.repository.namespace + '/' + context.repository.name + '/build/' + build.id);
       };
 
-      $scope.createRobot = function(context) {
+      $scope.handleRobotCreated = function(created, context) {
         var namespace = $scope.getNamespace(context);
-        CreateService.askCreateRobot(namespace, function(created) {
-          if (UserService.isOrganization(namespace)) {
-            $location.url('/organization/' + namespace + '?tab=robots&showRobot=' + created.name);
-          } else {
-            $location.url('/user/' + namespace + '?tab=robots&showRobot=' + created.name);
-          }
-        });
+        if (UserService.isOrganization(namespace)) {
+          $location.url('/organization/' + namespace + '?tab=robots&showRobot=' + created.name);
+        } else {
+          $location.url('/user/' + namespace + '?tab=robots&showRobot=' + created.name);
+        }
       };
 
-      $scope.createTeam = function(context) {
+      $scope.handleTeamCreated = function(created, context) {
+        var namespace = $scope.getNamespace(context);
+        $location.url('/organization/' + namespace + '/teams/' + created.name);
+      };
+
+      $scope.askCreateRobot = function(context) {
         var namespace = $scope.getNamespace(context);
         if (!namespace || !UserService.isNamespaceAdmin(namespace)) { return; }
 
-        CreateService.askCreateTeam(namespace, function(created) {
-            $location.url('/organization/' + namespace + '/teams/' + created.name);
-        });
+        $scope.createRobotInfo = {
+          'namespace': namespace
+        };
+      };
+
+      $scope.askCreateTeam = function(context) {
+        var namespace = $scope.getNamespace(context);
+        if (!namespace || !UserService.isNamespaceAdmin(namespace)) { return; }
+
+        $scope.createTeamInfo = {
+          'namespace': namespace
+        };
       };
     }
   };
diff --git a/static/js/directives/ui/repo-list-table.js b/static/js/directives/ui/repo-list-table.js
index 7a5e98e53..4b9b59fdd 100644
--- a/static/js/directives/ui/repo-list-table.js
+++ b/static/js/directives/ui/repo-list-table.js
@@ -13,30 +13,22 @@ angular.module('quay').directive('repoListTable', function () {
       'namespaces': '=namespaces',
       'starToggled': '&starToggled'
     },
-    controller: function($scope, $element, $filter) {
-      var orderBy = $filter('orderBy');
-
+    controller: function($scope, $element, $filter, TableService) {
       $scope.repositories = null;
       $scope.orderedRepositories = [];
 
       $scope.maxPopularity = 0;
       $scope.options = {
         'predicate': 'popularity',
-        'reverse': true
+        'reverse': false,
+        'filter': null
       };
 
       var buildOrderedRepositories = function() {
         if (!$scope.repositories) { return; }
-        var modifier = $scope.options.reverse ? '-' : '';
-        var fields = [modifier + $scope.options.predicate];
 
-        // Secondary ordering by full name.
-        if ($scope.options.predicate != 'full_name') {
-          fields.push('full_name');
-        }
-
-        var ordered = orderBy($scope.repositories, fields, false);
-        $scope.orderedRepositories = ordered;
+        $scope.orderedRepositories = TableService.buildOrderedItems($scope.repositories, $scope.options,
+            [], ['last_modified_datetime', 'popularity'])
       };
 
       $scope.tablePredicateClass = function(name, predicate, reverse) {
@@ -92,7 +84,7 @@ angular.module('quay').directive('repoListTable', function () {
           (resource.value || []).forEach(function(repository) {
             var repositoryInfo = $.extend(repository, {
               'full_name': repository.namespace + '/' + repository.name,
-              'last_modified_datetime': (new Date(repository.last_modified || 0)).valueOf() * (-1)
+              'last_modified_datetime': TableService.getReversedTimestamp(repository.last_modified),
             });
 
             $scope.repositories.push(repositoryInfo);
diff --git a/static/js/directives/ui/repository-permissions-table.js b/static/js/directives/ui/repository-permissions-table.js
index a4e98b361..a832ac207 100644
--- a/static/js/directives/ui/repository-permissions-table.js
+++ b/static/js/directives/ui/repository-permissions-table.js
@@ -28,7 +28,7 @@ angular.module('quay').directive('repositoryPermissionsTable', function () {
       'repository': '=repository',
       'isEnabled': '=isEnabled'
     },
-    controller: function($scope, $element, ApiService, Restangular, UtilService, RolesService) {
+    controller: function($scope, $element, ApiService, RolesService) {
       $scope.permissionResources = {'team': {}, 'user': {}};
       $scope.permissionCache = {};
       $scope.permissions = {};
@@ -69,13 +69,6 @@ angular.module('quay').directive('repositoryPermissionsTable', function () {
 
       loadAllPermissions();
 
-      var getPermissionEndpoint = function(entityName, kind) {
-        var namespace = $scope.repository.namespace;
-        var name = $scope.repository.name;
-        var url = UtilService.getRestUrl('repository', namespace, name, 'permissions', kind, entityName);
-        return Restangular.one(url);
-      };
-
       $scope.buildEntityForPermission = function(permission, kind) {
         var key = permission.name + ':' + kind;
         if ($scope.permissionCache[key]) {
@@ -146,51 +139,36 @@ angular.module('quay').directive('repositoryPermissionsTable', function () {
       };
 
       $scope.deleteRole = function(entityName, kind) {
-        var errorHandler = ApiService.errorDisplay('Cannot change permission', function(resp) {
-          if (resp.status == 409) {
-            return 'Cannot change permission as you do not have the authority';
+        RolesService.deleteRepositoryRole($scope.repository, kind, entityName, function(status) {
+          if (status) {
+            delete $scope.permissions[kind][entityName];
           }
         });
-
-        var endpoint = getPermissionEndpoint(entityName, kind);
-        endpoint.customDELETE().then(function() {
-          delete $scope.permissions[kind][entityName];
-        }, errorHandler);
       };
 
       $scope.addRole = function(entityName, role, kind, opt_callback) {
-        var permission = {
-          'role': role,
-        };
-
-        var errorHandler = ApiService.errorDisplay('Cannot change permission', function() {
-          opt_callback && opt_callback(false);
+        RolesService.setRepositoryRole($scope.repository, role, kind, entityName, function(status, result) {
           $scope.addPermissionInfo = {
             'role': readRole
           };
+
+          if (status) {
+            $scope.permissions[kind][entityName] = result;
+          }
+
+          opt_callback && opt_callback(status);
         });
-
-        var endpoint = getPermissionEndpoint(entityName, kind);
-        endpoint.customPUT(permission).then(function(result) {
-          $scope.permissions[kind][entityName] = result;
-          $scope.addPermissionInfo = {
-            'role': readRole
-          };
-          opt_callback && opt_callback(true)
-        }, errorHandler);
       };
 
       $scope.setRole = function(role, entityName, kind) {
-        var errorDisplay = ApiService.errorDisplay(function(resp) {
-          $scope.permissions[kind][entityName] = {'role': currentRole};
+        var currentRole = $scope.permissions[kind][entityName].role;
+        RolesService.setRepositoryRole($scope.repository, role, kind, entityName, function(status) {
+          if (status) {
+            $scope.permissions[kind][entityName]['role'] = role;
+          } else {
+            $scope.permissions[kind][entityName]['role'] = currentRole;
+          }
         });
-
-        var permission = $scope.permissions[kind][entityName];
-        var currentRole = permission.role;
-        permission.role = role;
-
-        var endpoint = getPermissionEndpoint(entityName, kind);
-        endpoint.customPUT(permission).then(function() {}, errorDisplay);
       };
     }
   };
diff --git a/static/js/directives/ui/robots-manager.js b/static/js/directives/ui/robots-manager.js
index 0f1e9ded1..d34833f69 100644
--- a/static/js/directives/ui/robots-manager.js
+++ b/static/js/directives/ui/robots-manager.js
@@ -13,15 +13,14 @@ angular.module('quay').directive('robotsManager', function () {
       'user': '=user',
       'isEnabled': '=isEnabled'
     },
-    controller: function($scope, $element, ApiService, $routeParams, $location, CreateService,
-                         Config, $rootScope) {
-      $scope.ROBOT_PATTERN = ROBOT_PATTERN;
-
+    controller: function($scope, $element, ApiService, $routeParams, $location, Config, $rootScope) {
       $scope.robots = null;
       $scope.loading = false;
       $scope.Config = Config;
       $scope.feedback = null;
+
       $scope.robotDisplayInfo = null;
+      $scope.createRobotInfo = null;
 
       // Listen for route changes and update the tabs accordingly.
       var locationListener = $rootScope.$on('$routeUpdate', function(){
@@ -96,22 +95,10 @@ angular.module('quay').directive('robotsManager', function () {
         return name.substr(0, plus);
       };
 
-      $scope.createRobot = function(name) {
-        if (!name) { return; }
-
-        CreateService.createRobotAccount(ApiService, !!$scope.organization, $scope.organization ? $scope.organization.name : '', name,
-          function(created) {
-            created.teams = [];
-            created.repositories = [];
-            $scope.robots.push(created);
-            $scope.feedback = {
-              'kind': 'success',
-              'message': 'Robot account {robot} was created',
-              'data': {
-                'robot': name
-              }
-            };
-          });
+      $scope.askCreateRobot = function() {
+        $scope.createRobotInfo = {
+          'namespace': $scope.organization ? $scope.organization.name : $scope.user.username
+        };
       };
 
       $scope.deleteRobot = function(info) {
@@ -131,7 +118,6 @@ angular.module('quay').directive('robotsManager', function () {
         }, ApiService.errorDisplay('Cannot delete robot account'));
       };
 
-
       $scope.askDeleteRobot = function(info) {
         bootbox.confirm('Are you sure you want to delete robot ' + info.name + '?', function(resp) {
           if (resp) {
@@ -140,6 +126,10 @@ angular.module('quay').directive('robotsManager', function () {
         });
       };
 
+      $scope.robotCreated = function() {
+        update();
+      };
+
       var update = function() {
         if (!$scope.user && !$scope.organization) { return; }
         if ($scope.loading || !$scope.isEnabled) { return; }
diff --git a/static/js/directives/ui/teams-manager.js b/static/js/directives/ui/teams-manager.js
index 9e67ca853..80a09efe1 100644
--- a/static/js/directives/ui/teams-manager.js
+++ b/static/js/directives/ui/teams-manager.js
@@ -12,8 +12,7 @@ angular.module('quay').directive('teamsManager', function () {
       'organization': '=organization',
       'isEnabled': '=isEnabled'
     },
-    controller: function($scope, $element, ApiService, CreateService, $timeout, UserService) {
-      $scope.TEAM_PATTERN = TEAM_PATTERN;
+    controller: function($scope, $element, ApiService, $timeout, UserService) {
       $scope.teamRoles = [
           { 'id': 'member', 'title': 'Member', 'kind': 'default' },
           { 'id': 'creator', 'title': 'Creator', 'kind': 'success' },
@@ -27,6 +26,7 @@ angular.module('quay').directive('teamsManager', function () {
       $scope.showingMembers = false;
       $scope.fullMemberList = null;
       $scope.feedback = null;
+      $scope.createTeamInfo = null;
 
       var loadTeamMembers = function() {
         if (!$scope.organization || !$scope.isEnabled) { return; }
@@ -107,35 +107,27 @@ angular.module('quay').directive('teamsManager', function () {
         }, errorHandler);
       };
 
-      $scope.createTeam = function(teamname) {
-        if (!teamname) {
-          return;
-        }
+      $scope.askCreateTeam = function(teamname) {
+        $scope.createTeamInfo = {
+          'namespace': $scope.organization.name
+        };
+      };
 
-        if ($scope.organization.teams[teamname]) {
-          $('#team-' + teamname).removeClass('highlight');
-          setTimeout(function() {
-            $('#team-' + teamname).addClass('highlight');
-          }, 10);
-          return;
-        }
+      $scope.handleTeamCreated = function(created) {
+        var teamname = created.name;
+        $scope.organization.teams[teamname] = created;
+        $scope.members[teamname] = {};
+        $scope.members[teamname].members = [];
+        $scope.organization.ordered_teams.push(teamname);
+        $scope.orderedTeams.push(created);
 
-        var orgname = $scope.organization.name;
-        CreateService.createOrganizationTeam(ApiService, orgname, teamname, function(created) {
-          $scope.organization.teams[teamname] = created;
-          $scope.members[teamname] = {};
-          $scope.members[teamname].members = [];
-          $scope.organization.ordered_teams.push(teamname);
-          $scope.orderedTeams.push(created);
-
-          $scope.feedback = {
-            'kind': 'success',
-            'message': 'Team {team} created',
-            'data': {
-              'team': teamname
-            }
-          };
-        });
+        $scope.feedback = {
+          'kind': 'success',
+          'message': 'Team {team} created',
+          'data': {
+            'team': teamname
+          }
+        };
       };
 
       $scope.askDeleteTeam = function(teamname) {
diff --git a/static/js/pages/user-view.js b/static/js/pages/user-view.js
index 92efb21d6..30cc4d719 100644
--- a/static/js/pages/user-view.js
+++ b/static/js/pages/user-view.js
@@ -41,6 +41,7 @@
     var loadUser = function() {
       $scope.userResource = ApiService.getUserInformationAsResource({'username': username}).get(function(user) {
         $scope.context.viewuser = user;
+        $scope.viewuser = user;
 
         // Load the repositories.
         $timeout(function() {
diff --git a/static/js/services/create-service.js b/static/js/services/create-service.js
deleted file mode 100644
index d52d3cf92..000000000
--- a/static/js/services/create-service.js
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * Service which exposes various methods for creating entities on the backend.
- */
-angular.module('quay').factory('CreateService', ['ApiService', 'UserService', function(ApiService, UserService) {
-  var createService = {};
-
-  createService.createRobotAccount = function(ApiService, is_org, orgname, name, callback) {
-    ApiService.createRobot(is_org ? orgname : null, null, {'robot_shortname': name})
-              .then(callback, ApiService.errorDisplay('Cannot create robot account'));
-  };
-
-  createService.createOrganizationTeam = function(ApiService, orgname, teamname, callback) {
-    var data = {
-      'name': teamname,
-      'role': 'member'
-    };
-
-    var params = {
-      'orgname': orgname,
-      'teamname': teamname
-    };
-
-    ApiService.updateOrganizationTeam(data, params)
-              .then(callback, ApiService.errorDisplay('Cannot create team'));
-  };
-
-  createService.askCreateRobot = function(namespace, callback) {
-    if (!namespace || !UserService.isNamespaceAdmin(namespace)) { return; }
-
-    var isorg = UserService.isOrganization(namespace);
-    bootbox.prompt('Enter the name of the new robot account', function(robotname) {
-      if (!robotname) { return; }
-
-      var regex = new RegExp(ROBOT_PATTERN);
-      if (!regex.test(robotname)) {
-        bootbox.alert('Invalid robot account name');
-        return;
-      }
-
-      createService.createRobotAccount(ApiService, isorg, namespace, robotname, callback);
-    });
-  };
-
-  createService.askCreateTeam = function(namespace, callback) {
-    if (!namespace || !UserService.isNamespaceAdmin(namespace)) { return; }
-
-    bootbox.prompt('Enter the name of the new team', function(teamname) {
-      if (!teamname) { return; }
-
-      var regex = new RegExp(TEAM_PATTERN);
-      if (!regex.test(teamname)) {
-        bootbox.alert('Invalid team name');
-        return;
-      }
-
-      createService.createOrganizationTeam(ApiService, namespace, teamname,  callback);
-    });
-  };
-
-  return createService;
-}]);
diff --git a/static/js/services/roles-service.js b/static/js/services/roles-service.js
index bf83e0f77..2f9854d51 100644
--- a/static/js/services/roles-service.js
+++ b/static/js/services/roles-service.js
@@ -1,20 +1,57 @@
 /**
  * Service which defines the various role groups.
  */
-angular.module('quay').factory('RolesService', [function() {
+angular.module('quay').factory('RolesService', ['UtilService', 'Restangular', 'ApiService', function(UtilService, Restangular, ApiService) {
   var roleService = {};
 
-  roleService.repoRoles = [
+  roleService.repoRolesOrNone = [
+    { 'id': 'none', 'title': 'None', 'kind': 'default', 'description': 'No permissions on the repository' },
+
     { 'id': 'read', 'title': 'Read', 'kind': 'success', 'description': 'Can view and pull from the repository' },
     { 'id': 'write', 'title': 'Write', 'kind': 'success', 'description': 'Can view, pull and push to the repository' },
     { 'id': 'admin', 'title': 'Admin', 'kind': 'primary', 'description': 'Full admin access, pull and push on the repository' }
   ];
 
+  roleService.repoRoles = roleService.repoRolesOrNone.slice(1);
+
   roleService.teamRoles = [
     { 'id': 'member', 'title': 'Member', 'kind': 'default', 'description': 'Inherits all permissions of the team' },
     { 'id': 'creator', 'title': 'Creator', 'kind': 'success', 'description': 'Member and can create new repositories' },
     { 'id': 'admin', 'title': 'Admin', 'kind': 'primary', 'description': 'Full admin access to the organization' }
   ];
 
+  var getPermissionEndpoint = function(repository, entityName, kind) {
+    var namespace = repository.namespace;
+    var name = repository.name;
+    var url = UtilService.getRestUrl('repository', namespace, name, 'permissions', kind, entityName);
+    return Restangular.one(url);
+  };
+
+  roleService.deleteRepositoryRole = function(repository, entityKind, entityName, callback) {
+    var errorDisplay = ApiService.errorDisplay('Cannot change permission', function(resp) {
+      callback(false);
+    });
+
+    var endpoint = getPermissionEndpoint(repository, entityName, kind);
+    endpoint.customDELETE().then(function() {
+      callback(true);
+    }, errorHandler);
+  };
+
+  roleService.setRepositoryRole = function(repository, role, entityKind, entityName, callback) {
+    var errorDisplay = ApiService.errorDisplay('Cannot change permission', function(resp) {
+      callback(false);
+    });
+
+    var permission = {
+      'role': role
+    };
+
+    var endpoint = getPermissionEndpoint(repository, entityName, entityKind);
+    endpoint.customPUT(permission).then(function(resp) {
+      callback(true, resp);
+    }, errorDisplay);
+  };
+
   return roleService;
 }]);
diff --git a/static/js/services/table-service.js b/static/js/services/table-service.js
index 08fbf9d8d..37a2a4e8e 100644
--- a/static/js/services/table-service.js
+++ b/static/js/services/table-service.js
@@ -22,6 +22,14 @@ angular.module('quay').factory('TableService', ['AngularViewArray', function(Ang
     options.predicate = predicate;
   };
 
+  tableService.getReversedTimestamp = function(datetime) {
+    if (!datetime) {
+      return -Number.MAX_VALUE;
+    }
+
+    return (new Date(datetime)).valueOf() * (-1);
+  };
+
   tableService.buildOrderedItems = function(items, options, filterFields, numericFields, opt_extrafilter) {
     var orderedItems = AngularViewArray.create();
 
diff --git a/static/js/services/ui-service.js b/static/js/services/ui-service.js
index 5bf0726dc..126a1c216 100644
--- a/static/js/services/ui-service.js
+++ b/static/js/services/ui-service.js
@@ -82,7 +82,7 @@ angular.module('quay').factory('UIService', ['$timeout', '$rootScope', '$locatio
   CheckStateController.prototype.rebuildCheckedList_ = function() {
     var that = this;
     this.checked = [];
-    this.items.forEach(function(item) {
+    this.allItems_.forEach(function(item) {
       if (that.allCheckedMap_[item[that.itemKey_]]) {
         that.checked.push(item);
       }
diff --git a/static/js/services/user-service.js b/static/js/services/user-service.js
index d6bce37cc..aa3d8f424 100644
--- a/static/js/services/user-service.js
+++ b/static/js/services/user-service.js
@@ -122,6 +122,19 @@ function(ApiService, CookieService, $rootScope, Config) {
     return !!org;
   };
 
+  userService.getNamespace = function(namespace) {
+    var org = userService.getOrganization(namespace);
+    if (org) {
+      return org;
+    }
+
+    if (namespace == userResponse.username) {
+      return userResponse;
+    }
+
+    return null;
+  };
+
   userService.currentUser = function() {
     return userResponse;
   };