- 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()
|
||||
|
||||
|
||||
# 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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
142
static/js/app.js
142
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,
|
||||
|
|
|
@ -20,15 +20,24 @@
|
|||
<!-- Side tabs -->
|
||||
<div class="col-md-2">
|
||||
<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>
|
||||
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing Options</a></li>
|
||||
<li ng-show="hasPaidBusinessPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a></li>
|
||||
<!-- Billing Related -->
|
||||
<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="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="#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="#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 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>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -91,12 +100,12 @@
|
|||
</div>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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="alert alert-success" ng-show="changeEmailSent">An e-mail has been sent to {{ sentEmail }} to verify the change.</div>
|
||||
|
||||
|
@ -177,12 +186,12 @@
|
|||
</div>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
|
|
|
@ -73,6 +73,7 @@
|
|||
|
||||
<script type="text/javascript">
|
||||
window.__endpoints = {{ route_data|safe }}.apis;
|
||||
window.__features = {{ feature_set|safe }};
|
||||
window.__token = '{{ csrf_token() }}';
|
||||
</script>
|
||||
|
||||
|
|
Reference in a new issue