Changed license nagging to a modal dialog with license status aware text and a form for updating the license key. Form provides JSON validation via CodeMirror.
This commit is contained in:
Chris Houseknecht
2014-08-04 18:09:26 -04:00
parent 587fb35b59
commit 07b9f6b12c
7 changed files with 261 additions and 18 deletions

View File

@@ -511,7 +511,7 @@ angular.module('Tower', [
if ($rootScope.current_user === undefined || $rootScope.current_user === null) {
Authorization.restoreUserInfo(); //user must have hit browser refresh
}
CheckLicense();
CheckLicense.test();
}
activateTab();

View File

@@ -0,0 +1,37 @@
/*********************************************
* Copyright (c) 2014 AnsibleWorks, Inc.
*
* License.js
* Form definition for Organization model
*
*
*/
angular.module('LicenseUpdateFormDefinition', [])
.value('LicenseUpdateForm', {
name: 'license',
well: false,
fields: {
license_json: {
label: 'License Key:',
type: 'textarea',
addRequired: true,
editRequird: true,
rows: 10,
'default': '---'
}
},
buttons: {
form_submit: {
label: "Submit",
"class": "pull-right btn-primary",
ngClick: "submitLicenseKey()",
ngDisabled: true
}
},
related: { }
}); //LicenseUpdateForm

View File

@@ -9,7 +9,8 @@
'use strict';
angular.module('AccessHelper', ['RestServices', 'Utilities', 'ngCookies'])
angular.module('AccessHelper', ['RestServices', 'Utilities', 'ngCookies', 'LicenseUpdateFormDefinition', 'FormGenerator', 'ParseHelper', 'ModalDialog', 'VariablesHelper'])
.factory('CheckAccess', ['$rootScope', 'Alert', 'Rest', 'GetBasePath', 'ProcessErrors',
function ($rootScope, Alert, Rest, GetBasePath, ProcessErrors) {
return function (params) {
@@ -48,6 +49,199 @@ angular.module('AccessHelper', ['RestServices', 'Utilities', 'ngCookies'])
}
])
.factory('CheckLicense', ['$rootScope', '$compile', 'CreateDialog', 'Store', 'LicenseUpdateForm', 'GenerateForm', 'TextareaResize', 'ToJSON', 'GetBasePath', 'Rest', 'ProcessErrors', 'Alert',
function($rootScope, $compile, CreateDialog, Store, LicenseUpdateForm, GenerateForm, TextareaResize, ToJSON, GetBasePath, Rest, ProcessErrors, Alert) {
return {
getRemainingDays: function(time_remaining) {
// assumes time_remaining will be in seconds
var tr = parseInt(time_remaining, 10);
return Math.floor(tr / 86400);
},
shouldNotify: function(license) {
if (license && typeof license === 'object' && Object.keys(license).length > 0) {
// we have a license object
if (!license.valid_key) {
// missing valid key
return true;
}
else if (license.free_instances <= 0) {
// host count exceeded
return true;
}
else if (this.getRemainingDays(license.time_remaining) < 15) {
// below 15 days remaining on license
return true;
}
return false;
} else {
// missing license object
return true;
}
},
getHTML: function(license) {
var title, html, result = {};
if (license && typeof license === 'object' && Object.keys(license).length > 0 && license.valid_key !== undefined) {
// we have a license
if (!license.valid_key) {
title = "Invalid License";
html = "<div id=\"license-notification-body\"><div style=\"margin-top:5px; margin-bottom:25px;\"><p>The Ansible Tower license is invalid. Please visit " +
"<a href=\"http://ansible.com/license\" target=\"_blank\">http://ansible.com/license</a> to obtain a valid license key. " +
"Copy and paste the key in the field below and click the Submit button.</p></div>";
}
else if (this.getRemainingDays(license.time_remaining) <= 0) {
if (parseInt(license.grace_period_remaining,10) > 86400) {
title = "License Expired";
html = "<div id=\"license-notification-body\"><div style=\"margin-top:5px; margin-bottom:25px;\"><p>Thank you for using Ansible Tower. The Ansible Tower license " +
"has expired. You will no longer be able to run playbooks after " + this.getRemainingDays(license.grace_period_remaining) + " days</p>" +
"<p>Please visit <a href=\"http://ansible.com/license\" target=\"_blank\">ansible.com/license</a> to purchse a valid license. " +
"Copy and paste the new license key in the field below and click the Submit button.</p></div>";
} else {
title = "License Expired";
html = "<div id=\"license-notification-body\"><div style=\"margin-top:5px; margin-bottom:25px;\"><p>Thank you for using Ansible Tower. The Ansible Tower license " +
"has expired, and the 30 day grace period has been exceeded. To continue using Tower to run playbooks and adding managed hosts a " +
"valid license key is required.</p><p>Please visit <a href=\"ansible.com/license\" target=\"_blank\">http://ansible.com/license</a> to " +
"purchse a license. Copy and paste the new license key in the field below and click the Submit button.</p>";
}
}
else if (this.getRemainingDays(license.time_remaining) < 15) {
html = "<div id=\"license-notification-body\"><div style=\"margin-top:5px; margin-bottom:25px;\"><p>Thank you for using Ansible Tower. The Ansible Tower license " +
"has " + this.getRemainingDays(license.time_remaining) + " remaining.</p>" +
"<p>Extend your Ansible Tower license by visiting <a href=\"ansible.com/license\" target=\"_blank\">http://ansible.com/license</a>. " +
"Copy and paste the new license key in the field below and click the Submit button.</p></div>";
}
else if (license.free_instances <= 0) {
title = "Host Count Exceeded";
html = "<div id=\"license-notification-body\"><div style=\"margin-top:5px; margin-bottom:25px;\"><p>The Ansible Tower license has reached capacity for the number of " +
"managed hosts allowed. No additional hosts can be added.</p><p>To extend the Ansible Tower license please visit " +
"<a href=\"http://ansible.com/license\" target=\"_blank\">ansible.com/license</a>. " +
"Copy and paste the new license key in the field below and click the Submit button.</p>";
}
} else {
// No license
title = "License Required";
html = "<div id=\"license-notification-body\"><div style=\"margin-top:5px; margin-bottom:25px;\"><p>Thank you for trying Ansible Tower. A <strong>FREE</strong> trial license is available for various infrastructure sizes, as well as free unlimited use for up to ten nodes.<p>" +
"<p>Visit <a href=\"http://ansible.com/license\" target=\"_blank\">ansible.com/license</a> to obtain a free license key. Copy and paste the key in the field below and " +
"click the Submit button.</p></div>";
}
html += GenerateForm.buildHTML(LicenseUpdateForm, { mode: 'edit', showButtons: true, breadCrumbs: false });
html += "</div>";
result.body = html;
result.title = title;
return result;
},
test: function() {
var license = Store('license'),
notify = this.shouldNotify(license),
html, buttons, scope;
if (license && typeof license === 'object' && Object.keys(license).length > 0) {
if (license.tested) {
return true;
}
license.tested = true;
Store('license',license); //update with tested flag
}
if (!notify) {
return true;
}
scope = $rootScope.$new();
html = this.getHTML(license);
$('#license-modal-dialog').html(html.body);
scope.flashMessage = null;
scope.parseType = 'json';
scope.license_json = " ";
scope.removeLicenseDialogReady = scope.$on('LicenseDialogReady', function() {
var e = angular.element(document.getElementById('license-modal-dialog'));
$compile(e)(scope);
$('#license-modal-dialog').dialog('open');
});
scope.submitLicenseKey = function() {
var url = GetBasePath('config'),
json_data = ToJSON(scope.parseType, scope.license_json);
if (typeof json_data === 'object' && Object.keys(json_data).length > 0) {
Rest.setUrl(url);
Rest.post(json_data)
.success(function () {
$('#license-modal-dialog').dialog('close');
Alert('License Accepted', 'The Ansible Tower license was updated. To view or update license information in the future choose View License from the Account menu.','alert-info');
})
.error(function (data, status) {
scope.license_json_api_error = "A valid license key in JSON format is required";
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to update license. POST returned: ' + status
});
});
} else {
scope.license_json_api_error = "A valid license key in JSON format is required";
}
};
buttons = [{
label: "Cancel",
onClick: function() {
$('#license-modal-dialog').dialog('close');
},
"class": "btn btn-default",
"id": "license-cancel-button"
}];
CreateDialog({
scope: scope,
buttons: buttons,
width: 700,
height: 625,
minWidth: 400,
title: html.title,
id: 'license-modal-dialog',
clonseOnEscape: false,
onClose: function() {
if (scope.codeMirror) {
scope.codeMirror.destroy();
}
$('#license-modal-dialog').empty();
},
onResizeStop: function() {
TextareaResize({
scope: scope,
textareaId: 'license_license_json',
modalId: 'license-modal-dialog',
formId: 'license-notification-body',
fld: 'license_json',
bottom_margin: 30,
parse: true,
onChange: function() { scope.license_json_api_error = ''; }
});
},
onOpen: function() {
setTimeout(function() {
TextareaResize({
scope: scope,
textareaId: 'license_license_json',
modalId: 'license-modal-dialog',
formId: 'license-notification-body',
fld: 'license_json',
bottom_margin: 30,
parse: true,
onChange: function() { scope.license_json_api_error = ''; }
});
$('#cm-license_json-container .CodeMirror textarea').focus();
}, 300);
},
callback: 'LicenseDialogReady'
});
}
};
}]);
/*
.factory('CheckLicense', ['$rootScope', 'Store', 'Alert', '$location', 'Authorization',
function ($rootScope, Store, Alert, $location, Authorization) {
return function () {
@@ -103,4 +297,7 @@ angular.module('AccessHelper', ['RestServices', 'Utilities', 'ngCookies'])
}
};
}
]);
]);
*/

