create 1/3+2/3 LESS layout, normalize site-wide footer positioning, move CheckLicense factory to separate file, upgrade URL, add file picker + form reset behavior, #1055, #1057, #1007

This commit is contained in:
Leigh Johnson 2016-02-29 15:01:44 -05:00
parent 0971642d20
commit 211ae9afad
10 changed files with 325 additions and 191 deletions

View File

@ -60,7 +60,7 @@ body {
}
#content-container {
margin-top: 40px;
padding-bottom: 40px;
}
.group-breadcrumbs {

View File

@ -6,7 +6,7 @@
color: #848992;
width: 100%;
z-index: 1040;
position: absolute;
position: fixed;
right: 0;
left: 0;
bottom: 0;

View File

@ -0,0 +1,62 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default
['$state', '$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors', function($state, $rootScope, Rest, GetBasePath, ProcessErrors){
return {
get: function() {
var defaultUrl = GetBasePath('config');
Rest.setUrl(defaultUrl);
return Rest.get()
.success(function(res){
return res
})
.error(function(res, status){
ProcessErrors($rootScope, res, status, null, {hdr: 'Error!',
msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status});
});
},
post: function(license, eula){
var defaultUrl = GetBasePath('config');
Rest.setUrl(defaultUrl);
var data = license;
data.eula_accepted = eula;
return Rest.post(JSON.stringify(data))
.success(function(res){
return res
})
.error(function(res, status){
ProcessErrors($rootScope, res, status, null, {hdr: 'Error!',
msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status});
});
},
// Checks current license validity
// Intended to for runtime or pre-state checks
// Returns false if invalid
valid: function(license) {
if (!license.valid_key){
return false
}
else if (license.free_instances <= 0){
return false
}
// notify if less than 15 days remaining
else if (license.time_remaining / 1000 / 60 / 60 / 24 > 15){
return false
}
return true
},
notify: function(){
self = this;
this.get()
.then(function(res){
self.valid(res.data.license_info) ? null : $state.go('license');
});
}
}
}
];

View File

@ -0,0 +1,16 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default
[function(){
return {
restrict: 'A',
link: function(scope, el, attrs){
var onChange = scope.$eval(attrs.fileOnChange);
el.bind('change', onChange);
}
}
}];

View File

@ -1,14 +1,17 @@
/*
* Style conventions
* .ModuleName-component-subComponent
* Naming describes components of the view
*/
@import "awx/ui/client/src/shared/branding/colors.default.less";
@import "awx/ui/client/src/shared/layouts/one-plus-two.less";
.License-container{
display: flex;
flex-direction: row;
flex-wrap: wrap;
.OnePlusTwo-container;
}
.License-container label{
text-transform: uppercase;
color: @default-interface-txt;
font-weight: 500;
.License-field--label{
.OnePlusTwo-left--detailsLabel;
}
.License-management .CodeMirror-scroll{
min-height: 140px;
@ -19,20 +22,16 @@
}
.License-eula textarea{
width: 100%;
height: 140px;
height: 300px;
}
.License-field label{
width: 155px;
}
.License-field span{
width: 255px;
display: inline-block;
.License-field--content{
.OnePlusTwo-left--detailsContent;
}
.License-field{
word-wrap: break-word;
width: 100%;
margin-top: 4px;
margin-bottom: 4px;
.OnePlusTwo-left--detailsRow;
}
.License-greenText{
color: @submit-button-bg;
@ -41,40 +40,27 @@
color: #d9534f;
}
.License-fields{
display: flex;
flex-flow: row wrap;
justify-content: space-around;
.OnePlusTwo-left--details;
}
.License-details {
height: 520px;
width: 460px;
flex-grow: 1;
display: inline-block;
background-color: @default-bg;
padding: 20px;
border-radius: 5px;
border: @default-interface-txt;
align-items: baseline;
margin-top: 20px;
margin-bottom: 20px;
margin-right: 20px;
.OnePlusTwo-left--panel(600px);
}
.License-titleText {
color: @default-interface-txt;
font-size: 14px;
font-weight: bold;
margin-right: 10px;
margin-bottom: 10px;
text-transform: uppercase;
.OnePlusTwo-panelHeader;
}
.License-management{
height: 520px;
flex-grow: 2;
display: inline-block;
background-color: @default-bg;
padding: 20px;
border-radius: 5px;
border: @default-interface-txt;
align-items: baseline;
margin-top: 20px;
.OnePlusTwo-right--panel(600px);
}
.License-submit--container{
height: 33px;
}
.License-submit--success{
line-height:33px;
margin: 0 10px 0 0;
}
.License-file--container {
margin: 20px 0 20px 0;
input[type=file] {
display: none;
}
}

View File

@ -9,35 +9,47 @@ export default
'GetBasePath', 'Rest', 'ProcessErrors', 'CheckLicense', 'moment',
function( Wait, $state, $scope, $location,
GetBasePath, Rest, ProcessErrors, CheckLicense, moment){
// codemirror
var textArea = document.getElementById('License-codemirror');
var editor = CodeMirror.fromTextArea(textArea, {
lineNumbers: true,
mode: 'json'
});
editor.on('blur', function(cm){
$scope.newLicense.file = cm.getValue()
});
$scope.getKey = function(event){
// Mimic HTML5 spec, show filename
$scope.fileName = event.target.files[0].name;
// Grab the key from the raw license file
var raw = new FileReader();
// readAsFoo runs async
raw.onload = function(){
$scope.newLicense.file = JSON.parse(raw.result);
}
raw.readAsText(event.target.files[0]);
};
// HTML5 spec doesn't provide a way to customize file input css
// So we hide the default input, show our own, and simulate clicks to the hidden input
$scope.fakeClick = function(){
$('#License-file').click();
//document.getElementById('License-file').click();
}
$scope.newLicense = {};
$scope.submit = function(e){
Wait('start')
$scope.submit = function(event){
Wait('start');
CheckLicense.post($scope.newLicense.file, $scope.newLicense.eula)
.success(function(res){
console.log(res)
reset();
init();
$scope.success = true;
});
}
};
var calcDaysRemaining = function(ms){
// calculate the number of days remaining on the license
var duration = moment.duration(ms);
return duration.days()
}
};
var calcExpiresOn = function(days){
// calculate the expiration date of the license
return moment().add(days, 'days').calendar()
}
Wait('start');
CheckLicense.get()
};
var init = function(){
$scope.fileName = "Please choose a file..."
Wait('start');
CheckLicense.get()
.then(function(res){
$scope.license = res.data;
$scope.time = {};
@ -46,5 +58,10 @@ export default
$scope.valid = CheckLicense.valid($scope.license.license_info);
Wait('stop');
});
}
};
var reset = function(){
document.getElementById('License-form').reset()
};
init();
}
];

