Merge pull request #2529 from coreos-inc/search-ui
Implement new search UI
This commit is contained in:
		
						commit
						5a9a231754
					
				
					 23 changed files with 649 additions and 393 deletions
				
			
		|  | @ -44,8 +44,9 @@ nav.navbar-default .navbar-nav>li>a.active { | |||
|   width: 150px; | ||||
| } | ||||
| 
 | ||||
| .header-bar-element .header-bar-content.search-visible { | ||||
|   box-shadow: 0px 1px 4px #ccc; | ||||
| .header-bar-element .search-box-element { | ||||
|       margin-top: 10px; | ||||
|       margin-right: 16px; | ||||
| } | ||||
| 
 | ||||
| .header-bar-element .header-bar-content { | ||||
|  | @ -57,151 +58,6 @@ nav.navbar-default .navbar-nav>li>a.active { | |||
|   background: white; | ||||
| } | ||||
| 
 | ||||
| .header-bar-element .search-box { | ||||
|   position: absolute; | ||||
|   left: 0px; | ||||
|   right: 0px; | ||||
|   top: -60px; | ||||
|   z-index: 4; | ||||
|   height: 56px; | ||||
|   transition: top 0.3s 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: 20px; | ||||
|   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: 18px; | ||||
|   width: 100%; | ||||
|   padding: 6px; | ||||
|   border: 0px; | ||||
| } | ||||
| 
 | ||||
| .header-bar-element .search-results { | ||||
|   position: absolute; | ||||
|   left: 0px; | ||||
|   right: 0px; | ||||
|   top: -106px; | ||||
|   z-index: 3; | ||||
|   transition: top 0.4s cubic-bezier(.23,.88,.72,.98), height 0.25s 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: 106px; | ||||
| } | ||||
| 
 | ||||
| .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 .result-description { | ||||
|   overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
|   max-height: 24px; | ||||
|   padding-left: 10px; | ||||
|   display: inline-block; | ||||
|   color: #aaa; | ||||
|   vertical-align: middle; | ||||
|   margin-top: 2px; | ||||
| } | ||||
| 
 | ||||
| .header-bar-element .search-results li .description img { | ||||
|   display: none; | ||||
| } | ||||
| 
 | ||||
| .header-bar-element .search-results li .score:before { | ||||
|   content: "Score: "; | ||||
| } | ||||
| 
 | ||||
| .header-bar-element .search-results li .score { | ||||
|   float: right; | ||||
|   color: #ccc; | ||||
| } | ||||
| 
 | ||||
| .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; | ||||
| } | ||||
| 
 | ||||
| .header-bar-element .avatar { | ||||
|   margin-right: 6px; | ||||
| } | ||||
|  | @ -248,3 +104,16 @@ nav.navbar-default .navbar-nav>li>a.active { | |||
|   text-align: center; | ||||
|   display: inline-block; | ||||
| } | ||||
| 
 | ||||
| .header-bar-element .block-search { | ||||
|   padding: 6px; | ||||
|   padding-top: 0px; | ||||
|   margin-top: 0px; | ||||
|   text-align: right | ||||
| } | ||||
| 
 | ||||
| .header-bar-element .block-search search-box { | ||||
|   margin-top: -6px; | ||||
|   display: inline-block; | ||||
|   margin-bottom: 6px; | ||||
| } | ||||
							
								
								
									
										78
									
								
								static/css/directives/ui/search-box.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								static/css/directives/ui/search-box.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,78 @@ | |||
| .search-box-element { | ||||
|     display: inline-block; | ||||
|     position: relative; | ||||
| } | ||||
| 
 | ||||
| .search-box-element input { | ||||
|     width: 300px; | ||||
|     display: inline-block; | ||||
|     border-radius: 0px; | ||||
|     height: 30px; | ||||
|     font-style: italic; | ||||
| } | ||||
| 
 | ||||
| .search-box-element .search-icon { | ||||
|     position: absolute; | ||||
|     font-size: 18px; | ||||
|     color: #ccc; | ||||
|     top: 2px; | ||||
|     right: 6px; | ||||
| } | ||||
| 
 | ||||
| .search-box-element .search-icon .cor-loader-inline { | ||||
|     top: -2px; | ||||
|     right: 2px; | ||||
|     position: absolute; | ||||
| } | ||||
| 
 | ||||
| .search-box-element .search-icon .cor-loader-inline .co-m-loader-dot__one, | ||||
| .search-box-element .search-icon .cor-loader-inline .co-m-loader-dot__two, | ||||
| .search-box-element .search-icon .cor-loader-inline .co-m-loader-dot__three { | ||||
|     background: #ccc; | ||||
| } | ||||
| 
 | ||||
| .search-box-result .kind { | ||||
|   text-transform: uppercase; | ||||
|   font-size: 12px; | ||||
|   display: inline-block; | ||||
|   margin-right: 10px; | ||||
|   color: #aaa; | ||||
|   width: 40px; | ||||
|   text-align: right; | ||||
| } | ||||
| 
 | ||||
| .search-box-result .avatar { | ||||
|   margin-left: 6px; | ||||
|   margin-right: 2px; | ||||
| } | ||||
| 
 | ||||
| .search-box-result i.fa { | ||||
|   margin-left: 6px; | ||||
|   margin-right: 4px; | ||||
| } | ||||
| 
 | ||||
| .search-box-result .result-description { | ||||
|   overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
|   max-height: 24px; | ||||
|   padding-left: 10px; | ||||
|   display: inline-block; | ||||
|   color: #aaa; | ||||
|   vertical-align: middle; | ||||
|   margin-top: 2px; | ||||
| } | ||||
| 
 | ||||
| .search-box-result .description img { | ||||
|   display: none; | ||||
| } | ||||
| 
 | ||||
| .search-box-result .result-name { | ||||
|   vertical-align: middle; | ||||
| } | ||||
| 
 | ||||
| .search-box-result .clarification { | ||||
|   font-size: 12px; | ||||
|   margin-left: 6px; | ||||
|   display: inline-block; | ||||
|   vertical-align: middle; | ||||
| } | ||||
							
								
								
									
										15
									
								
								static/css/directives/ui/typeahead.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								static/css/directives/ui/typeahead.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| .tt-menu { | ||||
|     background-color: #fff; | ||||
|     border: 1px solid #ccc; | ||||
|     border: 1px solid rgba(0, 0, 0, 0.2); | ||||
|     right: 0px; | ||||
| } | ||||
| 
 | ||||
| .tt-suggestion.tt-is-under-cursor { | ||||
|     color: #fff; | ||||
|     background-color: #428bca; | ||||
| } | ||||
| 
 | ||||
| .tt-hint { | ||||
|   display: none; | ||||
| } | ||||
							
								
								
									
										84
									
								
								static/css/pages/search.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								static/css/pages/search.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,84 @@ | |||
| .search { | ||||
|     padding: 40px; | ||||
| } | ||||
| 
 | ||||
| .search .empty { | ||||
|   margin-top: 40px; | ||||
| } | ||||
| 
 | ||||
| .search .search-top-bar { | ||||
|     text-align: center; | ||||
|     padding: 10px; | ||||
| } | ||||
| 
 | ||||
| .search .search-results-section { | ||||
|     border-top: 1px solid #ccc; | ||||
|     padding-top: 16px; | ||||
|     margin-top: 30px; | ||||
| } | ||||
| 
 | ||||
| .search .search-results-section h5 { | ||||
|   display: block; | ||||
|   text-align: center; | ||||
|   color: #aaa; | ||||
|   text-transform: uppercase; | ||||
| } | ||||
| 
 | ||||
| .search .search-results li { | ||||
|     padding-bottom: 10px; | ||||
|     margin-bottom: 24px; | ||||
|     border-bottom: 1px solid #eee; | ||||
| } | ||||
| 
 | ||||
| .search .search-results li .result-info-bar { | ||||
|     color: #888; | ||||
| } | ||||
| 
 | ||||
| .search .search-results li .result-info-bar .activity { | ||||
|     float: right; | ||||
| } | ||||
| 
 | ||||
| .search .search-results li .result-info-bar .activity .strength-indicator { | ||||
|     display: inline-block; | ||||
|     margin-left: 10px; | ||||
| } | ||||
| 
 | ||||
| .search .search-results li .description .markdown-view-content p { | ||||
|     display: none; | ||||
| } | ||||
| 
 | ||||
| .search .search-results li .description .markdown-view-content p:first-child { | ||||
|     display: block; | ||||
|     overflow: hidden; | ||||
|     max-height: 4em; | ||||
| } | ||||
| 
 | ||||
| .search .search-results li h4 { | ||||
|     vertical-align: middle; | ||||
| } | ||||
| 
 | ||||
| .search .search-results li h4 .fa { | ||||
|     margin-right: 6px; | ||||
|     display: inline-block; | ||||
| } | ||||
| 
 | ||||
| .search .search-results li .star-count { | ||||
|     float: right; | ||||
|     color: #888; | ||||
|     line-height: 26px; | ||||
| } | ||||
| 
 | ||||
| .search .search-results li .star-count .star-count-number { | ||||
|    display: inline-block; | ||||
| } | ||||
| 
 | ||||
| .search .search-results li .star-icon { | ||||
|   color: #ffba6d; | ||||
|   font-size: 26px; | ||||
|   margin-left: 10px; | ||||
|   vertical-align: middle; | ||||
| } | ||||
| 
 | ||||
| .search .search-results li .search-result-box { | ||||
|    padding: 6px; | ||||
| } | ||||
|  | @ -1,6 +1,6 @@ | |||
| <span class="header-bar-parent"> | ||||
|   <div class="header-bar-element"> | ||||
|     <div class="header-bar-content" ng-class="searchVisible ? 'search-visible' : ''"> | ||||
|     <div class="header-bar-content"> | ||||
|       <!-- Quay --> | ||||
|       <div class="navbar-header"> | ||||
|         <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse"> | ||||
|  | @ -10,11 +10,6 @@ | |||
|           <span id="quay-logo" ng-style="{'background-image': 'url(' + getEnterpriseLogo() + ')'}" | ||||
|                 ng-class="Config.ENTERPRISE_LOGO_URL ? 'enterprise-logo' : 'hosted-logo'"></span> | ||||
|         </a> | ||||
|         <span class="user-tools visible-xs" style="float: right;"> | ||||
|           <i class="fa fa-search fa-lg user-tool" ng-click="toggleSearch()" | ||||
|              data-placement="bottom" data-title="Search" bs-tooltip | ||||
|              ng-if="searchingAllowed"></i> | ||||
|         </span> | ||||
|       </div> | ||||
| 
 | ||||
|       <!-- Collapsable stuff --> | ||||
|  | @ -55,10 +50,8 @@ | |||
|         <!-- 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()" | ||||
|                  data-placement="bottom" data-title="Search - Keyboard Shortcut: /" bs-tooltip | ||||
|                  ng-if="searchingAllowed"></i> | ||||
|             <span class="navbar-left user-tools hidden-sm"> | ||||
|               <search-box ng-if="searchingAllowed"></search-box> | ||||
|             </span> | ||||
|           </li> | ||||
|           <li> | ||||
|  | @ -146,70 +139,11 @@ | |||
|           </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="search" placeholder="(Enter Search Terms)" | ||||
|                ng-model-options="{'debounce': 250}" ng-model="currentSearchQuery" | ||||
|                ng-keydown="handleSearchKeyDown($event)"> | ||||
|       <div class="visible-sm block-search" ng-if="searchingAllowed"> | ||||
|         <search-box></search-box> | ||||
|       </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 class="score" style="display: none">{{ result.score.toString().substr(0, 4) }}</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 ng-switch-when="robot"> | ||||
|               <i class="fa ci-robot"></i> | ||||
|               <span class="result-name">{{ result.name }}</span> | ||||
|             </span> | ||||
|             <span ng-switch-when="doc"> | ||||
|               <i class="fa fa-book"></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="result-description" ng-if="result.description"> | ||||
|                 <div class="description markdown-view" content="result.description" | ||||
|                      first-line-only="true" placeholder-needed="false"></div> | ||||
|               </div> | ||||
|             </span> | ||||
|           </span> | ||||
|         </li> | ||||
|       </ul> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="create-robot-dialog" info="createRobotInfo" | ||||
|          robot-created="handleRobotCreated(robot, currentPageContext)"> | ||||
|     </div> | ||||
|  |  | |||
|  | @ -235,11 +235,13 @@ angular.module('quay').directive('entitySearch', function () { | |||
| 
 | ||||
|         // Setup the typeahead.
 | ||||
|         $(input).typeahead({ | ||||
|           'highlight': true | ||||
|           'highlight': true, | ||||
|           'hint': false, | ||||
|         }, { | ||||
|           display: 'value', | ||||
|           source: entitySearchB.ttAdapter(), | ||||
|           templates: { | ||||
|             'empty': function(info) { | ||||
|             'notFound': function(info) { | ||||
|               // Only display the empty dialog if the server load has finished.
 | ||||
|               if (info.resultKind == 'remote') { | ||||
|                 var val = $(input).val(); | ||||
|  |  | |||
|  | @ -28,18 +28,6 @@ angular.module('quay').directive('headerBar', function () { | |||
|         hotkeysAdded = true; | ||||
| 
 | ||||
|         // Register hotkeys.
 | ||||
|         if ($scope.searchingAllowed) { | ||||
|           hotkeys.add({ | ||||
|             combo: '/', | ||||
|             description: 'Show search', | ||||
|             callback: function(e) { | ||||
|               e.preventDefault(); | ||||
|               e.stopPropagation(); | ||||
|               $scope.toggleSearch(); | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
| 
 | ||||
|         if (!cUser.anonymous) { | ||||
|           hotkeys.add({ | ||||
|             combo: 'alt+c', | ||||
|  | @ -57,9 +45,6 @@ angular.module('quay').directive('headerBar', function () { | |||
|       $scope.Features = Features; | ||||
|       $scope.notificationService = NotificationService; | ||||
|       $scope.searchingAllowed = false; | ||||
|       $scope.searchVisible = false; | ||||
|       $scope.currentSearchQuery = null; | ||||
|       $scope.searchResultState = null; | ||||
|       $scope.showBuildDialogCounter = 0; | ||||
| 
 | ||||
|       // Monitor any user changes and place the current user into the scope.
 | ||||
|  | @ -79,69 +64,6 @@ angular.module('quay').directive('headerBar', function () { | |||
|         $scope.currentPageContext['repository'] = r; | ||||
|       }); | ||||
| 
 | ||||
|       var documentSearchMaxResults = 10; | ||||
|       var documentSearchScoreThreshold = 0.9; | ||||
| 
 | ||||
|       var conductDocumentationSearch = function(query) { | ||||
|         if (!query) { return; } | ||||
| 
 | ||||
|         var mapper = function(result, score) { | ||||
|           return { | ||||
|             'kind': 'doc', | ||||
|             'name': result.title.replace(/'\;/g, "'"), | ||||
|             'score': score, | ||||
|             'href': Config.DOCUMENTATION_LOCATION + result.url | ||||
|           } | ||||
|         }; | ||||
| 
 | ||||
|         DocumentationService.findDocumentation($scope, query.split(' '), function(results) { | ||||
|           if (!$scope.searchVisible) { return; } | ||||
| 
 | ||||
|           var currentResults = $scope.searchResultState['results'] || []; | ||||
|           results.forEach(function(result) { | ||||
|             if (currentResults.length < documentSearchMaxResults) { | ||||
|               currentResults.push(result); | ||||
|             } | ||||
|           }); | ||||
| 
 | ||||
|           $scope.searchResultState = { | ||||
|             'state': currentResults.length ? 'results' : 'no-results', | ||||
|             'results': currentResults, | ||||
|             'current': currentResults.length ? 0 : -1 | ||||
|           }; | ||||
|         }, mapper, documentSearchScoreThreshold); | ||||
|       } | ||||
| 
 | ||||
|       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 || query != $scope.currentSearchQuery) { return; } | ||||
| 
 | ||||
|           $scope.searchResultState = { | ||||
|             'state': resp.results.length ? 'results' : 'no-results', | ||||
|             'results': resp.results, | ||||
|             'current': resp.results.length ? 0 : -1 | ||||
|           }; | ||||
| 
 | ||||
|           if (resp.results.length < documentSearchMaxResults) { | ||||
|             conductDocumentationSearch(query); | ||||
|           } | ||||
|         }, function(resp) { | ||||
|           $scope.searchResultState = null; | ||||
|         }, /* background */ true); | ||||
|       }; | ||||
| 
 | ||||
|       $scope.$watch('currentSearchQuery', conductSearch); | ||||
| 
 | ||||
|       $scope.signout = function() { | ||||
|         ApiService.logout().then(function() { | ||||
|           UserService.load(); | ||||
|  | @ -153,75 +75,6 @@ angular.module('quay').directive('headerBar', function () { | |||
|         return Config.getEnterpriseLogo(); | ||||
|       }; | ||||
| 
 | ||||
|       $scope.toggleSearch = function() { | ||||
|         $scope.searchVisible = !$scope.searchVisible; | ||||
|         if ($scope.searchVisible) { | ||||
|           $('#search-box-input').focus(); | ||||
|           if ($scope.currentSearchQuery) { | ||||
|             conductSearch($scope.currentSearchQuery); | ||||
|           } | ||||
|         } else { | ||||
|           $('#search-box-input').blur() | ||||
|           $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 (e.keyCode == 27) { | ||||
|           $scope.toggleSearch(); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         var state = $scope.searchResultState; | ||||
|         if (!state || !state['results']) { return; } | ||||
| 
 | ||||
|         if (e.keyCode == 40) { | ||||
|           state['current']++; | ||||
|           e.preventDefault(); | ||||
|         } else if (e.keyCode == 38) { | ||||
|           state['current']--; | ||||
|           e.preventDefault(); | ||||
|         } else if (e.keyCode == 13) { | ||||
|           var current = state['current']; | ||||
|           if (current >= 0 && current < state['results'].length) { | ||||
|             $scope.showResult(state['results'][current]); | ||||
|           } | ||||
|           e.preventDefault(); | ||||
|         } | ||||
| 
 | ||||
|         if (state['current'] < -1) { | ||||
|           state['current'] = state['results'].length - 1; | ||||
|         } else if (state['current'] >= state['results'].length) { | ||||
|           state['current'] = 0; | ||||
|         } | ||||
|       }; | ||||
| 
 | ||||
|       $scope.showResult = function(result) { | ||||
|         $scope.toggleSearch(); | ||||
|         $timeout(function() { | ||||
|           if (result['kind'] == 'doc') { | ||||
|             window.location = result['href']; | ||||
|             return; | ||||
|           } | ||||
| 
 | ||||
|           $scope.currentSearchQuery = ''; | ||||
|           $location.url(result['href']) | ||||
|         }, 500); | ||||
|       }; | ||||
| 
 | ||||
|       $scope.setCurrentResult = function(result) { | ||||
|         if (!$scope.searchResultState) { return; } | ||||
|         $scope.searchResultState['current'] = result; | ||||
|       }; | ||||
| 
 | ||||
|       $scope.getNamespace = function(context) { | ||||
|         if (!context) { return null; } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										54
									
								
								static/js/directives/ui/search-box/search-box.component.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								static/js/directives/ui/search-box/search-box.component.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,54 @@ | |||
| <span class="search-box-element"> | ||||
|   <script type="text/ng-template" id="search-result-template"> | ||||
|     <div class="search-box-result"> | ||||
|       <span class="kind">{{ result.title || 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"> | ||||
|             in | ||||
|             <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 ng-switch-when="robot"> | ||||
|           <i class="fa ci-robot"></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="result-description" ng-if="result.description"> | ||||
|             <div class="description markdown-view" content="result.description" | ||||
|                  first-line-only="true" placeholder-needed="false"></div> | ||||
|           </div> | ||||
|         </span> | ||||
|       </span> | ||||
|     </div> | ||||
|   </script> | ||||
| 
 | ||||
|   <input class="form-control" type="text" placeholder="search" | ||||
|          ng-model="$ctrl.enteredQuery" | ||||
|          typeahead="$ctrl.onTypeahead($event)" | ||||
|          ta-display-key="name" | ||||
|          ta-suggestion-tmpl="search-result-template" | ||||
|          ta-clear-on-select="true" | ||||
|          (ta-selected)="$ctrl.onSelected($event)" | ||||
|          (ta-entered)="$ctrl.onEntered($event)"> | ||||
|   <span class="search-icon"> | ||||
|     <span class="cor-loader-inline" ng-if="$ctrl.isSearching"></span> | ||||
|     <i class="fa fa-search" ng-if="!$ctrl.isSearching"></i> | ||||
|   </span> | ||||
| </span> | ||||
							
								
								
									
										56
									
								
								static/js/directives/ui/search-box/search-box.component.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								static/js/directives/ui/search-box/search-box.component.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,56 @@ | |||
| import { Input, Component, Inject } from 'ng-metadata/core'; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * A component that displays a search box with autocomplete. | ||||
|  */ | ||||
| @Component({ | ||||
|   selector: 'search-box', | ||||
|   templateUrl: '/static/js/directives/ui/search-box/search-box.component.html', | ||||
| }) | ||||
| export class SearchBoxComponent { | ||||
|   @Input('<query') public enteredQuery: string = ''; | ||||
| 
 | ||||
|   private isSearching: boolean = false; | ||||
|   private currentQuery: string = ''; | ||||
|   private autocompleteSelected: boolean = false; | ||||
| 
 | ||||
|   constructor(@Inject('ApiService') private ApiService: any, | ||||
|               @Inject('$timeout') private $timeout: ng.ITimeoutService, | ||||
|               @Inject('$location') private $location: ng.ILocationService) { | ||||
|   } | ||||
| 
 | ||||
|   private onTypeahead($event): void { | ||||
|     this.currentQuery = $event['query']; | ||||
|     if (this.currentQuery.length < 3) { | ||||
|       $event['callback']([]); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     var params = { | ||||
|        'query': this.currentQuery, | ||||
|     }; | ||||
| 
 | ||||
|     this.ApiService.conductSearch(null, params).then((resp) => { | ||||
|       if (this.currentQuery == $event['query']) { | ||||
|         $event['callback'](resp.results); | ||||
|         this.autocompleteSelected = false; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   private onSelected($event): void { | ||||
|     this.autocompleteSelected = true; | ||||
|     this.$timeout(() => { | ||||
|       this.$location.url($event['result']['href']) | ||||
|     }, 100); | ||||
|   } | ||||
| 
 | ||||
|   private onEntered($event): void { | ||||
|     this.$timeout(() => { | ||||
|       $event['callback'](true); // Clear the value.
 | ||||
|       this.$location.url('/search'); | ||||
|       this.$location.search('q', $event['value']); | ||||
|     }, 10); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										90
									
								
								static/js/directives/ui/typeahead/typeahead.directive.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								static/js/directives/ui/typeahead/typeahead.directive.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,90 @@ | |||
| import { Input, Output, Directive, Inject, AfterContentInit, EventEmitter, HostListener } from 'ng-metadata/core'; | ||||
| import * as $ from 'jquery'; | ||||
| 
 | ||||
| /** | ||||
|  * Directive which decorates an <input> with a typeahead autocomplete. | ||||
|  */ | ||||
| @Directive({ | ||||
|   selector: '[typeahead]', | ||||
| }) | ||||
| export class TypeaheadDirective implements AfterContentInit { | ||||
|   @Output('typeahead') typeahead = new EventEmitter<any>(); | ||||
| 
 | ||||
|   @Input('taDisplayKey') displayKey: string = ''; | ||||
|   @Input('taSuggestionTmpl') suggestionTemplate: string = ''; | ||||
|   @Input('taClearOnSelect') clearOnSelect: boolean = false; | ||||
| 
 | ||||
|   @Output('taSelected') selected = new EventEmitter<any>(); | ||||
|   @Output('taEntered') entered = new EventEmitter<any>(); | ||||
| 
 | ||||
|   private itemSelected: boolean = false; | ||||
| 
 | ||||
|   constructor(@Inject('$element') private $element: ng.IAugmentedJQuery, | ||||
|               @Inject('$compile') private $compile: ng.ICompileService, | ||||
|               @Inject('$scope') private $scope: ng.IScope, | ||||
|               @Inject('$templateRequest') private $templateRequest: ng.ITemplateRequestService) { | ||||
|   } | ||||
| 
 | ||||
|   public ngAfterContentInit(): void { | ||||
|     var templates = null; | ||||
|     if (this.suggestionTemplate) { | ||||
|       templates = {} | ||||
| 
 | ||||
|       if (this.suggestionTemplate) { | ||||
|         templates['suggestion'] = this.buildTemplateHandler(this.suggestionTemplate); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     $(this.$element).on('typeahead:select', (ev, suggestion) => { | ||||
|       if (this.clearOnSelect) { | ||||
|         $(this.$element).typeahead('val', ''); | ||||
|       } | ||||
|       this.selected.emit({'result': suggestion}) | ||||
|       this.itemSelected = true; | ||||
|     }); | ||||
| 
 | ||||
|     $(this.$element).typeahead( | ||||
|       { | ||||
|         highlight: false, | ||||
|         hint: false, | ||||
|       }, | ||||
|       { | ||||
|         templates: templates, | ||||
|         display: this.displayKey, | ||||
|         source: (query, results, asyncResults) => { | ||||
|           this.typeahead.emit({'query': query, 'callback': asyncResults}); | ||||
|           this.itemSelected = false; | ||||
|         }, | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('keyup', ['$event']) | ||||
|   public onKeyup(event: JQueryKeyEventObject): void { | ||||
|     if (!this.itemSelected && event.keyCode == 13) { | ||||
|       this.entered.emit({ | ||||
|         'value': $(this.$element).typeahead('val'), | ||||
|         'callback': (reset: boolean) => { | ||||
|           if (reset) { | ||||
|             this.itemSelected = false; | ||||
|             $(this.$element).typeahead('val', ''); | ||||
|           } | ||||
|        } | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private buildTemplateHandler(templateUrl: string): Function { | ||||
|     return (value) => { | ||||
|       var resultDiv = document.createElement('div'); | ||||
|       this.$templateRequest(templateUrl).then((tplContent) => { | ||||
|         var tplEl = document.createElement('span'); | ||||
|         tplEl.innerHTML = tplContent.trim(); | ||||
|         var scope = this.$scope.$new(true); | ||||
|         scope['result'] = value; | ||||
|         this.$compile(tplEl)(scope); | ||||
|         resultDiv.appendChild(tplEl); | ||||
|       }); | ||||
|       return resultDiv; | ||||
|     }; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										49
									
								
								static/js/pages/search.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								static/js/pages/search.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,49 @@ | |||
| (function() { | ||||
|   /** | ||||
|    * Search page. | ||||
|    */ | ||||
|   angular.module('quayPages').config(['pages', function(pages) { | ||||
|     pages.create('search', 'search.html', SearchCtrl, { | ||||
|       'title': 'Search' | ||||
|     }); | ||||
|   }]); | ||||
| 
 | ||||
|   function SearchCtrl($scope, ApiService, $routeParams, $location) { | ||||
|     var refreshResults = function() { | ||||
|       $scope.currentPage = ($routeParams['page'] || '1') * 1; | ||||
| 
 | ||||
|       var params = { | ||||
|         'query': $routeParams['q'], | ||||
|         'page': $scope.currentPage | ||||
|       }; | ||||
| 
 | ||||
|       $scope.maxPopularity = 0; | ||||
|       $scope.resultsResource = ApiService.conductRepoSearchAsResource(params).get(function(resp) { | ||||
|         $scope.results = resp['results']; | ||||
|         $scope.hasAdditional = resp['has_additional']; | ||||
|         $scope.startIndex = resp['start_index']; | ||||
|         resp['results'].forEach(function(result) { | ||||
|           $scope.maxPopularity = Math.max($scope.maxPopularity, result['popularity']); | ||||
|         }); | ||||
|       }); | ||||
|     }; | ||||
| 
 | ||||
|     $scope.previousPage = function() { | ||||
|       $location.search('page', (($routeParams['page'] || 1) * 1) - 1); | ||||
|     }; | ||||
| 
 | ||||
|     $scope.nextPage = function() { | ||||
|       $location.search('page', (($routeParams['page'] || 1) * 1) + 1); | ||||
|     }; | ||||
| 
 | ||||
|     $scope.currentQuery = $routeParams['q']; | ||||
|     refreshResults(); | ||||
| 
 | ||||
|     $scope.$on('$routeUpdate', function(){ | ||||
|       $scope.currentQuery = $routeParams['q']; | ||||
|       refreshResults(); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   SearchCtrl.$inject = ['$scope', 'ApiService', '$routeParams', '$location']; | ||||
| })(); | ||||
|  | @ -54,6 +54,9 @@ function provideRoutes($routeProvider: ng.route.IRouteProvider, | |||
|   } | ||||
| 
 | ||||
|   routeBuilder | ||||
|     // Search
 | ||||
|     .route('/search', 'search') | ||||
| 
 | ||||
|     // Application View
 | ||||
|     .route('/application/:namespace/:name', 'app-view') | ||||
| 
 | ||||
|  |  | |||
|  | @ -18,6 +18,8 @@ import { TagSigningDisplayComponent } from './directives/ui/tag-signing-display/ | |||
| import { RepositorySigningConfigComponent } from './directives/ui/repository-signing-config/repository-signing-config.component'; | ||||
| import { TimeMachineSettingsComponent } from './directives/ui/time-machine-settings/time-machine-settings.component'; | ||||
| import { DurationInputComponent } from './directives/ui/duration-input/duration-input.component'; | ||||
| import { SearchBoxComponent } from './directives/ui/search-box/search-box.component'; | ||||
| import { TypeaheadDirective } from './directives/ui/typeahead/typeahead.directive'; | ||||
| import { BuildServiceImpl } from './services/build/build.service.impl'; | ||||
| import { AvatarServiceImpl } from './services/avatar/avatar.service.impl'; | ||||
| import { DockerfileServiceImpl } from './services/dockerfile/dockerfile.service.impl'; | ||||
|  | @ -52,6 +54,8 @@ import { QuayRequireDirective } from './directives/structural/quay-require/quay- | |||
|     RepositorySigningConfigComponent, | ||||
|     TimeMachineSettingsComponent, | ||||
|     DurationInputComponent, | ||||
|     SearchBoxComponent, | ||||
|     TypeaheadDirective, | ||||
|   ], | ||||
|   providers: [ | ||||
|     ViewArrayImpl, | ||||
|  |  | |||
							
								
								
									
										7
									
								
								static/lib/typeahead.bundle.min.js
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								static/lib/typeahead.bundle.min.js
									
										
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										47
									
								
								static/partials/search.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								static/partials/search.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,47 @@ | |||
| <div class="cor-container search"> | ||||
|   <div class="search-top-bar"> | ||||
|     <search-box query="currentQuery"></search-box> | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="search-results-section"> | ||||
|     <h5>Applications and Repositories</h5> | ||||
|     <div class="resource-view" resource="resultsResource" error-message="'Could not search results'"> | ||||
|         <div class="empty" ng-if="!results.length"> | ||||
|           <div class="empty-primary-msg">No matching applications or repositories found</div> | ||||
|           <div class="empty-secondary-msg"> | ||||
|             Please try changing your query. | ||||
|           </div> | ||||
|         </div> | ||||
|         <ol class="search-results" start="{{ startIndex + 1 }}"> | ||||
|             <li ng-repeat="result in results"> | ||||
|               <div class="search-result-box"> | ||||
|                 <span class="star-count"> | ||||
|                   <span class="star-count-number">{{ result.stars }}</span> | ||||
|                   <i class="star-icon starred fa fa-star"></i> | ||||
|                 </span> | ||||
|                 <h4> | ||||
|                     <i class="fa fa-hdd-o" ng-if="result.kind == 'repository'"></i> | ||||
|                     <i class="fa ci-app-cube" ng-if="result.kind == 'application'"></i> | ||||
|                     <a href="{{ result.href }}">{{ result.namespace.name }}/{{ result.name }}</a> | ||||
|                 </h4> | ||||
|                 <p class="description"> | ||||
|                   <span class="markdown-view" content="result.description"></span> | ||||
|                 </p> | ||||
|                 <p class="result-info-bar"> | ||||
|                   Last Modified:  <span am-time-ago="result.last_modified * 1000"></span> | ||||
| 
 | ||||
|                   <span class="activity"> | ||||
|                     activity | ||||
|                     <span class="strength-indicator" value="::result.popularity" | ||||
|                           maximum="::maxPopularity" | ||||
|                           log-base="10"></span> | ||||
|                   </span> | ||||
|                 </p> | ||||
|               </div> | ||||
|             </li> | ||||
|         </ol> | ||||
|         <a class="btn btn-default" ng-click="previousPage()" ng-if="currentPage > 1">Previous Page</a> | ||||
|         <a class="btn btn-default" ng-click="nextPage()" ng-if="hasAdditional">Next Page</a> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
		Reference in a new issue