Start on new interactive search
This commit is contained in:
parent
2ece1170a1
commit
951b0cbab8
7 changed files with 505 additions and 107 deletions
|
@ -679,6 +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):
|
||||
matching_user_orgs = ((User.username ** (entity_prefix + '%')) & (User.robot == False))
|
||||
matching_robots = ((User.username ** (user.username + '+%' + entity_prefix + '%')) &
|
||||
(User.robot == True))
|
||||
|
||||
query = (User.select()
|
||||
.where(matching_user_orgs | matching_robots)
|
||||
.limit(10))
|
||||
|
||||
return query
|
||||
|
||||
def get_matching_user_teams(team_prefix, user):
|
||||
query = (Team.select()
|
||||
.join(User)
|
||||
.switch(Team)
|
||||
.join(TeamMember)
|
||||
.where(TeamMember.user == user, Team.name ** (team_prefix + '%'))
|
||||
.limit(10))
|
||||
return query
|
||||
|
||||
|
||||
def get_matching_teams(team_prefix, organization):
|
||||
query = Team.select().where(Team.name ** (team_prefix + '%'),
|
||||
Team.organization == organization)
|
||||
|
@ -942,9 +963,17 @@ def get_matching_repositories(repo_term, username=None):
|
|||
search_clauses = (Repository.name ** ('%' + name_term + '%') &
|
||||
Namespace.username ** ('%' + namespace_term + '%'))
|
||||
|
||||
final = visible.where(search_clauses).limit(10)
|
||||
return list(final)
|
||||
return visible.where(search_clauses).limit(10)
|
||||
|
||||
def get_repository_pull_counts(repositories):
|
||||
repo_pull = LogEntryKind.get(name = 'pull_repo')
|
||||
|
||||
return (Repository.select(Repository.id, fn.Count(LogEntry.id))
|
||||
.where(Repository.id << [r.id for r in repositories])
|
||||
.join(LogEntry, JOIN_LEFT_OUTER)
|
||||
.where(LogEntry.kind == repo_pull)
|
||||
.group_by(LogEntry.repository)
|
||||
.tuples())
|
||||
|
||||
def change_password(user, new_password):
|
||||
if not validate_password(new_password):
|
||||
|
|
|
@ -6,8 +6,10 @@ from auth.permissions import (OrganizationMemberPermission, ViewTeamPermission,
|
|||
AdministerOrganizationPermission)
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from auth import scopes
|
||||
from app import avatar
|
||||
from app import avatar, get_app_url
|
||||
from operator import itemgetter
|
||||
|
||||
import math
|
||||
|
||||
@resource('/v1/entities/<prefix>')
|
||||
class EntitySearch(ApiResource):
|
||||
|
@ -101,6 +103,79 @@ class EntitySearch(ApiResource):
|
|||
}
|
||||
|
||||
|
||||
@resource('/v1/find/all')
|
||||
class ConductSearch(ApiResource):
|
||||
""" Resource for finding users, repositories, teams, etc. """
|
||||
@parse_args
|
||||
@query_param('query', 'The search query.', type=str, default='')
|
||||
@require_scope(scopes.READ_REPO)
|
||||
@nickname('conductSearch')
|
||||
def get(self, args):
|
||||
""" Get a list of entities and resources that match the specified query. """
|
||||
prefix = args['query']
|
||||
username = None
|
||||
results = []
|
||||
|
||||
def entity_view(entity):
|
||||
kind = 'user'
|
||||
avatar_data = avatar.get_data_for_user(entity)
|
||||
href = '/user/' + entity.username
|
||||
|
||||
if entity.organization:
|
||||
kind = 'organization'
|
||||
avatar_data = avatar.get_data_for_org(entity)
|
||||
href = '/organization/' + entity.username
|
||||
elif entity.robot:
|
||||
kind = 'robot'
|
||||
href = '/user?tab=robots'
|
||||
avatar_data = None
|
||||
|
||||
return {
|
||||
'kind': kind,
|
||||
'avatar': avatar_data,
|
||||
'name': entity.username,
|
||||
'score': 1,
|
||||
'href': href
|
||||
}
|
||||
|
||||
if get_authenticated_user():
|
||||
username = get_authenticated_user().username
|
||||
|
||||
# Find the matching teams.
|
||||
matching_teams = model.get_matching_user_teams(prefix, get_authenticated_user())
|
||||
for team in matching_teams:
|
||||
results.append({
|
||||
'kind': 'team',
|
||||
'name': team.name,
|
||||
'organization': entity_view(team.organization),
|
||||
'avatar': avatar.get_data_for_team(team),
|
||||
'score': 2,
|
||||
'href': '/organization/' + team.organization.username + '/teams/' + team.name
|
||||
})
|
||||
|
||||
# Find the matching repositories.
|
||||
matching_repos = model.get_matching_repositories(prefix, username)
|
||||
matching_repo_counts = {t[0]: t[1] for t in model.get_repository_pull_counts(matching_repos)}
|
||||
|
||||
for repo in matching_repos:
|
||||
results.append({
|
||||
'kind': 'repository',
|
||||
'namespace': entity_view(repo.namespace_user),
|
||||
'name': repo.name,
|
||||
'description': repo.description,
|
||||
'is_public': repo.visibility.name == 'public',
|
||||
'score': math.log(matching_repo_counts.get(repo.id, 1), 10),
|
||||
'href': '/repository/' + repo.namespace_user.username + '/' + repo.name
|
||||
})
|
||||
|
||||
# Find the matching users, robots and organizations.
|
||||
matching_entities = model.get_matching_user_entities(prefix, get_authenticated_user())
|
||||
for entity in matching_entities:
|
||||
results.append(entity_view(entity))
|
||||
|
||||
return {'results': sorted(results, key=itemgetter('score'), reverse=True)}
|
||||
|
||||
|
||||
@resource('/v1/find/repository')
|
||||
class FindRepositories(ApiResource):
|
||||
""" Resource for finding repositories. """
|
||||
|
|
172
static/css/directives/ui/header-bar.css
Normal file
172
static/css/directives/ui/header-bar.css
Normal file
|
@ -0,0 +1,172 @@
|
|||
nav.navbar {
|
||||
border: 0px;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
nav.navbar-default .navbar-nav>li>a {
|
||||
letter-spacing: 0.5px;
|
||||
color: #428bca;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
nav.navbar-default .navbar-nav>li>a.active {
|
||||
color: #f04c5c;
|
||||
}
|
||||
|
||||
.navbar-default .navbar-nav>li>a:hover, .navbar-default .navbar-nav>li>a:focus {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.navbar-default .navbar-nav>.open>a, .navbar-default .navbar-nav>.open>a:hover, .navbar-default .navbar-nav>.open>a:focus {
|
||||
cursor: pointer;
|
||||
background: rgba(255, 255, 255, 0.4) !important;
|
||||
}
|
||||
|
||||
.header-bar-element .header-bar-content.search-visible {
|
||||
box-shadow: 0px 1px 4px #ccc;
|
||||
}
|
||||
|
||||
.header-bar-element .header-bar-content {
|
||||
z-index: 4;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.header-bar-element .search-box {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
top: -50px;
|
||||
z-index: 3;
|
||||
height: 83px;
|
||||
transition: top 0.7s cubic-bezier(.23,.88,.72,.98);
|
||||
background: white;
|
||||
box-shadow: 0px 1px 16px #444;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-box.search-visible {
|
||||
top: 50px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-box.results-visible {
|
||||
box-shadow: 0px 1px 4px #ccc;
|
||||
}
|
||||
|
||||
.header-bar-element .search-box .search-label {
|
||||
display: inline-block;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #ccc;
|
||||
margin-right: 10px;
|
||||
position: absolute;
|
||||
top: 34px;
|
||||
left: 14px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-box .search-box-wrapper {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 100px;
|
||||
right: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-box .search-box-wrapper input {
|
||||
font-size: 28px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
top: -130px;
|
||||
z-index: 2;
|
||||
transition: top 0.7s cubic-bezier(.23,.88,.72,.98), height 0.5s ease-in-out;
|
||||
|
||||
background: white;
|
||||
box-shadow: 0px 1px 16px #444;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results.loading, .header-bar-element .search-results.results {
|
||||
top: 130px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results.loading {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results.no-results {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results ul {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results li {
|
||||
list-style: none;
|
||||
padding: 6px;
|
||||
margin-bottom: 4px;
|
||||
padding-left: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results li .kind {
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
color: #aaa;
|
||||
width: 80px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results .avatar {
|
||||
margin-left: 6px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results li.current {
|
||||
background: rgb(223, 242, 255);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results li i.fa {
|
||||
margin-left: 6px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results li .description {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-height: 24px;
|
||||
padding-left: 10px;
|
||||
display: inline-block;
|
||||
color: #aaa;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results li a {
|
||||
color: black;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results li .result-name {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results li .clarification {
|
||||
font-size: 12px;
|
||||
margin-left: 6px;
|
||||
display: inline-block;
|
||||
}
|
|
@ -150,30 +150,6 @@
|
|||
max-width: none !important;
|
||||
}
|
||||
|
||||
nav.navbar {
|
||||
border: 0px;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
nav.navbar-default .navbar-nav>li>a {
|
||||
letter-spacing: 0.5px;
|
||||
color: #428bca;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
nav.navbar-default .navbar-nav>li>a.active {
|
||||
color: #f04c5c;
|
||||
}
|
||||
|
||||
.navbar-default .navbar-nav>li>a:hover, .navbar-default .navbar-nav>li>a:focus {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.navbar-default .navbar-nav>.open>a, .navbar-default .navbar-nav>.open>a:hover, .navbar-default .navbar-nav>.open>a:focus {
|
||||
cursor: pointer;
|
||||
background: rgba(255, 255, 255, 0.4) !important;
|
||||
}
|
||||
|
||||
.notification-view-element {
|
||||
cursor: pointer;
|
||||
margin-bottom: 10px;
|
||||
|
|
|
@ -1,81 +1,140 @@
|
|||
<!-- Quay -->
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse" style="padding: 0px; padding-left: 4px; padding-right: 4px;">
|
||||
≡
|
||||
</button>
|
||||
<a class="navbar-brand" href="/" target="{{ appLinkTarget() }}">
|
||||
<span id="quay-logo" ng-style="{'background-image': 'url(' + getEnterpriseLogo() + ')'}"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Collapsable stuff -->
|
||||
<div class="collapse navbar-collapse navbar-ex1-collapse">
|
||||
<ul class="nav navbar-nav navbar-links">
|
||||
<li><a ng-href="/tour/" target="{{ appLinkTarget() }}" quay-section="tour">Tour</a></li>
|
||||
<li><a ng-href="/repository/" target="{{ appLinkTarget() }}" quay-section="repository">Repositories</a></li>
|
||||
<li><a href="http://docs.quay.io/" target="_blank">Docs</a></li>
|
||||
<li><a ng-href="/tutorial/" target="{{ appLinkTarget() }}" quay-section="tutorial">Tutorial</a></li>
|
||||
<li quay-require="['BILLING']"><a ng-href="/plans/" target="{{ appLinkTarget() }}" quay-section="plans">Pricing</a></li>
|
||||
<li><a ng-href="{{ user.organizations.length ? '/organizations/' : '/tour/organizations/' }}" target="{{ appLinkTarget() }}" quay-section="organization">Organizations</a></li>
|
||||
</ul>
|
||||
|
||||
<!-- Phone -->
|
||||
<ul class="nav navbar-nav navbar-right visible-xs" ng-switch on="user.anonymous">
|
||||
<li ng-switch-when="false">
|
||||
<a href="/user/" class="user-view" target="{{ appLinkTarget() }}">
|
||||
<span class="avatar" size="32" data="user.avatar"></span>
|
||||
{{ user.username }}
|
||||
<div class="header-bar-element">
|
||||
<div class="header-bar-content" ng-class="searchVisible ? 'search-visible' : ''">
|
||||
<!-- Quay -->
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse" style="padding: 0px; padding-left: 4px; padding-right: 4px;">
|
||||
≡
|
||||
</button>
|
||||
<a class="navbar-brand" href="/" target="{{ appLinkTarget() }}">
|
||||
<span id="quay-logo" ng-style="{'background-image': 'url(' + getEnterpriseLogo() + ')'}"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-switch-default>
|
||||
<a class="user-view" href="/signin/" target="{{ appLinkTarget() }}">Sign in</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Normal -->
|
||||
<ul class="nav navbar-nav navbar-right hidden-xs" ng-switch on="user.anonymous">
|
||||
<li>
|
||||
<form class="navbar-form navbar-left" role="search">
|
||||
<div class="form-group">
|
||||
<span class="repo-search"></span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<span class="navbar-left user-tools" ng-show="!user.anonymous">
|
||||
<a href="/new/"><i class="fa fa-upload user-tool" bs-tooltip="tooltip.title" data-placement="bottom" data-title="Create new repository" data-container="body"></i></a>
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li class="dropdown" ng-switch-when="false">
|
||||
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown user-view" data-toggle="dropdown">
|
||||
<span class="avatar" size="32" data="user.avatar"></span>
|
||||
{{ user.username }}
|
||||
<span class="notifications-bubble"></span>
|
||||
<b class="caret"></b>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/user/{{ user.username }}?tab=settings" target="{{ appLinkTarget() }}" ng-if="isNewLayout">
|
||||
Account Settings
|
||||
</a>
|
||||
<a href="/user/" target="{{ appLinkTarget() }}" ng-if="!isNewLayout">
|
||||
Account Settings
|
||||
</a>
|
||||
</li>
|
||||
<li ng-if="notificationService.notifications.length">
|
||||
<a href="javascript:void(0)" data-template="/static/directives/notification-bar.html"
|
||||
data-animation="am-slide-right" bs-aside="aside" data-container="body">
|
||||
Notifications
|
||||
<span class="notifications-bubble"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
|
||||
<li ng-if="user.super_user"><a href="/superuser/"><strong>Super User Admin Panel</strong></a></li>
|
||||
<li><a href="javascript:void(0)" ng-click="signout()">Sign out</a></li>
|
||||
<!-- Collapsable stuff -->
|
||||
<div class="collapse navbar-collapse navbar-ex1-collapse">
|
||||
<ul class="nav navbar-nav navbar-links">
|
||||
<li><a ng-href="/tour/" target="{{ appLinkTarget() }}" quay-section="tour">Tour</a></li>
|
||||
<li><a ng-href="/repository/" target="{{ appLinkTarget() }}" quay-section="repository">Repositories</a></li>
|
||||
<li><a href="http://docs.quay.io/" target="_blank">Docs</a></li>
|
||||
<li><a ng-href="/tutorial/" target="{{ appLinkTarget() }}" quay-section="tutorial">Tutorial</a></li>
|
||||
<li quay-require="['BILLING']"><a ng-href="/plans/" target="{{ appLinkTarget() }}" quay-section="plans">Pricing</a></li>
|
||||
<li><a ng-href="{{ user.organizations.length ? '/organizations/' : '/tour/organizations/' }}" target="{{ appLinkTarget() }}" quay-section="organization">Organizations</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li ng-switch-default>
|
||||
<a class="user-view" href="/signin/" target="{{ appLinkTarget() }}">Sign in</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div><!-- /.navbar-collapse -->
|
||||
|
||||
<!-- Phone -->
|
||||
<ul class="nav navbar-nav navbar-right visible-xs" ng-switch on="user.anonymous">
|
||||
<li ng-switch-when="false">
|
||||
<a href="/user/" class="user-view" target="{{ appLinkTarget() }}">
|
||||
<span class="avatar" size="32" data="user.avatar"></span>
|
||||
{{ user.username }}
|
||||
</a>
|
||||
</li>
|
||||
<li ng-switch-default>
|
||||
<a class="user-view" href="/signin/" target="{{ appLinkTarget() }}">Sign in</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Normal -->
|
||||
<ul class="nav navbar-nav navbar-right hidden-xs" ng-switch on="user.anonymous">
|
||||
<li>
|
||||
<span class="navbar-left user-tools">
|
||||
<i class="fa fa-search fa-lg user-tool" ng-click="toggleSearch()"></i>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="navbar-left user-tools" ng-show="!user.anonymous">
|
||||
<a href="/new/"><i class="fa fa-plus user-tool" bs-tooltip="tooltip.title" data-placement="bottom" data-title="Create new repository" data-container="body"></i></a>
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li class="dropdown" ng-switch-when="false">
|
||||
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown user-view" data-toggle="dropdown">
|
||||
<span class="avatar" size="32" data="user.avatar"></span>
|
||||
{{ user.username }}
|
||||
<span class="notifications-bubble"></span>
|
||||
<b class="caret"></b>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/user/{{ user.username }}?tab=settings" target="{{ appLinkTarget() }}" ng-if="isNewLayout">
|
||||
Account Settings
|
||||
</a>
|
||||
<a href="/user/" target="{{ appLinkTarget() }}" ng-if="!isNewLayout">
|
||||
Account Settings
|
||||
</a>
|
||||
</li>
|
||||
<li ng-if="notificationService.notifications.length">
|
||||
<a href="javascript:void(0)" data-template="/static/directives/notification-bar.html"
|
||||
data-animation="am-slide-right" bs-aside="aside" data-container="body">
|
||||
Notifications
|
||||
<span class="notifications-bubble"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
|
||||
<li ng-if="user.super_user"><a href="/superuser/"><strong>Super User Admin Panel</strong></a></li>
|
||||
<li><a href="javascript:void(0)" ng-click="signout()">Sign out</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li ng-switch-default>
|
||||
<a class="user-view" href="/signin/" target="{{ appLinkTarget() }}">Sign in</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div><!-- /.navbar-collapse -->
|
||||
</div>
|
||||
|
||||
<div class="search-box" ng-class="getSearchBoxClasses(searchVisible, searchResultState)">
|
||||
<div class="search-label">Search For</div>
|
||||
<div class="search-box-wrapper">
|
||||
<input id="search-box-input" type="text" placeholder="(Enter Search Terms)"
|
||||
ng-model-options="{'debounce': 250}" ng-model="currentSearchQuery"
|
||||
ng-keydown="handleSearchKeyDown($event)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-results"
|
||||
ng-class="searchVisible && searchResultState ? searchResultState.state : ''"
|
||||
ng-class="{'height': (searchResultState.results.length * 40) + 28}">
|
||||
<div class="cor-loader" ng-if="searchResultState.state == 'loading'"></div>
|
||||
<div ng-if="searchResultState.state == 'no-results'">No matching results found</div>
|
||||
<ul ng-if="searchResultState.state == 'results'">
|
||||
<li ng-repeat="result in searchResultState.results" ng-mouseover="setCurrentResult($index)"
|
||||
ng-class="searchResultState.current == $index ? 'current' : ''"
|
||||
ng-click="showResult(result)">
|
||||
<span class="kind">{{ result.kind }}</span>
|
||||
<span ng-switch on="result.kind">
|
||||
<!-- Team -->
|
||||
<span ng-switch-when="team">
|
||||
<strong>
|
||||
<span class="avatar" data="result.avatar" size="16"></span>
|
||||
<span class="result-name">{{ result.name }}</span>
|
||||
</strong>
|
||||
<span class="clarification">
|
||||
under organization
|
||||
<span class="avatar" data="result.organization.avatar" size="16"></span>
|
||||
<span class="result-name">{{ result.organization.name }}</span>
|
||||
</span>
|
||||
</span>
|
||||
<span ng-switch-when="user">
|
||||
<span class="avatar" data="result.avatar" size="16"></span>
|
||||
<span class="result-name">{{ result.name }}</span>
|
||||
</span>
|
||||
<span ng-switch-when="organization">
|
||||
<span class="avatar" data="result.avatar" size="16"></span>
|
||||
<span class="result-name">{{ result.name }}</span>
|
||||
</span>
|
||||
<span href="/user/{{ result.name }}" ng-switch-when="robot">
|
||||
<i class="fa fa-wrench"></i>
|
||||
<span class="result-name">{{ result.name }}</span>
|
||||
</span>
|
||||
<span ng-switch-when="repository">
|
||||
<span class="avatar" data="result.namespace.avatar" size="16"></span>
|
||||
<span class="result-name">{{ result.namespace.name }}/{{ result.name }}</span>
|
||||
<div class="description" ng-if="result.description">
|
||||
{{ result.description }}
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -37,7 +37,7 @@ quayPages.constant('pages', {
|
|||
|
||||
quayDependencies = ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment',
|
||||
'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml',
|
||||
'ngAnimate', 'core-ui', 'core-config-setup', 'quayPages'];
|
||||
'core-ui', 'core-config-setup', 'quayPages'];
|
||||
|
||||
if (window.__config && window.__config.MIXPANEL_KEY) {
|
||||
quayDependencies.push('angulartics');
|
||||
|
|
|
@ -12,14 +12,42 @@ angular.module('quay').directive('headerBar', function () {
|
|||
restrict: 'C',
|
||||
scope: {
|
||||
},
|
||||
controller: function($scope, $element, $location, UserService, PlanService, ApiService, NotificationService, Config) {
|
||||
controller: function($scope, $element, $location, $timeout, UserService, PlanService, ApiService, NotificationService, Config) {
|
||||
$scope.notificationService = NotificationService;
|
||||
$scope.searchVisible = false;
|
||||
$scope.currentSearchQuery = null;
|
||||
$scope.searchResultState = null;
|
||||
|
||||
// Monitor any user changes and place the current user into the scope.
|
||||
UserService.updateUserIn($scope);
|
||||
|
||||
$scope.isNewLayout = Config.isNewLayout();
|
||||
|
||||
|
||||
var conductSearch = function(query) {
|
||||
if (!query) { $scope.searchResultState = null; return; }
|
||||
|
||||
$scope.searchResultState = {
|
||||
'state': 'loading'
|
||||
};
|
||||
|
||||
var params = {
|
||||
'query': query
|
||||
};
|
||||
|
||||
ApiService.conductSearch(null, params).then(function(resp) {
|
||||
if (!$scope.searchVisible) { return; }
|
||||
|
||||
$scope.searchResultState = {
|
||||
'state': resp.results.length ? 'results' : 'no-results',
|
||||
'results': resp.results,
|
||||
'current': -1
|
||||
};
|
||||
}, /* background */ true);
|
||||
};
|
||||
|
||||
$scope.$watch('currentSearchQuery', conductSearch);
|
||||
|
||||
$scope.signout = function() {
|
||||
ApiService.logout().then(function() {
|
||||
UserService.load();
|
||||
|
@ -41,6 +69,65 @@ angular.module('quay').directive('headerBar', function () {
|
|||
|
||||
return Config.ENTERPRISE_LOGO_URL;
|
||||
};
|
||||
|
||||
$scope.toggleSearch = function() {
|
||||
$scope.searchVisible = !$scope.searchVisible;
|
||||
if ($scope.searchVisible) {
|
||||
$('#search-box-input').focus();
|
||||
if ($scope.currentSearchQuery) {
|
||||
conductSearch($scope.currentSearchQuery);
|
||||
}
|
||||
} else {
|
||||
$scope.searchResultState = null;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getSearchBoxClasses = function(searchVisible, searchResultState) {
|
||||
var classes = searchVisible ? 'search-visible ' : '';
|
||||
if (searchResultState) {
|
||||
classes += 'results-visible';
|
||||
}
|
||||
return classes;
|
||||
};
|
||||
|
||||
$scope.handleSearchKeyDown = function(e) {
|
||||
if (!$scope.searchResultState) { return; }
|
||||
|
||||
if (e.keyCode == 40) {
|
||||
$scope.searchResultState['current']++;
|
||||
e.preventDefault();
|
||||
} else if (e.keyCode == 38) {
|
||||
$scope.searchResultState['current']--;
|
||||
e.preventDefault();
|
||||
} else if (e.keyCode == 13) {
|
||||
var current = $scope.searchResultState['current'];
|
||||
if (current >= 0 &&
|
||||
current < $scope.searchResultState['results'].length) {
|
||||
$scope.showResult($scope.searchResultState['results'][current]);
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (!$scope.searchResultState) { return; }
|
||||
|
||||
if ($scope.searchResultState['current'] < -1) {
|
||||
$scope.searchResultState['current'] = $scope.searchResultState['results'].length - 1;
|
||||
} else if ($scope.searchResultState['current'] >= $scope.searchResultState['results'].length) {
|
||||
$scope.searchResultState['current'] = 0;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showResult = function(result) {
|
||||
$scope.toggleSearch();
|
||||
$timeout(function() {
|
||||
$location.url(result['href'])
|
||||
}, 500);
|
||||
};
|
||||
|
||||
$scope.setCurrentResult = function(result) {
|
||||
if (!$scope.searchResultState) { return; }
|
||||
$scope.searchResultState['current'] = result;
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
|
|
Reference in a new issue