- 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".
This commit is contained in:
Joseph Schorr 2014-04-04 23:26:10 -04:00
parent 4f4112b18d
commit c374e8146a
6 changed files with 185 additions and 13 deletions

View file

@ -109,8 +109,15 @@ class DefaultConfig(object):
STATUS_TAGS[tag_name] = tag_svg.read() STATUS_TAGS[tag_name] = tag_svg.read()
# Feature Flag: Whether billing is required.
FEATURE_BILLING = False 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): class FakeTransaction(object):
def __enter__(self): def __enter__(self):

View file

@ -17,6 +17,8 @@ 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
import features
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
route_data = None route_data = None
@ -109,12 +111,17 @@ def handle_dme(ex):
def random_string(): def random_string():
return 'REMOVEME'
random = SystemRandom() random = SystemRandom()
return ''.join([random.choice(string.ascii_uppercase + string.digits) for _ in range(8)]) return ''.join([random.choice(string.ascii_uppercase + string.digits) for _ in range(8)])
def render_page_template(name, **kwargs): def render_page_template(name, **kwargs):
resp = make_response(render_template(name, route_data=json.dumps(get_route_data()), 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' resp.headers['X-FRAME-OPTIONS'] = 'DENY'
return resp return resp

View file

@ -1,8 +1,14 @@
_FEATURES = {}
def import_features(config_dict): def import_features(config_dict):
for feature, feature_val in config_dict.items(): for feature, feature_val in config_dict.items():
if feature.startswith('FEATURE_'): if feature.startswith('FEATURE_'):
feature_name = feature[8:] 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): class FeatureNameValue(object):
@ -14,7 +20,7 @@ class FeatureNameValue(object):
return '%s => %s' % (self.name, self.value) return '%s => %s' % (self.name, self.value)
def __repr__(self): def __repr__(self):
return self.value return str(self.value)
def __cmp__(self, other): def __cmp__(self, other):
return self.value.__cmp__(other) return self.value.__cmp__(other)

View file

@ -434,6 +434,37 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
return metadataService; 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) { $provide.factory('ApiService', ['Restangular', function(Restangular) {
var apiService = {}; 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 () { quayApp.directive('entityReference', function () {
var directiveDefinitionObject = { var directiveDefinitionObject = {
priority: 0, priority: 0,

View file

@ -20,15 +20,24 @@
<!-- Side tabs --> <!-- Side tabs -->
<div class="col-md-2"> <div class="col-md-2">
<ul class="nav nav-pills nav-stacked"> <ul class="nav nav-pills nav-stacked">
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a></li> <!-- Billing Related -->
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing Options</a></li> <li class="active" quay-require="['BILLING']"><a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a></li>
<li ng-show="hasPaidBusinessPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a></li> <li ng-show="hasPaidPlan" quay-require="['BILLING']">
<a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing Options</a>
</li>
<li ng-show="hasPaidBusinessPlan" quay-require="['BILLING']">
<a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a>
</li>
<!-- Non-billing -->
<li quay-classes="{'!BILLING': 'active'}"><a href="javascript:void(0)" data-toggle="tab" data-target="#email">Account E-mail</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#email">Account E-mail</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Change Password</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Change Password</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#github">GitHub Login</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#github" quay-require="['GITHUB_LOGIN']">GitHub Login</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#authorized" ng-click="loadAuthedApps()">Authorized Applications</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#authorized" ng-click="loadAuthedApps()">Authorized Applications</a></li>
<li ng-show="hasPaidBusinessPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a></li> <li quay-show="Features.USER_LOG_ACCESS || hasPaidBusinessPlan">
<a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a>
</li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#migrate" id="migrateTab">Convert to Organization</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#migrate" id="migrateTab">Convert to Organization</a></li>
</ul> </ul>
</div> </div>
@ -91,12 +100,12 @@
</div> </div>
<!-- Plans tab --> <!-- Plans tab -->
<div id="plan" class="tab-pane active"> <div id="plan" class="tab-pane active" quay-require="['BILLING']">
<div class="plan-manager" user="user.username" ready-for-plan="readyForPlan()" plan-changed="planChanged(plan)"></div> <div class="plan-manager" user="user.username" ready-for-plan="readyForPlan()" plan-changed="planChanged(plan)"></div>
</div> </div>
<!-- E-mail address tab --> <!-- E-mail address tab -->
<div id="email" class="tab-pane"> <div id="email" class="tab-pane" quay-classes="{'!BILLING': 'active'}">
<div class="row"> <div class="row">
<div class="alert alert-success" ng-show="changeEmailSent">An e-mail has been sent to {{ sentEmail }} to verify the change.</div> <div class="alert alert-success" ng-show="changeEmailSent">An e-mail has been sent to {{ sentEmail }} to verify the change.</div>
@ -177,12 +186,12 @@
</div> </div>
<!-- Billing options tab --> <!-- Billing options tab -->
<div id="billingoptions" class="tab-pane"> <div id="billingoptions" class="tab-pane" quay-require="['BILLING']">
<div class="billing-options" user="user"></div> <div class="billing-options" user="user"></div>
</div> </div>
<!-- Billing History tab --> <!-- Billing History tab -->
<div id="billing" class="tab-pane"> <div id="billing" class="tab-pane" quay-require="['BILLING']">
<div class="billing-invoices" user="user" visible="invoicesShown"></div> <div class="billing-invoices" user="user" visible="invoicesShown"></div>
</div> </div>

View file

@ -73,6 +73,7 @@
<script type="text/javascript"> <script type="text/javascript">
window.__endpoints = {{ route_data|safe }}.apis; window.__endpoints = {{ route_data|safe }}.apis;
window.__features = {{ feature_set|safe }};
window.__token = '{{ csrf_token() }}'; window.__token = '{{ csrf_token() }}';
</script> </script>