View File

@ -1,72 +1,99 @@
<div class="License-container">
<div class="License-details ng-cloak">
<div class="License-titleText">Details</div>
<div class="License-fields">
<div class="License-field">
<label>License</label>
<span ng-show='valid'><i class="fa fa-circle License-greenText"></i> Valid</span>
<span ng-show='invalid'><i class="fa fa-circle License-redText"></i> Invalid</span>
</div>
<div class="License-field">
<label>Version</label>
<span>{{license.version}}</span>
</div>
<div class="License-field">
<label>License Type</label>
<span>{{license.license_info.license_type}}</span>
</div>
<div class="License-field">
<label>Subscription</label>
<span>{{license.license_info.subscription_name}}</span>
</div>
<div class="License-field">
<label>License Key</label>
<span>{{license.license_info.license_key}}</span>
</div>
<div class="License-field">
<label>Expires On</label>
<span>{{time.expiresOn}}</span>
</div>
<div class="License-field">
<label>Time Remaining</label>
<span>{{time.remaining}} Days</span>
</div>
<div class="License-field">
<label>Hosts Available</label>
<span>{{license.license_info.available_instances}}</span>
</div>
<div class="License-field">
<label>Hosts Used</label>
<span>{{license.license_info.current_instances}}</span>
</div>
<div class="License-field License-greenText">
<label>Hosts Remaining</label>
<span>{{license.license_info.free_instances}}</span>
</div>
</div>
<br>
<p>If you are ready to upgrade, please contact us by clicking the button below</p>
<button class="btn btn-default">Upgrade</button>
</div>
<div class="License-management">
<div class="License-titleText">License Management</div>
<p>Copy and paste the contents of your license in the field below, agree to the End User License Agreement, and click submit.</p>
<form name="license">
<div class="form-group License-file prepend-asterisk ">
<label>License File</label>
<textarea id="License-codemirror" placeholder="Please paste your license here"></textarea>
</div>
<div class="License-titleText prepend-asterisk"> End User License Agreement</div>
<div class="form-group License-eula">
<textarea class="form-control">{{license.eula}}
</textarea>
</div>
<div class="form-group">
<div class="checkbox">
<label><input type="checkbox" ng-model="newLicense.eula" required> I agree to the End User License Agreement</label>
<button ng-click="submit()" class="btn btn-success pull-right" ng-disabled="newLicense.file == null || newLicense.eula == null">Submit</button>
<div class="License-details">
<div class="Panel">
<div class="License-titleText">Details</div>
<div class="License-fields">
<div class="License-field">
<div class="License-field--label">License</div>
<div class="License-field--content">
<span ng-show='valid'><i class="fa fa-circle License-greenText"></i> Valid</span>
<span ng-show='invalid'><i class="fa fa-circle License-redText"></i> Invalid</span>
</div>
</div>
<div class="License-field">
<div class="License-field--label">Version</div>
<div class="License-field--content">
{{license.version}}
</div>
</div>
<div class="License-field">
<div class="License-field--label">License Type</div>
<div class="License-field--content">
{{license.license_info.license_type}}
</div>
</div>
<div class="License-field">
<div class="License-field--label">Subscription</div>
<div class="License-field--content">
{{license.license_info.subscription_name}}
</div>
</div>
<div class="License-field">
<div class="License-field--label">License Key</div>
<div class="License-field--content">
{{license.license_info.license_key}}
</div>
</div>
<div class="License-field">
<div class="License-field--label">Expires On</div>
<div class="License-field--content">
{{time.expiresOn}}
</div>
</div>
<div class="License-field">
<div class="License-field--label">Time Remaining</div>
<div class="License-field--content">
{{time.remaining}} Day
</div>
</div>
<div class="License-field">
<div class="License-field--label">Hosts Available</div>
<div class="License-field--content">
{{license.license_info.available_instances}}
</div>
</div>
<div class="License-field">
<div class="License-field--label">Hosts Used</div>
<div class="License-field--content">
{{license.license_info.current_instances}}
</div>
</div>
<div class="License-field License-greenText">
<div class="License-field--label">Hosts Remaining</div>
<div class="License-field--content">
{{license.license_info.free_instances}}
</div>
</div>
</div>
</form>
<p>If you are ready to upgrade, please contact us by clicking the button below</p>
<a href="https://www.ansible.com/renew" target="_blank"><button class="btn btn-default">Upgrade</button></a>
</div>
</div>
<div class="License-management">
<div class="Panel">
<div class="License-titleText">License Management</div>
<p>Choose your license file, agree to the End User License Agreement, and click submit.</p>
<form id="License-form" name="license">
<div class="input-group License-file--container">
<span class="btn btn-default input-group-addon" ng-click="fakeClick()">Browse...</span>
<input class="form-control" ng-disabled="true" placeholder="{{fileName}}" />
<input id="License-file" class="form-control" type="file" file-on-change="getKey"/>
</div>
<div class="License-titleText prepend-asterisk"> End User License Agreement</div>
<div class="form-group License-eula">
<textarea class="form-control">{{license.eula}}
</textarea>
</div>
<div class="form-group">
<div class="checkbox">
<div class="License-details--label"><input type="checkbox" ng-model="newLicense.eula" required> I agree to the End User License Agreement</div>
<div class="License-submit--container pull-right">
<span ng-hide="success == null || false" class="License-greenText License-submit--success pull-left">Save successful!</span>
<button ng-click="submit()" class="btn btn-success pull-right" ng-disabled="newLicense.file.license_key == null || newLicense.eula == null">Submit</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>

