├── dashboard-1.png ├── dashboard-2.png ├── app ├── static │ ├── img │ │ ├── favicon.ico │ │ ├── glyphicons.png │ │ └── openstack-logo-full.png │ ├── js │ │ ├── globalize.min.js │ │ ├── routes.js │ │ ├── jquery.flot.byte.js │ │ ├── signals.min.js │ │ ├── crossroads.min.js │ │ ├── openstack.dash.js │ │ ├── jquery.flot.time.js │ │ ├── jquery.flot.tooltip.js │ │ └── bootstrap.min.js │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.svg │ ├── report.html │ ├── instances.html │ ├── index.html │ └── css │ │ ├── dash.css │ │ └── bootstrap.icon-large.min.css ├── static.py ├── mongo.py ├── config │ └── application_sample.yaml ├── utils.py ├── settings.py ├── app.py ├── caching.py ├── routes.py ├── api.py └── openstack.py ├── setup ├── setup.sh └── requirements.txt ├── scripts ├── toprc └── monit_instances.sh ├── .gitignore ├── README.md └── LICENSE /dashboard-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renato-farias/openstack-instances-monitoring/HEAD/dashboard-1.png -------------------------------------------------------------------------------- /dashboard-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renato-farias/openstack-instances-monitoring/HEAD/dashboard-2.png -------------------------------------------------------------------------------- /app/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renato-farias/openstack-instances-monitoring/HEAD/app/static/img/favicon.ico -------------------------------------------------------------------------------- /app/static/img/glyphicons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renato-farias/openstack-instances-monitoring/HEAD/app/static/img/glyphicons.png -------------------------------------------------------------------------------- /app/static/js/globalize.min.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renato-farias/openstack-instances-monitoring/HEAD/app/static/js/globalize.min.js -------------------------------------------------------------------------------- /app/static/img/openstack-logo-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renato-farias/openstack-instances-monitoring/HEAD/app/static/img/openstack-logo-full.png -------------------------------------------------------------------------------- /setup/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo apt-get update 4 | sudo apt-get -y install vim virtualenvwrapper python-pip python-dev mongodb memcached curl 5 | -------------------------------------------------------------------------------- /setup/requirements.txt: -------------------------------------------------------------------------------- 1 | flask==0.10.1 2 | gunicorn==19.3.0 3 | pyaml==15.5.7 4 | pymongo==3.0.2 5 | requests==2.7.0 6 | ipython==3.1.0 7 | python-memcached==1.57 8 | -------------------------------------------------------------------------------- /app/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renato-farias/openstack-instances-monitoring/HEAD/app/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /app/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renato-farias/openstack-instances-monitoring/HEAD/app/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /app/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renato-farias/openstack-instances-monitoring/HEAD/app/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /app/static.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask import current_app 4 | 5 | def index(resource=None): 6 | return current_app.send_static_file('index.html') 7 | 8 | 9 | def instances(): 10 | return current_app.send_static_file('instances.html') 11 | 12 | 13 | def report(): 14 | return current_app.send_static_file('report.html') 15 | -------------------------------------------------------------------------------- /app/mongo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | 3 | import pymongo 4 | 5 | import settings 6 | 7 | def get_mongodb(): 8 | try: 9 | c = pymongo.MongoClient(settings.MONGODB_HOST, 10 | settings.MONGODB_PORT) 11 | return c[settings.MONGODB_BASE] 12 | except: 13 | return None 14 | 15 | def get_instance_collection(instance_id): 16 | c = get_mongodb() 17 | if c: 18 | return c['instance_%s' % instance_id] 19 | return None 20 | -------------------------------------------------------------------------------- /app/config/application_sample.yaml: -------------------------------------------------------------------------------- 1 | mongodb: 2 | host: localhost 3 | port: 27017 4 | 5 | 6 | memcache: 7 | host: 'localhost' 8 | port: 11211 9 | 10 | 11 | projects: 12 | Project1: 13 | auth_url: 'http://keystone.endopint.project1:35357' 14 | auth_user: admin 15 | auth_pass: admin 16 | auth_tentant: 'admin' 17 | all_tentants: true 18 | Project2: 19 | auth_url: 'http://keystone.endopint.project1:35357' 20 | auth_user: admin 21 | auth_pass: admin 22 | auth_tentant: 'admin' 23 | all_tentants: true 24 | -------------------------------------------------------------------------------- /app/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | 5 | from flask import make_response 6 | 7 | def myjsonify(data=None, code=200, headers=None): 8 | data = [] if not data else data 9 | r = make_response(json.dumps(data, 10 | indent=2, 11 | sort_keys=True, 12 | ensure_ascii=False, 13 | encoding='utf8') + '\n', 14 | code) 15 | r.headers['Content-Type'] = 'application/json; charset=utf-8' 16 | if headers: 17 | for k,v in headers.items(): r.headers[k] = v 18 | return r 19 | -------------------------------------------------------------------------------- /app/static/js/routes.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 3 | var url_prefix = '/dashboard'; 4 | 5 | crossroads.addRoute('/', function() { 6 | _load_all_instances(); 7 | }); 8 | crossroads.addRoute(url_prefix, function() { 9 | _load_all_instances(); 10 | }); 11 | crossroads.addRoute(url_prefix + '/instances', function() { 12 | _load_all_instances(); 13 | }); 14 | crossroads.addRoute(url_prefix + '/report', function() { 15 | _load_report(); 16 | }); 17 | crossroads.parse(document.location.pathname); 18 | }); 19 | -------------------------------------------------------------------------------- /app/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import yaml 4 | 5 | from os import getenv 6 | 7 | config = yaml.load(open('config/application.yaml')) 8 | 9 | APP_ENV = getenv('APP_ENV', 'development') 10 | APPLICATION_NAME = 'openstack_monitoring' 11 | 12 | MONGODB_HOST = config.get('mongodb', {}).get('host', 'localhost') 13 | MONGODB_PORT = config.get('mongodb', {}).get('port', 27017) 14 | MONGODB_BASE = config.get('mongodb', {}).get('base', APPLICATION_NAME) 15 | 16 | MEMCACHED_HOST = config.get('memcached', {}).get('host', 'localhost') 17 | MEMCACHED_PORT = config.get('memcached', {}).get('port', 11211) 18 | 19 | PROJECTS = config.get('projects', []) 20 | -------------------------------------------------------------------------------- /scripts/toprc: -------------------------------------------------------------------------------- 1 | RCfile for "top with windows" # shameless braggin' 2 | Id:a, Mode_altscr=0, Mode_irixps=0, Delay_time=3.000, Curwin=0 3 | Def fieldscur=AEHIOQTWKNMbcdfgjplrsuvyzX 4 | winflags=62777, sortindx=10, maxtasks=0 5 | summclr=1, msgsclr=1, headclr=3, taskclr=1 6 | Job fieldscur=ABcefgjlrstuvyzMKNHIWOPQDX 7 | winflags=62777, sortindx=0, maxtasks=0 8 | summclr=6, msgsclr=6, headclr=7, taskclr=6 9 | Mem fieldscur=ANOPQRSTUVbcdefgjlmyzWHIKX 10 | winflags=62777, sortindx=13, maxtasks=0 11 | summclr=5, msgsclr=5, headclr=4, taskclr=5 12 | Usr fieldscur=ABDECGfhijlopqrstuvyzMKNWX 13 | winflags=62777, sortindx=4, maxtasks=0 14 | summclr=3, msgsclr=3, headclr=2, taskclr=3 15 | -------------------------------------------------------------------------------- /app/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import caching 5 | import settings 6 | import memcache 7 | 8 | from flask import Flask 9 | from routes import routes 10 | from werkzeug.contrib.cache import MemcachedCache 11 | 12 | 13 | app = Flask(settings.APPLICATION_NAME, static_url_path='') 14 | app.secret_key = 'openstack_monitoring' 15 | 16 | app.register_blueprint(routes) 17 | 18 | servers = ['%s:%s' % (settings.MEMCACHED_HOST, str(settings.MEMCACHED_PORT))] 19 | memcache.SERVER_MAX_VALUE_LENGTH = 1024*1024*10 20 | 21 | cache = MemcachedCache(servers) 22 | 23 | def setup_app(app): 24 | caching.load_servers() 25 | 26 | 27 | #setup_app(app) 28 | 29 | 30 | if __name__ == '__main__': 31 | try: 32 | app.run(debug=True) 33 | except Exception, e: 34 | import sys, traceback 35 | traceback.print_exc(file=sys.stdout) 36 | print str(e) 37 | -------------------------------------------------------------------------------- /app/static/report.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | Usage Report 5 |

6 |
7 |
8 |
9 |
10 |

Current Usage

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
ProjectVMs TotalvCPUs TotalMemory Total (GB)Disk Total (GB)
25 |
26 |
27 |
28 |
29 | 32 | -------------------------------------------------------------------------------- /app/caching.py: -------------------------------------------------------------------------------- 1 | import app 2 | import settings 3 | 4 | from openstack import load_servers_list, load_users_list, load_flavors_list, load_tenants_list, load_images_list 5 | 6 | 7 | def load_users(): 8 | users = load_users_list() 9 | app.cache.set('users_list', users, timeout=0) 10 | 11 | 12 | def load_servers(): 13 | servers = load_servers_list() 14 | app.cache.set('servers_list', servers, timeout=0) 15 | 16 | 17 | def load_flavors(): 18 | flavors = load_flavors_list() 19 | app.cache.set('flavors_list', flavors, timeout=0) 20 | 21 | 22 | def load_tenants(): 23 | tenants = load_tenants_list() 24 | app.cache.set('tenants_list', tenants, timeout=0) 25 | 26 | 27 | def load_images(): 28 | images = load_images_list() 29 | app.cache.set('images_list', images, timeout=0) 30 | 31 | 32 | def renew(): 33 | load_images() 34 | load_tenants() 35 | load_flavors() 36 | load_users() 37 | load_servers() 38 | print 'Cache loaded' 39 | return 'okay' 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | 64 | application.yaml 65 | -------------------------------------------------------------------------------- /scripts/monit_instances.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export COLUMNS=512 4 | 5 | IFS=' 6 | ' 7 | 8 | MONITORING_SERVICE_IP="http://10.131.80.28:8000" 9 | 10 | instance_ps='qemu-system' 11 | 12 | if [ ! -e "${HOME}/.toprc" ]; then 13 | wget -c ${MONITORING_SERVICE_IP}/scripts/toprc -O ${HOME}/.toprc 14 | fi 15 | 16 | 17 | PSS=$(top -c -b -p $(pgrep ${instance_ps} -d ',') -n 1 |grep ${instance_ps}) 18 | 19 | hv_mem=$(free -k |sed -n '2p' |awk '{print $2}') 20 | hv_cpu=$(cat /proc/cpuinfo |grep processor |wc -l) 21 | 22 | 23 | for i in ${PSS}; do 24 | IFS=' ' 25 | read cpu mem instance <<<$(echo ${i}| awk '{match($0,"instance-[0-9a-zA-Z]{8}",a)}END{print $9, $10, a[0]}') 26 | instance_cpu=$(virsh vcpucount ${instance} |awk '{if (($1=="current") && ($2=="live")) {print $3}}') 27 | instance_mem=$(virsh dommemstat ${instance} |awk '{if ($1=="actual") {print $2}}') 28 | total_mem=$(echo "scale=4;(((${hv_mem}/100)*${mem})/${instance_mem})*100"|bc) 29 | total_cpu=$(echo "scale=4;(((${hv_cpu}/100)*${cpu})/${instance_cpu})*100"|bc) 30 | instance_id=$(virsh dominfo ${instance} |awk '{if ($1=="UUID:") {print $2}}') 31 | curl --silent -d "instance_id=${instance_id}&cpu=${total_cpu}&mem=${total_mem}" "${MONITORING_SERVICE_IP}/monitoring/post" 32 | done 33 | -------------------------------------------------------------------------------- /app/static/instances.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | Search 5 |

6 |
7 |
8 | 9 |
10 |
11 |
12 | 13 | 14 | 17 | 18 |
19 |
20 |
21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 |

29 | Instances 30 |

