From d09f2f6e221fcfb8077d33d9a3af72585dfd04f5 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 7 Apr 2015 18:33:43 -0400 Subject: [PATCH] Get the new context-sensitive new menu working --- data/model/legacy.py | 25 ++- endpoints/api/search.py | 29 +++- static/css/directives/ui/header-bar.css | 55 ++++++- static/css/pages/team-view.css | 4 + static/css/quay.css | 11 -- static/directives/header-bar.html | 144 +---------------- static/directives/new-header-bar.html | 189 +++++++++++++++++++++++ static/directives/old-header-bar.html | 78 ++++++++++ static/js/directives/ui/header-bar.js | 88 ++++++++++- static/js/directives/ui/teams-manager.js | 2 +- static/js/pages/repo-view.js | 1 + static/js/pages/user-view.js | 2 +- static/partials/user-view.html | 26 ++-- 13 files changed, 461 insertions(+), 193 deletions(-) create mode 100644 static/directives/new-header-bar.html create mode 100644 static/directives/old-header-bar.html 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 @@ -
- - - - -
-
-
No matching results found
-
    -
  • - {{ result.kind }} - {{ result.score }} - - - - - - {{ result.name }} - - - under organization - - {{ result.organization.name }} - - - - - {{ result.name }} - - - - {{ result.name }} - - - - {{ result.name }} - - - - {{ result.namespace.name }}/{{ result.name }} -
    - {{ result.description }} -
    -
    -
    -
  • -
-
-
+ + + 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 matching results found
+
    +
  • + {{ result.kind }} + {{ result.score }} + + + + + + {{ result.name }} + + + under organization + + {{ result.organization.name }} + + + + + {{ result.name }} + + + + {{ result.name }} + + + + {{ result.name }} + + + + {{ result.namespace.name }}/{{ result.name }} +
    + {{ result.description }} +
    +
    +
    +
  • +
+
+
\ 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

-
+

Billing Invoices

-
@@ -110,7 +110,7 @@ - + @@ -124,7 +124,7 @@ @@ -193,7 +193,7 @@
Convert to organization
-
+
Current E-mail Address:{{ user.email }}{{ viewuser.email }}
New E-mail Address: