From c374e8146a1b9fbba498d47adc094c606398862f Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 4 Apr 2014 23:26:10 -0400 Subject: [PATCH] - Add code for placing the features information on the frontend - Add a Features service for examining feature flags on the frontend - Add a directive (quay-requires) that matches feature flags and, if any one does not match, removes the element from the DOM - Add a directive (quay-show) that injects the features into the scope so that expressions of the form "Features.BILLING || something" work out of the box to show/hide the element - Add a directive (quay-classes) that allows for setting of CSS classes on an element based on feature expression(s) such as {"!BILLING": "active"} (e.g. the BILLING flag is set to false, add the class "active". --- config.py | 7 ++ endpoints/common.py | 9 +- features/__init__.py | 10 ++- static/js/app.js | 142 ++++++++++++++++++++++++++++++++ static/partials/user-admin.html | 29 ++++--- templates/base.html | 1 + 6 files changed, 185 insertions(+), 13 deletions(-) diff --git a/config.py b/config.py index bf1790ba5..b335e4d13 100644 --- a/config.py +++ b/config.py @@ -109,8 +109,15 @@ class DefaultConfig(object): STATUS_TAGS[tag_name] = tag_svg.read() + # Feature Flag: Whether billing is required. FEATURE_BILLING = False + # Feature Flag: Whether user accounts automatically have usage log access. + FEATURE_USER_LOG_ACCESS = True + + # Feature Flag: Whether GitHub login is supported. + FEATURE_GITHUB_LOGIN = False + class FakeTransaction(object): def __enter__(self): diff --git a/endpoints/common.py b/endpoints/common.py index a2e3625e0..07965a838 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -17,6 +17,8 @@ from endpoints.api.discovery import swagger_route_data from werkzeug.routing import BaseConverter from functools import wraps +import features + logger = logging.getLogger(__name__) route_data = None @@ -109,12 +111,17 @@ def handle_dme(ex): def random_string(): + return 'REMOVEME' + random = SystemRandom() return ''.join([random.choice(string.ascii_uppercase + string.digits) for _ in range(8)]) def render_page_template(name, **kwargs): resp = make_response(render_template(name, route_data=json.dumps(get_route_data()), - cache_buster=random_string(), **kwargs)) + feature_set=json.dumps(features.get_features()), + cache_buster=random_string(), + **kwargs)) + resp.headers['X-FRAME-OPTIONS'] = 'DENY' return resp diff --git a/features/__init__.py b/features/__init__.py index ef9ee253a..9318a9b4a 100644 --- a/features/__init__.py +++ b/features/__init__.py @@ -1,8 +1,14 @@ +_FEATURES = {} + def import_features(config_dict): for feature, feature_val in config_dict.items(): if feature.startswith('FEATURE_'): feature_name = feature[8:] - globals()[feature_name] = FeatureNameValue(feature_name, feature_val) + _FEATURES[feature_name] = globals()[feature_name] = FeatureNameValue(feature_name, feature_val) + + +def get_features(): + return {key: _FEATURES[key].value for key in _FEATURES} class FeatureNameValue(object): @@ -14,7 +20,7 @@ class FeatureNameValue(object): return '%s => %s' % (self.name, self.value) def __repr__(self): - return self.value + return str(self.value) def __cmp__(self, other): return self.value.__cmp__(other) diff --git a/static/js/app.js b/static/js/app.js index 311b531fb..532512bb0 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -434,6 +434,37 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu return metadataService; }]); + $provide.factory('Features', [function() { + if (!window.__features) { + return {}; + } + + var features = window.__features; + features.getFeature = function(name, opt_defaultValue) { + var value = features[name]; + if (value == null) { + return opt_defaultValue; + } + return value; + }; + + features.hasFeature = function(name) { + return !!features.getFeature(name); + }; + + features.matchesFeatures = function(list) { + for (var i = 0; i < list.length; ++i) { + var value = features.getFeature(list[i]); + if (!value) { + return false; + } + } + return true; + }; + + return features; + }]); + $provide.factory('ApiService', ['Restangular', function(Restangular) { var apiService = {}; @@ -1241,6 +1272,117 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu }); +function buildConditionalLinker($animate, name, evaluator) { + // Based off of a solution found here: http://stackoverflow.com/questions/20325480/angularjs-whats-the-best-practice-to-add-ngif-to-a-directive-programmatically + return function ($scope, $element, $attr, ctrl, $transclude) { + var block; + var childScope; + var roles; + + $attr.$observe(name, function (value) { + if (evaluator($scope.$eval(value))) { + if (!childScope) { + childScope = $scope.$new(); + $transclude(childScope, function (clone) { + block = { + startNode: clone[0], + endNode: clone[clone.length++] = document.createComment(' end ' + name + ': ' + $attr[name] + ' ') + }; + $animate.enter(clone, $element.parent(), $element); + }); + } + } else { + if (childScope) { + childScope.$destroy(); + childScope = null; + } + + if (block) { + $animate.leave(getBlockElements(block)); + block = null; + } + } + }); + } +} + +quayApp.directive('quayRequire', function ($animate, Features) { + return { + transclude: 'element', + priority: 600, + terminal: true, + restrict: 'A', + link: buildConditionalLinker($animate, 'quayRequire', function(value) { + return Features.matchesFeatures(value); + }) + }; +}); + + +quayApp.directive('quayShow', function($animate, Features) { + return { + priority: 590, + restrict: 'A', + link: function($scope, $element, $attr, ctrl, $transclude) { + $scope.Features = Features; + $scope.$watch($attr.quayShow, function(result) { + $animate[!!result ? 'removeClass' : 'addClass']($element, 'ng-hide'); + }); + } + }; +}); + + +quayApp.directive('quayClasses', function(Features) { + return { + priority: 580, + restrict: 'A', + link: function($scope, $element, $attr, ctrl, $transclude) { + + // Borrowed from ngClass. + function flattenClasses(classVal) { + if(angular.isArray(classVal)) { + return classVal.join(' '); + } else if (angular.isObject(classVal)) { + var classes = [], i = 0; + angular.forEach(classVal, function(v, k) { + if (v) { + classes.push(k); + } + }); + return classes.join(' '); + } + + return classVal; + } + + function removeClass(classVal) { + $attr.$removeClass(flattenClasses(classVal)); + } + + + function addClass(classVal) { + $attr.$addClass(flattenClasses(classVal)); + } + + $scope.$watch($attr.quayClasses, function(result) { + for (var expr in result) { + if (!result.hasOwnProperty(expr)) { continue; } + + // Evaluate the expression with the entire features list added. + var value = $scope.$eval(expr, Features); + if (value) { + addClass(result[expr]); + } else { + removeClass(result[expr]); + } + } + }); + } + }; +}); + + quayApp.directive('entityReference', function () { var directiveDefinitionObject = { priority: 0, diff --git a/static/partials/user-admin.html b/static/partials/user-admin.html index 0fada5347..fcfb157c1 100644 --- a/static/partials/user-admin.html +++ b/static/partials/user-admin.html @@ -20,15 +20,24 @@
@@ -91,12 +100,12 @@ -
+
-
+
An e-mail has been sent to {{ sentEmail }} to verify the change.
@@ -177,12 +186,12 @@
-
+
-
+
diff --git a/templates/base.html b/templates/base.html index 59e427da1..5a1638574 100644 --- a/templates/base.html +++ b/templates/base.html @@ -73,6 +73,7 @@