31 |
32 |
33 | 34 |
35 |
36 | 37 | 53 | -------------------------------------------------------------------------------- /app/routes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | 3 | from flask import Blueprint, current_app 4 | 5 | from api import _servers, \ 6 | _search, \ 7 | _users, \ 8 | _get_server, \ 9 | _flavors, \ 10 | _tenants, \ 11 | _images, \ 12 | _post_monitoring, \ 13 | _get_monitoring, \ 14 | _get_usage_report 15 | from static import index, report, instances 16 | from caching import renew 17 | 18 | routes = Blueprint('routes', __name__, static_folder='../static') 19 | 20 | routes.add_url_rule('/', view_func=index, methods=['GET']) 21 | routes.add_url_rule('/report', view_func=report, methods=['GET']) 22 | routes.add_url_rule('/instances', view_func=instances, methods=['GET']) 23 | routes.add_url_rule('/dashboard', view_func=index, methods=['GET']) 24 | routes.add_url_rule('/dashboard/', view_func=index, methods=['GET']) 25 | routes.add_url_rule('/users', view_func=_users, methods=['GET']) 26 | routes.add_url_rule('/servers', view_func=_servers, methods=['GET']) 27 | routes.add_url_rule('/flavors', view_func=_flavors, methods=['GET']) 28 | routes.add_url_rule('/tenants', view_func=_tenants, methods=['GET']) 29 | routes.add_url_rule('/images', view_func=_images, methods=['GET']) 30 | routes.add_url_rule('/server/', view_func=_get_server, methods=['GET']) 31 | routes.add_url_rule('/search/', view_func=_search, methods=['GET']) 32 | routes.add_url_rule('/renew', view_func=renew, methods=['GET']) 33 | routes.add_url_rule('/monitoring//', view_func=_get_monitoring, methods=['GET']) 34 | routes.add_url_rule('/monitoring/post', view_func=_post_monitoring, methods=['POST']) 35 | routes.add_url_rule('/report/resources', view_func=_get_usage_report, methods=['GET']) 36 | 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # openstack-instances-monitoring 2 | 3 | OpenStack Instances Monitor is a simple project to show how much each instance in your project is consuming on your hypervisors. 4 | 5 | 6 | ### Installing the tool 7 | 8 | * clone this repository 9 | ```sh 10 | $ git clone git@github.com:renato-farias/openstack-instances-monitoring.git 11 | ``` 12 | * installing all dependecies (OS) 13 | ```sh 14 | $ cd openstack-instances-monitoring 15 | $ setup/setup.sh 16 | ``` 17 | * installing all dependecies (Python Application) 18 | ```sh 19 | $ cd openstack-instances-monitoring 20 | $ mkvirtualenv monitoring 21 | $ workon monitoring 22 | $ pip install -r setup/requirements.txt 23 | ``` 24 | * change the setting and run the app 25 | ```sh 26 | $ cd openstack-instances-monitoring/app 27 | $ mv application_sample.yaml application.yaml 28 | $ gunicorn -b 0.0.0.0:8000 app:app --log-level info --env APP_ENV='production' --reload -w 4 --timeout 60 29 | ``` 30 | * point your browser to http://SERVER:8000/ 31 | * In your first access access: http://SERVER:8000/renew - To make the initial cache. 32 | * insert in the root's cron the following entry (It's responsible to collect data from openstack API avoiding to take too time to get some informations about all instances): 33 | ```sh 34 | 1 * * * * curl http://localhost:8000/renew 35 | ``` 36 | 37 | ### Installing the collector on the Hypervisors 38 | * Get the script 39 | ```sh 40 | # wget -c http://SERVER:8000/scripts/monit_instances.sh 41 | ``` 42 | * Edit the script pointing the server IP or Name to send data. 43 | * insert in the root's cron the following entry: 44 | ```sh 45 | * * * * * /root/monit_instances.sh 46 | ``` 47 | 48 | 49 | ### Customization 50 | It's a tool with a simple endpoit API to collect data. Feel free using your imagination to customize this. 51 | 52 | 53 | ### How it looks 54 | ![Dashboard Image 1](dashboard-1.png) 55 | ![Dashboard Image 2](dashboard-2.png) 56 | 57 | I'd like to say thank you to Crapworks. I used his template from [Ceph-Dash](https://github.com/Crapworks/ceph-dash) to turn easy my job =] 58 | -------------------------------------------------------------------------------- /app/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OpenStack Instances Monitor 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 18 |

Instances Monitor

19 |

20 | 21 | 22 | 23 | 24 |

