Fix UI for real license handling

Following this change, the user gets detailed errors and entitlement information
This commit is contained in:
Joseph Schorr 2016-10-19 15:35:34 -04:00
parent e450b109a2
commit 213cc856e4
9 changed files with 172 additions and 136 deletions

View file

@ -288,13 +288,12 @@ class SuperUserSetAndValidateLicense(ApiResource):
statuses = decoded_license.validate({}) statuses = decoded_license.validate({})
all_met = all(status.is_met() for status in statuses) all_met = all(status.is_met() for status in statuses)
if not all_met: if all_met:
raise InvalidRequest('License is insufficient')
config_provider.save_license(license_contents) config_provider.save_license(license_contents)
return { return {
'decoded': {}, 'status': [status.as_dict(for_private=True) for status in statuses],
'success': True 'success': all_met,
} }

View file

@ -856,12 +856,10 @@ class SuperUserLicense(ApiResource):
statuses = decoded_license.validate(app.config) statuses = decoded_license.validate(app.config)
all_met = all(status.is_met() for status in statuses) all_met = all(status.is_met() for status in statuses)
if not all_met:
raise InvalidRequest('License is insufficient')
return { return {
'decoded': {}, 'status': [status.as_dict(for_private=True) for status in statuses],
'success': True 'success': all_met,
} }
abort(403) abort(403)
@ -882,16 +880,14 @@ class SuperUserLicense(ApiResource):
statuses = decoded_license.validate(app.config) statuses = decoded_license.validate(app.config)
all_met = all(status.is_met() for status in statuses) all_met = all(status.is_met() for status in statuses)
if not all_met: if all_met:
raise InvalidRequest('License is insufficient') # Save the license and update the license check thread.
config_provider.save_license(license_contents) config_provider.save_license(license_contents)
license_validator.compute_license_sufficiency() license_validator.compute_license_sufficiency()
return { return {
'decoded': {}, 'status': [status.as_dict(for_private=True) for status in statuses],
'success': True 'success': all_met,
} }
abort(403) abort(403)

View file

@ -567,6 +567,11 @@ a:focus {
margin-right: 4px; margin-right: 4px;
} }
.config-license-field-element .required {
background-color: #f5f5f5;
color: #333;
}
.config-license-field-element textarea { .config-license-field-element textarea {
padding: 10px; padding: 10px;
margin-bottom: 10px; margin-bottom: 10px;
@ -577,9 +582,8 @@ a:focus {
margin-bottom: 26px; margin-bottom: 26px;
} }
.config-license-field-element table td:first-child { .config-license-field-element table {
width: 150px; margin-top: 20px;
font-weight: bold;
} }
.config-license-field-element .fa { .config-license-field-element .fa {
@ -594,6 +598,10 @@ a:focus {
color: #D64456; color: #D64456;
} }
.config-license-field-element li {
padding: 4px;
}
.co-checkbox { .co-checkbox {
position: relative; position: relative;
} }

View file

@ -36,6 +36,10 @@
margin-bottom: 16px; margin-bottom: 16px;
} }
.initial-setup-modal .config-license-field {
margin-top: 30px;
}
.initial-setup-modal .license-valid .fa { .initial-setup-modal .license-valid .fa {
margin-right: 6px; margin-right: 6px;
} }
@ -43,14 +47,3 @@
.initial-setup-modal .license-valid table { .initial-setup-modal .license-valid table {
margin-top: 40px; margin-top: 40px;
} }
.initial-setup-modal .license-valid table td {
border: 0px;
padding: 4px;
}
.initial-setup-modal .license-valid table td:first-child {
font-weight: bold;
max-width: 100px;
padding-right: 20px;
}

View file

@ -3,19 +3,54 @@
config if the license is invalid (since this box will be empty and therefore "required") --> config if the license is invalid (since this box will be empty and therefore "required") -->
<input type="text" name="licenseRequiredBox" ng-model="requiredBox" style="visibility: hidden; height: 1px; position: absolute;" required> <input type="text" name="licenseRequiredBox" ng-model="requiredBox" style="visibility: hidden; height: 1px; position: absolute;" required>
<div class="cor-loader-inline" ng-show="state == 'loading-license'"></div> <div class="cor-loader-inline" ng-show="state == LicenseStates.validating"></div>
<div class="license-valid license-status" ng-show="state == 'license-valid'"> <div class="license-valid license-status" ng-show="state == LicenseStates.valid">
<h4><i class="fa fa-check-circle"></i>License Valid</h4> <h4><i class="fa fa-check-circle"></i>License Valid</h4>
<table class="co-table"> <table class="co-table">
<tr><td>Product:</td><td>{{ licenseDecoded.publicProductName || licenseDecoded.productName }}</td></tr> <thead>
<tr><td>Plan:</td><td>{{ licenseDecoded.publicPlanName || licenseDecoded.planName }}</td></tr> <td>Requirement</td>
<td>Required Count</td>
<td>Subscription</td>
<td>Subscription Count</td>
<td>Expiration Date</td>
</thead>
<tr ng-repeat="status in licenseStatus">
<td>{{ requirementTitles[status.requirement.name] }}</td>
<td>{{ status.requirement.count }}</td>
<td>{{ status.entitlement.product_name }}</td>
<td>{{ status.entitlement.count }}</td>
<td><span am-time-ago="status.entitlement.expiration.expiration_date" data-title="{{ status.entitlement.expiration.expiration_date }}" bs-tooltip></span></td>
</tr>
</table> </table>
</div> </div>
<div class="license-invalid license-status" ng-show="state == 'license-error'"> <div class="license-invalid license-status" ng-show="state == LicenseStates.invalid">
<h4><i class="fa fa-times-circle"></i> Validation Failed</h4> <h4><i class="fa fa-times-circle"></i> Validation Failed</h4>
<h5>{{ licenseError }}</h5> <h5 ng-if="licenseError">{{ licenseError }}</h5>
<h5 ng-if="!licenseError && licenseStatus">
<p>The following errors were found:</p>
<ul>
<li ng-repeat="status in licenseStatus" ng-if="status.status != 'EntitlementStatus.met'">
<div ng-switch on="status.status">
<!-- insufficient_count -->
<div ng-switch-when="EntitlementStatus.insufficient_count">
<strong>{{ requirementTitles[status.requirement.name] }}</strong>: <code class="required">{{ status.requirement.count }}</code> <span ng-if="status.requirement.count != 1">are</span><span ng-if="status.requirement.count == 1">is</span> required: License provides <code>{{ status.entitlement.count }}</code>
</div>
<!-- no_matching -->
<div ng-switch-when="EntitlementStatus.no_matching">
<strong>{{ requirementTitles[status.requirement.name] }}</strong>: License is missing requirement
</div>
<!-- expired -->
<div ng-switch-when="EntitlementStatus.expired">
<strong>{{ requirementTitles[status.requirement.name] }}</strong>: Requirement expired on <code>{{ status.entitlement.expiration.expiration_date }}</code>
</div>
</div>
</li>
</ul>
</h5>
</div> </div>
<button class="btn btn-default" ng-show="!showingEditor" ng-click="showEditor($event)"><i class="fa fa-pencil"></i> Update License</button> <button class="btn btn-default" ng-show="!showingEditor" ng-click="showEditor($event)"><i class="fa fa-pencil"></i> Update License</button>
@ -28,12 +63,12 @@
<textarea id="enterLicenseBox" ng-model="licenseContents" class="form-control" <textarea id="enterLicenseBox" ng-model="licenseContents" class="form-control"
placeholder="Paste your raw license here, which should already be in base64 format: GtqMjMwNDgyM3Vq..." placeholder="Paste your raw license here, which should already be in base64 format: GtqMjMwNDgyM3Vq..."
ng-readonly="state == 'validating-license'"></textarea> ng-readonly="state == LicenseStates.validating"></textarea>
<button class="btn btn-primary" ng-show="state != 'validating-license'" <button class="btn btn-primary" ng-show="state != LicenseStates.validating"
ng-click="validateAndUpdate($event)" ng-disabled="!licenseContents">Update License</button> ng-click="validateAndUpdate($event)" ng-disabled="!licenseContents">Update License</button>
<div class="license-validating" ng-show="state == 'validating-license'"> <div class="license-validating" ng-show="state == LicenseStates.validating">
<span class="cor-loader-inline"></span> Validating License <span class="cor-loader-inline"></span> Validating License
</div> </div>
</div> </div>

View file

@ -1256,24 +1256,52 @@ angular.module("core-config-setup", ['angularFileUpload'])
transclude: false, transclude: false,
restrict: 'C', restrict: 'C',
scope: { scope: {
'isValid': '=?isValid',
'forSetup': '@forSetup'
}, },
controller: function($scope, $element, ApiService, UserService) { controller: function($scope, $element, ApiService, UserService) {
$scope.state = 'loading-license'; $scope.LicenseStates = {
$scope.showingEditor = false; none: 'no-license',
loading: 'license-loading',
valid: 'license-valid',
invalid: 'license-error',
validating: 'validating-license'
};
$scope.state = $scope.forSetup == 'true' ? $scope.LicenseStates.none : $scope.LicenseStates.loading;
$scope.showingEditor = $scope.forSetup == 'true';
$scope.requiredBox = ''; $scope.requiredBox = '';
var loadLicense = function() { $scope.requirementTitles = {
ApiService.getLicense().then(function(resp) { 'software.quay': 'Quay Enterprise',
$scope.state = 'license-valid'; 'software.quay.regions': 'Distributed Storage Regions'
$scope.showingEditor = false; };
$scope.licenseDecoded = resp['decoded'];
$scope.requiredBox = 'filled'; var handleLicenseSuccess = function(resp) {
}, function(resp) { $scope.state = resp['success'] ? $scope.LicenseStates.valid : $scope.LicenseStates.invalid;
$scope.requiredBox = resp['success'] ? 'filled' : '';
$scope.showingEditor = !resp['success'];
$scope.licenseStatus = resp['status'];
$scope.licenseError = null;
$scope.isValid = resp['success'];
};
var handleLicenseError = function(resp) {
$scope.licenseError = ApiService.getErrorMessage(resp); $scope.licenseError = ApiService.getErrorMessage(resp);
$scope.licenseStatus = null;
$scope.state = 'license-error'; $scope.state = 'license-error';
$scope.showingEditor = true; $scope.showingEditor = true;
$scope.requiredBox = ''; $scope.requiredBox = '';
}); $scope.isValid = false;
};
var loadLicense = function() {
if ($scope.forSetup == 'true') {
$scope.state = $scope.LicenseStates.none;
return;
}
ApiService.getLicense().then(handleLicenseSuccess, handleLicenseError);
}; };
UserService.updateUserIn($scope, function(user) { UserService.updateUserIn($scope, function(user) {
@ -1293,23 +1321,17 @@ angular.module("core-config-setup", ['angularFileUpload'])
$event.preventDefault(); $event.preventDefault();
$event.stopPropagation(); $event.stopPropagation();
$scope.state = 'validating-license'; $scope.state = $scope.LicenseStates.validating;
var data = { var data = {
'license': $scope.licenseContents 'license': $scope.licenseContents
}; };
ApiService.updateLicense(data).then(function(resp) { if ($scope.forSetup == 'true') {
$scope.state = 'license-valid'; ApiService.suSetAndValidateLicense(data).then(handleLicenseSuccess, handleLicenseError);
$scope.showingEditor = false; } else {
$scope.licenseDecoded = resp['decoded']; ApiService.updateLicense(data).then(handleLicenseSuccess, handleLicenseError);
$scope.requiredBox = 'filled'; }
}, function(resp) {
$scope.licenseError = ApiService.getErrorMessage(resp);
$scope.state = 'license-error';
$scope.showingEditor = true;
$scope.requiredBox = '';
});
}; };
} }
}; };

