Commit 6e9a9e18 authored by Daniele Venzano's avatar Daniele Venzano
Browse files

Start work on improved web interface

parent ce5d0687
Pipeline #4444 failed with stages
in 56 seconds
...@@ -28,8 +28,9 @@ def web_init() -> List[tornado.web.URLSpec]: ...@@ -28,8 +28,9 @@ def web_init() -> List[tornado.web.URLSpec]:
"""Tornado init for the web interface.""" """Tornado init for the web interface."""
web_routes = [ web_routes = [
tornado.web.url(r'/', zoe_api.web.start.RootWeb, name='root'), tornado.web.url(r'/', zoe_api.web.start.RootWeb, name='root'),
tornado.web.url(r'/user', zoe_api.web.start.HomeWeb, name='home_user'), tornado.web.url(r'/home', zoe_api.web.start.HomeWeb, name='home'),
tornado.web.url(r'/login', zoe_api.web.start.LoginWeb, name='login'), tornado.web.url(r'/login', zoe_api.web.start.LoginWeb, name='login'),
tornado.web.url(r'/logout', zoe_api.web.start.LogoutWeb, name='logout'),
tornado.web.url(r'/executions/new', zoe_api.web.executions.ExecutionDefineWeb, name='execution_define'), tornado.web.url(r'/executions/new', zoe_api.web.executions.ExecutionDefineWeb, name='execution_define'),
tornado.web.url(r'/executions/start', zoe_api.web.executions.ExecutionStartWeb, name='execution_start'), tornado.web.url(r'/executions/start', zoe_api.web.executions.ExecutionStartWeb, name='execution_start'),
......
...@@ -23,7 +23,7 @@ from tornado.escape import json_decode ...@@ -23,7 +23,7 @@ from tornado.escape import json_decode
from zoe_lib.config import get_conf from zoe_lib.config import get_conf
import zoe_api.exceptions import zoe_api.exceptions
from zoe_api.api_endpoint import APIEndpoint # pylint: disable=unused-import from zoe_api.api_endpoint import APIEndpoint # pylint: disable=unused-import
from zoe_api.web.utils import get_auth, catch_exceptions from zoe_api.web.utils import catch_exceptions
from zoe_api.web.custom_request_handler import ZoeRequestHandler from zoe_api.web.custom_request_handler import ZoeRequestHandler
......
...@@ -21,6 +21,8 @@ ...@@ -21,6 +21,8 @@
import json import json
import datetime import datetime
from typing import Union
import traceback
from jinja2 import Environment, FileSystemLoader, Markup from jinja2 import Environment, FileSystemLoader, Markup
...@@ -29,6 +31,8 @@ import tornado.web ...@@ -29,6 +31,8 @@ import tornado.web
import zoe_lib.version import zoe_lib.version
import zoe_api.web.utils import zoe_api.web.utils
from zoe_api.api_endpoint import APIEndpoint
from zoe_lib.state import User
class JinjaApp(object): class JinjaApp(object):
...@@ -84,14 +88,20 @@ def tojson_filter(obj, **kwargs): ...@@ -84,14 +88,20 @@ def tojson_filter(obj, **kwargs):
class ZoeRequestHandler(tornado.web.RequestHandler): class ZoeRequestHandler(tornado.web.RequestHandler):
"""Custom Zoe Tornado handler.""" """Custom Zoe Tornado handler."""
def get_current_user(self): def get_current_user(self) -> Union[User, None]:
"""Implement cookie-auth the Tornado way.""" """Implement cookie-auth the Tornado way."""
user_id = self.get_secure_cookie("zoeweb_user") user_id = self.get_secure_cookie("zoe_web_user").decode('utf-8')
if not user_id: if not user_id:
return None self.redirect("/login")
user = self.application.api_endpoint.user_get(user_id) return
user = self.application.api_endpoint.user_identify(user_id)
if user is None or not user.enabled: if user is None or not user.enabled:
return None self.clear_cookie("zoe_web_user")
if not user.enabled:
self.redirect("/login?why=disabled")
else:
self.redirect("/login")
return
return user return user
def initialize(self, *args_, **kwargs_): def initialize(self, *args_, **kwargs_):
...@@ -101,6 +111,8 @@ class ZoeRequestHandler(tornado.web.RequestHandler): ...@@ -101,6 +111,8 @@ class ZoeRequestHandler(tornado.web.RequestHandler):
raise RuntimeError("Needs jinja2 Environment. Initialize with JinjaApp.init_app first") raise RuntimeError("Needs jinja2 Environment. Initialize with JinjaApp.init_app first")
else: else:
self._jinja_env = self.application.settings['jinja_environment'] self._jinja_env = self.application.settings['jinja_environment']
self.api_endpoint = self.application.api_endpoint # type: APIEndpoint
assert isinstance(self.api_endpoint, APIEndpoint)
def _render(self, template, **kwargs): def _render(self, template, **kwargs):
""" todo: support multiple template preprocessors """ """ todo: support multiple template preprocessors """
...@@ -128,7 +140,7 @@ class ZoeRequestHandler(tornado.web.RequestHandler): ...@@ -128,7 +140,7 @@ class ZoeRequestHandler(tornado.web.RequestHandler):
try: try:
html = self._render(template, **kwargs) html = self._render(template, **kwargs)
except Exception: except Exception:
zoe_api.web.utils.error_page(self, 'Jinja2 template exception', 500) zoe_api.web.utils.error_page(self, traceback.format_exc(), 500)
return return
self.finish(html) self.finish(html)
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
import json import json
import zoe_api.exceptions import zoe_api.exceptions
from zoe_api.web.utils import get_auth, catch_exceptions from zoe_api.web.utils import catch_exceptions
from zoe_api.api_endpoint import APIEndpoint # pylint: disable=unused-import from zoe_api.api_endpoint import APIEndpoint # pylint: disable=unused-import
from zoe_api.web.custom_request_handler import ZoeRequestHandler from zoe_api.web.custom_request_handler import ZoeRequestHandler
......
...@@ -15,77 +15,79 @@ ...@@ -15,77 +15,79 @@
"""Main points of entry for the Zoe web interface.""" """Main points of entry for the Zoe web interface."""
import re
import tornado.web import tornado.web
from zoe_api.api_endpoint import APIEndpoint # pylint: disable=unused-import from zoe_api.api_endpoint import APIEndpoint # pylint: disable=unused-import
from zoe_api.web.utils import get_auth_login, get_auth, catch_exceptions from zoe_api.web.utils import get_auth_login, catch_exceptions
from zoe_api.web.custom_request_handler import ZoeRequestHandler from zoe_api.web.custom_request_handler import ZoeRequestHandler
from zoe_api.exceptions import ZoeAuthException
class RootWeb(ZoeRequestHandler): class RootWeb(ZoeRequestHandler):
"""Handler class""" """Handler class"""
def initialize(self, **kwargs):
"""Initializes the request handler."""
super().initialize(**kwargs)
self.api_endpoint = self.application.api_endpoint # type: APIEndpoint
@tornado.web.authenticated @tornado.web.authenticated
def get(self): def get(self):
"""Home page.""" """Home page."""
self.render('index.html') self.redirect('/home')
class LoginWeb(ZoeRequestHandler): class LoginWeb(ZoeRequestHandler):
"""The login web page.""" """The login web page."""
def initialize(self, **kwargs):
"""Initializes the request handler."""
super().initialize(**kwargs)
self.api_endpoint = self.application.api_endpoint # type: APIEndpoint
@catch_exceptions
def get(self): def get(self):
"""Login page.""" """Login page."""
self.render('login.html') why = self.get_argument('why', None)
self.render('login.html', **{'why': why})
@catch_exceptions
def post(self): def post(self):
"""Try to authenticate.""" """Try to authenticate."""
username = self.get_argument("username", "") username = self.get_argument("username", "")
password = self.get_argument("password", "") password = self.get_argument("password", "")
uid, role = get_auth_login(username, password) if not re.match(r'[a-zA-Z0-9]+', username) or len(password) == 0:
self.redirect('/login?why=invalid')
try:
uid, role = get_auth_login(self.application.api_endpoint, username, password)
except ZoeAuthException as e:
self.redirect('/login?why={}'.format(e.message))
return
self.set_secure_cookie('zoe_web_user', uid)
self.redirect('/home')
if not self.get_secure_cookie('zoe'):
cookie_val = uid + '.' + role class LogoutWeb(ZoeRequestHandler):
self.set_secure_cookie('zoe', cookie_val) """The logout web page."""
self.redirect(self.get_argument("next", u"/user"))
def get(self):
"""Logout and redirect to login page."""
self.clear_cookie('zoe_web_user')
self.redirect('/login')
class HomeWeb(ZoeRequestHandler): class HomeWeb(ZoeRequestHandler):
"""Handler class""" """Handler class"""
def initialize(self, **kwargs):
"""Initializes the request handler."""
super().initialize(**kwargs)
self.api_endpoint = kwargs['api_endpoint'] # type: APIEndpoint
@catch_exceptions @tornado.web.authenticated
def get(self): def get(self):
"""Home page with authentication.""" """Home page with authentication."""
uid, role = get_auth(self) filters = {
'user_id': self.current_user.username,
if role == 'guest': 'status': 'running'
return self._aml_homepage(uid) }
executions = self.api_endpoint.execution_list(self.current_user, **filters)
executions = self.api_endpoint.execution_list(uid, role) filters['status'] = 'starting'
executions += self.api_endpoint.execution_list(self.current_user, **filters)
filters['status'] = 'scheduled'
executions += self.api_endpoint.execution_list(self.current_user, **filters)
filters['status'] = 'terminated'
filters['limit'] = 5
executions += self.api_endpoint.execution_list(self.current_user, **filters)
template_vars = { template_vars = {
'executions': sorted(executions, key=lambda e: e.id), 'executions': sorted(executions, key=lambda e: e.id),
'is_admin': role == 'admin', 'user': self.current_user
} }
self.render('home_user.html', **template_vars) self.render('home_user.html', **template_vars)
def _aml_homepage(self, uid):
"""Home page for students of the AML course."""
template_vars = {
'uid': uid
}
return self.render('home_guest.html', **template_vars)
/** /// Knockout Mapping plugin v2.4.1
* Created by venzano on 13/07/2017. /// (c) 2013 Steven Sanderson, Roy Jacobs - http://knockoutjs.com/
*/ /// License: MIT (http://www.opensource.org/licenses/mit-license.php)
(function(e){"function"===typeof require&&"object"===typeof exports&&"object"===typeof module?e(require("knockout"),exports):"function"===typeof define&&define.amd?define(["knockout","exports"],e):e(ko,ko.mapping={})})(function(e,f){function y(b,c){var a,d;for(d in c)if(c.hasOwnProperty(d)&&c[d])if(a=f.getType(b[d]),d&&b[d]&&"array"!==a&&"string"!==a)y(b[d],c[d]);else if("array"===f.getType(b[d])&&"array"===f.getType(c[d])){a=b;for(var e=d,l=b[d],n=c[d],t={},g=l.length-1;0<=g;--g)t[l[g]]=l[g];for(g=
n.length-1;0<=g;--g)t[n[g]]=n[g];l=[];n=void 0;for(n in t)l.push(t[n]);a[e]=l}else b[d]=c[d]}function E(b,c){var a={};y(a,b);y(a,c);return a}function z(b,c){for(var a=E({},b),e=L.length-1;0<=e;e--){var f=L[e];a[f]&&(a[""]instanceof Object||(a[""]={}),a[""][f]=a[f],delete a[f])}c&&(a.ignore=h(c.ignore,a.ignore),a.include=h(c.include,a.include),a.copy=h(c.copy,a.copy),a.observe=h(c.observe,a.observe));a.ignore=h(a.ignore,j.ignore);a.include=h(a.include,j.include);a.copy=h(a.copy,j.copy);a.observe=h(a.observe,
j.observe);a.mappedProperties=a.mappedProperties||{};a.copiedProperties=a.copiedProperties||{};return a}function h(b,c){"array"!==f.getType(b)&&(b="undefined"===f.getType(b)?[]:[b]);"array"!==f.getType(c)&&(c="undefined"===f.getType(c)?[]:[c]);return e.utils.arrayGetDistinctValues(b.concat(c))}function F(b,c,a,d,k,l,n){var t="array"===f.getType(e.utils.unwrapObservable(c));l=l||"";if(f.isMapped(b)){var g=e.utils.unwrapObservable(b)[p];a=E(g,a)}var j=n||k,h=function(){return a[d]&&a[d].create instanceof
Function},x=function(b){var f=G,g=e.dependentObservable;e.dependentObservable=function(a,b,c){c=c||{};a&&"object"==typeof a&&(c=a);var d=c.deferEvaluation,M=!1;c.deferEvaluation=!0;a=new H(a,b,c);if(!d){var g=a,d=e.dependentObservable;e.dependentObservable=H;a=e.isWriteableObservable(g);e.dependentObservable=d;d=H({read:function(){M||(e.utils.arrayRemoveItem(f,g),M=!0);return g.apply(g,arguments)},write:a&&function(a){return g(a)},deferEvaluation:!0});d.__DO=g;a=d;f.push(a)}return a};e.dependentObservable.fn=
H.fn;e.computed=e.dependentObservable;b=e.utils.unwrapObservable(k)instanceof Array?a[d].create({data:b||c,parent:j,skip:N}):a[d].create({data:b||c,parent:j});e.dependentObservable=g;e.computed=e.dependentObservable;return b},u=function(){return a[d]&&a[d].update instanceof Function},v=function(b,f){var g={data:f||c,parent:j,target:e.utils.unwrapObservable(b)};e.isWriteableObservable(b)&&(g.observable=b);return a[d].update(g)};if(n=I.get(c))return n;d=d||"";if(t){var t=[],s=!1,m=function(a){return a};
a[d]&&a[d].key&&(m=a[d].key,s=!0);e.isObservable(b)||(b=e.observableArray([]),b.mappedRemove=function(a){var c="function"==typeof a?a:function(b){return b===m(a)};return b.remove(function(a){return c(m(a))})},b.mappedRemoveAll=function(a){var c=C(a,m);return b.remove(function(a){return-1!=e.utils.arrayIndexOf(c,m(a))})},b.mappedDestroy=function(a){var c="function"==typeof a?a:function(b){return b===m(a)};return b.destroy(function(a){return c(m(a))})},b.mappedDestroyAll=function(a){var c=C(a,m);return b.destroy(function(a){return-1!=
e.utils.arrayIndexOf(c,m(a))})},b.mappedIndexOf=function(a){var c=C(b(),m);a=m(a);return e.utils.arrayIndexOf(c,a)},b.mappedGet=function(a){return b()[b.mappedIndexOf(a)]},b.mappedCreate=function(a){if(-1!==b.mappedIndexOf(a))throw Error("There already is an object with the key that you specified.");var c=h()?x(a):a;u()&&(a=v(c,a),e.isWriteableObservable(c)?c(a):c=a);b.push(c);return c});n=C(e.utils.unwrapObservable(b),m).sort();g=C(c,m);s&&g.sort();s=e.utils.compareArrays(n,g);n={};var J,A=e.utils.unwrapObservable(c),
y={},z=!0,g=0;for(J=A.length;g<J;g++){var r=m(A[g]);if(void 0===r||r instanceof Object){z=!1;break}y[r]=A[g]}var A=[],B=0,g=0;for(J=s.length;g<J;g++){var r=s[g],q,w=l+"["+g+"]";switch(r.status){case "added":var D=z?y[r.value]:K(e.utils.unwrapObservable(c),r.value,m);q=F(void 0,D,a,d,b,w,k);h()||(q=e.utils.unwrapObservable(q));w=O(e.utils.unwrapObservable(c),D,n);q===N?B++:A[w-B]=q;n[w]=!0;break;case "retained":D=z?y[r.value]:K(e.utils.unwrapObservable(c),r.value,m);q=K(b,r.value,m);F(q,D,a,d,b,w,
k);w=O(e.utils.unwrapObservable(c),D,n);A[w]=q;n[w]=!0;break;case "deleted":q=K(b,r.value,m)}t.push({event:r.status,item:q})}b(A);a[d]&&a[d].arrayChanged&&e.utils.arrayForEach(t,function(b){a[d].arrayChanged(b.event,b.item)})}else if(P(c)){b=e.utils.unwrapObservable(b);if(!b){if(h())return s=x(),u()&&(s=v(s)),s;if(u())return v(s);b={}}u()&&(b=v(b));I.save(c,b);if(u())return b;Q(c,function(d){var f=l.length?l+"."+d:d;if(-1==e.utils.arrayIndexOf(a.ignore,f))if(-1!=e.utils.arrayIndexOf(a.copy,f))b[d]=
c[d];else if("object"!=typeof c[d]&&"array"!=typeof c[d]&&0<a.observe.length&&-1==e.utils.arrayIndexOf(a.observe,f))b[d]=c[d],a.copiedProperties[f]=!0;else{var g=I.get(c[d]),k=F(b[d],c[d],a,d,b,f,b),g=g||k;if(0<a.observe.length&&-1==e.utils.arrayIndexOf(a.observe,f))b[d]=g(),a.copiedProperties[f]=!0;else{if(e.isWriteableObservable(b[d])){if(g=e.utils.unwrapObservable(g),b[d]()!==g)b[d](g)}else g=void 0===b[d]?g:e.utils.unwrapObservable(g),b[d]=g;a.mappedProperties[f]=!0}}})}else switch(f.getType(c)){case "function":u()?
e.isWriteableObservable(c)?(c(v(c)),b=c):b=v(c):b=c;break;default:if(e.isWriteableObservable(b))return q=u()?v(b):e.utils.unwrapObservable(c),b(q),q;h()||u();b=h()?x():e.observable(e.utils.unwrapObservable(c));u()&&b(v(b))}return b}function O(b,c,a){for(var d=0,e=b.length;d<e;d++)if(!0!==a[d]&&b[d]===c)return d;return null}function R(b,c){var a;c&&(a=c(b));"undefined"===f.getType(a)&&(a=b);return e.utils.unwrapObservable(a)}function K(b,c,a){b=e.utils.unwrapObservable(b);for(var d=0,f=b.length;d<
f;d++){var l=b[d];if(R(l,a)===c)return l}throw Error("When calling ko.update*, the key '"+c+"' was not found!");}function C(b,c){return e.utils.arrayMap(e.utils.unwrapObservable(b),function(a){return c?R(a,c):a})}function Q(b,c){if("array"===f.getType(b))for(var a=0;a<b.length;a++)c(a);else for(a in b)c(a)}function P(b){var c=f.getType(b);return("object"===c||"array"===c)&&null!==b}function T(){var b=[],c=[];this.save=function(a,d){var f=e.utils.arrayIndexOf(b,a);0<=f?c[f]=d:(b.push(a),c.push(d))};
this.get=function(a){a=e.utils.arrayIndexOf(b,a);return 0<=a?c[a]:void 0}}function S(){var b={},c=function(a){var c;try{c=a}catch(e){c="$$$"}a=b[c];void 0===a&&(a=new T,b[c]=a);return a};this.save=function(a,b){c(a).save(a,b)};this.get=function(a){return c(a).get(a)}}var p="__ko_mapping__",H=e.dependentObservable,B=0,G,I,L=["create","update","key","arrayChanged"],N={},x={include:["_destroy"],ignore:[],copy:[],observe:[]},j=x;f.isMapped=function(b){return(b=e.utils.unwrapObservable(b))&&b[p]};f.fromJS=
function(b){if(0==arguments.length)throw Error("When calling ko.fromJS, pass the object you want to convert.");try{B++||(G=[],I=new S);var c,a;2==arguments.length&&(arguments[1][p]?a=arguments[1]:c=arguments[1]);3==arguments.length&&(c=arguments[1],a=arguments[2]);a&&(c=E(c,a[p]));c=z(c);var d=F(a,b,c);a&&(d=a);if(!--B)for(;G.length;){var e=G.pop();e&&(e(),e.__DO.throttleEvaluation=e.throttleEvaluation)}d[p]=E(d[p],c);return d}catch(f){throw B=0,f;}};f.fromJSON=function(b){var c=e.utils.parseJson(b);
arguments[0]=c;return f.fromJS.apply(this,arguments)};f.updateFromJS=function(){throw Error("ko.mapping.updateFromJS, use ko.mapping.fromJS instead. Please note that the order of parameters is different!");};f.updateFromJSON=function(){throw Error("ko.mapping.updateFromJSON, use ko.mapping.fromJSON instead. Please note that the order of parameters is different!");};f.toJS=function(b,c){j||f.resetDefaultOptions();if(0==arguments.length)throw Error("When calling ko.mapping.toJS, pass the object you want to convert.");
if("array"!==f.getType(j.ignore))throw Error("ko.mapping.defaultOptions().ignore should be an array.");if("array"!==f.getType(j.include))throw Error("ko.mapping.defaultOptions().include should be an array.");if("array"!==f.getType(j.copy))throw Error("ko.mapping.defaultOptions().copy should be an array.");c=z(c,b[p]);return f.visitModel(b,function(a){return e.utils.unwrapObservable(a)},c)};f.toJSON=function(b,c){var a=f.toJS(b,c);return e.utils.stringifyJson(a)};f.defaultOptions=function(){if(0<arguments.length)j=
arguments[0];else return j};f.resetDefaultOptions=function(){j={include:x.include.slice(0),ignore:x.ignore.slice(0),copy:x.copy.slice(0)}};f.getType=function(b){if(b&&"object"===typeof b){if(b.constructor===Date)return"date";if(b.constructor===Array)return"array"}return typeof b};f.visitModel=function(b,c,a){a=a||{};a.visitedObjects=a.visitedObjects||new S;var d,k=e.utils.unwrapObservable(b);if(P(k))a=z(a,k[p]),c(b,a.parentName),d="array"===f.getType(k)?[]:{};else return c(b,a.parentName);a.visitedObjects.save(b,
d);var l=a.parentName;Q(k,function(b){if(!(a.ignore&&-1!=e.utils.arrayIndexOf(a.ignore,b))){var j=k[b],g=a,h=l||"";"array"===f.getType(k)?l&&(h+="["+b+"]"):(l&&(h+="."),h+=b);g.parentName=h;if(!(-1===e.utils.arrayIndexOf(a.copy,b)&&-1===e.utils.arrayIndexOf(a.include,b)&&k[p]&&k[p].mappedProperties&&!k[p].mappedProperties[b]&&k[p].copiedProperties&&!k[p].copiedProperties[b]&&"array"!==f.getType(k)))switch(f.getType(e.utils.unwrapObservable(j))){case "object":case "array":case "undefined":g=a.visitedObjects.get(j);
d[b]="undefined"!==f.getType(g)?g:f.visitModel(j,c,a);break;default:d[b]=c(j,a.parentName)}}});return d}});
body { body {
font-family: sans-serif; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
max-width: 90%;
margin-left: 20px; margin-left: 20px;
} }
a:link { a:link {
color: rgba(79, 140, 30, 1); color: #00a9e1;
text-decoration: none; text-decoration: none;
} }
a:visited { a:visited {
color: rgba(79, 140, 30, 1); color: #00a9e1;
text-decoration: none; text-decoration: none;
} }
div.user_info { /* Header on top of almost all pages */
font-size: smaller; div.top-header {
clear: both; top: 0;
padding-top: 20px; left: 0;
width: 100%;
height: 5em;
background-color: white;
border-bottom: solid 1px black;
} }
table.app_list { div.top-header img.logo {
border-collapse: collapse; height: 100%;
} display: block;
table.app_list tr {
border-top: 1px black solid;
}
table.app_list td {
padding-right: 1.5em;
}
div.status_line {
float: left; float: left;
} }
span#app_name { div.top-header ul.header-menu {
font-family: cursive;
font-size: larger;
}
span#status {
font-size: smaller;
}
div.copyright {
float: left; float: left;
padding-left: 3em; list-style: none;
} margin: 0;
height: 5em;
textarea#log {
width: 90%;
height: 40em;
}
span.fakelink {
color: rgba(79, 140, 30, 1);
text-decoration: none;
cursor: pointer;
}
#wrapper {
width: 800px;
} }
#navigation { div.top-header ul.header-menu li {
background-color: #fff; float: left;
border: #ddd 1px solid; padding-left: 0.5em;
border-radius: 10px; padding-right: 0.5em;
margin: 10px; display: flex;
padding: 10px; align-items: center;
height: 100%;
border: 1px solid transparent;
transition: border-color ease-in-out 0.15s;
font-size: 1.3em;
} }
#navigation li { div.top-header ul.header-menu li:hover {
margin: 2px 0; color: #353390;
} }
label.error { div.top-header span.current-user {
color: #ff0000; float: right;
margin-left: 10px; display: flex;
position: relative; align-items: center;
height: 100%;
} }
.wizard { /* Main content of the page */
background-color: #fff; #content {
border: #ddd 1px solid; width: 90%;
border-radius: 10px;
margin: 10px;
padding: 10px;
} }
.wizard .wizard-header { /* Login form */
background-color: #f4f4f4; div.login-form {
border-bottom: #ddd 1px solid; position: absolute;
border-top-left-radius: 10px; top: 80px;
border-top-right-radius: 10px; left: 50%;
padding: 5px 10px; margin: 0 0 0 -195px;
margin: 0 0 10px 0; width: 390px;
border: 1px solid #ddd;
max-height: none;
border-radius: 6px;
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
background-clip: border-box;
} }
.wizard .wizard-step { div.login-form img.logo {
margin: 10px 0; width: 95%;
margin: 0 auto;
display: block;
} }
.wizard .wizard-step p { div.login-form input {
padding: 5px; width: 90%;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
border: 1px solid #ccc;
border-radius: 4px;
display: block;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
padding: 6px 12px;
} }
.navigation { div.login-form input:focus {
border-top: #ddd 1px solid; border-color: #33c0c4;
margin-top: 10px; outline: 0;
padding-top: 10px; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(51, 192, 196, 0.6);
} }
.navigation ul { div.login-form label {
margin: 0; margin-bottom: 5px;
padding: 0; text-align: left;
list-style: none; color: #555555;
font-weight: bold;
display: inline-block;
} }
.navigation li { div.login-form div.form-btn {
float: left; float: right;
margin-right: 10px;
} }
.clearfix:before, .clearfix:after { div.login-form fieldset {
content: "\0020"; border: 0;
display: block;
height: 0;
visibility: hidden;
} }
.clearfix:after { div.login-form button {
clear: both; white-space: nowrap;
padding: 6px 12px;
border-radius: 4px;
margin-bottom: 8px;
margin-right: 8px;
border: 1px solid transparent;
} }