Merge pull request #2529 from coreos-inc/search-ui

Implement new search UI
This commit is contained in:
josephschorr 2017-05-02 15:56:59 -04:00 committed by GitHub
commit 5a9a231754
23 changed files with 649 additions and 393 deletions

View file

@ -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;
}

View 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;
}

View 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;
}

View 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;
}

View file

@ -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>

View file

@ -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();

View file

@ -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(/&#39\;/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; }

View 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>

View 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);
}
}

View 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
View 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'];
})();

View file

@ -54,6 +54,9 @@ function provideRoutes($routeProvider: ng.route.IRouteProvider,
}
routeBuilder
// Search
.route('/search', 'search')
// Application View
.route('/application/:namespace/:name', 'app-view')

View file

@ -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,

File diff suppressed because one or more lines are too long

View 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>