diff --git a/data/model/legacy.py b/data/model/legacy.py
index b31787d33..e84535ec6 100644
--- a/data/model/legacy.py
+++ b/data/model/legacy.py
@@ -679,34 +679,27 @@ def get_user_or_org_by_customer_id(customer_id):
except User.DoesNotExist:
return None
-def get_matching_user_entities(entity_prefix, user):
+def get_matching_entities(entity_prefix):
matching_user_orgs = ((User.username ** (entity_prefix + '%')) & (User.robot == False))
-
- if user is not None:
- matching_robots = ((User.username ** (user.username + '+%' + entity_prefix + '%')) &
- (User.robot == True))
- else:
- matching_robots = False
+ matching_robots = ((User.username ** ('%+%' + entity_prefix + '%')) & (User.robot == True))
query = (User.select()
- .where(matching_user_orgs | matching_robots)
- .limit(10))
+ .where(matching_user_orgs | matching_robots))
return query
-def get_matching_user_teams(team_prefix, user):
+def get_matching_user_teams(team_prefix, user, limit=10):
query = (Team.select()
.join(User)
.switch(Team)
.join(TeamMember)
.where(TeamMember.user == user, Team.name ** (team_prefix + '%'))
.distinct(Team.id)
- .limit(10))
+ .limit(limit))
return query
-
-def get_matching_admined_teams(team_prefix, user):
+def get_matching_admined_teams(team_prefix, user, limit=10):
admined_orgs = (get_user_organizations(user.username)
.switch(Team)
.join(TeamRole)
@@ -718,7 +711,7 @@ def get_matching_admined_teams(team_prefix, user):
.join(TeamMember)
.where(Team.name ** (team_prefix + '%'), Team.organization << (admined_orgs))
.distinct(Team.id)
- .limit(10))
+ .limit(limit))
return query
@@ -968,7 +961,7 @@ def _get_public_repo_visibility():
return _public_repo_visibility_cache
-def get_matching_repositories(repo_term, username=None):
+def get_matching_repositories(repo_term, username=None, limit=10):
namespace_term = repo_term
name_term = repo_term
@@ -986,7 +979,7 @@ def get_matching_repositories(repo_term, username=None):
search_clauses = (Repository.name ** ('%' + name_term + '%') &
Namespace.username ** ('%' + namespace_term + '%'))
- return visible.where(search_clauses).limit(10)
+ return visible.where(search_clauses).limit(limit)
def get_repository_pull_counts(repositories):
repo_pull = LogEntryKind.get(name = 'pull_repo')
diff --git a/endpoints/api/search.py b/endpoints/api/search.py
index 37a597af6..9e835445e 100644
--- a/endpoints/api/search.py
+++ b/endpoints/api/search.py
@@ -9,6 +9,7 @@ from auth import scopes
from app import avatar, get_app_url
from operator import itemgetter
from stringscore import liquidmetal
+from util.names import parse_robot_username
import math
@@ -130,8 +131,13 @@ class ConductSearch(ApiResource):
avatar_data = avatar.get_data_for_org(entity)
href = '/organization/' + entity.username
elif entity.robot:
+ parts = parse_robot_username(entity.username)
+ if parts[0] == username:
+ href = '/user/' + username + '?tab=robots&showRobot=' + entity.username
+ else:
+ href = '/organization/' + parts[0] + '?tab=robots&showRobot=' + entity.username
+
kind = 'robot'
- href = '/user?tab=robots'
avatar_data = None
return {
@@ -148,7 +154,7 @@ class ConductSearch(ApiResource):
# Find the matching teams where the user is a member.
encountered_teams = set()
- matching_teams = model.get_matching_user_teams(query, get_authenticated_user())
+ matching_teams = model.get_matching_user_teams(query, get_authenticated_user(), limit=5)
for team in matching_teams:
if team.id in encountered_teams:
continue
@@ -165,7 +171,7 @@ class ConductSearch(ApiResource):
})
# Find matching teams in orgs admined by the user.
- matching_teams = model.get_matching_admined_teams(query, get_authenticated_user())
+ matching_teams = model.get_matching_admined_teams(query, get_authenticated_user(), limit=5)
for team in matching_teams:
if team.id in encountered_teams:
continue
@@ -183,7 +189,7 @@ class ConductSearch(ApiResource):
# Find the matching repositories.
- matching_repos = model.get_matching_repositories(query, username)
+ matching_repos = model.get_matching_repositories(query, username, limit=5)
matching_repo_counts = {t[0]: t[1] for t in model.get_repository_pull_counts(matching_repos)}
for repo in matching_repos:
@@ -197,10 +203,23 @@ class ConductSearch(ApiResource):
'href': '/repository/' + repo.namespace_user.username + '/' + repo.name
})
+
# Find the matching users, robots and organizations.
- matching_entities = model.get_matching_user_entities(query, get_authenticated_user())
+ matching_entities = model.get_matching_entities(query)
+ entity_count = 0
for entity in matching_entities:
+ # If the entity is a robot, filter it to only match those that are under the current
+ # user or can be administered by the organization.
+ if entity.robot:
+ orgname = parse_robot_username(entity.username)[0]
+ if not AdministerOrganizationPermission(orgname).can() and not orgname == username:
+ continue
+
results.append(entity_view(entity))
+ entity_count = entity_count + 1
+ if entity_count >= 5:
+ break
+
for result in results:
result['score'] = result['score'] * liquidmetal.score(result['name'], query)
diff --git a/static/css/directives/ui/header-bar.css b/static/css/directives/ui/header-bar.css
index 70c339f50..c9d320221 100644
--- a/static/css/directives/ui/header-bar.css
+++ b/static/css/directives/ui/header-bar.css
@@ -27,7 +27,7 @@ nav.navbar-default .navbar-nav>li>a.active {
}
.header-bar-element .header-bar-content {
- z-index: 4;
+ z-index: 5;
position: absolute;
top: 0px;
left: 0px;
@@ -40,7 +40,7 @@ nav.navbar-default .navbar-nav>li>a.active {
left: 0px;
right: 0px;
top: -50px;
- z-index: 3;
+ z-index: 4;
height: 83px;
transition: top 0.7s cubic-bezier(.23,.88,.72,.98);
background: white;
@@ -88,7 +88,7 @@ nav.navbar-default .navbar-nav>li>a.active {
left: 0px;
right: 0px;
top: -130px;
- z-index: 2;
+ z-index: 3;
transition: top 0.7s cubic-bezier(.23,.88,.72,.98), height 0.5s ease-in-out;
background: white;
@@ -173,4 +173,51 @@ nav.navbar-default .navbar-nav>li>a.active {
font-size: 12px;
margin-left: 6px;
display: inline-block;
-}
\ No newline at end of file
+}
+
+.header-bar-element .avatar {
+ margin-right: 6px;
+}
+
+.user-tools {
+ position: relative;
+ display: inline-block;
+}
+
+.user-tools .user-tool {
+ font-size: 24px;
+ margin-top: 14px;
+ color: #428bca;
+ margin-right: 20px;
+}
+
+.user-tools.with-menu {
+ margin-right: 6px;
+}
+
+.user-tools .caret {
+ position: absolute;
+ top: 3px;
+ left: 23px
+}
+
+.user-tools .notifications-bubble {
+ position: absolute;
+ top: 2px;
+ left: 13px;
+}
+
+.user-tools i.user-tool:hover {
+ cursor: pointer;
+ color: #333;
+}
+
+.user-tools .new-menu {
+ background: transparent !important;
+}
+
+.header-bar-element .context-dropdown i.fa {
+ width: 16px;
+ text-align: center;
+ display: inline-block;
+}
diff --git a/static/css/pages/team-view.css b/static/css/pages/team-view.css
index 26df5412d..0e9a8d7a0 100644
--- a/static/css/pages/team-view.css
+++ b/static/css/pages/team-view.css
@@ -13,6 +13,10 @@
padding-bottom: 10px;
}
+.team-view .team-view-header button i.fa {
+ margin-right: 4px;
+}
+
.team-view .team-view-header > h3 {
margin-top: 10px;
}
diff --git a/static/css/quay.css b/static/css/quay.css
index a6c025e18..6bcf5bb7b 100644
--- a/static/css/quay.css
+++ b/static/css/quay.css
@@ -742,17 +742,6 @@ i.toggle-icon:hover {
100% { transform: scale(1); }
}
-.user-tools .user-tool {
- font-size: 24px;
- margin-top: 14px;
- color: #428bca;
-}
-
-.user-tools i.user-tool:hover {
- cursor: pointer;
- color: #333;
-}
-
.status-box a {
padding: 6px;
color: black;
diff --git a/static/directives/header-bar.html b/static/directives/header-bar.html
index 9066d8114..43ef0847a 100644
--- a/static/directives/header-bar.html
+++ b/static/directives/header-bar.html
@@ -1,141 +1,3 @@
-
+
diff --git a/static/directives/new-header-bar.html b/static/directives/new-header-bar.html
new file mode 100644
index 000000000..847f83fc0
--- /dev/null
+++ b/static/directives/new-header-bar.html
@@ -0,0 +1,189 @@
+
\ No newline at end of file
diff --git a/static/directives/old-header-bar.html b/static/directives/old-header-bar.html
new file mode 100644
index 000000000..f18a9a9e1
--- /dev/null
+++ b/static/directives/old-header-bar.html
@@ -0,0 +1,78 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/static/js/directives/ui/header-bar.js b/static/js/directives/ui/header-bar.js
index 8c7373e22..6d332c870 100644
--- a/static/js/directives/ui/header-bar.js
+++ b/static/js/directives/ui/header-bar.js
@@ -12,7 +12,7 @@ angular.module('quay').directive('headerBar', function () {
restrict: 'C',
scope: {
},
- controller: function($scope, $element, $location, $timeout, UserService, PlanService, ApiService, NotificationService, Config) {
+ controller: function($rootScope, $scope, $element, $location, $timeout, UserService, PlanService, ApiService, NotificationService, Config, CreateService) {
$scope.notificationService = NotificationService;
$scope.searchVisible = false;
$scope.currentSearchQuery = null;
@@ -23,6 +23,19 @@ angular.module('quay').directive('headerBar', function () {
$scope.isNewLayout = Config.isNewLayout();
+ $scope.currentPageContext = {};
+
+ $rootScope.$watch('currentPage.scope.viewuser', function(u) {
+ $scope.currentPageContext['viewuser'] = u;
+ });
+
+ $rootScope.$watch('currentPage.scope.organization', function(o) {
+ $scope.currentPageContext['organization'] = o;
+ });
+
+ $rootScope.$watch('currentPage.scope.repository', function(r) {
+ $scope.currentPageContext['repository'] = r;
+ });
var conductSearch = function(query) {
if (!query) { $scope.searchResultState = null; return; }
@@ -43,6 +56,8 @@ angular.module('quay').directive('headerBar', function () {
'results': resp.results,
'current': -1
};
+ }, function(resp) {
+ $scope.searchResultState = null;
}, /* background */ true);
};
@@ -133,6 +148,77 @@ angular.module('quay').directive('headerBar', function () {
if (!$scope.searchResultState) { return; }
$scope.searchResultState['current'] = result;
};
+
+ $scope.getNamespace = function(context) {
+ if (!context) { return null; }
+
+ if (context.repository && context.repository.namespace) {
+ return context.repository.namespace;
+ }
+
+ if (context.organization && context.organization.name) {
+ return context.organization.name;
+ }
+
+ if (context.viewuser && context.viewuser.username) {
+ return context.viewuser.username;
+ }
+
+ return null;
+ };
+
+ $scope.canAdmin = function(namespace) {
+ if (!namespace) { return false; }
+ return UserService.isNamespaceAdmin(namespace);
+ };
+
+ $scope.isOrganization = function(namespace) {
+ if (!namespace) { return false; }
+ return UserService.isOrganization(namespace);
+ };
+
+ $scope.createRobot = function(context) {
+ var namespace = $scope.getNamespace(context);
+ 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, function(created) {
+ if (isorg) {
+ $location.url('/organization/' + namespace + '?tab=robots');
+ } else {
+ $location.url('/user/' + namespace + '?tab=robots');
+ }
+ });
+ });
+ };
+
+ $scope.createTeam = function(context) {
+ var namespace = $scope.getNamespace(context);
+ 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, function(created) {
+ $location.url('/organization/' + namespace + '/teams/' + teamname);
+ });
+ });
+ };
}
};
return directiveDefinitionObject;
diff --git a/static/js/directives/ui/teams-manager.js b/static/js/directives/ui/teams-manager.js
index 52b434b47..818946e41 100644
--- a/static/js/directives/ui/teams-manager.js
+++ b/static/js/directives/ui/teams-manager.js
@@ -47,7 +47,7 @@ angular.module('quay').directive('teamsManager', function () {
};
var loadOrderedTeams = function() {
- if (!$scope.organization) { return; }
+ if (!$scope.organization || !$scope.organization.ordered_teams) { return; }
$scope.orderedTeams = [];
$scope.organization.ordered_teams.map(function(name) {
diff --git a/static/js/pages/repo-view.js b/static/js/pages/repo-view.js
index 5e9d631f7..7cb8ed365 100644
--- a/static/js/pages/repo-view.js
+++ b/static/js/pages/repo-view.js
@@ -63,6 +63,7 @@
};
$scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) {
+ $scope.repository = repo;
$scope.viewScope.repository = repo;
// Load the remainder of the data async, so we don't block the initial view from
diff --git a/static/js/pages/user-view.js b/static/js/pages/user-view.js
index 5f02b48f1..7a1f056aa 100644
--- a/static/js/pages/user-view.js
+++ b/static/js/pages/user-view.js
@@ -34,7 +34,7 @@
var loadUser = function() {
$scope.userResource = ApiService.getUserInformationAsResource({'username': username}).get(function(user) {
- $scope.user = user;
+ $scope.viewuser = user;
// Load the repositories.
$timeout(function() {
diff --git a/static/partials/user-view.html b/static/partials/user-view.html
index bcdb8bca9..fdbde2029 100644
--- a/static/partials/user-view.html
+++ b/static/partials/user-view.html
@@ -5,12 +5,12 @@
-
- {{ user.username }}
+
+ {{ viewuser.username }}
-
+
-
-
+
+
@@ -60,29 +60,29 @@
Plan Usage and Billing
-
+
@@ -110,7 +110,7 @@
Current E-mail Address:
- {{ user.email }}
+ {{ viewuser.email }}
New E-mail Address:
@@ -124,7 +124,7 @@
Change E-mail Address
@@ -193,7 +193,7 @@