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({})
all_met = all(status.is_met() for status in statuses)
if not all_met:
raise InvalidRequest('License is insufficient')
if all_met:
config_provider.save_license(license_contents)
config_provider.save_license(license_contents)
return {
'decoded': {},
'success': True
'status': [status.as_dict(for_private=True) for status in statuses],
'success': all_met,
}

View file

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

View file

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

View file

@ -36,6 +36,10 @@
margin-bottom: 16px;
}
.initial-setup-modal .config-license-field {
margin-top: 30px;
}
.initial-setup-modal .license-valid .fa {
margin-right: 6px;
}
@ -43,14 +47,3 @@
.initial-setup-modal .license-valid table {
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") -->
<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>
<table class="co-table">
<tr><td>Product:</td><td>{{ licenseDecoded.publicProductName || licenseDecoded.productName }}</td></tr>
<tr><td>Plan:</td><td>{{ licenseDecoded.publicPlanName || licenseDecoded.planName }}</td></tr>
<thead>
<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>
</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>
<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>
<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"
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>
<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
</div>
</div>

View file

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

View file

@ -40,12 +40,6 @@
// License is being uploaded.
'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.
'CONFIG_DB': 'config-db',
@ -105,9 +99,7 @@
$scope.currentState = {
'hasDatabaseSSLCert': false,
'licenseContents': '',
'licenseError': null,
'licenseDecoded': null
'licenseValid': false
};
$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.currentStep = state;
ContainerService.restartContainer(function() {

View file

@ -130,21 +130,9 @@
The container must be restarted to apply the configuration changes.
</div>
<!-- Content: VALIDATED_LICENSE -->
<div class="modal-body license-valid" style="padding: 20px;"
ng-show="isStep(currentStep, States.VALIDATED_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'">
<!-- Content: UPLOAD_LICENSE -->
<div class="modal-body upload-license entering" style="padding: 20px;"
ng-show="isStep(currentStep, States.UPLOAD_LICENSE)">
<h4>
Quay Enterprise License
</h4>
@ -152,13 +140,7 @@
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>.
</div>
<textarea id="enterLicenseBox" ng-model="currentState.licenseContents" placeholder="Paste your raw license here, which should already be in base64 format: GtqMjMwNDgyM3Vq..."
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 class="config-license-field" for-setup="true" is-valid="currentState.licenseValid"></div>
</div>
<!-- Content: DB_SETUP or DB_SETUP_ERROR -->
@ -259,26 +241,12 @@
Database Validation Issue: {{ errors.DatabaseValidationError }}
</div>
<!-- Footer: UPLOAD_LICENSE or VALIDATING_LICENSE -->
<!-- Footer: UPLOAD_LICENSE -->
<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)">
Validate License
</button>
</div>
<!-- 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
ng-show="isStep(currentStep, States.UPLOAD_LICENSE)">
<button type="submit" class="btn btn-primary" ng-click="beginSetup()"
ng-disabled="!currentState.licenseValid">
Continue
</button>
</div>

View file

@ -61,6 +61,20 @@ class Entitlement(object):
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):
""" 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
@ -105,6 +119,19 @@ class Expiration(object):
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):
""" An EntitlementStatus represent the current effectiveness of an
@ -162,6 +189,23 @@ class EntitlementValidationResult(object):
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):
""" License represents a fully decoded and validated (but potentially expired) license. """