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