View file

@ -40,12 +40,6 @@
// License is being uploaded. // License is being uploaded.
'UPLOAD_LICENSE': 'upload-license', 'UPLOAD_LICENSE': 'upload-license',
// License is being validated.
'VALIDATING_LICENSE': 'upload-license-validating',
// License is validated.
'VALIDATED_LICENSE': 'upload-license-validated',
// DB is being configured. // DB is being configured.
'CONFIG_DB': 'config-db', 'CONFIG_DB': 'config-db',
@ -105,9 +99,7 @@
$scope.currentState = { $scope.currentState = {
'hasDatabaseSSLCert': false, 'hasDatabaseSSLCert': false,
'licenseContents': '', 'licenseValid': false
'licenseError': null,
'licenseDecoded': null
}; };
$scope.$watch('currentStep', function(currentStep) { $scope.$watch('currentStep', function(currentStep) {
@ -144,27 +136,6 @@
} }
}); });
$scope.validateLicense = function() {
$scope.currentStep = $scope.States.VALIDATING_LICENSE;
var data = {
'license': $scope.currentState.licenseContents
};
ApiService.suSetAndValidateLicense(data).then(function(resp) {
$scope.currentStep = $scope.States.VALIDATED_LICENSE;
$scope.currentState.licenseError = null;
$scope.currentState.licenseDecoded = resp['decoded'];
}, function(resp) {
$scope.currentStep = $scope.States.UPLOAD_LICENSE;
$scope.currentState.licenseError = ApiService.getErrorMessage(resp);
$scope.currentState.licenseContents = '';
$scope.currentState.licenseDecoded = null;
});
};
$scope.restartContainer = function(state) { $scope.restartContainer = function(state) {
$scope.currentStep = state; $scope.currentStep = state;
ContainerService.restartContainer(function() { ContainerService.restartContainer(function() {

View file

@ -130,21 +130,9 @@
The container must be restarted to apply the configuration changes. The container must be restarted to apply the configuration changes.
</div> </div>
<!-- Content: VALIDATED_LICENSE --> <!-- Content: UPLOAD_LICENSE -->
<div class="modal-body license-valid" style="padding: 20px;" <div class="modal-body upload-license entering" style="padding: 20px;"
ng-show="isStep(currentStep, States.VALIDATED_LICENSE)"> ng-show="isStep(currentStep, States.UPLOAD_LICENSE)">
<h5><i class="fa fa-check"></i> License Validated</h5>
Your license has been validated and saved. Please press "Next" to continue setup of your Quay Enterprise installation.
<table class="co-table">
<tr><td>Product:</td><td>{{ currentState.licenseDecoded.publicProductName || currentState.licenseDecoded.productName }}</td></tr>
<tr><td>Plan:</td><td>{{ currentState.licenseDecoded.publicPlanName || currentState.licenseDecoded.planName }}</td></tr>
</table>
</div>
<!-- Content: UPLOAD_LICENSE or VALIDATING_LICENSE -->
<div class="modal-body upload-license" style="padding: 20px;"
ng-show="isStep(currentStep, States.UPLOAD_LICENSE, States.VALIDATING_LICENSE)"
ng-class="isStep(currentStep, States.VALIDATING_LICENSE) ? 'validating' : 'entering'">
<h4> <h4>
Quay Enterprise License Quay Enterprise License
</h4> </h4>
@ -152,13 +140,7 @@
Please provide your Quay Enterprise License. It can be found under the "Raw Format" tab Please provide your Quay Enterprise License. It can be found under the "Raw Format" tab
of your Quay Enterprise subscription in the <a href="https://account.tectonic.com" target="_blank">Tectonic Account</a>. of your Quay Enterprise subscription in the <a href="https://account.tectonic.com" target="_blank">Tectonic Account</a>.
</div> </div>
<textarea id="enterLicenseBox" ng-model="currentState.licenseContents" placeholder="Paste your raw license here, which should already be in base64 format: GtqMjMwNDgyM3Vq..." <div class="config-license-field" for-setup="true" is-valid="currentState.licenseValid"></div>
ng-readonly="isStep(currentStep, States.VALIDATING_LICENSE)"></textarea>
<div class="license-invalid" ng-visible="isStep(currentStep, States.UPLOAD_LICENSE) && currentState.licenseError">
<h5><i class="fa fa-times-circle"></i> Validation Failed</h5>
<h6>{{ currentState.licenseError }}</h6>
Please try copying your license from the Tectonic Account again.
</div>
</div> </div>
<!-- Content: DB_SETUP or DB_SETUP_ERROR --> <!-- Content: DB_SETUP or DB_SETUP_ERROR -->
@ -259,26 +241,12 @@
Database Validation Issue: {{ errors.DatabaseValidationError }} Database Validation Issue: {{ errors.DatabaseValidationError }}
</div> </div>
<!-- Footer: UPLOAD_LICENSE or VALIDATING_LICENSE --> <!-- Footer: UPLOAD_LICENSE -->
<div class="modal-footer" <div class="modal-footer"
ng-show="isStep(currentStep, States.UPLOAD_LICENSE, States.VALIDATING_LICENSE)">
<div ng-show="isStep(currentStep, States.VALIDATING_LICENSE)">
<span class="cor-loader-inline"></span>
Validating License...
</div>
<button type="submit" class="btn btn-primary" ng-click="validateLicense()"
ng-disabled="!currentState.licenseContents"
ng-show="isStep(currentStep, States.UPLOAD_LICENSE)"> ng-show="isStep(currentStep, States.UPLOAD_LICENSE)">
Validate License <button type="submit" class="btn btn-primary" ng-click="beginSetup()"
</button> ng-disabled="!currentState.licenseValid">
</div> Continue
<!-- Footer: VALIDATED_LICENSE -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.VALIDATED_LICENSE)">
<button type="submit" class="btn btn-primary" ng-click="beginSetup()">
Next
</button> </button>
</div> </div>

View file

@ -61,6 +61,20 @@ class Entitlement(object):
expiration=repr(self.expiration), expiration=repr(self.expiration),
)) ))
def as_dict(self, for_private=False):
data = {
'name': self.name,
}
if for_private:
data.update({
'count': self.count,
'product_name': self.product_name,
'expiration': self.expiration.as_dict(for_private=True),
})
return data
class ExpirationType(Enum): class ExpirationType(Enum):
""" An enum which represents the different possible types of expirations. If """ An enum which represents the different possible types of expirations. If
you posess an expired enum, you can use this to figure out at what level you posess an expired enum, you can use this to figure out at what level
@ -105,6 +119,19 @@ class Expiration(object):
grace_period=repr(self.grace_period), grace_period=repr(self.grace_period),
)) ))
def as_dict(self, for_private=False):
data = {
'expiration_type': str(self.expiration_type),
}
if for_private:
data.update({
'expiration_date': str(self.expiration_date),
'grace_period': str(self.grace_period),
})
return data
class EntitlementStatus(IntEnum): class EntitlementStatus(IntEnum):
""" An EntitlementStatus represent the current effectiveness of an """ An EntitlementStatus represent the current effectiveness of an
@ -162,6 +189,23 @@ class EntitlementValidationResult(object):
entitlement=repr(self.entitlement), entitlement=repr(self.entitlement),
)) ))
def as_dict(self, for_private=False):
def req_view():
return {
'name': self.requirement.name,
'count': self.requirement.count,
}
data = {
'requirement': req_view(),
'status': str(self.get_status()),
}
if self.entitlement is not None:
data['entitlement'] = self.entitlement.as_dict(for_private=for_private)
return data
class License(object): class License(object):
""" License represents a fully decoded and validated (but potentially expired) license. """ """ License represents a fully decoded and validated (but potentially expired) license. """