- 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:
parent
4f4112b18d
commit
c374e8146a
6 changed files with 185 additions and 13 deletions
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
142
static/js/app.js
142
static/js/app.js
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
Reference in a new issue