diff --git a/config.py b/config.py index 7a81c68cd..38045805f 100644 --- a/config.py +++ b/config.py @@ -19,10 +19,10 @@ def build_requests_session(): CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'MIXPANEL_KEY', 'STRIPE_PUBLISHABLE_KEY', 'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN', '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 = {} for name in CLIENT_WHITELIST: if name.lower().find('secret') >= 0: @@ -232,5 +232,8 @@ class DefaultConfig(object): '#5254a3', '#6b6ecf', '#9c9ede', '#9ecae1', '#31a354', '#b5cf6b', '#a1d99b', '#8c6d31', '#ad494a', '#e7ba52', '#a55194'] + # The location of the Quay documentation. + DOCUMENTATION_LOCATION = 'http://docs.quay.io' + # Experiment: Async garbage collection EXP_ASYNC_GARBAGE_COLLECTION = [] diff --git a/endpoints/common.py b/endpoints/common.py index 05f3bdb1e..7469c58be 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -20,7 +20,7 @@ from auth import scopes from endpoints.api.discovery import swagger_route_data from werkzeug.routing import BaseConverter from functools import wraps -from config import getFrontendVisibleConfig +from config import frontend_visible_config from external_libraries import get_external_javascript, get_external_css import features @@ -180,7 +180,7 @@ def render_page_template(name, **kwargs): main_scripts=add_cachebusters(main_scripts), library_scripts=add_cachebusters(library_scripts), 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()), scope_set=json.dumps(scopes.app_scopes(app.config)), mixpanel_key=app.config.get('MIXPANEL_KEY', ''), diff --git a/static/directives/header-bar.html b/static/directives/header-bar.html index 9f8f17762..27d3ff4cd 100644 --- a/static/directives/header-bar.html +++ b/static/directives/header-bar.html @@ -181,10 +181,14 @@ {{ result.name }} - + {{ result.name }} + + + {{ result.name }} + {{ result.namespace.name }}/{{ result.name }} diff --git a/static/js/directives/ui/header-bar.js b/static/js/directives/ui/header-bar.js index 1a8a60508..1162ef240 100644 --- a/static/js/directives/ui/header-bar.js +++ b/static/js/directives/ui/header-bar.js @@ -13,7 +13,8 @@ angular.module('quay').directive('headerBar', function () { scope: { }, 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 userUpdated = function(cUser) { $scope.searchingAllowed = Features.ANONYMOUS_ACCESS || !cUser.anonymous; @@ -71,6 +72,39 @@ 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; } @@ -90,6 +124,10 @@ angular.module('quay').directive('headerBar', function () { 'results': resp.results, 'current': resp.results.length ? 0 : -1 }; + + if (resp.results.length < documentSearchMaxResults) { + conductDocumentationSearch(query); + } }, function(resp) { $scope.searchResultState = null; }, /* background */ true); @@ -178,6 +216,11 @@ angular.module('quay').directive('headerBar', function () { $scope.showResult = function(result) { $scope.toggleSearch(); $timeout(function() { + if (result['kind'] == 'doc') { + window.location = result['href']; + return; + } + $scope.currentSearchQuery = ''; $location.url(result['href']) }, 500); diff --git a/static/js/services/documentation-service.js b/static/js/services/documentation-service.js new file mode 100644 index 000000000..c61c76c76 --- /dev/null +++ b/static/js/services/documentation-service.js @@ -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; +}]); \ No newline at end of file