From 07b9f6b12c31cbf71030a148e339a6c34f378ba7 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Mon, 4 Aug 2014 18:09:26 -0400 Subject: [PATCH] License 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. --- awx/ui/static/js/app.js | 2 +- awx/ui/static/js/forms/LicenseUpdate.js | 37 ++++ awx/ui/static/js/helpers/Access.js | 201 +++++++++++++++++- awx/ui/static/js/helpers/Parse.js | 2 +- awx/ui/static/lib/ansible/Modal.js | 32 +-- .../static/lib/ansible/generator-helpers.js | 3 +- awx/ui/templates/ui/index.html | 2 + 7 files changed, 261 insertions(+), 18 deletions(-) create mode 100644 awx/ui/static/js/forms/LicenseUpdate.js diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index 0444a68a4b..e2d2c30702 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -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(); diff --git a/awx/ui/static/js/forms/LicenseUpdate.js b/awx/ui/static/js/forms/LicenseUpdate.js new file mode 100644 index 0000000000..064eb1cdb6 --- /dev/null +++ b/awx/ui/static/js/forms/LicenseUpdate.js @@ -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 diff --git a/awx/ui/static/js/helpers/Access.js b/awx/ui/static/js/helpers/Access.js index 878f636eb7..a6f20107dc 100644 --- a/awx/ui/static/js/helpers/Access.js +++ b/awx/ui/static/js/helpers/Access.js @@ -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 = "

The Ansible Tower license is invalid. Please visit " + + "http://ansible.com/license to obtain a valid license key. " + + "Copy and paste the key in the field below and click the Submit button.

"; + } + else if (this.getRemainingDays(license.time_remaining) <= 0) { + if (parseInt(license.grace_period_remaining,10) > 86400) { + title = "License Expired"; + html = "

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

" + + "

Please visit ansible.com/license to purchse a valid license. " + + "Copy and paste the new license key in the field below and click the Submit button.

"; + } else { + title = "License Expired"; + html = "

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.

Please visit http://ansible.com/license to " + + "purchse a license. Copy and paste the new license key in the field below and click the Submit button.

"; + } + } + else if (this.getRemainingDays(license.time_remaining) < 15) { + html = "

Thank you for using Ansible Tower. The Ansible Tower license " + + "has " + this.getRemainingDays(license.time_remaining) + " remaining.

" + + "

Extend your Ansible Tower license by visiting http://ansible.com/license. " + + "Copy and paste the new license key in the field below and click the Submit button.

"; + } + else if (license.free_instances <= 0) { + title = "Host Count Exceeded"; + html = "

The Ansible Tower license has reached capacity for the number of " + + "managed hosts allowed. No additional hosts can be added.

To extend the Ansible Tower license please visit " + + "ansible.com/license. " + + "Copy and paste the new license key in the field below and click the Submit button.

"; + } + } else { + // No license + title = "License Required"; + html = "

Thank you for trying Ansible Tower. A FREE trial license is available for various infrastructure sizes, as well as free unlimited use for up to ten nodes.

" + + "

Visit ansible.com/license to obtain a free license key. Copy and paste the key in the field below and " + + "click the Submit button.

"; + } + html += GenerateForm.buildHTML(LicenseUpdateForm, { mode: 'edit', showButtons: true, breadCrumbs: false }); + html += "
"; + 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']) } }; } -]); \ No newline at end of file +]); +*/ + + diff --git a/awx/ui/static/js/helpers/Parse.js b/awx/ui/static/js/helpers/Parse.js index 0ad2d1ad78..29600388b2 100644 --- a/awx/ui/static/js/helpers/Parse.js +++ b/awx/ui/static/js/helpers/Parse.js @@ -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', diff --git a/awx/ui/static/lib/ansible/Modal.js b/awx/ui/static/lib/ansible/Modal.js index 80eed2950e..e63ee3bb92 100644 --- a/awx/ui/static/lib/ansible/Modal.js +++ b/awx/ui/static/lib/ansible/Modal.js @@ -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
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 }); } }; }]); \ No newline at end of file diff --git a/awx/ui/static/lib/ansible/generator-helpers.js b/awx/ui/static/lib/ansible/generator-helpers.js index 3d3714a793..137660602e 100644 --- a/awx/ui/static/lib/ansible/generator-helpers.js +++ b/awx/ui/static/lib/ansible/generator-helpers.js @@ -51,7 +51,7 @@ angular.module('GeneratorHelpers', []) result = "ng-show=\"" + value + "\" "; break; case 'icon': - // new method of constructing icon tag. Replces Icon method. + // new method of constructing icon tag. Replaces Icon method. result = ""; @@ -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': diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index 4c90d51ecf..d2e3d144e5 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -100,6 +100,7 @@ + @@ -393,6 +394,7 @@
+