View File

@@ -13,7 +13,7 @@
angular.module('ParseHelper', ['Utilities', 'AngularCodeMirrorModule'])
.factory('ParseTypeChange', ['Alert', 'AngularCodeMirror', function (Alert, AngularCodeMirror) {
return function (params) {
var scope = params.scope,
field_id = params.field_id,
fld = (params.variable) ? params.variable : 'variables',

View File

@@ -8,11 +8,11 @@
*
*
*/
'use strict';
angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
/**
*
* CreateDialog({
@@ -30,7 +30,7 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
* callback: - String to pass to scope.$emit() after dialog is created, optional
* })
*
* Note that the dialog will be created but not opened. It's up to the caller to open it. Use callback
* Note that the dialog will be created but not opened. It's up to the caller to open it. Use callback
* option to respond to dialog created event.
*/
.factory('CreateDialog', ['Empty', function(Empty) {
@@ -65,12 +65,12 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
"id": "dialog-ok-button"
}];
}
buttons = {};
buttonSet.forEach( function(btn) {
buttons[btn.label] = btn.onClick;
});
// Set modal dimensions based on viewport width
ww = $(document).width();
wh = $('body').height();
@@ -90,7 +90,7 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
create: function () {
// Fix the close button
$('.ui-dialog[aria-describedby="' + id + '"]').find('.ui-dialog-titlebar button').empty().attr({'class': 'close'}).text('x');
setTimeout(function() {
// Make buttons bootstrapy
$('.ui-dialog[aria-describedby="' + id + '"]').find('.ui-dialog-buttonset button').each(function () {
@@ -161,26 +161,32 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
}])
/**
* TextareaResize({
* TextareaResize({
* scope: - $scope associated with the textarea element
* textareaId: - id attribute value of the textarea
* modalId: - id attribute of the <div> element used to create the modal
* formId: - id attribute of the textarea's parent form
* parse: - if true, call ParseTypeChange and replace textarea with codemirror editor
* fld: - optional, form field name
* bottom_margin: - optional, integer value for additional margin to leave below the textarea
* onChange; - optional, function to call when the textarea value changes
* })
*
* Use to resize a textarea field contained on a modal. Has only been tested where the
* Use to resize a textarea field contained on a modal. Has only been tested where the
* form contains 1 textarea and the the textarea is at the bottom of the form/modal.
*
**/
.factory('TextareaResize', ['ParseTypeChange', 'Wait', function(ParseTypeChange, Wait){
return function(params) {
var scope = params.scope,
textareaId = params.textareaId,
modalId = params.modalId,
formId = params.formId,
fld = params.fld,
parse = (params.parse === undefined) ? true : params.parse,
bottom_margin = (params.bottom_margin) ? params.bottom_margin : 0,
onChange = params.onChange,
textarea,
formHeight, model, windowHeight, offset, rows;
@@ -188,7 +194,7 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
Wait('stop');
}
// Attempt to create the largest textarea field that will fit on the window. Minimum
// Attempt to create the largest textarea field that will fit on the window. Minimum
// height is 6 rows, so on short windows you will see vertical scrolling
textarea = $('#' + textareaId);
if (scope.codeMirror) {
@@ -199,16 +205,16 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
textarea.attr('rows', 1);
formHeight = $('#' + formId).height();
windowHeight = $('#' + modalId).height() - 20; //leave a margin of 20px
offset = Math.floor(windowHeight - formHeight);
offset = Math.floor(windowHeight - formHeight - bottom_margin);
rows = Math.floor(offset / 20);
rows = (rows < 6) ? 6 : rows;
textarea.attr('rows', rows);
while(rows > 6 && $('#' + formId).height() > $('#' + modalId).height()) {
while(rows > 6 && ($('#' + formId).height() > $('#' + modalId).height() + bottom_margin)) {
rows--;
textarea.attr('rows', rows);
}
if (parse) {
ParseTypeChange({ scope: scope, field_id: textareaId, onReady: waitStop });
ParseTypeChange({ scope: scope, field_id: textareaId, onReady: waitStop, variable: fld, onChange: onChange });
}
};
}]);

View File

@@ -51,7 +51,7 @@ angular.module('GeneratorHelpers', [])
result = "ng-show=\"" + value + "\" ";
break;
case 'icon':
// new method of constructing <i> icon tag. Replces Icon method.
// new method of constructing <i> icon tag. Replaces Icon method.
result = "<i class=\"fa fa-" + value;
result += (obj.iconSize) ? " " + obj.iconSize : "";
result += "\"></i>";
@@ -127,6 +127,7 @@ angular.module('GeneratorHelpers', [])
icon = 'fa-arrow-left';
break;
case 'save':
case 'form_submit':
icon = 'fa-check-square-o';
break;
case 'properties':

View File

@@ -100,6 +100,7 @@
<script src="{{ STATIC_URL }}js/forms/JobSummary.js"></script>
<script src="{{ STATIC_URL }}js/forms/JobVarsPrompt.js"></script>
<script src="{{ STATIC_URL }}js/forms/LicenseForm.js"></script>
<script src="{{ STATIC_URL }}js/forms/LicenseUpdate.js"></script>
<script src="{{ STATIC_URL }}js/forms/Source.js"></script>
<script src="{{ STATIC_URL }}js/forms/LogViewerStatus.js"></script>
<script src="{{ STATIC_URL }}js/forms/LogViewerOptions.js"></script>
@@ -393,6 +394,7 @@
</div><!-- modal -->
<div id="help-modal-dialog" style="display: none;"></div>
<div id="license-modal-dialog" style="display: none;"></div>
</div><!-- container -->