25 |
26 |
27 | 33 |
34 | 35 |
36 | 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/static/css/dash.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #fff; 3 | } 4 | 5 | .well { 6 | background-color: #ffe; 7 | } 8 | 9 | h1 { 10 | color: #1c1e22; 11 | } 12 | 13 | .panel-body, .panel-instance-list { 14 | background-color: #ffc; 15 | } 16 | 17 | .panel-instance-list { 18 | padding-top: 20px; 19 | padding-left: 10px; 20 | padding-right: 10px; 21 | } 22 | 23 | .panel-instance { 24 | /*background-color: #7a8288;*/ 25 | /*background-color: #414344;*/ 26 | background-color: #fff; 27 | border-color: #999; 28 | } 29 | 30 | .panel-instance .panel-body { 31 | background-color: #fff; 32 | color: #000; 33 | font-weight: normal; 34 | } 35 | 36 | .strong { 37 | font-weight: bold; 38 | } 39 | 40 | .block { 41 | display: block; 42 | } 43 | 44 | .panel-instance .panel-title { 45 | color: #000; 46 | } 47 | 48 | .panel.panel-primary { 49 | background-color: #fff; 50 | } 51 | 52 | .panel-instance h3 { 53 | color: #1c1e22; 54 | } 55 | 56 | .icon-ok { 57 | color: #62C462; 58 | } 59 | 60 | .icon-warn { 61 | color: #F89406; 62 | } 63 | 64 | .icon-err { 65 | color: #EE5F5B; 66 | } 67 | 68 | .panel-body { 69 | font-weight: bold; 70 | } 71 | 72 | .panel-title { 73 | font-weight: bold; 74 | } 75 | 76 | .cd-collapsable h3:after { 77 | font-family: 'Glyphicons Halflings'; 78 | content: "\e113"; 79 | cursor: pointer; 80 | float: right; 81 | } 82 | 83 | .cd-collapsed h3:after { 84 | font-family: 'Glyphicons Halflings'; 85 | content: "\e114"; 86 | cursor: pointer; 87 | float: right; 88 | } 89 | 90 | .graphite { 91 | width: 500px; 92 | height: 150px; 93 | font-size: 14px; 94 | line-height: 1.2em; 95 | } 96 | 97 | .influxdb { 98 | width: 500px; 99 | height: 150px; 100 | font-size: 14px; 101 | line-height: 1.2em; 102 | } 103 | 104 | #tooltip { 105 | position: absolute; 106 | display: none; 107 | border: 1px solid #000; 108 | padding: 2px; 109 | background-color: #272b30; 110 | } 111 | 112 | .row.menu { 113 | margin-top: 10px; 114 | } 115 | 116 | span.link { 117 | color: #000; 118 | cursor: pointer; 119 | } 120 | 121 | table { 122 | background-color: #fff; 123 | } 124 | 125 | table thead, table tbody, table tr, table td { 126 | color: #000; 127 | } 128 | 129 | h4 { 130 | color: #111; 131 | } 132 | -------------------------------------------------------------------------------- /app/static/js/jquery.flot.byte.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | "use strict"; 3 | 4 | var options = {}; 5 | 6 | //Round to nearby lower multiple of base 7 | function floorInBase(n, base) { 8 | return base * Math.floor(n / base); 9 | } 10 | 11 | function init(plot) { 12 | plot.hooks.processDatapoints.push(function (plot) { 13 | $.each(plot.getAxes(), function(axisName, axis) { 14 | var opts = axis.options; 15 | if (opts.mode === "byte" || opts.mode === "byteRate") { 16 | axis.tickGenerator = function (axis) { 17 | var returnTicks = [], 18 | tickSize = 2, 19 | delta = axis.delta, 20 | steps = 0, 21 | tickMin = 0, 22 | tickVal, 23 | tickCount = 0; 24 | 25 | //Set the reference for the formatter 26 | if (opts.mode === "byteRate") { 27 | axis.rate = true; 28 | } 29 | 30 | //Enforce maximum tick Decimals 31 | if (typeof opts.tickDecimals === "number") { 32 | axis.tickDecimals = opts.tickDecimals; 33 | } else { 34 | axis.tickDecimals = 2; 35 | } 36 | 37 | //Count the steps 38 | while (Math.abs(delta) >= 1024) { 39 | steps++; 40 | delta /= 1024; 41 | } 42 | 43 | //Set the tick size relative to the remaining delta 44 | while (tickSize <= 1024) { 45 | if (delta <= tickSize) { 46 | break; 47 | } 48 | tickSize *= 2; 49 | } 50 | 51 | //Tell flot the tickSize we've calculated 52 | if (typeof opts.minTickSize !== "undefined" && tickSize < opts.minTickSize) { 53 | axis.tickSize = opts.minTickSize; 54 | } else { 55 | axis.tickSize = tickSize * Math.pow(1024,steps); 56 | } 57 | 58 | //Calculate the new ticks 59 | tickMin = floorInBase(axis.min, axis.tickSize); 60 | do { 61 | tickVal = tickMin + (tickCount++) * axis.tickSize; 62 | returnTicks.push(tickVal); 63 | } while (tickVal < axis.max); 64 | 65 | return returnTicks; 66 | }; 67 | 68 | axis.tickFormatter = function(size, axis) { 69 | var ext, steps = 0; 70 | 71 | while (Math.abs(size) >= 1024) { 72 | steps++; 73 | size /= 1024; 74 | } 75 | 76 | 77 | switch (steps) { 78 | case 0: ext = " B"; break; 79 | case 1: ext = " KiB"; break; 80 | case 2: ext = " MiB"; break; 81 | case 3: ext = " GiB"; break; 82 | case 4: ext = " TiB"; break; 83 | case 5: ext = " PiB"; break; 84 | case 6: ext = " EiB"; break; 85 | case 7: ext = " ZiB"; break; 86 | case 8: ext = " YiB"; break; 87 | } 88 | 89 | 90 | if (typeof axis.rate !== "undefined") { 91 | ext += "/s"; 92 | } 93 | 94 | return (size.toFixed(axis.tickDecimals) + ext); 95 | }; 96 | } 97 | }); 98 | }); 99 | } 100 | 101 | $.plot.plugins.push({ 102 | init: init, 103 | options: options, 104 | name: "byte", 105 | version: "0.1" 106 | }); 107 | })(jQuery); -------------------------------------------------------------------------------- /app/static/js/signals.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | JS Signals 4 | Released under the MIT license 5 | Author: Miller Medeiros 6 | Version: 1.0.0 - Build: 268 (2012/11/29 05:48 PM) 7 | */ 8 | (function(i){function h(a,b,c,d,e){this._listener=b;this._isOnce=c;this.context=d;this._signal=a;this._priority=e||0}function g(a,b){if(typeof a!=="function")throw Error("listener is a required param of {fn}() and should be a Function.".replace("{fn}",b));}function e(){this._bindings=[];this._prevParams=null;var a=this;this.dispatch=function(){e.prototype.dispatch.apply(a,arguments)}}h.prototype={active:!0,params:null,execute:function(a){var b;this.active&&this._listener&&(a=this.params?this.params.concat(a): 9 | a,b=this._listener.apply(this.context,a),this._isOnce&&this.detach());return b},detach:function(){return this.isBound()?this._signal.remove(this._listener,this.context):null},isBound:function(){return!!this._signal&&!!this._listener},isOnce:function(){return this._isOnce},getListener:function(){return this._listener},getSignal:function(){return this._signal},_destroy:function(){delete this._signal;delete this._listener;delete this.context},toString:function(){return"[SignalBinding isOnce:"+this._isOnce+ 10 | ", isBound:"+this.isBound()+", active:"+this.active+"]"}};e.prototype={VERSION:"1.0.0",memorize:!1,_shouldPropagate:!0,active:!0,_registerListener:function(a,b,c,d){var e=this._indexOfListener(a,c);if(e!==-1){if(a=this._bindings[e],a.isOnce()!==b)throw Error("You cannot add"+(b?"":"Once")+"() then add"+(!b?"":"Once")+"() the same listener without removing the relationship first.");}else a=new h(this,a,b,c,d),this._addBinding(a);this.memorize&&this._prevParams&&a.execute(this._prevParams);return a}, 11 | _addBinding:function(a){var b=this._bindings.length;do--b;while(this._bindings[b]&&a._priority<=this._bindings[b]._priority);this._bindings.splice(b+1,0,a)},_indexOfListener:function(a,b){for(var c=this._bindings.length,d;c--;)if(d=this._bindings[c],d._listener===a&&d.context===b)return c;return-1},has:function(a,b){return this._indexOfListener(a,b)!==-1},add:function(a,b,c){g(a,"add");return this._registerListener(a,!1,b,c)},addOnce:function(a,b,c){g(a,"addOnce");return this._registerListener(a, 12 | !0,b,c)},remove:function(a,b){g(a,"remove");var c=this._indexOfListener(a,b);c!==-1&&(this._bindings[c]._destroy(),this._bindings.splice(c,1));return a},removeAll:function(){for(var a=this._bindings.length;a--;)this._bindings[a]._destroy();this._bindings.length=0},getNumListeners:function(){return this._bindings.length},halt:function(){this._shouldPropagate=!1},dispatch:function(a){if(this.active){var b=Array.prototype.slice.call(arguments),c=this._bindings.length,d;if(this.memorize)this._prevParams= 13 | b;if(c){d=this._bindings.slice();this._shouldPropagate=!0;do c--;while(d[c]&&this._shouldPropagate&&d[c].execute(b)!==!1)}}},forget:function(){this._prevParams=null},dispose:function(){this.removeAll();delete this._bindings;delete this._prevParams},toString:function(){return"[Signal active:"+this.active+" numListeners:"+this.getNumListeners()+"]"}};var f=e;f.Signal=e;typeof define==="function"&&define.amd?define(function(){return f}):typeof module!=="undefined"&&module.exports?module.exports=f:i.signals= 14 | f})(this); 15 | -------------------------------------------------------------------------------- /app/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | 3 | import re 4 | import datetime 5 | 6 | from flask import request 7 | from utils import myjsonify 8 | from mongo import get_instance_collection 9 | from openstack import get_servers, \ 10 | get_users, \ 11 | get_flavors, \ 12 | get_tenants, \ 13 | get_images, \ 14 | get_server, \ 15 | get_user, \ 16 | get_flavor, \ 17 | get_tenant, \ 18 | get_image 19 | 20 | def _servers(): 21 | return myjsonify({'instances': get_servers()}) 22 | 23 | def _users(): 24 | return myjsonify({'users': get_users()}) 25 | 26 | def _flavors(): 27 | return myjsonify({'flavors': get_flavors()}) 28 | 29 | 30 | def _tenants(): 31 | return myjsonify({'flavors': get_tenants()}) 32 | 33 | 34 | def _images(): 35 | return myjsonify({'images': get_images()}) 36 | 37 | def _search(s): 38 | l = [] 39 | 40 | if len(s) >= 3: 41 | servers = get_servers() 42 | try: 43 | for i in servers: 44 | if s in i['name']: 45 | l.append(i) 46 | except Exception, e: 47 | print str(e) 48 | 49 | return myjsonify({'instances': l}) 50 | 51 | 52 | def _get_server(server_id): 53 | s = get_server(server_id) 54 | if s is not None: 55 | u = get_user(s['user_id']) 56 | s.pop('user_id') 57 | s['user'] = u 58 | 59 | f = get_flavor(s['flavor_id']) 60 | s.pop('flavor_id') 61 | s['flavor'] = f 62 | 63 | t = get_tenant(s['tenant_id']) 64 | s.pop('tenant_id') 65 | s['tenant'] = t 66 | 67 | i = get_image(s['image_id']) 68 | s.pop('image_id') 69 | s['image'] = i 70 | 71 | return myjsonify(s) 72 | 73 | 74 | def _post_monitoring(): 75 | f = request.form 76 | instance_id = f['instance_id'] 77 | cpu_usage = f['cpu'] 78 | mem_usage = f['mem'] 79 | 80 | if cpu_usage == '' or cpu_usage is None: 81 | cpu_usage = 0 82 | 83 | if mem_usage == '' or mem_usage is None: 84 | mem_usage = 0 85 | 86 | d = { 87 | 'log_date': datetime.datetime.now(), 88 | 'cpu': float(cpu_usage), 89 | 'mem': float(mem_usage) 90 | } 91 | get_instance_collection(instance_id).insert_one(d) 92 | return myjsonify({'status': 200}) 93 | 94 | 95 | def _get_monitoring(instance_id, monitorying_type): 96 | 97 | q = {} 98 | s = { 99 | '_id': 0, 100 | 'log_date': 1 101 | } 102 | 103 | if monitorying_type == 'cpu': 104 | s['cpu'] = 1 105 | elif monitorying_type == 'mem': 106 | s['mem'] = 1 107 | 108 | d = [] 109 | result = get_instance_collection(instance_id).find(q, s).sort('log_date') 110 | for r in result: 111 | if r[monitorying_type] == '' or r[monitorying_type] == None: 112 | r[monitorying_type] = 0 113 | d.append([ 114 | # new_date, 115 | int(r['log_date'].strftime('%s')) * 1000, 116 | float(r[monitorying_type]) 117 | ]) 118 | return myjsonify({'data': d}) 119 | 120 | 121 | def _get_usage_report(): 122 | # initializing the counter 123 | counter_zero = {'vms': 0, 'mem': 0, 'cpu': 0, 'disk': 0} 124 | report = {'_total': counter_zero.copy()} 125 | servers = get_servers() 126 | flavors = get_flavors() 127 | for s in servers: 128 | if s['project'] not in report.keys(): 129 | report[s['project']] = counter_zero.copy() 130 | # increasing the vm number 131 | report['_total']['vms'] += 1 132 | report[s['project']]['vms'] += 1 133 | # get flavor's information 134 | for f in flavors: 135 | if f['id'] == s['flavor_id']: 136 | # increasing the cpu number 137 | report['_total']['cpu'] += f['vcpus'] 138 | report[s['project']]['cpu'] += f['vcpus'] 139 | # increasing the mem number 140 | report['_total']['mem'] += (f['ram']/1024) 141 | report[s['project']]['mem'] += (f['ram']/1024) 142 | # increasing the mem number 143 | report['_total']['disk'] += f['disk'] 144 | report[s['project']]['disk'] += f['disk'] 145 | break 146 | 147 | report['_size'] = len(report.keys())-1 148 | 149 | return myjsonify(report) 150 | -------------------------------------------------------------------------------- /app/openstack.py: -------------------------------------------------------------------------------- 1 | import app 2 | import json 3 | import requests 4 | import settings 5 | 6 | os_token = None 7 | headers = {'content-type': 'application/json'} 8 | 9 | auth_cache = {} 10 | 11 | projects = settings.PROJECTS 12 | 13 | def auth(project_name): 14 | project = projects[project_name] 15 | auth_cache[project_name] = {} 16 | 17 | json_post = { 18 | 'auth': { 19 | 'tenantName': project['auth_tentant'], 20 | 'passwordCredentials': { 21 | 'username': project['auth_user'], 22 | 'password': project['auth_pass'] 23 | } 24 | } 25 | } 26 | 27 | # cleaning up older token 28 | if 'X-Auth-Token' in headers.keys(): 29 | headers.pop('X-Auth-Token') 30 | 31 | r = requests.post('%s/v2.0/tokens' % project['auth_url'], data=json.dumps(json_post), headers=headers) 32 | auth_cache[project_name]['token'] = r.json()['access']['token']['id'] 33 | for item in r.json()['access']['serviceCatalog']: 34 | if item['name'] == 'nova': 35 | auth_cache[project_name]['nova_admin_url'] = item['endpoints'][0]['publicURL'] 36 | if item['name'] == 'keystone': 37 | auth_cache[project_name]['keystone_admin_url'] = item['endpoints'][0]['adminURL'] 38 | 39 | 40 | def get_token(project_name): 41 | if project_name not in auth_cache.keys(): 42 | auth(project_name) 43 | return auth_cache[project_name]['token'] 44 | 45 | 46 | def load_tenants_list(): 47 | l = [] 48 | for project in projects.keys(): 49 | headers['X-Auth-Token'] = get_token(project) 50 | r = requests.get("%s/tenants" % auth_cache[project]['keystone_admin_url'], headers=headers) 51 | for t in r.json()['tenants']: 52 | l.append({ 53 | 'id': t['id'], 54 | 'name': t['name'] 55 | }) 56 | return l 57 | 58 | 59 | def load_images_list(): 60 | l = [] 61 | for project in projects.keys(): 62 | headers['X-Auth-Token'] = get_token(project) 63 | r = requests.get("%s/images/detail?all_tenants=1" % auth_cache[project]['nova_admin_url'], headers=headers) 64 | for i in r.json()['images']: 65 | l.append({ 66 | 'id': i['id'], 67 | 'name': i['name'] 68 | }) 69 | return l 70 | 71 | 72 | def load_servers_list(): 73 | l = [] 74 | for project in projects.keys(): 75 | headers['X-Auth-Token'] = get_token(project) 76 | r = requests.get("%s/servers/detail?all_tenants=1" % auth_cache[project]['nova_admin_url'], headers=headers) 77 | for s in r.json()['servers']: 78 | 79 | networks = [] 80 | for nets in s['addresses']: 81 | for net in s['addresses'][nets]: 82 | networks.append({ 83 | 'type': net['OS-EXT-IPS:type'], 84 | 'addr': net['addr'], 85 | }) 86 | 87 | l.append({ 88 | 'id': s['id'], 89 | 'name': s['name'], 90 | 'status': s['status'].lower(), 91 | 'networks': networks, 92 | 'user_id': s['user_id'], 93 | 'flavor_id': s['flavor']['id'], 94 | 'image_id': s['image']['id'], 95 | 'tenant_id': s['tenant_id'], 96 | 'hypervisor': s['OS-EXT-SRV-ATTR:host'], 97 | 'project': str(project) 98 | }) 99 | return l 100 | 101 | 102 | def load_users_list(): 103 | l = [] 104 | for project in projects.keys(): 105 | headers['X-Auth-Token'] = get_token(project) 106 | r = requests.get("%s/users" % auth_cache[project]['keystone_admin_url'], headers=headers) 107 | for u in r.json()['users']: 108 | l.append({ 109 | 'id': u['id'], 110 | 'name': u['name'], 111 | 'email': u['email'], 112 | 'username': u['username'] 113 | }) 114 | return l 115 | 116 | 117 | def load_flavors_list(): 118 | l = [] 119 | for project in projects.keys(): 120 | headers['X-Auth-Token'] = get_token(project) 121 | r = requests.get('%s/flavors/detail' % auth_cache[project]['nova_admin_url'], headers=headers) 122 | for f in r.json()['flavors']: 123 | l.append({ 124 | 'id': f['id'], 125 | 'name': f['name'], 126 | 'ram': f['ram'], 127 | 'vcpus': f['vcpus'], 128 | 'disk': f['disk'] 129 | }) 130 | return l 131 | 132 | 133 | def list_servers(): 134 | return app.cache.get('servers_list') 135 | 136 | 137 | def list_users(): 138 | return app.cache.get('users_list') 139 | 140 | 141 | def list_flavors(): 142 | return app.cache.get('flavors_list') 143 | 144 | 145 | def list_tenants(): 146 | return app.cache.get('tenants_list') 147 | 148 | 149 | def list_images(): 150 | return app.cache.get('images_list') 151 | 152 | 153 | def get_servers(): 154 | return list_servers() 155 | 156 | 157 | def get_server(server_id): 158 | for s in get_servers(): 159 | if s['id'] == server_id: 160 | return s 161 | return None 162 | 163 | 164 | def get_users(): 165 | return list_users() 166 | 167 | 168 | def get_user(user_id): 169 | for u in get_users(): 170 | if u['id'] == user_id: 171 | return u 172 | return None 173 | 174 | 175 | def get_flavors(): 176 | return list_flavors() 177 | 178 | 179 | def get_flavor(flavor_id): 180 | for f in get_flavors(): 181 | if f['id'] == flavor_id: 182 | return f 183 | return None 184 | 185 | 186 | def get_tenants(): 187 | return list_tenants() 188 | 189 | 190 | def get_tenant(tenant_id): 191 | for t in get_tenants(): 192 | if t['id'] == tenant_id: 193 | return t 194 | return None 195 | 196 | 197 | def get_images(): 198 | return list_images() 199 | 200 | 201 | def get_image(image_id): 202 | for i in get_images(): 203 | if i['id'] == image_id: 204 | return i 205 | return None 206 | -------------------------------------------------------------------------------- /app/static/js/crossroads.min.js: -------------------------------------------------------------------------------- 1 | /** @license 2 | * crossroads 3 | * Author: Miller Medeiros | MIT License 4 | * v0.12.2 (2015/07/31 18:37) 5 | */ 6 | ;(function(){var a=function(a){function e(a,b){if(a.indexOf)return a.indexOf(b);var c=a.length;while(c--)if(a[c]===b)return c;return-1}function f(a,b){var c=e(a,b);c!==-1&&a.splice(c,1)}function g(a,b){return"[object "+b+"]"===Object.prototype.toString.call(a)}function h(a){return g(a,"RegExp")}function i(a){return g(a,"Array")}function j(a){return typeof a=="function"}function k(a){var b;return a===null||a==="null"?b=null:a==="true"?b=!0:a==="false"?b=!1:a===d||a==="undefined"?b=d:a===""||isNaN(a)?b=a:b=parseFloat(a),b}function l(a){var b=a.length,c=[];while(b--)c[b]=k(a[b]);return c}function m(a,b){var c=(a||"").replace("?","").split("&"),d=/([^=]+)=(.+)/,e=-1,f={},g,h,j,l;while(h=c[++e])g=h.indexOf("="),l=h.substring(0,g),j=decodeURIComponent(h.substring(g+1)),b!==!1&&(j=k(j)),l in f?i(f[l])?f[l].push(j):f[l]=[f[l],j]:f[l]=j;return f}function n(){this.bypassed=new a.Signal,this.routed=new a.Signal,this._routes=[],this._prevRoutes=[],this._piped=[],this.resetState()}function o(b,c,d,e){var f=h(b),g=e.patternLexer;this._router=e,this._pattern=b,this._paramsIds=f?null:g.getParamIds(b),this._optionalParamsIds=f?null:g.getOptionalParamsIds(b),this._matchRegexp=f?b:g.compilePattern(b,e.ignoreCase),this.matched=new a.Signal,this.switched=new a.Signal,c&&this.matched.add(c),this._priority=d||0}var b,c,d;return c=/t(.+)?/.exec("t")[1]==="",n.prototype={greedy:!1,greedyEnabled:!0,ignoreCase:!0,ignoreState:!1,shouldTypecast:!1,normalizeFn:null,resetState:function(){this._prevRoutes.length=0,this._prevMatchedRequest=null,this._prevBypassedRequest=null},create:function(){return new n},addRoute:function(a,b,c){var d=new o(a,b,c,this);return this._sortedInsert(d),d},removeRoute:function(a){f(this._routes,a),a._destroy()},removeAllRoutes:function(){var a=this.getNumRoutes();while(a--)this._routes[a]._destroy();this._routes.length=0},parse:function(a,b){a=a||"",b=b||[];if(!this.ignoreState&&(a===this._prevMatchedRequest||a===this._prevBypassedRequest))return;var c=this._getMatchedRoutes(a),d=0,e=c.length,f;if(e){this._prevMatchedRequest=a,this._notifyPrevRoutes(c,a),this._prevRoutes=c;while(d' 26 | + '
' 27 | + '

' 28 | + ' ' 29 | + instance.name 30 | + ' (' 31 | + instance.id 32 | + ')' 33 | + '

