Add documentation search to the main search bar
This commit is contained in:
parent
db841de26f
commit
8a8955d234
5 changed files with 142 additions and 6 deletions
|
@ -19,10 +19,10 @@ def build_requests_session():
|
||||||
CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'MIXPANEL_KEY',
|
CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'MIXPANEL_KEY',
|
||||||
'STRIPE_PUBLISHABLE_KEY', 'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN',
|
'STRIPE_PUBLISHABLE_KEY', 'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN',
|
||||||
'AUTHENTICATION_TYPE', 'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT',
|
'AUTHENTICATION_TYPE', 'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT',
|
||||||
'CONTACT_INFO', 'AVATAR_KIND', 'LOCAL_OAUTH_HANDLER']
|
'CONTACT_INFO', 'AVATAR_KIND', 'LOCAL_OAUTH_HANDLER', 'DOCUMENTATION_LOCATION']
|
||||||
|
|
||||||
|
|
||||||
def getFrontendVisibleConfig(config_dict):
|
def frontend_visible_config(config_dict):
|
||||||
visible_dict = {}
|
visible_dict = {}
|
||||||
for name in CLIENT_WHITELIST:
|
for name in CLIENT_WHITELIST:
|
||||||
if name.lower().find('secret') >= 0:
|
if name.lower().find('secret') >= 0:
|
||||||
|
@ -232,5 +232,8 @@ class DefaultConfig(object):
|
||||||
'#5254a3', '#6b6ecf', '#9c9ede', '#9ecae1', '#31a354', '#b5cf6b', '#a1d99b',
|
'#5254a3', '#6b6ecf', '#9c9ede', '#9ecae1', '#31a354', '#b5cf6b', '#a1d99b',
|
||||||
'#8c6d31', '#ad494a', '#e7ba52', '#a55194']
|
'#8c6d31', '#ad494a', '#e7ba52', '#a55194']
|
||||||
|
|
||||||
|
# The location of the Quay documentation.
|
||||||
|
DOCUMENTATION_LOCATION = 'http://docs.quay.io'
|
||||||
|
|
||||||
# Experiment: Async garbage collection
|
# Experiment: Async garbage collection
|
||||||
EXP_ASYNC_GARBAGE_COLLECTION = []
|
EXP_ASYNC_GARBAGE_COLLECTION = []
|
||||||
|
|
|
@ -20,7 +20,7 @@ from auth import scopes
|
||||||
from endpoints.api.discovery import swagger_route_data
|
from endpoints.api.discovery import swagger_route_data
|
||||||
from werkzeug.routing import BaseConverter
|
from werkzeug.routing import BaseConverter
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from config import getFrontendVisibleConfig
|
from config import frontend_visible_config
|
||||||
from external_libraries import get_external_javascript, get_external_css
|
from external_libraries import get_external_javascript, get_external_css
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
@ -180,7 +180,7 @@ def render_page_template(name, **kwargs):
|
||||||
main_scripts=add_cachebusters(main_scripts),
|
main_scripts=add_cachebusters(main_scripts),
|
||||||
library_scripts=add_cachebusters(library_scripts),
|
library_scripts=add_cachebusters(library_scripts),
|
||||||
feature_set=json.dumps(features.get_features()),
|
feature_set=json.dumps(features.get_features()),
|
||||||
config_set=json.dumps(getFrontendVisibleConfig(app.config)),
|
config_set=json.dumps(frontend_visible_config(app.config)),
|
||||||
oauth_set=json.dumps(get_oauth_config()),
|
oauth_set=json.dumps(get_oauth_config()),
|
||||||
scope_set=json.dumps(scopes.app_scopes(app.config)),
|
scope_set=json.dumps(scopes.app_scopes(app.config)),
|
||||||
mixpanel_key=app.config.get('MIXPANEL_KEY', ''),
|
mixpanel_key=app.config.get('MIXPANEL_KEY', ''),
|
||||||
|
|
|
@ -181,10 +181,14 @@
|
||||||
<span class="avatar" data="result.avatar" size="16"></span>
|
<span class="avatar" data="result.avatar" size="16"></span>
|
||||||
<span class="result-name">{{ result.name }}</span>
|
<span class="result-name">{{ result.name }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span href="/user/{{ result.name }}" ng-switch-when="robot">
|
<span ng-switch-when="robot">
|
||||||
<i class="fa ci-robot"></i>
|
<i class="fa ci-robot"></i>
|
||||||
<span class="result-name">{{ result.name }}</span>
|
<span class="result-name">{{ result.name }}</span>
|
||||||
</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 ng-switch-when="repository">
|
||||||
<span class="avatar" data="result.namespace.avatar" size="16"></span>
|
<span class="avatar" data="result.namespace.avatar" size="16"></span>
|
||||||
<span class="result-name">{{ result.namespace.name }}/{{ result.name }}</span>
|
<span class="result-name">{{ result.namespace.name }}/{{ result.name }}</span>
|
||||||
|
|
|
@ -13,7 +13,8 @@ angular.module('quay').directive('headerBar', function () {
|
||||||
scope: {
|
scope: {
|
||||||
},
|
},
|
||||||
controller: function($rootScope, $scope, $element, $location, $timeout, hotkeys, UserService,
|
controller: function($rootScope, $scope, $element, $location, $timeout, hotkeys, UserService,
|
||||||
PlanService, ApiService, NotificationService, Config, CreateService, Features) {
|
PlanService, ApiService, NotificationService, Config, CreateService, Features,
|
||||||
|
DocumentationService) {
|
||||||
var hotkeysAdded = false;
|
var hotkeysAdded = false;
|
||||||
var userUpdated = function(cUser) {
|
var userUpdated = function(cUser) {
|
||||||
$scope.searchingAllowed = Features.ANONYMOUS_ACCESS || !cUser.anonymous;
|
$scope.searchingAllowed = Features.ANONYMOUS_ACCESS || !cUser.anonymous;
|
||||||
|
@ -71,6 +72,39 @@ angular.module('quay').directive('headerBar', function () {
|
||||||
$scope.currentPageContext['repository'] = r;
|
$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) {
|
var conductSearch = function(query) {
|
||||||
if (!query) { $scope.searchResultState = null; return; }
|
if (!query) { $scope.searchResultState = null; return; }
|
||||||
|
|
||||||
|
@ -90,6 +124,10 @@ angular.module('quay').directive('headerBar', function () {
|
||||||
'results': resp.results,
|
'results': resp.results,
|
||||||
'current': resp.results.length ? 0 : -1
|
'current': resp.results.length ? 0 : -1
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (resp.results.length < documentSearchMaxResults) {
|
||||||
|
conductDocumentationSearch(query);
|
||||||
|
}
|
||||||
}, function(resp) {
|
}, function(resp) {
|
||||||
$scope.searchResultState = null;
|
$scope.searchResultState = null;
|
||||||
}, /* background */ true);
|
}, /* background */ true);
|
||||||
|
@ -178,6 +216,11 @@ angular.module('quay').directive('headerBar', function () {
|
||||||
$scope.showResult = function(result) {
|
$scope.showResult = function(result) {
|
||||||
$scope.toggleSearch();
|
$scope.toggleSearch();
|
||||||
$timeout(function() {
|
$timeout(function() {
|
||||||
|
if (result['kind'] == 'doc') {
|
||||||
|
window.location = result['href'];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$scope.currentSearchQuery = '';
|
$scope.currentSearchQuery = '';
|
||||||
$location.url(result['href'])
|
$location.url(result['href'])
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
86
static/js/services/documentation-service.js
Normal file
86
static/js/services/documentation-service.js
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
/**
|
||||||
|
* Service which exposes access to the documentation metadata.
|
||||||
|
*/
|
||||||
|
angular.module('quay').factory('DocumentationService', ['Config', '$timeout', function(Config, $timeout) {
|
||||||
|
var documentationService = {};
|
||||||
|
var documentationData = null;
|
||||||
|
var documentationFailure = false;
|
||||||
|
|
||||||
|
var MINIMUM_KEYWORD_LENGTH = 3;
|
||||||
|
var TITLE_MATCH_SCORE = 1;
|
||||||
|
var CONTENT_MATCH_SCORE = 0.5;
|
||||||
|
|
||||||
|
documentationService.findDocumentation = function($scope, keywords, callback, opt_mapper, opt_threshold) {
|
||||||
|
opt_threshold = opt_threshold || 0;
|
||||||
|
|
||||||
|
documentationService.loadDocumentation(function(metadata) {
|
||||||
|
if (!metadata) {
|
||||||
|
$scope.$apply(function() {
|
||||||
|
callback([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = [];
|
||||||
|
|
||||||
|
metadata.forEach(function(page) {
|
||||||
|
var score = 0;
|
||||||
|
|
||||||
|
keywords.forEach(function(keyword) {
|
||||||
|
if (keyword.length < MINIMUM_KEYWORD_LENGTH) { return; }
|
||||||
|
|
||||||
|
var title = page.title || '';
|
||||||
|
var content = page.content || '';
|
||||||
|
|
||||||
|
if (title.toLowerCase().indexOf(keyword.toLowerCase()) >= 0) {
|
||||||
|
score += TITLE_MATCH_SCORE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.toLowerCase().indexOf(keyword.toLowerCase()) >= 0) {
|
||||||
|
score += CONTENT_MATCH_SCORE;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (score > opt_threshold) {
|
||||||
|
results.push(opt_mapper ? opt_mapper(page, score) : {'page': page, 'score': score});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$apply(function() {
|
||||||
|
results.sort(function(a, b) {
|
||||||
|
return b.score - a.score;
|
||||||
|
});
|
||||||
|
|
||||||
|
callback(results);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
documentationService.loadDocumentation = function(callback) {
|
||||||
|
if (documentationFailure) {
|
||||||
|
$timeout(function() {
|
||||||
|
callback(null);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (documentationData != null) {
|
||||||
|
$timeout(function() {
|
||||||
|
callback(documentationData);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax(Config.DOCUMENTATION_LOCATION + '/search.json')
|
||||||
|
.done(function(r) {
|
||||||
|
documentationData = r;
|
||||||
|
callback(documentationData);
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
documentationFailure = true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return documentationService;
|
||||||
|
}]);
|
Reference in a new issue