Commit bb86ce58 authored by Daniele Venzano's avatar Daniele Venzano 🏇

Merge branch 'devel/zapp-shop' into 'master'

New web UI and lots of fixes

See merge request !15
parents c331579e 7c1b6dae
......@@ -7,7 +7,7 @@ The main Zoe Components are:
* zoe master: the core component that performs application scheduling and talks to Swarm
* zoe api: the Zoe frontend, offering a web interface and a REST API
* zoe: command-line client
* command-line clients (zoe.py and zoe-admin.py)
The Zoe master is the core component of Zoe and communicates with the clients by using an internal ZeroMQ-based protocol. This protocol is designed to be robust, using the best practices from ZeroMQ documentation. A crash of the Api or of the Master process will not leave the other component inoperable, and when the faulted process restarts, work will restart where it was left.
......
......@@ -62,7 +62,7 @@ Master options:
Authentication:
* ``auth-type = text`` : Authentication type (text or ldap)
* ``auth-type = text`` : Authentication type (text, ldap or ldapsasl)
* ``auth-file = zoepass.csv`` : Path to the CSV file containing user,pass,role lines for text authentication
* ``ldap-server-uri = ldap://localhost`` : LDAP server to use for user authentication
* ``ldap-base-dn = ou=something,dc=any,dc=local`` : LDAP base DN for users
......@@ -77,6 +77,10 @@ Scheduler options:
Default options for the scheduler enable the traditional Zoe scheduler that was already available in the previous releases.
ZApp shop:
* ``zapp-shop-path = /var/lib/zoe-apps`` : Path where ZApp folders are stored
Backend choice:
* ``backend = <Swarm|Kubernetes>`` : cluster back-end to use to run ZApps
......
......@@ -9,6 +9,12 @@ To better work together we have established some rules on how to contribute.
If you need ideas on features that are waiting to be implemented, you can check the `roadmap <https://github.com/DistributedSystemsGroup/zoe/wiki/RoadMap>`_.
Development repository
----------------------
Development happens at `Eurecom's GitLab repository <https://gitlab.eurecom.fr/zoe/main>`_. The GitHub repository is a read-only mirror.
The choice of GitLab over GitHub is due to the CI pipeline that we set-up to test Zoe.
Bug reports and feature requests
--------------------------------
......
This diff is collapsed.
......@@ -17,7 +17,6 @@
from datetime import datetime, timedelta
import logging
import re
import os
import zoe_api.exceptions
......@@ -29,6 +28,8 @@ from zoe_lib.config import get_conf
log = logging.getLogger(__name__)
GUEST_QUOTA_MAX_EXECUTIONS = 1
class APIEndpoint:
"""
......@@ -71,10 +72,14 @@ class APIEndpoint:
except zoe_lib.exceptions.InvalidApplicationDescription as e:
raise zoe_api.exceptions.ZoeException('Invalid application description: ' + e.message)
if 3 > len(exec_name) > 128:
raise zoe_api.exceptions.ZoeException("Execution name must be between 4 and 128 characters long")
if not re.match(r'^[a-zA-Z0-9\-]+$', exec_name):
raise zoe_api.exceptions.ZoeException("Execution name can contain only letters, numbers and dashes. '{}' is not valid.".format(exec_name))
# quota check
if role == "guest":
running_execs = self.execution_list(uid, role, **{'status': 'running'})
running_execs += self.execution_list(uid, role, **{'status': 'starting'})
running_execs += self.execution_list(uid, role, **{'status': 'scheduled'})
running_execs += self.execution_list(uid, role, **{'status': 'submitted'})
if len(running_execs) > GUEST_QUOTA_MAX_EXECUTIONS:
raise zoe_api.exceptions.ZoeException('Guest users cannot run more than one execution at a time, quota exceeded.')
new_id = self.sql.execution_new(exec_name, uid, application_description)
success, message = self.master.execution_start(new_id)
......@@ -100,6 +105,9 @@ class APIEndpoint:
def execution_delete(self, uid, role, exec_id):
"""Delete an execution."""
if role != "admin":
raise zoe_api.exceptions.ZoeAuthException()
e = self.sql.execution_list(id=exec_id, only_one=True)
assert isinstance(e, zoe_lib.state.sql_manager.Execution)
if e is None:
......@@ -194,7 +202,7 @@ class APIEndpoint:
for port in service.description['ports']:
port_key = str(port['port_number']) + "/" + port['protocol']
backend_port = self.sql.port_list(only_one=True, service_id=service.id, internal_name=port_key)
if backend_port.external_ip is not None:
if backend_port is not None and backend_port.external_ip is not None:
endpoint = port['url_template'].format(**{"ip_port": backend_port.external_ip + ":" + str(backend_port.external_port)})
endpoints.append((port['name'], endpoint))
......
......@@ -48,6 +48,8 @@ def zoe_web_main() -> int:
if args.log_file != "stderr":
log_args['filename'] = args.log_file
logging.basicConfig(**log_args)
logging.getLogger("MARKDOWN").setLevel(logging.WARNING)
logging.getLogger("tornado").setLevel(logging.WARNING)
if config.get_conf().auth_type == 'ldap' and not zoe_api.auth.ldap.LDAP_AVAILABLE:
log.error("LDAP authentication requested, but 'pyldap' module not installed.")
......
......@@ -73,7 +73,9 @@ def get_auth(handler: tornado.web.RequestHandler):
if handler.get_secure_cookie('zoe'):
cookie_val = str(handler.get_secure_cookie('zoe'))
uid, role = cookie_val[2:-1].split('.')
log.info('Authentication done using cookie')
log.debug('Authentication done using cookie')
if role == "guest":
raise ZoeRestAPIException('Guest users cannot use the API, ask for a role upgrade', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
return uid, role
auth_header = handler.request.headers.get('Authorization')
......@@ -118,6 +120,9 @@ def get_auth(handler: tornado.web.RequestHandler):
raise ZoeRestAPIException('missing or wrong authentication information', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
log.debug('Authentication done using auth-mechanism')
if role == "guest":
raise ZoeRestAPIException('Guest users cannot use the API, ask for a role upgrade', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
return uid, role
......
sonar.projectName=zoe_api
sonar.host.url=http://your-sonarqube-server-address
sonar.sources=.
sonar.language=py
sonar.sourceEncoding=UTF-8
......@@ -22,6 +22,7 @@ import tornado.web
import zoe_api.web.start
import zoe_api.web.websockets
import zoe_api.web.executions
import zoe_api.web.zapp_shop
from zoe_lib.version import ZOE_API_VERSION, ZOE_VERSION
......@@ -38,15 +39,18 @@ def web_init(api_endpoint) -> List[tornado.web.URLSpec]:
tornado.web.url(r'/login', zoe_api.web.start.LoginWeb, route_args, name='login'),
tornado.web.url(r'/logout', zoe_api.web.start.LogoutWeb, route_args, name='logout'),
tornado.web.url(r'/executions/new', zoe_api.web.executions.ExecutionDefineWeb, route_args, name='execution_define'),
tornado.web.url(r'/executions', zoe_api.web.executions.ExecutionListWeb, route_args, name='execution_list'),
tornado.web.url(r'/executions/start', zoe_api.web.executions.ExecutionStartWeb, route_args, name='execution_start'),
tornado.web.url(r'/executions/restart/([0-9]+)', zoe_api.web.executions.ExecutionRestartWeb, route_args, name='execution_restart'),
tornado.web.url(r'/executions/terminate/([0-9]+)', zoe_api.web.executions.ExecutionTerminateWeb, route_args, name='execution_terminate'),
tornado.web.url(r'/executions/delete/([0-9]+)', zoe_api.web.executions.ExecutionDeleteWeb, route_args, name='execution_delete'),
tornado.web.url(r'/executions/inspect/([0-9]+)', zoe_api.web.executions.ExecutionInspectWeb, route_args, name='execution_inspect'),
tornado.web.url(r'/service/logs/([0-9]+)', zoe_api.web.executions.ServiceLogsWeb, route_args, name='service_logs'),
tornado.web.url(r'/websocket', zoe_api.web.websockets.WebSocketEndpointWeb, route_args, name='websocket')
tornado.web.url(r'/websocket', zoe_api.web.websockets.WebSocketEndpointWeb, route_args, name='websocket'),
tornado.web.url(r'/zapp-shop', zoe_api.web.zapp_shop.ZAppShopHomeWeb, route_args, name='zappshop'),
tornado.web.url(r'/zapp-shop/logo/([a-z\-.]+)', zoe_api.web.zapp_shop.ZAppLogoWeb, route_args, name='zappshop_logo'),
tornado.web.url(r'/zapp-shop/start/([0-9a-z\-.]+)', zoe_api.web.zapp_shop.ZAppStartWeb, route_args, name='zappshop_start')
]
return web_routes
......
......@@ -23,27 +23,6 @@ from zoe_api.api_endpoint import APIEndpoint # pylint: disable=unused-import
from zoe_api.web.custom_request_handler import ZoeRequestHandler
class ExecutionDefineWeb(ZoeRequestHandler):
"""Handler class"""
def initialize(self, **kwargs):
"""Initializes the request handler."""
super().initialize(**kwargs)
self.api_endpoint = kwargs['api_endpoint'] # type: APIEndpoint
@catch_exceptions
def get(self):
"""Define a new execution."""
uid, role = get_auth(self)
if uid is None:
return self.redirect(self.get_argument('next', u'/login'))
template_vars = {
"uid": uid,
"role": role,
}
self.render('execution_new.html', **template_vars)
class ExecutionStartWeb(ZoeRequestHandler):
"""Handler class"""
def initialize(self, **kwargs):
......@@ -67,7 +46,7 @@ class ExecutionStartWeb(ZoeRequestHandler):
self.redirect(self.reverse_url('execution_inspect', new_id))
class ExecutionRestartWeb(ZoeRequestHandler):
class ExecutionListWeb(ZoeRequestHandler):
"""Handler class"""
def initialize(self, **kwargs):
"""Initializes the request handler."""
......@@ -75,19 +54,23 @@ class ExecutionRestartWeb(ZoeRequestHandler):
self.api_endpoint = kwargs['api_endpoint'] # type: APIEndpoint
@catch_exceptions
def get(self, execution_id: int):
"""Restart an already defined (and not running) execution."""
def get(self):
"""Home page with authentication."""
uid, role = get_auth(self)
if uid is None:
return self.redirect(self.get_argument('next', u'/login'))
e = self.api_endpoint.execution_by_id(uid, role, execution_id)
new_id = self.api_endpoint.execution_start(uid, role, e.name, e.description)
executions = self.api_endpoint.execution_list(uid, role)
self.redirect(self.reverse_url('execution_inspect', new_id))
template_vars = {
"uid": uid,
"role": role,
'executions': sorted(executions, key=lambda e: e.id, reverse=True)
}
self.render('execution_list.html', **template_vars)
class ExecutionTerminateWeb(ZoeRequestHandler):
class ExecutionRestartWeb(ZoeRequestHandler):
"""Handler class"""
def initialize(self, **kwargs):
"""Initializes the request handler."""
......@@ -96,19 +79,18 @@ class ExecutionTerminateWeb(ZoeRequestHandler):
@catch_exceptions
def get(self, execution_id: int):
"""Terminate an execution."""
"""Restart an already defined (and not running) execution."""
uid, role = get_auth(self)
if uid is None:
return self.redirect(self.get_argument('next', u'/login'))
success, message = self.api_endpoint.execution_terminate(uid, role, execution_id)
if not success:
raise zoe_api.exceptions.ZoeException(message)
e = self.api_endpoint.execution_by_id(uid, role, execution_id)
new_id = self.api_endpoint.execution_start(uid, role, e.name, e.description)
self.redirect(self.reverse_url('home_user'))
self.redirect(self.reverse_url('execution_inspect', new_id))
class ExecutionDeleteWeb(ZoeRequestHandler):
class ExecutionTerminateWeb(ZoeRequestHandler):
"""Handler class"""
def initialize(self, **kwargs):
"""Initializes the request handler."""
......@@ -117,12 +99,12 @@ class ExecutionDeleteWeb(ZoeRequestHandler):
@catch_exceptions
def get(self, execution_id: int):
"""Delete an execution."""
"""Terminate an execution."""
uid, role = get_auth(self)
if uid is None:
return self.redirect(self.get_argument('next', u'/login'))
success, message = self.api_endpoint.execution_delete(uid, role, execution_id)
success, message = self.api_endpoint.execution_terminate(uid, role, execution_id)
if not success:
raise zoe_api.exceptions.ZoeException(message)
......
......@@ -86,22 +86,46 @@ class HomeWeb(ZoeRequestHandler):
if uid is None:
return self.redirect(self.get_argument('next', u'/login'))
if role == 'guest':
return self._aml_homepage(uid)
filters = {
"user_id": uid,
"limit": 5
}
last_executions = self.api_endpoint.execution_list(uid, role, **filters)
filters = {
"user_id": uid,
"status": "running"
}
last_running_executions = self.api_endpoint.execution_list(uid, role, **filters)
filters = {
"user_id": uid,
"status": "submitted"
}
last_running_executions += self.api_endpoint.execution_list(uid, role, **filters)
filters = {
"user_id": uid,
"status": "scheduled"
}
last_running_executions += self.api_endpoint.execution_list(uid, role, **filters)
filters = {
"user_id": uid,
"status": "starting"
}
last_running_executions += self.api_endpoint.execution_list(uid, role, **filters)
executions = self.api_endpoint.execution_list(uid, role)
running_reservations = [e.total_reservations for e in last_running_executions]
total_memory = sum([r.memory.max for r in running_reservations])
total_cores = sum([r.cores.max for r in running_reservations])
template_vars = {
"uid": uid,
"role": role,
'executions': sorted(executions, key=lambda e: e.id),
'is_admin': role == 'admin',
"total_memory": total_memory,
"total_cores": total_cores,
'last_executions': sorted(last_executions, key=lambda e: e.id),
'running_executions': sorted(last_running_executions, key=lambda e: e.id)
}
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)
This diff is collapsed.
//! moment-timezone.js
//! version : 0.5.13
//! Copyright (c) JS Foundation and other contributors
//! license : MIT
//! github.com/moment/moment-timezone
!function(a,b){"use strict";"function"==typeof define&&define.amd?define(["moment"],b):"object"==typeof module&&module.exports?module.exports=b(require("moment")):b(a.moment)}(this,function(a){"use strict";function b(a){return a>96?a-87:a>64?a-29:a-48}function c(a){var c,d=0,e=a.split("."),f=e[0],g=e[1]||"",h=1,i=0,j=1;for(45===a.charCodeAt(0)&&(d=1,j=-1),d;d<f.length;d++)c=b(f.charCodeAt(d)),i=60*i+c;for(d=0;d<g.length;d++)h/=60,c=b(g.charCodeAt(d)),i+=c*h;return i*j}function d(a){for(var b=0;b<a.length;b++)a[b]=c(a[b])}function e(a,b){for(var c=0;c<b;c++)a[c]=Math.round((a[c-1]||0)+6e4*a[c]);a[b-1]=1/0}function f(a,b){var c,d=[];for(c=0;c<b.length;c++)d[c]=a[b[c]];return d}function g(a){var b=a.split("|"),c=b[2].split(" "),g=b[3].split(""),h=b[4].split(" ");return d(c),d(g),d(h),e(h,g.length),{name:b[0],abbrs:f(b[1].split(" "),g),offsets:f(c,g),untils:h,population:0|b[5]}}function h(a){a&&this._set(g(a))}function i(a){var b=a.toTimeString(),c=b.match(/\([a-z ]+\)/i);c&&c[0]?(c=c[0].match(/[A-Z]/g),c=c?c.join(""):void 0):(c=b.match(/[A-Z]{3,5}/g),c=c?c[0]:void 0),"GMT"===c&&(c=void 0),this.at=+a,this.abbr=c,this.offset=a.getTimezoneOffset()}function j(a){this.zone=a,this.offsetScore=0,this.abbrScore=0}function k(a,b){for(var c,d;d=6e4*((b.at-a.at)/12e4|0);)c=new i(new Date(a.at+d)),c.offset===a.offset?a=c:b=c;return a}function l(){var a,b,c,d=(new Date).getFullYear()-2,e=new i(new Date(d,0,1)),f=[e];for(c=1;c<48;c++)b=new i(new Date(d,c,1)),b.offset!==e.offset&&(a=k(e,b),f.push(a),f.push(new i(new Date(a.at+6e4)))),e=b;for(c=0;c<4;c++)f.push(new i(new Date(d+c,0,1))),f.push(new i(new Date(d+c,6,1)));return f}function m(a,b){return a.offsetScore!==b.offsetScore?a.offsetScore-b.offsetScore:a.abbrScore!==b.abbrScore?a.abbrScore-b.abbrScore:b.zone.population-a.zone.population}function n(a,b){var c,e;for(d(b),c=0;c<b.length;c++)e=b[c],I[e]=I[e]||{},I[e][a]=!0}function o(a){var b,c,d,e=a.length,f={},g=[];for(b=0;b<e;b++){d=I[a[b].offset]||{};for(c in d)d.hasOwnProperty(c)&&(f[c]=!0)}for(b in f)f.hasOwnProperty(b)&&g.push(H[b]);return g}function p(){try{var a=Intl.DateTimeFormat().resolvedOptions().timeZone;if(a){var b=H[r(a)];if(b)return b;z("Moment Timezone found "+a+" from the Intl api, but did not have that data loaded.")}}catch(c){}var d,e,f,g=l(),h=g.length,i=o(g),k=[];for(e=0;e<i.length;e++){for(d=new j(t(i[e]),h),f=0;f<h;f++)d.scoreOffsetAt(g[f]);k.push(d)}return k.sort(m),k.length>0?k[0].zone.name:void 0}function q(a){return D&&!a||(D=p()),D}function r(a){return(a||"").toLowerCase().replace(/\//g,"_")}function s(a){var b,c,d,e;for("string"==typeof a&&(a=[a]),b=0;b<a.length;b++)d=a[b].split("|"),c=d[0],e=r(c),F[e]=a[b],H[e]=c,d[5]&&n(e,d[2].split(" "))}function t(a,b){a=r(a);var c,d=F[a];return d instanceof h?d:"string"==typeof d?(d=new h(d),F[a]=d,d):G[a]&&b!==t&&(c=t(G[a],t))?(d=F[a]=new h,d._set(c),d.name=H[a],d):null}function u(){var a,b=[];for(a in H)H.hasOwnProperty(a)&&(F[a]||F[G[a]])&&H[a]&&b.push(H[a]);return b.sort()}function v(a){var b,c,d,e;for("string"==typeof a&&(a=[a]),b=0;b<a.length;b++)c=a[b].split("|"),d=r(c[0]),e=r(c[1]),G[d]=e,H[d]=c[0],G[e]=d,H[e]=c[1]}function w(a){s(a.zones),v(a.links),A.dataVersion=a.version}function x(a){return x.didShowError||(x.didShowError=!0,z("moment.tz.zoneExists('"+a+"') has been deprecated in favor of !moment.tz.zone('"+a+"')")),!!t(a)}function y(a){return!(!a._a||void 0!==a._tzm)}function z(a){"undefined"!=typeof console&&"function"==typeof console.error&&console.error(a)}function A(b){var c=Array.prototype.slice.call(arguments,0,-1),d=arguments[arguments.length-1],e=t(d),f=a.utc.apply(null,c);return e&&!a.isMoment(b)&&y(f)&&f.add(e.parse(f),"minutes"),f.tz(d),f}function B(a){return function(){return this._z?this._z.abbr(this):a.call(this)}}function C(a){return function(){return this._z=null,a.apply(this,arguments)}}var D,E="0.5.13",F={},G={},H={},I={},J=a.version.split("."),K=+J[0],L=+J[1];(K<2||2===K&&L<6)&&z("Moment Timezone requires Moment.js >= 2.6.0. You are using Moment.js "+a.version+". See momentjs.com"),h.prototype={_set:function(a){this.name=a.name,this.abbrs=a.abbrs,this.untils=a.untils,this.offsets=a.offsets,this.population=a.population},_index:function(a){var b,c=+a,d=this.untils;for(b=0;b<d.length;b++)if(c<d[b])return b},parse:function(a){var b,c,d,e,f=+a,g=this.offsets,h=this.untils,i=h.length-1;for(e=0;e<i;e++)if(b=g[e],c=g[e+1],d=g[e?e-1:e],b<c&&A.moveAmbiguousForward?b=c:b>d&&A.moveInvalidForward&&(b=d),f<h[e]-6e4*b)return g[e];return g[i]},abbr:function(a){return this.abbrs[this._index(a)]},offset:function(a){return this.offsets[this._index(a)]}},j.prototype.scoreOffsetAt=function(a){this.offsetScore+=Math.abs(this.zone.offset(a.at)-a.offset),this.zone.abbr(a.at).replace(/[^A-Z]/g,"")!==a.abbr&&this.abbrScore++},A.version=E,A.dataVersion="",A._zones=F,A._links=G,A._names=H,A.add=s,A.link=v,A.load=w,A.zone=t,A.zoneExists=x,A.guess=q,A.names=u,A.Zone=h,A.unpack=g,A.unpackBase60=c,A.needsOffset=y,A.moveInvalidForward=!0,A.moveAmbiguousForward=!1;var M=a.fn;a.tz=A,a.defaultZone=null,a.updateOffset=function(b,c){var d,e=a.defaultZone;void 0===b._z&&(e&&y(b)&&!b._isUTC&&(b._d=a.utc(b._a)._d,b.utc().add(e.parse(b),"minutes")),b._z=e),b._z&&(d=b._z.offset(b),Math.abs(d)<16&&(d/=60),void 0!==b.utcOffset?b.utcOffset(-d,c):b.zone(d,c))},M.tz=function(b){return b?(this._z=t(b),this._z?a.updateOffset(this):z("Moment Timezone has no data for "+b+". See http://momentjs.com/timezone/docs/#/data-loading/."),this):this._z?this._z.name:void 0},M.zoneName=B(M.zoneName),M.zoneAbbr=B(M.zoneAbbr),M.utc=C(M.utc),a.tz.setDefault=function(b){return(K<2||2===K&&L<9)&&z("Moment Timezone setDefault() requires Moment.js >= 2.9.0. You are using Moment.js "+a.version+"."),a.defaultZone=b?t(b):null,a};var N=a.momentProperties;return"[object Array]"===Object.prototype.toString.call(N)?(N.push("_z"),N.push("_a")):N&&(N._z=null),a});
This source diff could not be displayed because it is too large. You can view the blob instead.
body {
font-family: sans-serif;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
max-width: 90%;
margin-left: 20px;
font-size: 95%;
}
a:link {
color: rgba(79, 140, 30, 1);
color: rgb(53, 51, 144);
text-decoration: none;
}
a:visited {
color: rgba(79, 140, 30, 1);
color: rgb(53, 51, 144);
text-decoration: none;
}
div.user_info {
font-size: smaller;
clear: both;
padding-top: 20px;
}
/* header */
table.app_list {
border-collapse: collapse;
div.header {
width: 100%;
height: 5em;
display: flex;
align-items: center;
border-bottom: 1px solid rgb(0, 169, 225);
}
table.app_list tr {
border-top: 1px black solid;
#logos img {
height: 4em;
}
table.app_list td {
padding-right: 1.5em;
#user_info {
padding-right: 1em;
padding-top: 0.4em;
position: absolute;
right: 0;
}
table.app_list tr.even {
background-color: #F3FEEA;
#nav {
font-size: 1.4em;
}
div.status_line {
.nav-item {
float: left;
padding-left: 1em;
}
#footer {
font-size: smaller;
}
#wrapper {
width: 800px;
.nav-item:hover a {
color: #000;
}
#navigation {
background-color: #fff;
border: #ddd 1px solid;
border-radius: 10px;
margin: 10px;
padding: 10px;
/* content */
#content {
clear: both;
}
#navigation li {
margin: 2px 0;
table.app_list {
border-collapse: collapse;
width: 95%;
}
label.error {
color: #ff0000;
margin-left: 10px;
position: relative;
table.app_list tr {
border-top: 1px #bad5e1 solid;
text-align: left;
}
.navigation {
border-top: #ddd 1px solid;
margin-top: 10px;
padding-top: 10px;
table.app_list td, table.app_list th {
padding-right: 1em;
padding-bottom: 0.5em;
padding-top: 0.5em;
}
.navigation ul {
margin: 0;
padding: 0;
list-style: none;
table.app_list tbody tr:nth-child(2n+1) {
background-color: #F3FEEA;
}
.navigation li {
float: left;
margin-right: 10px;
table.sortable th:not(.sorttable_sorted):not(.sorttable_sorted_reverse):not(.sorttable_nosort):after {
content: "\00A0\2195";
}
.clearfix:before, .clearfix:after {
content: "\0020";
display: block;
height: 0;
visibility: hidden;
table.sortable th::after, th.sorttable_sorted::after, th.sorttable_sorted_reverse::after {
content: " ";
display: inline-block;
}
.clearfix:after {
clear: both;
label.filter {
font-size: 1.4em;
font-weight: bold;
}
input {
margin-top: 5px;
}
section {
padding-bottom: 10px;
input.filter {
margin-top: 1.4em;
margin-bottom: 1.5em;
}
input.error input:invalid {
box-shadow: 0 0 4px red;
}
#loginbox {
......@@ -112,10 +110,6 @@ section {
width: 100%;
}
fieldset {
border: 0;
}
textarea.logoutput {
width: 100%;
height: 40em;
......@@ -126,10 +120,71 @@ textarea.logoutput {
overflow-y: auto;
}
#userinfo {
position: absolute;
top: 0;
right: 0;
padding-right: 1em;
padding-top: 0.4em;
.readable_description {
display: none;
}
div.zapp {
cursor: pointer;
width: 13em;
}
div.zapp p {
text-align: center;
}
div.zapp img {
width: 50%;
margin: 0 auto;
display: block;
}
div.zapp-list {
display: flex;
align-items: center;
}
div.zapp-category {
clear: both;
}
div.zapp-description {
font-size: smaller;
margin-bottom: 1em;
overflow: auto;
width: 60%;
}
div.zapp-description img {
float: left;
height: 8em;
margin: 10px 10px 0 5px;
}
h3.zapp-startup {
clear: both;
}
form#zapp_start_form input, form#zapp_start_form label {
display: block;
}
form#zapp_start_form label {
font-variant: small-caps;
}
form#zapp_start_form input {
font-family: "Lucida Console", Monaco, monospace;
}
/* footer */
div.status_line {
float: left;
}
#footer {
font-size: smaller;
clear: both;
}
function format_bytes(bytes, decimals) {
if(bytes === 0) {
document.write('0 Byte');
return;
}
var k = 1000;
var dm = decimals + 1 || 3;
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
document.write((bytes / Math.pow(k, i)).toPrecision(dm) + ' ' + sizes[i]);
}
......@@ -5,66 +5,23 @@
<meta charset="UTF-8">
<title>{% block title %}{% endblock %} - Zoe</title>
<script src="/static/jquery-2.1.4.min.js" type="application/javascript"></script>
<script src="/static/jquery-ui-1.11.4/jquery-ui.min.js" type="application/javascript"></script>
<script src="/static/jquery.wizard.js" type="application/javascript"></script>
<script src="/static/jquery.validate.min.js" type="application/javascript"></script>
<script src="/static/sorttable.js" type="application/javascript"></script>
<script src="/static/moment.min.js" type="application/javascript"></script>
<script src="/static/zoe.js" type="application/javascript"></script>
<link rel="stylesheet" href="/static/zoe.css" type="text/css">
<link rel="stylesheet" href="/static/jquery-ui-1.11.4/jquery-ui.min.css">
{% endblock %}
{% block custom_head %}
{% endblock %}
</head>
<body>
<script>
/* function update_status() {
$.getJSON("")
.done(function( data ) {
$("#num_nodes").text(data.num_nodes);
$("#num_containers").text(data.num_containers);
}).error(function( data ) {
$("#num_nodes").text("N/A");
$("#num_containers").text("N/A");
});
}
update_status();
window.setInterval(update_status, 5000);
*/
moment.locale(window.navigator.userLanguage || window.navigator.language);
function format_timestamp(ts) {
document.write(moment(ts).calendar())
}
function format_bytes(bytes, decimals) {
if(bytes == 0) {
document.write('0 Byte');
return;
}
var k = 1000;
var dm = decimals + 1 || 3;
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
document.write((bytes / Math.pow(k, i)).toPrecision(dm) + ' ' + sizes[i]);
}
</script>
{% if uid %}
<div id="userinfo">