' 34 | + '
' 35 | + '
' 36 | + '
' 37 | + '' 38 | } 39 | 40 | 41 | print_server_details = function(server) { 42 | ips = '' 43 | for (i=0; i < server.networks.length; i++) { 44 | ips += '
IP: ' + server.networks[i].addr + ' (' + server.networks[i].type + ')
' 45 | } 46 | return '
' 47 | + '
' 48 | + '
Environment: ' + server.project + '
' 49 | + '
Hypervisor Host: ' + server.hypervisor + '
' 50 | + '
Owner: ' + server.user.name + ' (' + server.user.email + ')
' 51 | + '
Tenant: ' + server.tenant.name + '
' 52 | + '
' 53 | + '
' 54 | + ips 55 | + '
' 56 | + '
' 57 | + '
Flavor Name: ' + server.flavor.name + '
' 58 | + '
vCPUs: ' + server.flavor.vcpus + '
' 59 | + '
RAM: ' + fmtBytes(server.flavor.ram*1024*1024) + '
' 60 | + '
Disk: ' + fmtBytes(server.flavor.disk*1024*1024*1024) + '
' 61 | + '
' 62 | + '
' 63 | + '
Image Name: ' + server.image.name + '
' 64 | + '
' 65 | + '
' 66 | + '
' 67 | + '
' 68 | + '
' 69 | + '
' 70 | + '
' 71 | + '
' 72 | + '
' 73 | + '
' 74 | } 75 | 76 | print_current_report = function(n, d) { 77 | line = '' 78 | + '' + n + '' 79 | + '' + d.vms + '' 80 | + '' + d.cpu + '' 81 | + '' + d.mem + '' 82 | + '' + d.disk + '' 83 | + ''; 84 | return line; 85 | } 86 | 87 | load_servers = function() { 88 | $('.panel-instance-list').empty(); 89 | $.ajax({ 90 | url: '/servers', 91 | cache: false, 92 | statusCode: { 93 | 200: function(data) { 94 | for (i = 0; i < data.instances.length; i++) { 95 | $('.panel-instance-list').append( 96 | print_server_container(data.instances[i]) 97 | ); 98 | if (i == data.instances.length-1) { 99 | turn_collapsable(); 100 | } 101 | } 102 | } 103 | } 104 | }); 105 | }; 106 | 107 | load_current_report = function() { 108 | $('.table-report > tbody').empty(); 109 | $.ajax({ 110 | url: '/report/resources', 111 | cache: false, 112 | statusCode: { 113 | 200: function(data) { 114 | $.each(data, function(index, value) { 115 | if ( index != '_total' && index != '_size') { 116 | $('.table-report > tbody').append( 117 | print_current_report(index, data[index]) 118 | ); 119 | } 120 | if (data['_size'] == $('.table-report > tbody tr').length ) { 121 | $('.table-report > tbody').append( 122 | print_current_report('Total', data['_total']) 123 | ); 124 | } 125 | }); 126 | } 127 | } 128 | }); 129 | }; 130 | 131 | search_servers = function(s) { 132 | $('.panel-instance-list').empty(); 133 | $.ajax({ 134 | url: '/search/' + s, 135 | cache: false, 136 | statusCode: { 137 | 200: function(data) { 138 | for (i = 0; i < data.instances.length; i++) { 139 | $('.panel-instance-list').append( 140 | print_server_container(data.instances[i]) 141 | ); 142 | if (i == data.instances.length-1) { 143 | turn_collapsable(); 144 | } 145 | } 146 | } 147 | } 148 | }); 149 | }; 150 | 151 | get_server = function(id) { 152 | $.ajax({ 153 | url: '/server/' + id, 154 | cache: false, 155 | statusCode: { 156 | 200: function(data) { 157 | $('.panel-instance[data-instance="' + id + '"] .panel-body').html( 158 | print_server_details(data) 159 | ); 160 | load_graph('.cpu_usage', data.id, 'cpu'); 161 | load_graph('.mem_usage', data.id, 'mem'); 162 | reloadGraph = setInterval(function() { 163 | load_graph('.cpu_usage', data.id, 'cpu'); 164 | load_graph('.mem_usage', data.id, 'mem'); 165 | }, 60000); 166 | } 167 | } 168 | }); 169 | }; 170 | 171 | turn_collapsable = function() { 172 | $('.cd-collapsable').on("click", function (e) { 173 | clearInterval(reloadGraph); 174 | $('.panel-heading.cd-collapsable').not('.cd-collapsed').parent('.panel').find('.panel-body').empty().slideUp(); 175 | if ($(this).hasClass('cd-collapsed')) { 176 | // expand the panel 177 | $(this).parent('.panel').find('.panel-body.hide'); 178 | $(this).parent('.panel').find('.panel-body').slideDown().removeClass('hide'); 179 | $(this).removeClass('cd-collapsed'); 180 | get_server($(this).parent('.panel').data('instance')); 181 | } 182 | else { 183 | // collapse the panel 184 | $(this).parent('.panel').find('.panel-body').slideUp(); 185 | $(this).addClass('cd-collapsed'); 186 | } 187 | }); 188 | } 189 | 190 | 191 | load_graph = function(target, instance_id, chart_type) { 192 | var chart_type = typeof chart_type !== 'undefined' ? chart_type : 'cpu'; 193 | var chart_title = ''; 194 | var chart_label = ''; 195 | if (chart_type == 'cpu') { 196 | chart_title = 'CPU usage'; 197 | chart_label = 'CPU'; 198 | } else if (chart_type == 'mem') { 199 | chart_title = 'Memory usage'; 200 | chart_label = 'Memory'; 201 | } 202 | $.getJSON('/monitoring/' + instance_id + '/' + chart_type, function (data) { 203 | 204 | $(target).highcharts({ 205 | chart: { 206 | zoomType: 'x', 207 | height: '300' 208 | }, 209 | title: { 210 | text: chart_title, 211 | align: 'left' 212 | }, 213 | xAxis: { 214 | type: 'datetime' 215 | }, 216 | yAxis: { 217 | title: { 218 | text: '% of usage' 219 | }, 220 | min: 0 221 | }, 222 | legend: { 223 | enabled: false 224 | }, 225 | plotOptions: { 226 | area: { 227 | fillColor: { 228 | linearGradient: { 229 | x1: 0, 230 | y1: 0, 231 | x2: 0, 232 | y2: 1 233 | }, 234 | stops: [ 235 | [0, Highcharts.getOptions().colors[0]], 236 | [1, Highcharts.Color(Highcharts.getOptions().colors[0]).setOpacity(0).get('rgba')] 237 | ] 238 | }, 239 | marker: { 240 | radius: 2 241 | }, 242 | lineWidth: 1, 243 | states: { 244 | hover: { 245 | lineWidth: 1 246 | } 247 | }, 248 | threshold: null 249 | } 250 | }, 251 | 252 | series: [{ 253 | type: 'area', 254 | name: chart_label, 255 | data: data.data 256 | }] 257 | }); 258 | }); 259 | } 260 | 261 | load_url = function(url, container) { 262 | $.ajax({ 263 | url: url, 264 | cache: false, 265 | statusCode: { 266 | 200: function(data) { 267 | $(container).empty(); 268 | $(container).html(data); 269 | } 270 | } 271 | }); 272 | }; 273 | 274 | change_url = function(url) { 275 | fullUrl = '/dashboard' 276 | if (url) { 277 | fullUrl += url; 278 | } 279 | window.history.pushState('', '', fullUrl); 280 | } 281 | 282 | _load_all_instances = function() { 283 | url = '/instances'; 284 | load_url(url, '.content'); 285 | change_url(url); 286 | } 287 | 288 | _load_report = function() { 289 | url = '/report'; 290 | load_url(url, '.content'); 291 | change_url(url); 292 | } 293 | 294 | $('.link.instances').click(function() { 295 | _load_all_instances(); 296 | }); 297 | 298 | $('.link.report').click(function() { 299 | _load_report(); 300 | }); 301 | 302 | }); 303 | -------------------------------------------------------------------------------- /app/static/js/jquery.flot.time.js: -------------------------------------------------------------------------------- 1 | /* Pretty handling of time axes. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | Set axis.mode to "time" to enable. See the section "Time series data" in 7 | API.txt for details. 8 | 9 | */ 10 | 11 | (function($) { 12 | 13 | var options = { 14 | xaxis: { 15 | timezone: null, // "browser" for local to the client or timezone for timezone-js 16 | timeformat: null, // format string to use 17 | twelveHourClock: false, // 12 or 24 time in time mode 18 | monthNames: null // list of names of months 19 | } 20 | }; 21 | 22 | // round to nearby lower multiple of base 23 | 24 | function floorInBase(n, base) { 25 | return base * Math.floor(n / base); 26 | } 27 | 28 | // Returns a string with the date d formatted according to fmt. 29 | // A subset of the Open Group's strftime format is supported. 30 | 31 | function formatDate(d, fmt, monthNames, dayNames) { 32 | 33 | if (typeof d.strftime == "function") { 34 | return d.strftime(fmt); 35 | } 36 | 37 | var leftPad = function(n, pad) { 38 | n = "" + n; 39 | pad = "" + (pad == null ? "0" : pad); 40 | return n.length == 1 ? pad + n : n; 41 | }; 42 | 43 | var r = []; 44 | var escape = false; 45 | var hours = d.getHours(); 46 | var isAM = hours < 12; 47 | 48 | if (monthNames == null) { 49 | monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 50 | } 51 | 52 | if (dayNames == null) { 53 | dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; 54 | } 55 | 56 | var hours12; 57 | 58 | if (hours > 12) { 59 | hours12 = hours - 12; 60 | } else if (hours == 0) { 61 | hours12 = 12; 62 | } else { 63 | hours12 = hours; 64 | } 65 | 66 | for (var i = 0; i < fmt.length; ++i) { 67 | 68 | var c = fmt.charAt(i); 69 | 70 | if (escape) { 71 | switch (c) { 72 | case 'a': c = "" + dayNames[d.getDay()]; break; 73 | case 'b': c = "" + monthNames[d.getMonth()]; break; 74 | case 'd': c = leftPad(d.getDate()); break; 75 | case 'e': c = leftPad(d.getDate(), " "); break; 76 | case 'h': // For back-compat with 0.7; remove in 1.0 77 | case 'H': c = leftPad(hours); break; 78 | case 'I': c = leftPad(hours12); break; 79 | case 'l': c = leftPad(hours12, " "); break; 80 | case 'm': c = leftPad(d.getMonth() + 1); break; 81 | case 'M': c = leftPad(d.getMinutes()); break; 82 | // quarters not in Open Group's strftime specification 83 | case 'q': 84 | c = "" + (Math.floor(d.getMonth() / 3) + 1); break; 85 | case 'S': c = leftPad(d.getSeconds()); break; 86 | case 'y': c = leftPad(d.getFullYear() % 100); break; 87 | case 'Y': c = "" + d.getFullYear(); break; 88 | case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; 89 | case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; 90 | case 'w': c = "" + d.getDay(); break; 91 | } 92 | r.push(c); 93 | escape = false; 94 | } else { 95 | if (c == "%") { 96 | escape = true; 97 | } else { 98 | r.push(c); 99 | } 100 | } 101 | } 102 | 103 | return r.join(""); 104 | } 105 | 106 | // To have a consistent view of time-based data independent of which time 107 | // zone the client happens to be in we need a date-like object independent 108 | // of time zones. This is done through a wrapper that only calls the UTC 109 | // versions of the accessor methods. 110 | 111 | function makeUtcWrapper(d) { 112 | 113 | function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { 114 | sourceObj[sourceMethod] = function() { 115 | return targetObj[targetMethod].apply(targetObj, arguments); 116 | }; 117 | }; 118 | 119 | var utc = { 120 | date: d 121 | }; 122 | 123 | // support strftime, if found 124 | 125 | if (d.strftime != undefined) { 126 | addProxyMethod(utc, "strftime", d, "strftime"); 127 | } 128 | 129 | addProxyMethod(utc, "getTime", d, "getTime"); 130 | addProxyMethod(utc, "setTime", d, "setTime"); 131 | 132 | var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; 133 | 134 | for (var p = 0; p < props.length; p++) { 135 | addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); 136 | addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); 137 | } 138 | 139 | return utc; 140 | }; 141 | 142 | // select time zone strategy. This returns a date-like object tied to the 143 | // desired timezone 144 | 145 | function dateGenerator(ts, opts) { 146 | if (opts.timezone == "browser") { 147 | return new Date(ts); 148 | } else if (!opts.timezone || opts.timezone == "utc") { 149 | return makeUtcWrapper(new Date(ts)); 150 | } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { 151 | var d = new timezoneJS.Date(); 152 | // timezone-js is fickle, so be sure to set the time zone before 153 | // setting the time. 154 | d.setTimezone(opts.timezone); 155 | d.setTime(ts); 156 | return d; 157 | } else { 158 | return makeUtcWrapper(new Date(ts)); 159 | } 160 | } 161 | 162 | // map of app. size of time units in milliseconds 163 | 164 | var timeUnitSize = { 165 | "second": 1000, 166 | "minute": 60 * 1000, 167 | "hour": 60 * 60 * 1000, 168 | "day": 24 * 60 * 60 * 1000, 169 | "month": 30 * 24 * 60 * 60 * 1000, 170 | "quarter": 3 * 30 * 24 * 60 * 60 * 1000, 171 | "year": 365.2425 * 24 * 60 * 60 * 1000 172 | }; 173 | 174 | // the allowed tick sizes, after 1 year we use 175 | // an integer algorithm 176 | 177 | var baseSpec = [ 178 | [1, "second"], [2, "second"], [5, "second"], [10, "second"], 179 | [30, "second"], 180 | [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], 181 | [30, "minute"], 182 | [1, "hour"], [2, "hour"], [4, "hour"], 183 | [8, "hour"], [12, "hour"], 184 | [1, "day"], [2, "day"], [3, "day"], 185 | [0.25, "month"], [0.5, "month"], [1, "month"], 186 | [2, "month"] 187 | ]; 188 | 189 | // we don't know which variant(s) we'll need yet, but generating both is 190 | // cheap 191 | 192 | var specMonths = baseSpec.concat([[3, "month"], [6, "month"], 193 | [1, "year"]]); 194 | var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], 195 | [1, "year"]]); 196 | 197 | function init(plot) { 198 | plot.hooks.processOptions.push(function (plot, options) { 199 | $.each(plot.getAxes(), function(axisName, axis) { 200 | 201 | var opts = axis.options; 202 | 203 | if (opts.mode == "time") { 204 | axis.tickGenerator = function(axis) { 205 | 206 | var ticks = []; 207 | var d = dateGenerator(axis.min, opts); 208 | var minSize = 0; 209 | 210 | // make quarter use a possibility if quarters are 211 | // mentioned in either of these options 212 | 213 | var spec = (opts.tickSize && opts.tickSize[1] === 214 | "quarter") || 215 | (opts.minTickSize && opts.minTickSize[1] === 216 | "quarter") ? specQuarters : specMonths; 217 | 218 | if (opts.minTickSize != null) { 219 | if (typeof opts.tickSize == "number") { 220 | minSize = opts.tickSize; 221 | } else { 222 | minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; 223 | } 224 | } 225 | 226 | for (var i = 0; i < spec.length - 1; ++i) { 227 | if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] 228 | + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 229 | && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { 230 | break; 231 | } 232 | } 233 | 234 | var size = spec[i][0]; 235 | var unit = spec[i][1]; 236 | 237 | // special-case the possibility of several years 238 | 239 | if (unit == "year") { 240 | 241 | // if given a minTickSize in years, just use it, 242 | // ensuring that it's an integer 243 | 244 | if (opts.minTickSize != null && opts.minTickSize[1] == "year") { 245 | size = Math.floor(opts.minTickSize[0]); 246 | } else { 247 | 248 | var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); 249 | var norm = (axis.delta / timeUnitSize.year) / magn; 250 | 251 | if (norm < 1.5) { 252 | size = 1; 253 | } else if (norm < 3) { 254 | size = 2; 255 | } else if (norm < 7.5) { 256 | size = 5; 257 | } else { 258 | size = 10; 259 | } 260 | 261 | size *= magn; 262 | } 263 | 264 | // minimum size for years is 1 265 | 266 | if (size < 1) { 267 | size = 1; 268 | } 269 | } 270 | 271 | axis.tickSize = opts.tickSize || [size, unit]; 272 | var tickSize = axis.tickSize[0]; 273 | unit = axis.tickSize[1]; 274 | 275 | var step = tickSize * timeUnitSize[unit]; 276 | 277 | if (unit == "second") { 278 | d.setSeconds(floorInBase(d.getSeconds(), tickSize)); 279 | } else if (unit == "minute") { 280 | d.setMinutes(floorInBase(d.getMinutes(), tickSize)); 281 | } else if (unit == "hour") { 282 | d.setHours(floorInBase(d.getHours(), tickSize)); 283 | } else if (unit == "month") { 284 | d.setMonth(floorInBase(d.getMonth(), tickSize)); 285 | } else if (unit == "quarter") { 286 | d.setMonth(3 * floorInBase(d.getMonth() / 3, 287 | tickSize)); 288 | } else if (unit == "year") { 289 | d.setFullYear(floorInBase(d.getFullYear(), tickSize)); 290 | } 291 | 292 | // reset smaller components 293 | 294 | d.setMilliseconds(0); 295 | 296 | if (step >= timeUnitSize.minute) { 297 | d.setSeconds(0); 298 | } 299 | if (step >= timeUnitSize.hour) { 300 | d.setMinutes(0); 301 | } 302 | if (step >= timeUnitSize.day) { 303 | d.setHours(0); 304 | } 305 | if (step >= timeUnitSize.day * 4) { 306 | d.setDate(1); 307 | } 308 | if (step >= timeUnitSize.month * 2) { 309 | d.setMonth(floorInBase(d.getMonth(), 3)); 310 | } 311 | if (step >= timeUnitSize.quarter * 2) { 312 | d.setMonth(floorInBase(d.getMonth(), 6)); 313 | } 314 | if (step >= timeUnitSize.year) { 315 | d.setMonth(0); 316 | } 317 | 318 | var carry = 0; 319 | var v = Number.NaN; 320 | var prev; 321 | 322 | do { 323 | 324 | prev = v; 325 | v = d.getTime(); 326 | ticks.push(v); 327 | 328 | if (unit == "month" || unit == "quarter") { 329 | if (tickSize < 1) { 330 | 331 | // a bit complicated - we'll divide the 332 | // month/quarter up but we need to take 333 | // care of fractions so we don't end up in 334 | // the middle of a day 335 | 336 | d.setDate(1); 337 | var start = d.getTime(); 338 | d.setMonth(d.getMonth() + 339 | (unit == "quarter" ? 3 : 1)); 340 | var end = d.getTime(); 341 | d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); 342 | carry = d.getHours(); 343 | d.setHours(0); 344 | } else { 345 | d.setMonth(d.getMonth() + 346 | tickSize * (unit == "quarter" ? 3 : 1)); 347 | } 348 | } else if (unit == "year") { 349 | d.setFullYear(d.getFullYear() + tickSize); 350 | } else { 351 | d.setTime(v + step); 352 | } 353 | } while (v < axis.max && v != prev); 354 | 355 | return ticks; 356 | }; 357 | 358 | axis.tickFormatter = function (v, axis) { 359 | 360 | var d = dateGenerator(v, axis.options); 361 | 362 | // first check global format 363 | 364 | if (opts.timeformat != null) { 365 | return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); 366 | } 367 | 368 | // possibly use quarters if quarters are mentioned in 369 | // any of these places 370 | 371 | var useQuarters = (axis.options.tickSize && 372 | axis.options.tickSize[1] == "quarter") || 373 | (axis.options.minTickSize && 374 | axis.options.minTickSize[1] == "quarter"); 375 | 376 | var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; 377 | var span = axis.max - axis.min; 378 | var suffix = (opts.twelveHourClock) ? " %p" : ""; 379 | var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; 380 | var fmt; 381 | 382 | if (t < timeUnitSize.minute) { 383 | fmt = hourCode + ":%M:%S" + suffix; 384 | } else if (t < timeUnitSize.day) { 385 | if (span < 2 * timeUnitSize.day) { 386 | fmt = hourCode + ":%M" + suffix; 387 | } else { 388 | fmt = "%b %d " + hourCode + ":%M" + suffix; 389 | } 390 | } else if (t < timeUnitSize.month) { 391 | fmt = "%b %d"; 392 | } else if ((useQuarters && t < timeUnitSize.quarter) || 393 | (!useQuarters && t < timeUnitSize.year)) { 394 | if (span < timeUnitSize.year) { 395 | fmt = "%b"; 396 | } else { 397 | fmt = "%b %Y"; 398 | } 399 | } else if (useQuarters && t < timeUnitSize.year) { 400 | if (span < timeUnitSize.year) { 401 | fmt = "Q%q"; 402 | } else { 403 | fmt = "Q%q %Y"; 404 | } 405 | } else { 406 | fmt = "%Y"; 407 | } 408 | 409 | var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); 410 | 411 | return rt; 412 | }; 413 | } 414 | }); 415 | }); 416 | } 417 | 418 | $.plot.plugins.push({ 419 | init: init, 420 | options: options, 421 | name: 'time', 422 | version: '1.0' 423 | }); 424 | 425 | // Time-axis support used to be in Flot core, which exposed the 426 | // formatDate function on the plot object. Various plugins depend 427 | // on the function, so we need to re-expose it here. 428 | 429 | $.plot.formatDate = formatDate; 430 | $.plot.dateGenerator = dateGenerator; 431 | 432 | })(jQuery); 433 | -------------------------------------------------------------------------------- /app/static/js/jquery.flot.tooltip.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jquery.flot.tooltip 3 | * 4 | * description: easy-to-use tooltips for Flot charts 5 | * version: 0.8.4 6 | * authors: Krzysztof Urbas @krzysu [myviews.pl],Evan Steinkerchner @Roundaround 7 | * website: https://github.com/krzysu/flot.tooltip 8 | * 9 | * build on 2014-08-06 10 | * released under MIT License, 2012 11 | */ 12 | (function ($) { 13 | // plugin options, default values 14 | var defaultOptions = { 15 | tooltip: false, 16 | tooltipOpts: { 17 | id: "flotTip", 18 | content: "%s | X: %x | Y: %y", 19 | // allowed templates are: 20 | // %s -> series label, 21 | // %lx -> x axis label (requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels), 22 | // %ly -> y axis label (requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels), 23 | // %x -> X value, 24 | // %y -> Y value, 25 | // %x.2 -> precision of X value, 26 | // %p -> percent 27 | xDateFormat: null, 28 | yDateFormat: null, 29 | monthNames: null, 30 | dayNames: null, 31 | shifts: { 32 | x: 10, 33 | y: 20 34 | }, 35 | defaultTheme: true, 36 | lines: false, 37 | 38 | // callbacks 39 | onHover: function (flotItem, $tooltipEl) {}, 40 | 41 | $compat: false 42 | } 43 | }; 44 | 45 | // object 46 | var FlotTooltip = function (plot) { 47 | // variables 48 | this.tipPosition = {x: 0, y: 0}; 49 | 50 | this.init(plot); 51 | }; 52 | 53 | // main plugin function 54 | FlotTooltip.prototype.init = function (plot) { 55 | var that = this; 56 | 57 | // detect other flot plugins 58 | var plotPluginsLength = $.plot.plugins.length; 59 | this.plotPlugins = []; 60 | 61 | if (plotPluginsLength) { 62 | for (var p = 0; p < plotPluginsLength; p++) { 63 | this.plotPlugins.push($.plot.plugins[p].name); 64 | } 65 | } 66 | 67 | plot.hooks.bindEvents.push(function (plot, eventHolder) { 68 | 69 | // get plot options 70 | that.plotOptions = plot.getOptions(); 71 | 72 | // if not enabled return 73 | if (that.plotOptions.tooltip === false || typeof that.plotOptions.tooltip === 'undefined') return; 74 | 75 | // shortcut to access tooltip options 76 | that.tooltipOptions = that.plotOptions.tooltipOpts; 77 | 78 | if (that.tooltipOptions.$compat) { 79 | that.wfunc = 'width'; 80 | that.hfunc = 'height'; 81 | } else { 82 | that.wfunc = 'innerWidth'; 83 | that.hfunc = 'innerHeight'; 84 | } 85 | 86 | // create tooltip DOM element 87 | var $tip = that.getDomElement(); 88 | 89 | // bind event 90 | $( plot.getPlaceholder() ).bind("plothover", plothover); 91 | 92 | $(eventHolder).bind('mousemove', mouseMove); 93 | }); 94 | 95 | plot.hooks.shutdown.push(function (plot, eventHolder){ 96 | $(plot.getPlaceholder()).unbind("plothover", plothover); 97 | $(eventHolder).unbind("mousemove", mouseMove); 98 | }); 99 | 100 | function mouseMove(e){ 101 | var pos = {}; 102 | pos.x = e.pageX; 103 | pos.y = e.pageY; 104 | plot.setTooltipPosition(pos); 105 | } 106 | 107 | function plothover(event, pos, item) { 108 | // Simple distance formula. 109 | var lineDistance = function (p1x, p1y, p2x, p2y) { 110 | return Math.sqrt((p2x - p1x) * (p2x - p1x) + (p2y - p1y) * (p2y - p1y)); 111 | }; 112 | 113 | // Here is some voodoo magic for determining the distance to a line form a given point {x, y}. 114 | var dotLineLength = function (x, y, x0, y0, x1, y1, o) { 115 | if (o && !(o = 116 | function (x, y, x0, y0, x1, y1) { 117 | if (typeof x0 !== 'undefined') return { x: x0, y: y }; 118 | else if (typeof y0 !== 'undefined') return { x: x, y: y0 }; 119 | 120 | var left, 121 | tg = -1 / ((y1 - y0) / (x1 - x0)); 122 | 123 | return { 124 | x: left = (x1 * (x * tg - y + y0) + x0 * (x * -tg + y - y1)) / (tg * (x1 - x0) + y0 - y1), 125 | y: tg * left - tg * x + y 126 | }; 127 | } (x, y, x0, y0, x1, y1), 128 | o.x >= Math.min(x0, x1) && o.x <= Math.max(x0, x1) && o.y >= Math.min(y0, y1) && o.y <= Math.max(y0, y1)) 129 | ) { 130 | var l1 = lineDistance(x, y, x0, y0), l2 = lineDistance(x, y, x1, y1); 131 | return l1 > l2 ? l2 : l1; 132 | } else { 133 | var a = y0 - y1, b = x1 - x0, c = x0 * y1 - y0 * x1; 134 | return Math.abs(a * x + b * y + c) / Math.sqrt(a * a + b * b); 135 | } 136 | }; 137 | 138 | if (item) { 139 | plot.showTooltip(item, pos); 140 | } else if (that.plotOptions.series.lines.show && that.tooltipOptions.lines === true) { 141 | var maxDistance = that.plotOptions.grid.mouseActiveRadius; 142 | 143 | var closestTrace = { 144 | distance: maxDistance + 1 145 | }; 146 | 147 | $.each(plot.getData(), function (i, series) { 148 | var xBeforeIndex = 0, 149 | xAfterIndex = -1; 150 | 151 | // Our search here assumes our data is sorted via the x-axis. 152 | // TODO: Improve efficiency somehow - search smaller sets of data. 153 | for (var j = 1; j < series.data.length; j++) { 154 | if (series.data[j - 1][0] <= pos.x && series.data[j][0] >= pos.x) { 155 | xBeforeIndex = j - 1; 156 | xAfterIndex = j; 157 | } 158 | } 159 | 160 | if (xAfterIndex === -1) { 161 | plot.hideTooltip(); 162 | return; 163 | } 164 | 165 | var pointPrev = { x: series.data[xBeforeIndex][0], y: series.data[xBeforeIndex][1] }, 166 | pointNext = { x: series.data[xAfterIndex][0], y: series.data[xAfterIndex][1] }; 167 | 168 | var distToLine = dotLineLength(series.xaxis.p2c(pos.x), series.yaxis.p2c(pos.y), series.xaxis.p2c(pointPrev.x), 169 | series.yaxis.p2c(pointPrev.y), series.xaxis.p2c(pointNext.x), series.yaxis.p2c(pointNext.y), false); 170 | 171 | if (distToLine < closestTrace.distance) { 172 | 173 | var closestIndex = lineDistance(pointPrev.x, pointPrev.y, pos.x, pos.y) < 174 | lineDistance(pos.x, pos.y, pointNext.x, pointNext.y) ? xBeforeIndex : xAfterIndex; 175 | 176 | var pointSize = series.datapoints.pointsize; 177 | 178 | // Calculate the point on the line vertically closest to our cursor. 179 | var pointOnLine = [ 180 | pos.x, 181 | pointPrev.y + ((pointNext.y - pointPrev.y) * ((pos.x - pointPrev.x) / (pointNext.x - pointPrev.x))) 182 | ]; 183 | 184 | var item = { 185 | datapoint: pointOnLine, 186 | dataIndex: closestIndex, 187 | series: series, 188 | seriesIndex: i 189 | }; 190 | 191 | closestTrace = { 192 | distance: distToLine, 193 | item: item 194 | }; 195 | } 196 | }); 197 | 198 | if (closestTrace.distance < maxDistance + 1) 199 | plot.showTooltip(closestTrace.item, pos); 200 | else 201 | plot.hideTooltip(); 202 | } else { 203 | plot.hideTooltip(); 204 | } 205 | } 206 | 207 | // Quick little function for setting the tooltip position. 208 | plot.setTooltipPosition = function (pos) { 209 | var $tip = that.getDomElement(); 210 | 211 | var totalTipWidth = $tip.outerWidth() + that.tooltipOptions.shifts.x; 212 | var totalTipHeight = $tip.outerHeight() + that.tooltipOptions.shifts.y; 213 | if ((pos.x - $(window).scrollLeft()) > ($(window)[that.wfunc]() - totalTipWidth)) { 214 | pos.x -= totalTipWidth; 215 | } 216 | if ((pos.y - $(window).scrollTop()) > ($(window)[that.hfunc]() - totalTipHeight)) { 217 | pos.y -= totalTipHeight; 218 | } 219 | that.tipPosition.x = pos.x; 220 | that.tipPosition.y = pos.y; 221 | }; 222 | 223 | // Quick little function for showing the tooltip. 224 | plot.showTooltip = function (target, position) { 225 | var $tip = that.getDomElement(); 226 | 227 | // convert tooltip content template to real tipText 228 | var tipText = that.stringFormat(that.tooltipOptions.content, target); 229 | 230 | $tip.html(tipText); 231 | plot.setTooltipPosition({ x: position.pageX, y: position.pageY }); 232 | $tip.css({ 233 | left: that.tipPosition.x + that.tooltipOptions.shifts.x, 234 | top: that.tipPosition.y + that.tooltipOptions.shifts.y 235 | }).show(); 236 | 237 | // run callback 238 | if (typeof that.tooltipOptions.onHover === 'function') { 239 | that.tooltipOptions.onHover(target, $tip); 240 | } 241 | }; 242 | 243 | // Quick little function for hiding the tooltip. 244 | plot.hideTooltip = function () { 245 | that.getDomElement().hide().html(''); 246 | }; 247 | }; 248 | 249 | /** 250 | * get or create tooltip DOM element 251 | * @return jQuery object 252 | */ 253 | FlotTooltip.prototype.getDomElement = function () { 254 | var $tip = $('#' + this.tooltipOptions.id); 255 | 256 | if( $tip.length === 0 ){ 257 | $tip = $('
').attr('id', this.tooltipOptions.id); 258 | $tip.appendTo('body').hide().css({position: 'absolute'}); 259 | 260 | if(this.tooltipOptions.defaultTheme) { 261 | $tip.css({ 262 | 'background': '#fff', 263 | 'z-index': '1040', 264 | 'padding': '0.4em 0.6em', 265 | 'border-radius': '0.5em', 266 | 'font-size': '0.8em', 267 | 'border': '1px solid #111', 268 | 'display': 'none', 269 | 'white-space': 'nowrap' 270 | }); 271 | } 272 | } 273 | 274 | return $tip; 275 | }; 276 | 277 | /** 278 | * core function, create tooltip content 279 | * @param {string} content - template with tooltip content 280 | * @param {object} item - Flot item 281 | * @return {string} real tooltip content for current item 282 | */ 283 | FlotTooltip.prototype.stringFormat = function (content, item) { 284 | 285 | var percentPattern = /%p\.{0,1}(\d{0,})/; 286 | var seriesPattern = /%s/; 287 | var xLabelPattern = /%lx/; // requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels, will be ignored if plugin isn't loaded 288 | var yLabelPattern = /%ly/; // requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels, will be ignored if plugin isn't loaded 289 | var xPattern = /%x\.{0,1}(\d{0,})/; 290 | var yPattern = /%y\.{0,1}(\d{0,})/; 291 | var xPatternWithoutPrecision = "%x"; 292 | var yPatternWithoutPrecision = "%y"; 293 | var customTextPattern = "%ct"; 294 | 295 | var x, y, customText, p; 296 | 297 | // for threshold plugin we need to read data from different place 298 | if (typeof item.series.threshold !== "undefined") { 299 | x = item.datapoint[0]; 300 | y = item.datapoint[1]; 301 | customText = item.datapoint[2]; 302 | } else if (typeof item.series.lines !== "undefined" && item.series.lines.steps) { 303 | x = item.series.datapoints.points[item.dataIndex * 2]; 304 | y = item.series.datapoints.points[item.dataIndex * 2 + 1]; 305 | // TODO: where to find custom text in this variant? 306 | customText = ""; 307 | } else { 308 | x = item.series.data[item.dataIndex][0]; 309 | y = item.series.data[item.dataIndex][1]; 310 | customText = item.series.data[item.dataIndex][2]; 311 | } 312 | 313 | // I think this is only in case of threshold plugin 314 | if (item.series.label === null && item.series.originSeries) { 315 | item.series.label = item.series.originSeries.label; 316 | } 317 | 318 | // if it is a function callback get the content string 319 | if (typeof(content) === 'function') { 320 | content = content(item.series.label, x, y, item); 321 | } 322 | 323 | // percent match for pie charts and stacked percent 324 | if (typeof (item.series.percent) !== 'undefined') { 325 | p = item.series.percent; 326 | } else if (typeof (item.series.percents) !== 'undefined') { 327 | p = item.series.percents[item.dataIndex]; 328 | } 329 | if (typeof p === 'number') { 330 | content = this.adjustValPrecision(percentPattern, content, p); 331 | } 332 | 333 | // series match 334 | if (typeof(item.series.label) !== 'undefined') { 335 | content = content.replace(seriesPattern, item.series.label); 336 | } else { 337 | //remove %s if label is undefined 338 | content = content.replace(seriesPattern, ""); 339 | } 340 | 341 | // x axis label match 342 | if (this.hasAxisLabel('xaxis', item)) { 343 | content = content.replace(xLabelPattern, item.series.xaxis.options.axisLabel); 344 | } else { 345 | //remove %lx if axis label is undefined or axislabels plugin not present 346 | content = content.replace(xLabelPattern, ""); 347 | } 348 | 349 | // y axis label match 350 | if (this.hasAxisLabel('yaxis', item)) { 351 | content = content.replace(yLabelPattern, item.series.yaxis.options.axisLabel); 352 | } else { 353 | //remove %ly if axis label is undefined or axislabels plugin not present 354 | content = content.replace(yLabelPattern, ""); 355 | } 356 | 357 | // time mode axes with custom dateFormat 358 | if (this.isTimeMode('xaxis', item) && this.isXDateFormat(item)) { 359 | content = content.replace(xPattern, this.timestampToDate(x, this.tooltipOptions.xDateFormat, item.series.xaxis.options)); 360 | } 361 | if (this.isTimeMode('yaxis', item) && this.isYDateFormat(item)) { 362 | content = content.replace(yPattern, this.timestampToDate(y, this.tooltipOptions.yDateFormat, item.series.yaxis.options)); 363 | } 364 | 365 | // set precision if defined 366 | if (typeof x === 'number') { 367 | content = this.adjustValPrecision(xPattern, content, x); 368 | } 369 | if (typeof y === 'number') { 370 | content = this.adjustValPrecision(yPattern, content, y); 371 | } 372 | 373 | // change x from number to given label, if given 374 | if (typeof item.series.xaxis.ticks !== 'undefined') { 375 | 376 | var ticks; 377 | if (this.hasRotatedXAxisTicks(item)) { 378 | // xaxis.ticks will be an empty array if tickRotor is being used, but the values are available in rotatedTicks 379 | ticks = 'rotatedTicks'; 380 | } else { 381 | ticks = 'ticks'; 382 | } 383 | 384 | // see https://github.com/krzysu/flot.tooltip/issues/65 385 | var tickIndex = item.dataIndex + item.seriesIndex; 386 | 387 | if (item.series.xaxis[ticks].length > tickIndex && !this.isTimeMode('xaxis', item)) { 388 | var valueX = (this.isCategoriesMode('xaxis', item)) ? item.series.xaxis[ticks][tickIndex].label : item.series.xaxis[ticks][tickIndex].v; 389 | if (valueX === x) { 390 | content = content.replace(xPattern, item.series.xaxis[ticks][tickIndex].label); 391 | } 392 | } 393 | } 394 | 395 | // change y from number to given label, if given 396 | if (typeof item.series.yaxis.ticks !== 'undefined') { 397 | for (var index in item.series.yaxis.ticks) { 398 | if (item.series.yaxis.ticks.hasOwnProperty(index)) { 399 | var valueY = (this.isCategoriesMode('yaxis', item)) ? item.series.yaxis.ticks[index].label : item.series.yaxis.ticks[index].v; 400 | if (valueY === y) { 401 | content = content.replace(yPattern, item.series.yaxis.ticks[index].label); 402 | } 403 | } 404 | } 405 | } 406 | 407 | // if no value customization, use tickFormatter by default 408 | if (typeof item.series.xaxis.tickFormatter !== 'undefined') { 409 | //escape dollar 410 | content = content.replace(xPatternWithoutPrecision, item.series.xaxis.tickFormatter(x, item.series.xaxis).replace(/\$/g, '$$')); 411 | } 412 | if (typeof item.series.yaxis.tickFormatter !== 'undefined') { 413 | //escape dollar 414 | content = content.replace(yPatternWithoutPrecision, item.series.yaxis.tickFormatter(y, item.series.yaxis).replace(/\$/g, '$$')); 415 | } 416 | 417 | if (customText) 418 | content = content.replace(customTextPattern, customText); 419 | 420 | return content; 421 | }; 422 | 423 | // helpers just for readability 424 | FlotTooltip.prototype.isTimeMode = function (axisName, item) { 425 | return (typeof item.series[axisName].options.mode !== 'undefined' && item.series[axisName].options.mode === 'time'); 426 | }; 427 | 428 | FlotTooltip.prototype.isXDateFormat = function (item) { 429 | return (typeof this.tooltipOptions.xDateFormat !== 'undefined' && this.tooltipOptions.xDateFormat !== null); 430 | }; 431 | 432 | FlotTooltip.prototype.isYDateFormat = function (item) { 433 | return (typeof this.tooltipOptions.yDateFormat !== 'undefined' && this.tooltipOptions.yDateFormat !== null); 434 | }; 435 | 436 | FlotTooltip.prototype.isCategoriesMode = function (axisName, item) { 437 | return (typeof item.series[axisName].options.mode !== 'undefined' && item.series[axisName].options.mode === 'categories'); 438 | }; 439 | 440 | // 441 | FlotTooltip.prototype.timestampToDate = function (tmst, dateFormat, options) { 442 | var theDate = $.plot.dateGenerator(tmst, options); 443 | return $.plot.formatDate(theDate, dateFormat, this.tooltipOptions.monthNames, this.tooltipOptions.dayNames); 444 | }; 445 | 446 | // 447 | FlotTooltip.prototype.adjustValPrecision = function (pattern, content, value) { 448 | 449 | var precision; 450 | var matchResult = content.match(pattern); 451 | if( matchResult !== null ) { 452 | if(RegExp.$1 !== '') { 453 | precision = RegExp.$1; 454 | value = value.toFixed(precision); 455 | 456 | // only replace content if precision exists, in other case use thickformater 457 | content = content.replace(pattern, value); 458 | } 459 | } 460 | return content; 461 | }; 462 | 463 | // other plugins detection below 464 | 465 | // check if flot-axislabels plugin (https://github.com/markrcote/flot-axislabels) is used and that an axis label is given 466 | FlotTooltip.prototype.hasAxisLabel = function (axisName, item) { 467 | return ($.inArray(this.plotPlugins, 'axisLabels') !== -1 && typeof item.series[axisName].options.axisLabel !== 'undefined' && item.series[axisName].options.axisLabel.length > 0); 468 | }; 469 | 470 | // check whether flot-tickRotor, a plugin which allows rotation of X-axis ticks, is being used 471 | FlotTooltip.prototype.hasRotatedXAxisTicks = function (item) { 472 | return ($.inArray(this.plotPlugins, 'tickRotor') !== -1 && typeof item.series.xaxis.rotatedTicks !== 'undefined'); 473 | }; 474 | 475 | // 476 | var init = function (plot) { 477 | new FlotTooltip(plot); 478 | }; 479 | 480 | // define Flot plugin 481 | $.plot.plugins.push({ 482 | init: init, 483 | options: defaultOptions, 484 | name: 'tooltip', 485 | version: '0.8.4' 486 | }); 487 | 488 | })(jQuery); 489 | -------------------------------------------------------------------------------- /app/static/css/bootstrap.icon-large.min.css: -------------------------------------------------------------------------------- 1 | .icon-large{background-image:url("../img/glyphicons.png");background-position:24px 24px;background-repeat:no-repeat;display:inline-block;height:28px;line-height:28px;vertical-align:text-bottom;width:28px}.icon-large.icon-glass{background-position:0 0}.icon-large.icon-leaf{background-position:0 -34px}.icon-large.icon-dog{background-position:0 -69px}.icon-large.icon-user{background-position:0 -104px}.icon-large.icon-girl{background-position:0 -136px}.icon-large.icon-car{background-position:0 -168px}.icon-large.icon-user-add{background-position:0 -200px}.icon-large.icon-user-remove{background-position:0 -232px}.icon-large.icon-film{background-position:0 -264px}.icon-large.icon-magic{background-position:0 -300px}.icon-large.icon-envelope{background-position:0 -330px}.icon-large.icon-camera{background-position:0 -360px}.icon-large.icon-heart{background-position:0 -390px}.icon-large.icon-beach-umbrella{background-position:0 -422px}.icon-large.icon-train{background-position:0 -457px}.icon-large.icon-print{background-position:0 -494px}.icon-large.icon-bin{background-position:0 -528px}.icon-large.icon-trash{background-position:0 -528px}.icon-large.icon-music{background-position:0 -566px}.icon-large.icon-note{background-position:0 -601px}.icon-large.icon-cogwheel{background-position:0 -636px}.icon-large.icon-cog{background-position:0 -636px}.icon-large.icon-home{background-position:0 -670px}.icon-large.icon-snowflake{background-position:0 -706px}.icon-large.icon-fire{background-position:0 -744px}.icon-large.icon-cogwheels{background-position:0 -780px}.icon-large.icon-parents{background-position:0 -816px}.icon-large.icon-binoculars{background-position:0 -848px}.icon-large.icon-road{background-position:0 -882px}.icon-large.icon-search{background-position:0 -916px}.icon-large.icon-cars{background-position:0 -950px}.icon-large.icon-pencil{background-position:0 -985px}.icon-large.icon-bus{background-position:0 -1020px}.icon-large.icon-wifi-alt{background-position:0 -1055px}.icon-large.icon-luggage{background-position:0 -1091px}.icon-large.icon-old-man{background-position:0 -1128px}.icon-large.icon-woman{background-position:0 -1162px}.icon-large.icon-file{background-position:0 -1194px}.icon-large.icon-credit{background-position:0 -1228px}.icon-large.icon-airplane,.icon-large.icon-plane{background-position:0 -1262px}.icon-large.icon-notes{background-position:0 -1297px}.icon-large.icon-stats{background-position:0 -1332px}.icon-large.icon-charts{background-position:0 -1367px}.icon-large.icon-pie-chart{background-position:0 -1401px}.icon-large.icon-group{background-position:0 -1436px}.icon-large.icon-keys{background-position:0 -1468px}.icon-large.icon-calendar{background-position:0 -1504px}.icon-large.icon-router{background-position:0 -1539px}.icon-large.icon-camera-small{background-position:0 -1575px}.icon-large.icon-dislikes{background-position:0 -1609px}.icon-large.icon-star-empty{background-position:0 -1609px}.icon-large.icon-star{background-position:0 -1643px}.icon-large.icon-link{background-position:0 -1677px}.icon-large.icon-eye-open{background-position:-1px -1704px}.icon-large.icon-eye-close{background-position:-1px -1737px}.icon-large.icon-alarm{background-position:0 -1771px}.icon-large.icon-clock{background-position:0 -1807px}.icon-large.icon-time{background-position:0 -1807px}.icon-large.icon-stopwatch{background-position:0 -1841px}.icon-large.icon-projector{background-position:0 -1878px}.icon-large.icon-history{background-position:0 -1913px}.icon-large.icon-truck{background-position:0 -1949px}.icon-large.icon-cargo{background-position:0 -1986px}.icon-large.icon-compass{background-position:-46px 0}.icon-large.icon-keynote{background-position:-46px -34px}.icon-large.icon-attach{background-position:-46px -74px}.icon-large.icon-power{background-position:-46px -108px}.icon-large.icon-off{background-position:-46px -108px}.icon-large.icon-lightbulb{background-position:-46px -142px}.icon-large.icon-tag{background-position:-46px -178px}.icon-large.icon-tags{background-position:-46px -212px}.icon-large.icon-cleaning{background-position:-46px -246px}.icon-large.icon-ruller{background-position:-46px -281px}.icon-large.icon-gift{background-position:-46px -305px}.icon-large.icon-umbrella{background-position:-46px -340px}.icon-large.icon-book{background-position:-46px -378px}.icon-large.icon-bookmark{background-position:-44px -412px}.icon-large.icon-signal{background-position:-46px -446px}.icon-large.icon-cup{background-position:-46px -479px}.icon-large.icon-stroller{background-position:-46px -513px}.icon-large.icon-headphones{background-position:-46px -549px}.icon-large.icon-headset{background-position:-46px -583px}.icon-large.icon-warning-sign{background-position:-46px -621px}.icon-large.icon-signal{background-position:-46px -655px}.icon-large.icon-retweet{background-position:-47px -680px}.icon-large.icon-refresh{background-position:-46px -714px}.icon-large.icon-roundabout{background-position:-46px -750px}.icon-large.icon-random{background-position:-46px -787px}.icon-large.icon-heat{background-position:-46px -817px}.icon-large.icon-repeat{background-position:-46px -852px}.icon-large.icon-display{background-position:-46px -888px}.icon-large.icon-log-book{background-position:-46px -922px}.icon-large.icon-adress-book{background-position:-46px -956px}.icon-large.icon-magnet{background-position:-46px -990px}.icon-large.icon-table{background-position:-46px -1023px}.icon-large.icon-adjust{background-position:-46px -1057px}.icon-large.icon-tint{background-position:-46px -1093px}.icon-large.icon-crop{background-position:-46px -1129px}.icon-large.icon-vector-path-square{background-position:-46px -1165px}.icon-large.icon-vector-path-circle{background-position:-46px -1199px}.icon-large.icon-vector-path-polygon{background-position:-46px -1233px}.icon-large.icon-vector-path-line{background-position:-46px -1268px}.icon-large.icon-vector-path-curve{background-position:-46px -1302px}.icon-large.icon-vector-path-all{background-position:-46px -1336px}.icon-large.icon-font{background-position:-46px -1370px}.icon-large.icon-italic{background-position:-46px -1403px}.icon-large.icon-bold{background-position:-46px -1437px}.icon-large.icon-text-underline{background-position:-46px -1471px}.icon-large.icon-text-strike{background-position:-46px -1505px}.icon-large.icon-text-height{background-position:-46px -1537px}.icon-large.icon-text-width{background-position:-46px -1571px}.icon-large.icon-text-resize{background-position:-46px -1605px}.icon-large.icon-left-indent,.icon-large.icon-indent-left{background-position:-46px -1641px}.icon-large.icon-right-indent,.icon-large.icon-indent-right{background-position:-46px -1673px}.icon-large.icon-align-left{background-position:-46px -1705px}.icon-large.icon-align-center{background-position:-46px -1736px}.icon-large.icon-align-right{background-position:-46px -1767px}.icon-large.icon-justify{background-position:-46px -1798px}.icon-large.icon-align-justify{background-position:-46px -1798px}.icon-large.icon-list{background-position:-46px -1829px}.icon-large.icon-text-smaller{background-position:-46px -1860px}.icon-large.icon-text-bigger{background-position:-46px -1886px}.icon-large.icon-embed{background-position:-47px -1910px}.icon-large.icon-embed-close{background-position:-47px -1940px}.icon-large.icon-adjust{background-position:-46px -1976px}.icon-large.icon-message-full{background-position:-92px 0}.icon-large.icon-message-empty{background-position:-92px -38px}.icon-large.icon-message-in{background-position:-92px -76px}.icon-large.icon-message-out{background-position:-92px -114px}.icon-large.icon-message-plus{background-position:-92px -152px}.icon-large.icon-message-minus{background-position:-92px -185px}.icon-large.icon-message-ban{background-position:-92px -218px}.icon-large.icon-message-flag{background-position:-92px -251px}.icon-large.icon-message-lock{background-position:-92px -284px}.icon-large.icon-message-new{background-position:-92px -318px}.icon-large.icon-inbox{background-position:-92px -350px}.icon-large.icon-inbox-plus{background-position:-92px -383px}.icon-large.icon-inbox-minus{background-position:-92px -420px}.icon-large.icon-inbox-lock{background-position:-92px -457px}.icon-large.icon-inbox-in{background-position:-92px -495px}.icon-large.icon-inbox-out{background-position:-92px -531px}.icon-large.icon-computer-locked{background-position:-92px -567px}.icon-large.icon-computer-service{background-position:-92px -601px}.icon-large.icon-computer-proces{background-position:-92px -635px}.icon-large.icon-phone{background-position:-92px -669px}.icon-large.icon-database-lock{background-position:-92px -704px}.icon-large.icon-database-plus{background-position:-92px -742px}.icon-large.icon-database-minus{background-position:-92px -779px}.icon-large.icon-database-ban{background-position:-92px -816px}.icon-large.icon-folder-open{background-position:-92px -853px}.icon-large.icon-folder-plus{background-position:-92px -885px}.icon-large.icon-folder-minus{background-position:-92px -920px}.icon-large.icon-folder-lock{background-position:-92px -955px}.icon-large.icon-folder-flag{background-position:-92px -991px}.icon-large.icon-folder-new{background-position:-92px -1026px}.icon-large.icon-check{background-position:-92px -1060px}.icon-large.icon-edit{background-position:-92px -1088px}.icon-large.icon-new-window{background-position:-92px -1119px}.icon-large.icon-more-windows{background-position:-92px -1151px}.icon-large.icon-show-big-thumbnails{background-position:-92px -1184px}.icon-large.icon-th-large{background-position:-92px -1184px}.icon-large.icon-show-thumbnails{background-position:-92px -1216px}.icon-large.icon-th{background-position:-92px -1216px}.icon-large.icon-show-thumbnails-with-lines{background-position:-92px -1248px}.icon-large.icon-th-list{background-position:-92px -1248px}.icon-large.icon-show-lines{background-position:-92px -1273px}.icon-large.icon-playlist{background-position:-92px -1298px}.icon-large.icon-picture{background-position:-92px -1332px}.icon-large.icon-imac{background-position:-92px -1362px}.icon-large.icon-macbook{background-position:-92px -1394px}.icon-large.icon-ipad{background-position:-92px -1419px}.icon-large.icon-iphone{background-position:-92px -1456px}.icon-large.icon-iphone-transfer{background-position:-92px -1490px}.icon-large.icon-iphone-exchange{background-position:-92px -1524px}.icon-large.icon-ipod{background-position:-92px -1558px}.icon-large.icon-ipod-shuffle{background-position:-92px -1590px}.icon-large.icon-ear-plugs{background-position:-92px -1613px}.icon-large.icon-albums{background-position:-92px -1647px}.icon-large.icon-step-backward{background-position:-92px -1675px}.icon-large.icon-fast-backward{background-position:-92px -1703px}.icon-large.icon-rewind,.icon-large.icon-backwards{background-position:-92px -1731px}.icon-large.icon-play{background-position:-92px -1759px}.icon-large.icon-pause{background-position:-92px -1787px}.icon-large.icon-stop{background-position:-92px -1813px}.icon-large.icon-forward{background-position:-92px -1837px}.icon-large.icon-fast-forward{background-position:-92px -1865px}.icon-large.icon-step-forward{background-position:-92px -1893px}.icon-large.icon-eject{background-position:-92px -1921px}.icon-large.icon-facetime-video{background-position:-92px -1948px}.icon-large.icon-download-alt{background-position:-92px -1974px}.icon-large.icon-mute,.icon-large.icon-volume-off{background-position:-138px 4px}.icon-large.icon-volume-down{background-position:-134px -22px}.icon-large.icon-volume-up{background-position:-138px -52px}.icon-large.icon-screenshot{background-position:-138px -88px}.icon-large.icon-move{background-position:-138px -125px}.icon-large.icon-more{background-position:-138px -159px}.icon-large.icon-brightness-reduce{background-position:-138px -176px}.icon-large.icon-brightness-increase{background-position:-138px -206px}.icon-large.icon-circle-plus,.icon-large.icon-plus-sign{background-position:-138px -240px}.icon-large.icon-circle-minus,.icon-large.icon-minus-sign{background-position:-138px -276px}.icon-large.icon-circle-remove,.icon-large.icon-remove-sign{background-position:-138px -312px}.icon-large.icon-circle-ok,.icon-large.icon-ok-sign{background-position:-138px -348px}.icon-large.icon-circle-question-mark,.icon-large.icon-question-sign{background-position:-138px -384px}.icon-large.icon-circle-info,.icon-large.icon-info-sign{background-position:-138px -420px}.icon-large.icon-circle-exclamation-mark,.icon-large.icon-exclamation-sign{background-position:-138px -456px}.icon-large.icon-remove{background-position:-138px -492px}.icon-large.icon-ok{background-position:-138px -528px}.icon-large.icon-ban{background-position:-138px -564px}.icon-large.icon-download{background-position:-138px -600px}.icon-large.icon-upload{background-position:-138px -636px}.icon-large.icon-shopping-cart{background-position:-138px -672px}.icon-large.icon-lock{background-position:-138px -705px}.icon-large.icon-unlock{background-position:-138px -741px}.icon-large.icon-electricity{background-position:-138px -777px}.icon-large.icon-cart-out{background-position:-138px -811px}.icon-large.icon-cart-in{background-position:-138px -846px}.icon-large.icon-left-arrow{background-position:-138px -880px}.icon-large.icon-right-arrow{background-position:-138px -908px}.icon-large.icon-down-arrow{background-position:-138px -936px}.icon-large.icon-up-arrow{background-position:-138px -966px}.icon-large.icon-resize-small{background-position:-138px -996px}.icon-large.icon-resize-full{background-position:-138px -1030px}.icon-large.icon-circle-arrow-left{background-position:-138px -1064px}.icon-large.icon-circle-arrow-right{background-position:-138px -1100px}.icon-large.icon-circle-arrow-top,.icon-large.icon-circle-arrow-up{background-position:-138px -1136px}.icon-large.icon-circle-arrow-down{background-position:-138px -1172px}.icon-large.icon-play-button{background-position:-138px -1208px}.icon-large.icon-play-circle{background-position:-138px -1208px}.icon-large.icon-unshare{background-position:-138px -1244px}.icon-large.icon-share{background-position:-138px -1272px}.icon-large.icon-thin-right-arrow,.icon-large.icon-chevron-right{background-position:-138px -1300px}.icon-large.icon-thin-arrow-left,.icon-large.icon-chevron-left{background-position:-138px -1332px}.icon-large.icon-bluetooth{background-position:-138px -1364px}.icon-large.icon-euro{background-position:-138px -1398px}.icon-large.icon-usd{background-position:-138px -1431px}.icon-large.icon-bp{background-position:-138px -1467px}.icon-large.icon-moon{background-position:-138px -1501px}.icon-large.icon-sun{background-position:-138px -1536px}.icon-large.icon-cloud{background-position:-138px -1570px}.icon-large.icon-direction{background-position:-138px -1597px}.icon-large.icon-brush{background-position:-138px -1633px}.icon-large.icon-pen{background-position:-138px -1666px}.icon-large.icon-zoom-in{background-position:-138px -1700px}.icon-large.icon-zoom-out{background-position:-138px -1735px}.icon-large.icon-pin{background-position:-138px -1770px}.icon-large.icon-riflescope{background-position:-138px -1805px}.icon-large.icon-rotation-lock{background-position:-138px -1840px}.icon-large.icon-flash{background-position:-138px -1874px}.icon-large.icon-google-maps,.icon-large.icon-map-marker{background-position:-138px -1909px}.icon-large.icon-anchor{background-position:-138px -1943px}.icon-large.icon-conversation{background-position:-138px -1978px}.icon-large.icon-chat{background-position:-184px 0}.icon-large.icon-male{background-position:-184px -29px}.icon-large.icon-female{background-position:-184px -61px}.icon-large.icon-asterisk{background-position:-184px -98px}.icon-large.icon-divide{background-position:-184px -128px}.icon-large.icon-snorkel-diving{background-position:-184px -154px}.icon-large.icon-scuba-diving{background-position:-184px -189px}.icon-large.icon-oxygen-bottle{background-position:-184px -223px}.icon-large.icon-fins{background-position:-184px -260px}.icon-large.icon-fishes{background-position:-184px -297px}.icon-large.icon-boat{background-position:-184px -337px}.icon-large.icon-delete-point{background-position:-184px -371px}.icon-large.icon-qrcode{background-position:-184px -398px}.icon-large.icon-barcode{background-position:-184px -432px}.icon-large.icon-pool{background-position:-184px -466px}.icon-large.icon-buoy{background-position:-184px -500px}.icon-large.icon-spade{background-position:-184px -534px}.icon-large.icon-bank{background-position:-184px -568px}.icon-large.icon-vcard{background-position:-184px -602px}.icon-large.icon-electrical-plug{background-position:-184px -636px}.icon-large.icon-flag{background-position:-184px -671px}.icon-large.icon-credit-card{background-position:-184px -707px}.icon-large.icon-keyboard-wireless{background-position:-184px -736px}.icon-large.icon-keyboard-wired{background-position:-184px -765px}.icon-large.icon-shield{background-position:-184px -800px}.icon-large.icon-ring{background-position:-184px -834px}.icon-large.icon-cake{background-position:-184px -868px}.icon-large.icon-drink{background-position:-184px -902px}.icon-large.icon-beer{background-position:-184px -936px}.icon-large.icon-fast-food{background-position:-184px -970px}.icon-large.icon-cutlery{background-position:-184px -1004px}.icon-large.icon-pizza{background-position:-184px -1038px}.icon-large.icon-birthday-cake{background-position:-184px -1077px}.icon-large.icon-tablet{background-position:-184px -1116px}.icon-large.icon-settings{background-position:-184px -1151px}.icon-large.icon-bullets{background-position:-184px -1185px}.icon-large.icon-cardio{background-position:-184px -1218px}.icon-large.icon-pants{background-position:-184px -1254px}.icon-large.icon-sweater{background-position:-184px -1288px}.icon-large.icon-fabric{background-position:-184px -1322px}.icon-large.icon-leather{background-position:-184px -1354px}.icon-large.icon-scissors{background-position:-184px -1388px}.icon-large.icon-podium{background-position:-184px -1425px}.icon-large.icon-skull{background-position:-184px -1456px}.icon-large.icon-celebration{background-position:-184px -1490px}.icon-large.icon-tea-kettle{background-position:-184px -1525px}.icon-large.icon-french-press{background-position:-184px -1558px}.icon-large.icon-coffe-cup{background-position:-184px -1593px}.icon-large.icon-pot{background-position:-184px -1622px}.icon-large.icon-grater{background-position:-184px -1654px}.icon-large.icon-kettle{background-position:-184px -1688px}.icon-large.icon-hospital{background-position:-184px -1722px}.icon-large.icon-hospital-h{background-position:-184px -1756px}.icon-large.icon-microphone{background-position:-184px -1790px}.icon-large.icon-webcam{background-position:-184px -1824px}.icon-large.icon-temple-christianity-church{background-position:-184px -1858px}.icon-large.icon-temple-islam{background-position:-184px -1893px}.icon-large.icon-temple-hindu{background-position:-184px -1927px}.icon-large.icon-temple-buddhist{background-position:-184px -1961px}.icon-large.icon-electrical-socket-eu{background-position:-230px 0}.icon-large.icon-electrical-socket-us{background-position:-230px -33px}.icon-large.icon-bomb{background-position:-230px -66px}.icon-large.icon-comments,.icon-large.icon-comment{background-position:-230px -102px}.icon-large.icon-flower{background-position:-230px -135px}.icon-large.icon-baseball{background-position:-230px -170px}.icon-large.icon-rugby{background-position:-230px -206px}.icon-large.icon-ax{background-position:-230px -240px}.icon-large.icon-table-tennis{background-position:-230px -275px}.icon-large.icon-bowling{background-position:-230px -309px}.icon-large.icon-tree-conifer{background-position:-230px -343px}.icon-large.icon-tree-deciduous{background-position:-230px -377px}.icon-large.icon-sort{background-position:-230px -412px}.icon-large.icon-filter{background-position:-230px -447px}.icon-large.icon-gamepad{background-position:-230px -481px}.icon-large.icon-playing-dices{background-position:-230px -510px}.icon-large.icon-calculator{background-position:-230px -543px}.icon-large.icon-tie{background-position:-230px -577px}.icon-large.icon-wallet{background-position:-230px -613px}.icon-large.icon-share{background-position:-230px -643px}.icon-large.icon-sampler{background-position:-230px -675px}.icon-large.icon-piano{background-position:-230px -707px}.icon-large.icon-web-browser{background-position:-230px -741px}.icon-large.icon-blog{background-position:-230px -773px}.icon-large.icon-dashboard{background-position:-230px -806px}.icon-large.icon-certificate{background-position:-230px -840px}.icon-large.icon-bell{background-position:-230px -875px}.icon-large.icon-candle{background-position:-230px -909px}.icon-large.icon-pin-classic{background-position:-230px -944px}.icon-large.icon-iphone-shake{background-position:-230px -978px}.icon-large.icon-pin-flag{background-position:-230px -1012px}.icon-large.icon-turtle{background-position:-230px -1044px}.icon-large.icon-rabbit{background-position:-230px -1070px}.icon-large.icon-globe{background-position:-230px -1102px}.icon-large.icon-briefcase{background-position:-230px -1136px}.icon-large.icon-hdd{background-position:-230px -1167px}.icon-large.icon-thumbs-up{background-position:-230px -1198px}.icon-large.icon-thumbs-down{background-position:-230px -1229px}.icon-large.icon-hand-right{background-position:-230px -1260px}.icon-large.icon-hand-left{background-position:-230px -1289px}.icon-large.icon-hand-up{background-position:-230px -1318px}.icon-large.icon-hand-down{background-position:-230px -1354px}.icon-large.icon-fullscreen{background-position:-230px -1391px}.icon-large.icon-shopping-bag{background-position:-230px -1425px}.icon-large.icon-book-open{background-position:-230px -1461px}.icon-large.icon-nameplate{background-position:-230px -1494px}.icon-large.icon-nameplate-alt{background-position:-230px -1525px}.icon-large.icon-vases{background-position:-230px -1557px}.icon-large.icon-announcement,.icon-large.icon-bullhorn{background-position:-230px -1591px}.icon-large.icon-dumbbell{background-position:-230px -1621px}.icon-large.icon-suitcase{background-position:-230px -1647px}.icon-large.icon-file-import{background-position:-230px -1679px}.icon-large.icon-file-export{background-position:-230px -1713px}.icon-large.icon-pinterest{background-position:-230px -1747px}.icon-large.icon-dropbox{background-position:-230px -1781px}.icon-large.icon-jolicloud{background-position:-230px -1815px}.icon-large.icon-yahoo{background-position:-230px -1849px}.icon-large.icon-blogger{background-position:-230px -1883px}.icon-large.icon-picasa{background-position:-230px -1917px}.icon-large.icon-amazon{background-position:-230px -1951px}.icon-large.icon-tumblr{background-position:-230px -1985px}.icon-large.icon-wordpress{background-position:-276px 0}.icon-large.icon-instapaper{background-position:-276px -34px}.icon-large.icon-evernote{background-position:-276px -68px}.icon-large.icon-xing{background-position:-276px -102px}.icon-large.icon-zootool{background-position:-276px -136px}.icon-large.icon-dribbble{background-position:-276px -170px}.icon-large.icon-deviantart{background-position:-276px -204px}.icon-large.icon-read-it-later{background-position:-276px -238px}.icon-large.icon-linked-in{background-position:-276px -272px}.icon-large.icon-forrst{background-position:-276px -306px}.icon-large.icon-pinboard{background-position:-276px -340px}.icon-large.icon-behance{background-position:-276px -374px}.icon-large.icon-github{background-position:-276px -408px}.icon-large.icon-youtube{background-position:-276px -442px}.icon-large.icon-skitch{background-position:-276px -476px}.icon-large.icon-quora{background-position:-276px -510px}.icon-large.icon-google-plus{background-position:-276px -544px}.icon-large.icon-spootify{background-position:-276px -578px}.icon-large.icon-stumbleupon{background-position:-276px -612px}.icon-large.icon-readability{background-position:-276px -646px}.icon-large.icon-facebook{background-position:-276px -680px}.icon-large.icon-twitter-t{background-position:-276px -714px}.icon-large.icon-twitter{background-position:-276px -748px}.icon-large.icon-buzz{background-position:-276px -782px}.icon-large.icon-vimeo{background-position:-276px -816px}.icon-large.icon-flickr{background-position:-276px -850px}.icon-large.icon-last-fm{background-position:-276px -884px}.icon-large.icon-rss{background-position:-276px -918px}.icon-large.icon-skype{background-position:-276px -952px} -------------------------------------------------------------------------------- /app/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.0.2 by @fat and @mdo 3 | * Copyright 2013 Twitter, Inc. 4 | * Licensed under http://www.apache.org/licenses/LICENSE-2.0 5 | * 6 | * Designed and built with all the love in the world by @mdo and @fat. 7 | */ 8 | 9 | if("undefined"==typeof jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]');if(a.length){var b=this.$element.find("input").prop("checked",!this.$element.hasClass("active")).trigger("change");"radio"===b.prop("type")&&a.find(".active").removeClass("active")}this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(''}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery); -------------------------------------------------------------------------------- /app/static/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | --------------------------------------------------------------------------------