View File

@ -6,66 +6,14 @@
import route from './license.route';
import controller from './license.controller';
import CheckLicense from './checkLicense.factory';
import fileOnChange from './fileOnChange.directive';
export default
angular.module('license', [])
.controller('licenseController', controller)
.factory('CheckLicense', ['$state', '$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors', function($state, $rootScope, Rest, GetBasePath, ProcessErrors){
return {
get: function() {
var defaultUrl = GetBasePath('config');
Rest.setUrl(defaultUrl);
return Rest.get()
.success(function(res){
return res
})
.error(function(res, status){
ProcessErrors($rootScope, res, status, null, {hdr: 'Error!',
msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status});
});
},
post: function(license, eula){
var defaultUrl = GetBasePath('config');
Rest.setUrl(defaultUrl);
var data = {
eula_accepted: eula,
license_key: license
};
console.log(data)
return Rest.post(JSON.stringify(data))
.success(function(res){
return res
})
.error(function(res, status){
ProcessErrors($rootScope, res, status, null, {hdr: 'Error!',
msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status});
});
},
// Checks current license validity
// Intended to for runtime or pre-state checks
// Returns false if invalid
valid: function(license) {
if (!license.valid_key){
return false
}
else if (license.free_instances <= 0){
return false
}
// notify if less than 15 days remaining
else if (license.time_remaining / 1000 / 60 / 60 / 24 > 15){
return false
}
return true
},
notify: function(){
this.get()
.then(function(res){
this.valid(res.license_info) ? null : $state.go('license');
});
}
}
}])
.directive('fileOnChange', fileOnChange)
.factory('CheckLicense', CheckLicense)
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route);
}]);

View File

@ -0,0 +1,79 @@
/*
* Large resolution: 1/3 + 2/3 width panels
* Small resolution: 100% width panels, stacked
* Options: static height, custom breakpoint
*
* Style conventions
* .ModuleName-component--subComponent
*/
@import "awx/ui/client/src/shared/branding/colors.default.less";
.OnePlusTwo-container(@height: 100%; @breakpoint: 900px){
height: @height;
display: flex;
flex-direction: row;
@media screen and (max-width: @breakpoint){
flex-direction: column;
}
}
.OnePlusTwo-left--panel(@height: 100%; @breakpoint: 900px) {
flex: 0 0;
height: @height;
width: 100%;
.Panel{
height: 100%;
}
@media screen and (min-width: @breakpoint){
max-width: 400px;
}
}
.OnePlusTwo-right--panel(@height: 100%; @breakpoint: 900px) {
height: @height;
flex: 1 0;
margin-left: 20px;
.Panel{
height: 100%;
}
@media screen and (max-width: @breakpoint){
flex-direction: column;
margin-left: 0px;
margin-top: 25px;
}
}
.OnePlusTwo-panelHeader {
color: @default-interface-txt;
font-size: 14px;
font-weight: bold;
margin-right: 10px;
text-transform: uppercase;
}
.OnePlusTwo-left--details {
margin-top: 25px;
}
.OnePlusTwo-left--detailsRow {
display: flex;
:not(:last-child){
margin-bottom: 20px;
}
}
.OnePlusTwo-left--detailsLabel {
width: 140px;
display: inline-block;
color: @default-interface-txt;
text-transform: uppercase;
font-weight: 400;
}
.OnePlusTwo-left--detailsContent {
display: inline-block;
max-width: 220px;
word-wrap: break-word;
}

View File

@ -41,7 +41,7 @@
<main-menu></main-menu>
<bread-crumb></bread-crumb>
<div class="container-fluid" id="#content-container">
<div class="container-fluid" id="content-container">
<div class="row">
<div class="col-lg-12">
<div ui-view id="main-view"></div>
@ -221,8 +221,7 @@
<div class="overlay"></div>
<div class="spinny"><i class="fa fa-cog fa-spin fa-2x"></i> <p>working...</p></div>
<!-- <div class="site-footer"></div> -->
</div>
<tower-footer></tower-footer>
<script>
// HACK: Need this to support global-dependent