├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── codecov.yml ├── config.py ├── docs ├── api.md ├── network.md ├── pihole.md ├── server.md ├── server_panel.png └── system.md ├── jsx ├── components │ ├── footer.jsx │ └── header.jsx ├── main.js ├── main.jsx └── routes │ ├── components │ ├── disk.jsx │ ├── hostname.jsx │ ├── logo.jsx │ ├── memory.jsx │ ├── network-external.jsx │ ├── network-internal-details.jsx │ ├── network-internal.jsx │ ├── pihole.jsx │ ├── processes.jsx │ ├── progressbar.jsx │ ├── swap.jsx │ ├── temperature.jsx │ ├── timed_component.jsx │ └── uptime.jsx │ ├── network.jsx │ └── panel.jsx ├── package.json ├── requirements.txt ├── run.py ├── serverpanel ├── __init__.py ├── controllers.py ├── ext │ ├── __init__.py │ └── serverinfo.py ├── static │ ├── css │ │ ├── bootstrap.css │ │ ├── bootstrap.min.css │ │ └── main.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── img │ │ └── svg │ │ │ └── rpi_logo.svg │ └── js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ ├── bundle.js │ │ └── jquery.min.js ├── templates │ └── main.html └── utils │ ├── __init__.py │ └── jsonify.py ├── tests.py ├── webpack.config.js └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | # PyCharm 57 | .idea/ 58 | .idea/* 59 | 60 | # ========================= 61 | # Operating System Files 62 | # ========================= 63 | 64 | # OSX 65 | # ========================= 66 | 67 | .DS_Store 68 | .AppleDouble 69 | .LSOverride 70 | 71 | # Thumbnails 72 | ._* 73 | 74 | # Files that might appear on external disk 75 | .Spotlight-V100 76 | .Trashes 77 | 78 | # Directories potentially created on remote AFP share 79 | .AppleDB 80 | .AppleDesktop 81 | Network Trash Folder 82 | Temporary Items 83 | .apdisk 84 | 85 | # Windows 86 | # ========================= 87 | 88 | # Windows image file caches 89 | Thumbs.db 90 | ehthumbs.db 91 | 92 | # Folder config file 93 | Desktop.ini 94 | 95 | # Recycle Bin used on file shares 96 | $RECYCLE.BIN/ 97 | 98 | # Windows Installer files 99 | *.cab 100 | *.msi 101 | *.msm 102 | *.msp 103 | 104 | # Windows shortcuts 105 | *.lnk 106 | .idea/vcs.xml 107 | .idea/vcs.xml 108 | .idea/.name 109 | 110 | node_modules/ 111 | venv/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.4" 5 | - "3.5" 6 | - "3.6" 7 | - "3.7" 8 | - "3.8-dev" 9 | 10 | # install codecov 11 | before_install: 12 | - pip install codecov 13 | 14 | # command to install dependencies 15 | install: 16 | - pip install -r requirements.txt 17 | 18 | # command to run tests 19 | script: 20 | - coverage run tests.py 21 | 22 | after_success: 23 | - codecov 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build docker 2 | # docker build -t CONTAINERNAME . 3 | 4 | # Run container (maps local port 80 to 80 in the container) 5 | # docker run -p 80:80 -d CONTAINERNAME 6 | 7 | 8 | FROM python:3.5-alpine 9 | 10 | WORKDIR /usr/src/app 11 | 12 | COPY requirements.txt ./ 13 | RUN apk update 14 | RUN apk add linux-headers g++ 15 | RUN pip3 install --no-cache-dir -r requirements.txt 16 | RUN pip3 install gunicorn 17 | 18 | COPY . . 19 | 20 | CMD [ "gunicorn", "-b", "0.0.0.0:80", "wsgi:application" ] 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Sebastian Proost 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/sepro/Flask-Server-Panel.svg?branch=master)](https://travis-ci.com/sepro/Flask-Server-Panel) [![codecov.io](https://codecov.io/github/sepro/Flask-Server-Panel/coverage.svg?precision=1)](https://codecov.io/github/sepro/Flask-Server-Panel/) 2 | 3 | # Flask-Server-Panel and API 4 | 5 | Server panel based on flask to show stats for a small private server. 6 | Designed specifically with a [Raspberry Pi](https://www.raspberrypi.org/) 7 | running [Pi-Hole](https://pi-hole.net/) in mind. 8 | 9 | The API that is queried by the front-end is located at /api/. 10 | For details check out the [API documentation](./docs/api.md). 11 | 12 | 13 | The back-end is based on Python [Flask](http://flask.pocoo.org/) with a 14 | front-end using [React.js](https://facebook.github.io/react/) and 15 | [Bootstrap](http://getbootstrap.com/) 16 | 17 | ![Flask-Server-Panel](./docs/server_panel.png "Server Panel") 18 | 19 | *Note: The Raspberry Pi logo changes color along with the temperature. 20 | Green is good, red means you have to invest in a new case with better 21 | cooling.* 22 | 23 | ## Getting started 24 | 25 | Installation instruction for deployment on a linux system. 26 | 27 | Clone the repository 28 | 29 | git clone https://github.com/sepro/Flask-Server-Panel.git Flask-Server-Panel 30 | 31 | Set up a virtual environment 32 | 33 | cd Flask-Server-Panel 34 | virtualenv --python=python3 venv 35 | 36 | Activate the environment and install packages 37 | 38 | source venv/bin/activate 39 | pip install -r requirements.txt 40 | 41 | Configure Flask-Server-Panel 42 | 43 | vim config.py 44 | 45 | Run tests and run app 46 | 47 | python tests.py 48 | 49 | python run.py 50 | 51 | ## Deploy (on x86_64, not rPi!) using Docker 52 | 53 | In case you would like to test the panel, a docker container is available. Do note that you will get stats from within the container and not the host system. However, for trying out the app this is the quickest way to get things running. 54 | 55 | docker pull sepro/flask-server-panel 56 | docker run -p 80:80 -d sepro/flask-server-panel 57 | 58 | This exposes gunicorn running in the container on port 80 to port 80 on the host system. 59 | 60 | ## Developing the front-end 61 | Install all packages through npm 62 | 63 | npm install 64 | 65 | Build ./serverpanel/static/js/bundle.js using webpack 66 | 67 | webpack -p 68 | 69 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "config.py" 3 | - "wsgi.py" 4 | - "run.py" -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | DEBUG = False 4 | TESTING = False 5 | 6 | ENABLE_PIHOLE = False 7 | PIHOLE_API = 'http://192.168.1.20/admin/api.php' 8 | 9 | CPU_TEMP = '/sys/class/thermal/thermal_zone0/temp' 10 | GEOLOCATOR = 'http://ipinfo.io/json' 11 | 12 | SECRET_KEY = os.urandom(24) 13 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # Flask-Server-Panel API 2 | 3 | The front-end constantly queries an API, you can have external apps 4 | fetch details directly from there (in JSON). The api can be accessed at 5 | /api/ . The whole API is read-only and only GET requests 6 | are supported. 7 | 8 | ## API entry point **/api/** 9 | 10 | ```javascript 11 | { 12 | "network": { 13 | "external": "/api/network/external", 14 | "io": "/api/network/io" 15 | }, 16 | "pihole": { 17 | "stats": "/api/pihole/stats" 18 | }, 19 | "server": { 20 | "hostname": "/api/server/hostname", 21 | "os": "/api/server/os", 22 | "uptime": "/api/server/uptime" 23 | }, 24 | "system": { 25 | "disk_space": "/api/system/disk/space", 26 | "memory": "/api/system/memory", 27 | "processes": "/api/system/processes", 28 | "swap": "/api/system/swap", 29 | "temp": "/api/system/temp" 30 | }, 31 | "version": "/api/version" 32 | } 33 | ``` 34 | 35 | The entry point should be rather self-explanatory, it contains links to 36 | more detailed sections of the API. 37 | 38 | This documentation is valid for version 1.0 which can be checked at **/api/version/** 39 | 40 | ```javascript 41 | { 42 | "name": "Flask-Sever-Panel", 43 | "version": "1.0" 44 | } 45 | ``` 46 | 47 | ## Details for subsections 48 | 49 | * [Network](./network.md) 50 | * [PiHole](./pihole.md) 51 | * [Server](./server.md) 52 | * [System](./system.md) 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /docs/network.md: -------------------------------------------------------------------------------- 1 | # Network details **/api/network/external** and **/api/network/io** 2 | 3 | Your exteral IP-address is determined using a third-party service 4 | [ipinfo.io](http://ipinfo.io/json) . It provides details about your IP, 5 | the location associated with it and the provider. 6 | 7 | ```javascript 8 | { 9 | "city": "", 10 | "country": "LU", 11 | "hostname": "provider", 12 | "ip": "127.0.0.1", 13 | "loc": "49.7500,6.1667", 14 | "org": "AS5577 root SA", 15 | "region": "" 16 | } 17 | ``` 18 | 19 | The internal network returns the input and output from local adapters in your system. 20 | 21 | ```javascript 22 | [ 23 | { 24 | "address": "127.0.0.1", 25 | "device": "lo", 26 | "io": { 27 | "bytes_recv": 127141942, 28 | "bytes_sent": 127141942, 29 | "dropin": 0, 30 | "dropout": 0, 31 | "errin": 0, 32 | "errout": 0, 33 | "packets_recv": 331057, 34 | "packets_sent": 331057 35 | } 36 | }, 37 | { 38 | "address": "unknown address", 39 | "device": "eth0", 40 | "io": { 41 | "bytes_recv": 0, 42 | "bytes_sent": 0, 43 | "dropin": 0, 44 | "dropout": 0, 45 | "errin": 0, 46 | "errout": 0, 47 | "packets_recv": 0, 48 | "packets_sent": 0 49 | } 50 | }, 51 | { 52 | "address": "192.168.1.25", 53 | "device": "wlan0", 54 | "io": { 55 | "bytes_recv": 168206324, 56 | "bytes_sent": 1533878635, 57 | "dropin": 508579, 58 | "dropout": 1, 59 | "errin": 0, 60 | "errout": 0, 61 | "packets_recv": 1181401, 62 | "packets_sent": 4910834 63 | } 64 | } 65 | ] 66 | ``` 67 | -------------------------------------------------------------------------------- /docs/pihole.md: -------------------------------------------------------------------------------- 1 | # PiHole stats **/api/pihole/stats/** 2 | 3 | If configured to do so the API can forward statistics from your PiHole 4 | installation. The same four statistics as the PiHole admin api provides 5 | are included here. 6 | 7 | ```javascript 8 | { 9 | "ads_blocked_today": 219, 10 | "ads_percentage_today": 13.2, 11 | "blocked_domains": 94519, 12 | "dns_queries_today": 1658, 13 | "enabled": true 14 | } 15 | ``` -------------------------------------------------------------------------------- /docs/server.md: -------------------------------------------------------------------------------- 1 | # Server details 2 | 3 | The hostname, os and uptime can be found at **/api/server/hostname**, 4 | **/api/server/os** and **/api/server/uptime** respectively. 5 | 6 | ```javascript 7 | { 8 | "hostname": "raspberrypi" 9 | } 10 | ``` 11 | 12 | ```javascript 13 | { 14 | "os_name": "Linux-4.4.26-v7+-armv7l-with-debian-8.0" 15 | } 16 | ``` 17 | 18 | ```javascript 19 | { 20 | "uptime": "11 days, 16:43:08" 21 | } 22 | ``` 23 | 24 | -------------------------------------------------------------------------------- /docs/server_panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sepro/Flask-Server-Panel/4763174aa8a6f74e72c14a67ec5be243b5e06ca2/docs/server_panel.png -------------------------------------------------------------------------------- /docs/system.md: -------------------------------------------------------------------------------- 1 | # System details 2 | 3 | Disk space, swap space, available momory, the cpu temperature and running 4 | processes can be found under the different sections of system. 5 | 6 | ## Disk space **/api/system/disk/space** 7 | ```javascript 8 | [ 9 | { 10 | "device": "/dev/root", 11 | "fstype": "ext4", 12 | "mountpoint": "/", 13 | "opts": "rw,noatime,data=ordered", 14 | "usage": { 15 | "free": 28488658944, 16 | "percent": 5.0, 17 | "total": 31341383680, 18 | "used": 1551548416 19 | } 20 | }, 21 | { 22 | "device": "/dev/mmcblk0p1", 23 | "fstype": "vfat", 24 | "mountpoint": "/boot", 25 | "opts": "rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,errors=remount-ro", 26 | "usage": { 27 | "free": 41066496, 28 | "percent": 34.7, 29 | "total": 62857216, 30 | "used": 21790720 31 | } 32 | } 33 | ] 34 | ``` 35 | 36 | ## Swap space **/api/system/swap/** 37 | ```javascript 38 | { 39 | "free": 104853504, 40 | "percent": 0.0, 41 | "sin": 0, 42 | "sout": 0, 43 | "total": 104853504, 44 | "used": 0 45 | } 46 | ``` 47 | 48 | ## Memory (RAM) **/api/system/memory/** 49 | ```javascript 50 | { 51 | "active": 232407040, 52 | "available": 855552000, 53 | "buffers": 52924416, 54 | "cached": 157040640, 55 | "free": 645586944, 56 | "inactive": 50503680, 57 | "percent": 11.8, 58 | "total": 970485760, 59 | "used": 324898816 60 | } 61 | ``` 62 | 63 | ## Temperature details **/api/system/temp/** 64 | ```javascript 65 | { 66 | "cpu": 43.312 67 | } 68 | ``` 69 | 70 | ## Processes running **/api/system/processes** 71 | ```javascript 72 | [ 73 | { 74 | "cpu_percentage": 0.1, 75 | "name": "RTW_CMD_THREAD", 76 | "pid": 354 77 | }, 78 | { 79 | "cpu_percentage": 0.1, 80 | "name": "php-cgi", 81 | "pid": 755 82 | }, 83 | 84 | ... 85 | 86 | { 87 | "cpu_percentage": 0.0, 88 | "name": "kworker/0:0", 89 | "pid": 28136 90 | } 91 | ] 92 | ``` -------------------------------------------------------------------------------- /jsx/components/footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | class Footer extends React.Component{ 5 | 6 | render() { 7 | return ( 8 |
9 |
10 |

Copyright Sebastian Proost

11 |
12 | ); 13 | } 14 | 15 | } 16 | 17 | export default Footer; -------------------------------------------------------------------------------- /jsx/components/header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | 5 | class Header extends React.Component{ 6 | 7 | render() { 8 | return ( 9 |
10 |
11 |
    12 |
  • Panel
  • 13 |
  • Network details
  • 14 |
15 |
16 |
17 | ); 18 | } 19 | 20 | } 21 | 22 | export default Header; -------------------------------------------------------------------------------- /jsx/main.js: -------------------------------------------------------------------------------- 1 | import Main from './Main.jsx'; -------------------------------------------------------------------------------- /jsx/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import {Router, Route, IndexRoute, browserHistory} from 'react-router' 5 | 6 | //import main components (which will appear on every page) 7 | import Header from './components/header.jsx'; 8 | import Footer from './components/footer.jsx'; 9 | 10 | //import the routes 11 | import Panel from './routes/panel.jsx'; 12 | import Network from './routes/network.jsx'; 13 | 14 | 15 | class Main extends React.Component{ 16 | 17 | render() { 18 | return ( 19 |
20 |
21 | 22 | {React.cloneElement(this.props.children, this.props.route)} 23 | 24 |
25 |
26 | ); 27 | } 28 | 29 | } 30 | 31 | const router = ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | 40 | render( 41 | router, 42 | document.getElementById('panel') 43 | ); -------------------------------------------------------------------------------- /jsx/routes/components/disk.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ProgressBar from './progressbar.jsx'; 4 | import TimedComponent from './timed_component.jsx'; 5 | 6 | 7 | class Disk extends TimedComponent { 8 | render() { 9 | 10 | return (

Disk

11 | {this.state.data.map(function(disk ,i){ 12 | return
{ disk.mountpoint } 13 | 14 |
; 15 | })} 16 | 17 |
18 | ); 19 | 20 | } 21 | } 22 | 23 | 24 | export default Disk; -------------------------------------------------------------------------------- /jsx/routes/components/hostname.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | 4 | class Hostname extends React.Component{ 5 | constructor(props) { 6 | super(props); 7 | this.state = {data: []}; 8 | } 9 | 10 | loadFromServer() { 11 | axios.get(this.props.url).then((response) => { 12 | this.setState({data: response.data}); 13 | }) 14 | } 15 | 16 | componentDidMount() { 17 | this.loadFromServer(); 18 | } 19 | 20 | render() { 21 | return (
Hostname : { this.state.data.hostname }
); 22 | } 23 | } 24 | 25 | export default Hostname; -------------------------------------------------------------------------------- /jsx/routes/components/logo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | 4 | class Logo extends React.Component{ 5 | constructor(props) { 6 | super(props); 7 | this.state = {color: "#00a985", data: {cpu: 20}}; 8 | } 9 | 10 | loadFromServer() { 11 | axios.get(this.props.url).then((response) => { 12 | var min = 30; 13 | var max = 80; 14 | var steps = 7; 15 | 16 | var colors = ["#37E500", "#77EC00", "#B7F300", "#F7FA00", "#F1A600", "#EB5300", "#E50000"]; 17 | 18 | var c = response.data.cpu > max ? max : response.data.cpu; 19 | c = c < min ? min : c; 20 | c -= min; 21 | c = Math.floor(((c * (steps-1))/(max-min))); 22 | 23 | // console.log(response.data.cpu, c); 24 | 25 | var color = colors[c]; 26 | 27 | this.setState({data: response.data, color: color}); 28 | }); 29 | } 30 | 31 | componentDidMount() { 32 | this.loadFromServer(); 33 | this.interval = setInterval(this.loadFromServer.bind(this), this.props.pollInterval); 34 | } 35 | 36 | componentWillUnmount() { 37 | clearInterval(this.interval); 38 | } 39 | 40 | render() { 41 | return (
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
); 58 | } 59 | } 60 | 61 | export default Logo; -------------------------------------------------------------------------------- /jsx/routes/components/memory.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ProgressBar from './progressbar.jsx'; 4 | import TimedComponent from './timed_component.jsx'; 5 | 6 | 7 | class Memory extends TimedComponent { 8 | render() { 9 | 10 | return (

Memory

11 | 12 |
13 | ); 14 | 15 | } 16 | }; 17 | 18 | export default Memory; -------------------------------------------------------------------------------- /jsx/routes/components/network-external.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import TimedComponent from './timed_component.jsx'; 4 | 5 | 6 | class NetworkExternal extends TimedComponent{ 7 | render() { 8 | return (
External IP : { this.state.data.ip } (country: { this.state.data.country })
); 9 | } 10 | } 11 | 12 | export default NetworkExternal; 13 | -------------------------------------------------------------------------------- /jsx/routes/components/network-internal-details.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import TimedComponent from './timed_component.jsx'; 4 | 5 | 6 | class NetworkInternalDetails extends TimedComponent{ 7 | render() { 8 | return (
{ this.state.data.map(function(network, i) { 9 | if (network.io.bytes_sent > 0) { 10 | return
11 |

{ network.device }: {network.address}

12 |

Total Traffic: { ((network.io.bytes_sent + network.io.bytes_recv)/(1024*1024*1024)).toFixed(2) } Gb 13 | (up: { (network.io.bytes_sent/(1024*1024*1024)).toFixed(2) } Gb, 14 | down: { (network.io.bytes_recv/(1024*1024*1024)).toFixed(2) } Gb)

15 |
16 | } 17 | }) 18 | } 19 |
); 20 | } 21 | } 22 | 23 | export default NetworkInternalDetails; -------------------------------------------------------------------------------- /jsx/routes/components/network-internal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import TimedComponent from './timed_component.jsx'; 4 | 5 | 6 | class NetworkInternal extends TimedComponent{ 7 | render() { 8 | return (

Local IP :

{ this.state.data.map(function(network, i) { 9 | if (network.io.bytes_sent > 0) { 10 | return

{ network.device }: {network.address}

11 | } 12 | }) 13 | }
14 |
); 15 | } 16 | } 17 | 18 | export default NetworkInternal; -------------------------------------------------------------------------------- /jsx/routes/components/pihole.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import TimedComponent from './timed_component.jsx'; 4 | 5 | 6 | class Pihole extends TimedComponent{ 7 | render() { 8 | if (this.state.data.enabled) { 9 | return (

Pi-Hole

10 |
11 |
12 |

{ this.state.data.dns_queries_today }

13 |

DNS queries today

14 |
15 |
16 |

{ this.state.data.ads_blocked_today }

17 |

Ads blocked today

18 |
19 |
20 |

{ this.state.data.ads_percentage_today.toFixed(2) } %

21 |

Percentage blocked

22 |
23 |
24 |

{ this.state.data.blocked_domains }

25 |

Domains blocked

26 |
27 |
28 |
); 29 | } else { 30 | return (
); 31 | } 32 | 33 | } 34 | } 35 | 36 | 37 | export default Pihole; -------------------------------------------------------------------------------- /jsx/routes/components/processes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import TimedComponent from './timed_component.jsx'; 4 | 5 | 6 | class Processes extends TimedComponent{ 7 | render() { 8 | 9 | return (

Processes (Top 5)

10 | 11 | 12 | 13 | 14 | 15 | {this.state.data.slice(0,5).map(function(process ,i){ 16 | return ; 17 | })} 18 | 19 |
PIDNameCPU %
{process.pid}{process.name}{process.cpu_percentage}
20 |
21 | ); 22 | 23 | } 24 | } 25 | 26 | 27 | export default Processes; -------------------------------------------------------------------------------- /jsx/routes/components/progressbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | class ProgressBar extends React.Component { 5 | render() { 6 | 7 | return ( 8 |
9 |
10 | Used { (this.props.used/1073741824).toFixed(2) } / { (this.props.total/1073741824).toFixed(2) } Gb (Free: { (this.props.free/1073741824).toFixed(2) } Gb) 11 |
12 |
13 | ); 14 | 15 | } 16 | } 17 | 18 | export default ProgressBar; -------------------------------------------------------------------------------- /jsx/routes/components/swap.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ProgressBar from './progressbar.jsx'; 4 | import TimedComponent from './timed_component.jsx'; 5 | 6 | 7 | class Swap extends TimedComponent { 8 | render() { 9 | 10 | return (

Swap

11 | 12 |
13 | ); 14 | 15 | } 16 | } 17 | 18 | export default Swap; -------------------------------------------------------------------------------- /jsx/routes/components/temperature.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import TimedComponent from './timed_component.jsx'; 4 | 5 | 6 | class Temperature extends TimedComponent{ 7 | render() { 8 | return (
CPU Temp : { typeof(this.state.data.cpu) != "undefined" ? this.state.data.cpu.toFixed(1) : 0 } °C
); 9 | } 10 | } 11 | 12 | export default Temperature; -------------------------------------------------------------------------------- /jsx/routes/components/timed_component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | 4 | 5 | class TimedComponent extends React.Component{ 6 | constructor(props) { 7 | super(props); 8 | this.state = {data: []}; 9 | } 10 | 11 | loadFromServer() { 12 | axios.get(this.props.url) 13 | .then((response) => { 14 | this.setState({data: response.data}); 15 | }) 16 | .catch((err) => { 17 | console.error(err); 18 | }) 19 | } 20 | 21 | componentDidMount() { 22 | this.loadFromServer(); 23 | this.interval = setInterval(this.loadFromServer.bind(this), this.props.pollInterval); 24 | } 25 | 26 | componentWillUnmount() { 27 | clearInterval(this.interval); 28 | } 29 | } 30 | 31 | export default TimedComponent; -------------------------------------------------------------------------------- /jsx/routes/components/uptime.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import TimedComponent from './timed_component.jsx'; 4 | 5 | 6 | class Uptime extends TimedComponent{ 7 | render() { 8 | return (
Uptime : { this.state.data.uptime }
); 9 | } 10 | } 11 | 12 | export default Uptime; -------------------------------------------------------------------------------- /jsx/routes/network.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | 4 | import NetworkInternalDetails from './components/network-internal-details.jsx'; 5 | import Hostname from './components/hostname.jsx'; 6 | 7 | class Network extends React.Component{ 8 | constructor(props) { 9 | super(props); 10 | this.state = {data: []}; 11 | } 12 | 13 | loadFromServer() { 14 | axios.get(this.props.url) 15 | .then((response) => { 16 | this.setState({data: response.data}); 17 | }) 18 | .catch((err) => { 19 | console.error(err); 20 | }); 21 | } 22 | 23 | componentDidMount() { 24 | this.loadFromServer(); 25 | } 26 | 27 | render() { 28 | if (this.state.data.server) { 29 | return ( 30 |
31 | 32 |

Local

33 |
34 | 35 |
36 | ); 37 | } else { 38 | return (
) 39 | } 40 | } 41 | 42 | } 43 | 44 | export default Network; -------------------------------------------------------------------------------- /jsx/routes/panel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | 4 | import Logo from './components/logo.jsx'; 5 | import Hostname from './components/hostname.jsx'; 6 | import Uptime from './components/uptime.jsx'; 7 | import Temperature from './components/temperature.jsx'; 8 | import Memory from './components/memory.jsx'; 9 | import Swap from './components/swap.jsx'; 10 | import Disk from './components/disk.jsx'; 11 | import Processes from './components/processes.jsx' 12 | import Pihole from './components/pihole.jsx' 13 | import NetworkExternal from './components/network-external.jsx' 14 | import NetworkInternal from './components/network-internal.jsx' 15 | 16 | 17 | class Panel extends React.Component{ 18 | constructor(props) { 19 | super(props); 20 | this.state = {data: []}; 21 | } 22 | 23 | loadFromServer() { 24 | axios.get(this.props.url) 25 | .then((response) => { 26 | this.setState({data: response.data}); 27 | }) 28 | .catch((err) => { 29 | console.error(err); 30 | }); 31 | } 32 | 33 | componentDidMount() { 34 | this.loadFromServer(); 35 | } 36 | 37 | render() { 38 | if (this.state.data.server) { 39 | return ( 40 |
41 |
42 |
43 | 44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 |
52 |
53 |
54 | 55 | 56 | 57 |
58 | 59 |
60 | 61 |
62 | ); 63 | } else { 64 | return (
); 65 | } 66 | } 67 | } 68 | 69 | export default Panel; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flask-server-panel", 3 | "version": "1.0.0", 4 | "description": "A server panel based on react and flask", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/sepro/Flask-Server-Panel.git" 12 | }, 13 | "author": "Sebastian Proost", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/sepro/Flask-Server-Panel/issues" 17 | }, 18 | "homepage": "https://github.com/sepro/Flask-Server-Panel#readme", 19 | "dependencies": { 20 | "axios": "^0.19.0", 21 | "jquery": "^3.0.0", 22 | "react": "^15.1.0", 23 | "react-dom": "^15.1.0", 24 | "react-lite": "^0.15.39", 25 | "react-router": "^3.0.0" 26 | }, 27 | "devDependencies": { 28 | "babel-core": "^6.4.5", 29 | "babel-loader": "^6.2.1", 30 | "babel-preset-es2015": "^6.3.13", 31 | "babel-preset-react": "^6.3.13", 32 | "webpack": "^1.12.12" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | blinker==1.4 2 | certifi==2019.3.9 3 | chardet==3.0.4 4 | click==6.7 5 | codecov==2.0.15 6 | coverage==4.5.1 7 | Flask==1.0.2 8 | Flask-Compress==1.4.0 9 | Flask-Testing==0.7.1 10 | idna==2.8 11 | itsdangerous==0.24 12 | Jinja2==2.10.1 13 | MarkupSafe==1.1.1 14 | psutil==5.6.6 15 | PyExecJS==1.5.1 16 | requests==2.21.0 17 | six==1.11.0 18 | uptime==3.0.1 19 | urllib3==1.24.2 20 | Werkzeug==0.15.5 21 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from serverpanel import create_app 3 | 4 | app = create_app('config') 5 | 6 | app.run() 7 | -------------------------------------------------------------------------------- /serverpanel/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_compress import Compress 3 | 4 | from serverpanel.ext.serverinfo import ServerInfo 5 | 6 | server_info = ServerInfo() 7 | compress = Compress() 8 | 9 | 10 | def create_app(config): 11 | app = Flask(__name__) 12 | 13 | app.config.from_object(config) 14 | 15 | server_info.init_app(app) 16 | compress.init_app(app) 17 | 18 | from serverpanel.controllers import main 19 | from serverpanel.controllers import api 20 | 21 | app.register_blueprint(main) 22 | app.register_blueprint(api, url_prefix='/api') 23 | 24 | return app 25 | -------------------------------------------------------------------------------- /serverpanel/controllers.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, url_for 2 | from serverpanel import server_info 3 | from serverpanel.utils.jsonify import jsonify 4 | 5 | main = Blueprint('main', __name__) 6 | api = Blueprint('api', __name__) 7 | 8 | 9 | @main.route('/network/') 10 | @main.route('/') 11 | def index(): 12 | return render_template('main.html') 13 | 14 | 15 | @api.route('/') 16 | @jsonify 17 | def api_index(): 18 | return {'version': url_for('api.api_version'), 19 | 'server': { 20 | 'hostname': url_for('api.api_server_hostname'), 21 | 'os': url_for('api.api_server_os'), 22 | 'uptime': url_for('api.api_server_uptime') 23 | }, 24 | 'system': { 25 | 'memory': url_for('api.api_system_memory'), 26 | 'swap': url_for('api.api_system_swap'), 27 | 'processes': url_for('api.api_system_processes'), 28 | 'disk_space': url_for('api.api_system_disk_space'), 29 | 'temp': url_for('api.api_system_temp') 30 | }, 31 | 'network': { 32 | 'io': url_for('api.api_network_io'), 33 | 'external': url_for('api.api_network_external') 34 | }, 35 | 'pihole': { 36 | 'stats': url_for('api.api_pihole_stats') 37 | } 38 | } 39 | 40 | 41 | @api.route('/version') 42 | @jsonify 43 | def api_version(): 44 | return {'version': '1.0', 'name': 'Flask-Sever-Panel'} 45 | 46 | 47 | @api.route('/server') 48 | @jsonify 49 | def api_server(): 50 | return {'hostname': url_for('api.api_server_hostname'), 51 | 'os': url_for('api.api_server_os'), 52 | 'uptime': url_for('api.api_server_uptime') 53 | } 54 | 55 | 56 | @api.route('/server/hostname') 57 | @jsonify 58 | def api_server_hostname(): 59 | """ 60 | Returns hostname as json object 61 | :return: 62 | """ 63 | hostname = server_info.get_hostname() 64 | 65 | return {'hostname': hostname} 66 | 67 | 68 | @api.route('/server/os') 69 | @jsonify 70 | def api_server_os(): 71 | """ 72 | Returns operating system as json object 73 | :return: 74 | """ 75 | os_name = server_info.get_os_name() 76 | 77 | return {'os_name': os_name} 78 | 79 | 80 | @api.route('/server/uptime') 81 | @jsonify 82 | def api_server_uptime(): 83 | """ 84 | Returns uptime as json object 85 | :return: 86 | """ 87 | uptime = server_info.get_uptime() 88 | 89 | return {'uptime': uptime} 90 | 91 | 92 | @api.route('/system/cpu/cores') 93 | @jsonify 94 | def api_system_cpu_cores(): 95 | return server_info.get_cpu_cores() 96 | 97 | 98 | @api.route('/system/cpu/load') 99 | @jsonify 100 | def api_system_cpu_load(): 101 | return server_info.get_cpu_load() 102 | 103 | 104 | @api.route('/system/memory') 105 | @jsonify 106 | def api_system_memory(): 107 | return server_info.get_virtual_memory() 108 | 109 | 110 | @api.route('/system/swap') 111 | @jsonify 112 | def api_system_swap(): 113 | return server_info.get_swap_memory() 114 | 115 | 116 | @api.route('/system/disk/space') 117 | @jsonify 118 | def api_system_disk_space(): 119 | return server_info.get_disk_space() 120 | 121 | 122 | @api.route('/system/disk/io') 123 | @jsonify 124 | def api_system_disk_io(): 125 | return server_info.get_disk_io() 126 | 127 | 128 | @api.route('/system/processes') 129 | @jsonify 130 | def api_system_processes(): 131 | return server_info.get_processes() 132 | 133 | 134 | @api.route('/system/temp') 135 | @jsonify 136 | def api_system_temp(): 137 | return server_info.get_temperature() 138 | 139 | 140 | @api.route('/network/io') 141 | @jsonify 142 | def api_network_io(): 143 | return server_info.get_network_io() 144 | 145 | 146 | @api.route('/network/external') 147 | @jsonify 148 | def api_network_external(): 149 | return server_info.get_network_external() 150 | 151 | 152 | @api.route('/pihole/stats') 153 | @jsonify 154 | def api_pihole_stats(): 155 | return server_info.get_pihole_stats() 156 | -------------------------------------------------------------------------------- /serverpanel/ext/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /serverpanel/ext/serverinfo.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from requests import get 3 | 4 | import uptime 5 | import psutil 6 | import platform 7 | import math 8 | import socket 9 | import json 10 | import os 11 | import random 12 | 13 | 14 | class ServerInfo: 15 | def __init__(self, app=None): 16 | self.server_type = None 17 | 18 | self.pihole_enabled = False 19 | self.pihole_api = None 20 | 21 | self.cpu_temp = None 22 | 23 | self.external_network = None 24 | 25 | if app is not None: 26 | self.init_app(app) 27 | 28 | def init_app(self, app): 29 | app.extensions = getattr(app, 'extensions', {}) 30 | app.extensions['flask-serverinfo'] = self 31 | 32 | self.server_type = platform.system() 33 | self.pihole_enabled = app.config['ENABLE_PIHOLE'] if 'ENABLE_PIHOLE' in app.config.keys() else False 34 | self.pihole_api = app.config['PIHOLE_API'] if 'PIHOLE_API' in app.config.keys() else None 35 | 36 | self.cpu_temp = app.config['CPU_TEMP'] if 'CPU_TEMP' in app.config.keys() else None 37 | self.external_network = app.config['GEOLOCATOR'] if 'GEOLOCATOR' in app.config.keys() else None 38 | 39 | self.testing = app.config['TESTING'] 40 | 41 | # these two functions need to be run once to give non-zero output 42 | psutil.cpu_percent(percpu=True, interval=None) 43 | self.get_processes() 44 | 45 | @staticmethod 46 | def get_hostname(): 47 | return socket.gethostname() 48 | 49 | @staticmethod 50 | def get_os_name(): 51 | return platform.platform() 52 | 53 | @staticmethod 54 | def get_uptime(): 55 | uptime_seconds = uptime.uptime() 56 | uptime_str = str(timedelta(seconds=math.floor(uptime_seconds))) 57 | 58 | return uptime_str 59 | 60 | @staticmethod 61 | def get_cpu_cores(): 62 | data = {'logical_cores': psutil.cpu_count(), 63 | 'physical_cores': psutil.cpu_count(logical=False) 64 | } 65 | 66 | return data 67 | 68 | @staticmethod 69 | def get_cpu_load(): 70 | return psutil.cpu_percent(percpu=True, interval=None) 71 | 72 | @staticmethod 73 | def get_virtual_memory(): 74 | mem = psutil.virtual_memory()._asdict() 75 | 76 | return mem 77 | 78 | @staticmethod 79 | def get_swap_memory(): 80 | mem = psutil.swap_memory()._asdict() 81 | 82 | return mem 83 | 84 | @staticmethod 85 | def get_disk_space(): 86 | disk = [{'device': v.device, 87 | 'mountpoint': v.mountpoint, 88 | 'fstype': v.fstype, 89 | 'opts': v.opts, 90 | 'usage': psutil.disk_usage(v.mountpoint)._asdict()} 91 | for v in psutil.disk_partitions() if v.fstype != ''] 92 | 93 | return disk 94 | 95 | @staticmethod 96 | def get_disk_io(): 97 | disk_io = [{'device': k, 98 | 'io': v._asdict()} 99 | for k, v in psutil.disk_io_counters(perdisk=True).items()] 100 | 101 | return disk_io 102 | 103 | @staticmethod 104 | def get_network_io(): 105 | network_config = {} 106 | 107 | for device, values in psutil.net_if_addrs().items(): 108 | for value in values: 109 | if value.family == socket.AF_INET: 110 | network_config[device] = value.address 111 | break 112 | 113 | network_io = [{'device': k, 114 | 'io': v._asdict(), 115 | 'address': network_config[k]} 116 | for k, v in psutil.net_io_counters(pernic=True).items() if k in network_config.keys()] 117 | 118 | return network_io 119 | 120 | def get_network_external(self): 121 | output = {} 122 | try: 123 | data = get(self.external_network).text 124 | output = json.loads(data) 125 | except: 126 | pass 127 | 128 | if all([a in output.keys() for a in ["ip", "country"]]): 129 | return output 130 | else: 131 | return {'ip': 'Unknown', 'country': 'Unknown'} 132 | 133 | @staticmethod 134 | def get_processes(): 135 | processes = [] 136 | 137 | for proc in psutil.process_iter(): 138 | # Ignore PID 0 (System idle on windows machines) 139 | if proc.pid != 0: 140 | processes.append({'pid': proc.pid, 141 | 'name': proc.name(), 142 | 'cpu_percentage': proc.cpu_percent(interval=None)}) 143 | 144 | return sorted(processes, key=lambda k: k['cpu_percentage'], reverse=True) 145 | 146 | def get_temperature(self): 147 | cpu_temp = -1 148 | try: 149 | if os.path.exists(self.cpu_temp): 150 | with open(self.cpu_temp) as infile: 151 | cpu_temp = int(infile.read())/1000 152 | except: 153 | pass 154 | 155 | return {'cpu': cpu_temp} 156 | 157 | def get_pihole_stats(self): 158 | if self.pihole_enabled and self.pihole_api is not None: 159 | stats = { 160 | "enabled": self.pihole_enabled, 161 | "blocked_domains": 0, 162 | "dns_queries_today": 0, 163 | "ads_blocked_today": 0, 164 | "ads_percentage_today": 0, 165 | "error": False 166 | } 167 | try: 168 | data = json.loads(get(self.pihole_api).text) 169 | 170 | stats = { 171 | "enabled": self.pihole_enabled, 172 | "blocked_domains": data['domains_being_blocked'], 173 | "dns_queries_today": data['dns_queries_today'], 174 | "ads_blocked_today": data['ads_blocked_today'], 175 | "ads_percentage_today": float(data['ads_percentage_today']), 176 | "error": False 177 | } 178 | except: 179 | stats["error"] = True 180 | 181 | return stats 182 | else: 183 | return {"enabled": False, "error": False} 184 | -------------------------------------------------------------------------------- /serverpanel/static/css/main.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Progress bars with centered text 3 | */ 4 | 5 | .progress { 6 | position: relative; 7 | } 8 | 9 | .progress span.progress-label { 10 | position: absolute; 11 | display: block; 12 | width: 100%; 13 | color: black; 14 | } 15 | 16 | /** 17 | * SVG Logo 18 | */ 19 | 20 | .svg-container { 21 | width:200px 22 | } 23 | -------------------------------------------------------------------------------- /serverpanel/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sepro/Flask-Server-Panel/4763174aa8a6f74e72c14a67ec5be243b5e06ca2/serverpanel/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /serverpanel/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sepro/Flask-Server-Panel/4763174aa8a6f74e72c14a67ec5be243b5e06ca2/serverpanel/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /serverpanel/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sepro/Flask-Server-Panel/4763174aa8a6f74e72c14a67ec5be243b5e06ca2/serverpanel/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /serverpanel/static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sepro/Flask-Server-Panel/4763174aa8a6f74e72c14a67ec5be243b5e06ca2/serverpanel/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /serverpanel/static/img/svg/rpi_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /serverpanel/static/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.6 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | 7 | if (typeof jQuery === 'undefined') { 8 | throw new Error('Bootstrap\'s JavaScript requires jQuery') 9 | } 10 | 11 | +function ($) { 12 | 'use strict'; 13 | var version = $.fn.jquery.split(' ')[0].split('.') 14 | if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1) || (version[0] > 2)) { 15 | throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher, but lower than version 3') 16 | } 17 | }(jQuery); 18 | 19 | /* ======================================================================== 20 | * Bootstrap: transition.js v3.3.6 21 | * http://getbootstrap.com/javascript/#transitions 22 | * ======================================================================== 23 | * Copyright 2011-2015 Twitter, Inc. 24 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 25 | * ======================================================================== */ 26 | 27 | 28 | +function ($) { 29 | 'use strict'; 30 | 31 | // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) 32 | // ============================================================ 33 | 34 | function transitionEnd() { 35 | var el = document.createElement('bootstrap') 36 | 37 | var transEndEventNames = { 38 | WebkitTransition : 'webkitTransitionEnd', 39 | MozTransition : 'transitionend', 40 | OTransition : 'oTransitionEnd otransitionend', 41 | transition : 'transitionend' 42 | } 43 | 44 | for (var name in transEndEventNames) { 45 | if (el.style[name] !== undefined) { 46 | return { end: transEndEventNames[name] } 47 | } 48 | } 49 | 50 | return false // explicit for ie8 ( ._.) 51 | } 52 | 53 | // http://blog.alexmaccaw.com/css-transitions 54 | $.fn.emulateTransitionEnd = function (duration) { 55 | var called = false 56 | var $el = this 57 | $(this).one('bsTransitionEnd', function () { called = true }) 58 | var callback = function () { if (!called) $($el).trigger($.support.transition.end) } 59 | setTimeout(callback, duration) 60 | return this 61 | } 62 | 63 | $(function () { 64 | $.support.transition = transitionEnd() 65 | 66 | if (!$.support.transition) return 67 | 68 | $.event.special.bsTransitionEnd = { 69 | bindType: $.support.transition.end, 70 | delegateType: $.support.transition.end, 71 | handle: function (e) { 72 | if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) 73 | } 74 | } 75 | }) 76 | 77 | }(jQuery); 78 | 79 | /* ======================================================================== 80 | * Bootstrap: alert.js v3.3.6 81 | * http://getbootstrap.com/javascript/#alerts 82 | * ======================================================================== 83 | * Copyright 2011-2015 Twitter, Inc. 84 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 85 | * ======================================================================== */ 86 | 87 | 88 | +function ($) { 89 | 'use strict'; 90 | 91 | // ALERT CLASS DEFINITION 92 | // ====================== 93 | 94 | var dismiss = '[data-dismiss="alert"]' 95 | var Alert = function (el) { 96 | $(el).on('click', dismiss, this.close) 97 | } 98 | 99 | Alert.VERSION = '3.3.6' 100 | 101 | Alert.TRANSITION_DURATION = 150 102 | 103 | Alert.prototype.close = function (e) { 104 | var $this = $(this) 105 | var selector = $this.attr('data-target') 106 | 107 | if (!selector) { 108 | selector = $this.attr('href') 109 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 110 | } 111 | 112 | var $parent = $(selector) 113 | 114 | if (e) e.preventDefault() 115 | 116 | if (!$parent.length) { 117 | $parent = $this.closest('.alert') 118 | } 119 | 120 | $parent.trigger(e = $.Event('close.bs.alert')) 121 | 122 | if (e.isDefaultPrevented()) return 123 | 124 | $parent.removeClass('in') 125 | 126 | function removeElement() { 127 | // detach from parent, fire event then clean up data 128 | $parent.detach().trigger('closed.bs.alert').remove() 129 | } 130 | 131 | $.support.transition && $parent.hasClass('fade') ? 132 | $parent 133 | .one('bsTransitionEnd', removeElement) 134 | .emulateTransitionEnd(Alert.TRANSITION_DURATION) : 135 | removeElement() 136 | } 137 | 138 | 139 | // ALERT PLUGIN DEFINITION 140 | // ======================= 141 | 142 | function Plugin(option) { 143 | return this.each(function () { 144 | var $this = $(this) 145 | var data = $this.data('bs.alert') 146 | 147 | if (!data) $this.data('bs.alert', (data = new Alert(this))) 148 | if (typeof option == 'string') data[option].call($this) 149 | }) 150 | } 151 | 152 | var old = $.fn.alert 153 | 154 | $.fn.alert = Plugin 155 | $.fn.alert.Constructor = Alert 156 | 157 | 158 | // ALERT NO CONFLICT 159 | // ================= 160 | 161 | $.fn.alert.noConflict = function () { 162 | $.fn.alert = old 163 | return this 164 | } 165 | 166 | 167 | // ALERT DATA-API 168 | // ============== 169 | 170 | $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) 171 | 172 | }(jQuery); 173 | 174 | /* ======================================================================== 175 | * Bootstrap: button.js v3.3.6 176 | * http://getbootstrap.com/javascript/#buttons 177 | * ======================================================================== 178 | * Copyright 2011-2015 Twitter, Inc. 179 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 180 | * ======================================================================== */ 181 | 182 | 183 | +function ($) { 184 | 'use strict'; 185 | 186 | // BUTTON PUBLIC CLASS DEFINITION 187 | // ============================== 188 | 189 | var Button = function (element, options) { 190 | this.$element = $(element) 191 | this.options = $.extend({}, Button.DEFAULTS, options) 192 | this.isLoading = false 193 | } 194 | 195 | Button.VERSION = '3.3.6' 196 | 197 | Button.DEFAULTS = { 198 | loadingText: 'loading...' 199 | } 200 | 201 | Button.prototype.setState = function (state) { 202 | var d = 'disabled' 203 | var $el = this.$element 204 | var val = $el.is('input') ? 'val' : 'html' 205 | var data = $el.data() 206 | 207 | state += 'Text' 208 | 209 | if (data.resetText == null) $el.data('resetText', $el[val]()) 210 | 211 | // push to event loop to allow forms to submit 212 | setTimeout($.proxy(function () { 213 | $el[val](data[state] == null ? this.options[state] : data[state]) 214 | 215 | if (state == 'loadingText') { 216 | this.isLoading = true 217 | $el.addClass(d).attr(d, d) 218 | } else if (this.isLoading) { 219 | this.isLoading = false 220 | $el.removeClass(d).removeAttr(d) 221 | } 222 | }, this), 0) 223 | } 224 | 225 | Button.prototype.toggle = function () { 226 | var changed = true 227 | var $parent = this.$element.closest('[data-toggle="buttons"]') 228 | 229 | if ($parent.length) { 230 | var $input = this.$element.find('input') 231 | if ($input.prop('type') == 'radio') { 232 | if ($input.prop('checked')) changed = false 233 | $parent.find('.active').removeClass('active') 234 | this.$element.addClass('active') 235 | } else if ($input.prop('type') == 'checkbox') { 236 | if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false 237 | this.$element.toggleClass('active') 238 | } 239 | $input.prop('checked', this.$element.hasClass('active')) 240 | if (changed) $input.trigger('change') 241 | } else { 242 | this.$element.attr('aria-pressed', !this.$element.hasClass('active')) 243 | this.$element.toggleClass('active') 244 | } 245 | } 246 | 247 | 248 | // BUTTON PLUGIN DEFINITION 249 | // ======================== 250 | 251 | function Plugin(option) { 252 | return this.each(function () { 253 | var $this = $(this) 254 | var data = $this.data('bs.button') 255 | var options = typeof option == 'object' && option 256 | 257 | if (!data) $this.data('bs.button', (data = new Button(this, options))) 258 | 259 | if (option == 'toggle') data.toggle() 260 | else if (option) data.setState(option) 261 | }) 262 | } 263 | 264 | var old = $.fn.button 265 | 266 | $.fn.button = Plugin 267 | $.fn.button.Constructor = Button 268 | 269 | 270 | // BUTTON NO CONFLICT 271 | // ================== 272 | 273 | $.fn.button.noConflict = function () { 274 | $.fn.button = old 275 | return this 276 | } 277 | 278 | 279 | // BUTTON DATA-API 280 | // =============== 281 | 282 | $(document) 283 | .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { 284 | var $btn = $(e.target) 285 | if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') 286 | Plugin.call($btn, 'toggle') 287 | if (!($(e.target).is('input[type="radio"]') || $(e.target).is('input[type="checkbox"]'))) e.preventDefault() 288 | }) 289 | .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { 290 | $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) 291 | }) 292 | 293 | }(jQuery); 294 | 295 | /* ======================================================================== 296 | * Bootstrap: carousel.js v3.3.6 297 | * http://getbootstrap.com/javascript/#carousel 298 | * ======================================================================== 299 | * Copyright 2011-2015 Twitter, Inc. 300 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 301 | * ======================================================================== */ 302 | 303 | 304 | +function ($) { 305 | 'use strict'; 306 | 307 | // CAROUSEL CLASS DEFINITION 308 | // ========================= 309 | 310 | var Carousel = function (element, options) { 311 | this.$element = $(element) 312 | this.$indicators = this.$element.find('.carousel-indicators') 313 | this.options = options 314 | this.paused = null 315 | this.sliding = null 316 | this.interval = null 317 | this.$active = null 318 | this.$items = null 319 | 320 | this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) 321 | 322 | this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element 323 | .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) 324 | .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) 325 | } 326 | 327 | Carousel.VERSION = '3.3.6' 328 | 329 | Carousel.TRANSITION_DURATION = 600 330 | 331 | Carousel.DEFAULTS = { 332 | interval: 5000, 333 | pause: 'hover', 334 | wrap: true, 335 | keyboard: true 336 | } 337 | 338 | Carousel.prototype.keydown = function (e) { 339 | if (/input|textarea/i.test(e.target.tagName)) return 340 | switch (e.which) { 341 | case 37: this.prev(); break 342 | case 39: this.next(); break 343 | default: return 344 | } 345 | 346 | e.preventDefault() 347 | } 348 | 349 | Carousel.prototype.cycle = function (e) { 350 | e || (this.paused = false) 351 | 352 | this.interval && clearInterval(this.interval) 353 | 354 | this.options.interval 355 | && !this.paused 356 | && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) 357 | 358 | return this 359 | } 360 | 361 | Carousel.prototype.getItemIndex = function (item) { 362 | this.$items = item.parent().children('.item') 363 | return this.$items.index(item || this.$active) 364 | } 365 | 366 | Carousel.prototype.getItemForDirection = function (direction, active) { 367 | var activeIndex = this.getItemIndex(active) 368 | var willWrap = (direction == 'prev' && activeIndex === 0) 369 | || (direction == 'next' && activeIndex == (this.$items.length - 1)) 370 | if (willWrap && !this.options.wrap) return active 371 | var delta = direction == 'prev' ? -1 : 1 372 | var itemIndex = (activeIndex + delta) % this.$items.length 373 | return this.$items.eq(itemIndex) 374 | } 375 | 376 | Carousel.prototype.to = function (pos) { 377 | var that = this 378 | var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) 379 | 380 | if (pos > (this.$items.length - 1) || pos < 0) return 381 | 382 | if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" 383 | if (activeIndex == pos) return this.pause().cycle() 384 | 385 | return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos)) 386 | } 387 | 388 | Carousel.prototype.pause = function (e) { 389 | e || (this.paused = true) 390 | 391 | if (this.$element.find('.next, .prev').length && $.support.transition) { 392 | this.$element.trigger($.support.transition.end) 393 | this.cycle(true) 394 | } 395 | 396 | this.interval = clearInterval(this.interval) 397 | 398 | return this 399 | } 400 | 401 | Carousel.prototype.next = function () { 402 | if (this.sliding) return 403 | return this.slide('next') 404 | } 405 | 406 | Carousel.prototype.prev = function () { 407 | if (this.sliding) return 408 | return this.slide('prev') 409 | } 410 | 411 | Carousel.prototype.slide = function (type, next) { 412 | var $active = this.$element.find('.item.active') 413 | var $next = next || this.getItemForDirection(type, $active) 414 | var isCycling = this.interval 415 | var direction = type == 'next' ? 'left' : 'right' 416 | var that = this 417 | 418 | if ($next.hasClass('active')) return (this.sliding = false) 419 | 420 | var relatedTarget = $next[0] 421 | var slideEvent = $.Event('slide.bs.carousel', { 422 | relatedTarget: relatedTarget, 423 | direction: direction 424 | }) 425 | this.$element.trigger(slideEvent) 426 | if (slideEvent.isDefaultPrevented()) return 427 | 428 | this.sliding = true 429 | 430 | isCycling && this.pause() 431 | 432 | if (this.$indicators.length) { 433 | this.$indicators.find('.active').removeClass('active') 434 | var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) 435 | $nextIndicator && $nextIndicator.addClass('active') 436 | } 437 | 438 | var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" 439 | if ($.support.transition && this.$element.hasClass('slide')) { 440 | $next.addClass(type) 441 | $next[0].offsetWidth // force reflow 442 | $active.addClass(direction) 443 | $next.addClass(direction) 444 | $active 445 | .one('bsTransitionEnd', function () { 446 | $next.removeClass([type, direction].join(' ')).addClass('active') 447 | $active.removeClass(['active', direction].join(' ')) 448 | that.sliding = false 449 | setTimeout(function () { 450 | that.$element.trigger(slidEvent) 451 | }, 0) 452 | }) 453 | .emulateTransitionEnd(Carousel.TRANSITION_DURATION) 454 | } else { 455 | $active.removeClass('active') 456 | $next.addClass('active') 457 | this.sliding = false 458 | this.$element.trigger(slidEvent) 459 | } 460 | 461 | isCycling && this.cycle() 462 | 463 | return this 464 | } 465 | 466 | 467 | // CAROUSEL PLUGIN DEFINITION 468 | // ========================== 469 | 470 | function Plugin(option) { 471 | return this.each(function () { 472 | var $this = $(this) 473 | var data = $this.data('bs.carousel') 474 | var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) 475 | var action = typeof option == 'string' ? option : options.slide 476 | 477 | if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) 478 | if (typeof option == 'number') data.to(option) 479 | else if (action) data[action]() 480 | else if (options.interval) data.pause().cycle() 481 | }) 482 | } 483 | 484 | var old = $.fn.carousel 485 | 486 | $.fn.carousel = Plugin 487 | $.fn.carousel.Constructor = Carousel 488 | 489 | 490 | // CAROUSEL NO CONFLICT 491 | // ==================== 492 | 493 | $.fn.carousel.noConflict = function () { 494 | $.fn.carousel = old 495 | return this 496 | } 497 | 498 | 499 | // CAROUSEL DATA-API 500 | // ================= 501 | 502 | var clickHandler = function (e) { 503 | var href 504 | var $this = $(this) 505 | var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 506 | if (!$target.hasClass('carousel')) return 507 | var options = $.extend({}, $target.data(), $this.data()) 508 | var slideIndex = $this.attr('data-slide-to') 509 | if (slideIndex) options.interval = false 510 | 511 | Plugin.call($target, options) 512 | 513 | if (slideIndex) { 514 | $target.data('bs.carousel').to(slideIndex) 515 | } 516 | 517 | e.preventDefault() 518 | } 519 | 520 | $(document) 521 | .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) 522 | .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) 523 | 524 | $(window).on('load', function () { 525 | $('[data-ride="carousel"]').each(function () { 526 | var $carousel = $(this) 527 | Plugin.call($carousel, $carousel.data()) 528 | }) 529 | }) 530 | 531 | }(jQuery); 532 | 533 | /* ======================================================================== 534 | * Bootstrap: collapse.js v3.3.6 535 | * http://getbootstrap.com/javascript/#collapse 536 | * ======================================================================== 537 | * Copyright 2011-2015 Twitter, Inc. 538 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 539 | * ======================================================================== */ 540 | 541 | 542 | +function ($) { 543 | 'use strict'; 544 | 545 | // COLLAPSE PUBLIC CLASS DEFINITION 546 | // ================================ 547 | 548 | var Collapse = function (element, options) { 549 | this.$element = $(element) 550 | this.options = $.extend({}, Collapse.DEFAULTS, options) 551 | this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + 552 | '[data-toggle="collapse"][data-target="#' + element.id + '"]') 553 | this.transitioning = null 554 | 555 | if (this.options.parent) { 556 | this.$parent = this.getParent() 557 | } else { 558 | this.addAriaAndCollapsedClass(this.$element, this.$trigger) 559 | } 560 | 561 | if (this.options.toggle) this.toggle() 562 | } 563 | 564 | Collapse.VERSION = '3.3.6' 565 | 566 | Collapse.TRANSITION_DURATION = 350 567 | 568 | Collapse.DEFAULTS = { 569 | toggle: true 570 | } 571 | 572 | Collapse.prototype.dimension = function () { 573 | var hasWidth = this.$element.hasClass('width') 574 | return hasWidth ? 'width' : 'height' 575 | } 576 | 577 | Collapse.prototype.show = function () { 578 | if (this.transitioning || this.$element.hasClass('in')) return 579 | 580 | var activesData 581 | var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') 582 | 583 | if (actives && actives.length) { 584 | activesData = actives.data('bs.collapse') 585 | if (activesData && activesData.transitioning) return 586 | } 587 | 588 | var startEvent = $.Event('show.bs.collapse') 589 | this.$element.trigger(startEvent) 590 | if (startEvent.isDefaultPrevented()) return 591 | 592 | if (actives && actives.length) { 593 | Plugin.call(actives, 'hide') 594 | activesData || actives.data('bs.collapse', null) 595 | } 596 | 597 | var dimension = this.dimension() 598 | 599 | this.$element 600 | .removeClass('collapse') 601 | .addClass('collapsing')[dimension](0) 602 | .attr('aria-expanded', true) 603 | 604 | this.$trigger 605 | .removeClass('collapsed') 606 | .attr('aria-expanded', true) 607 | 608 | this.transitioning = 1 609 | 610 | var complete = function () { 611 | this.$element 612 | .removeClass('collapsing') 613 | .addClass('collapse in')[dimension]('') 614 | this.transitioning = 0 615 | this.$element 616 | .trigger('shown.bs.collapse') 617 | } 618 | 619 | if (!$.support.transition) return complete.call(this) 620 | 621 | var scrollSize = $.camelCase(['scroll', dimension].join('-')) 622 | 623 | this.$element 624 | .one('bsTransitionEnd', $.proxy(complete, this)) 625 | .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) 626 | } 627 | 628 | Collapse.prototype.hide = function () { 629 | if (this.transitioning || !this.$element.hasClass('in')) return 630 | 631 | var startEvent = $.Event('hide.bs.collapse') 632 | this.$element.trigger(startEvent) 633 | if (startEvent.isDefaultPrevented()) return 634 | 635 | var dimension = this.dimension() 636 | 637 | this.$element[dimension](this.$element[dimension]())[0].offsetHeight 638 | 639 | this.$element 640 | .addClass('collapsing') 641 | .removeClass('collapse in') 642 | .attr('aria-expanded', false) 643 | 644 | this.$trigger 645 | .addClass('collapsed') 646 | .attr('aria-expanded', false) 647 | 648 | this.transitioning = 1 649 | 650 | var complete = function () { 651 | this.transitioning = 0 652 | this.$element 653 | .removeClass('collapsing') 654 | .addClass('collapse') 655 | .trigger('hidden.bs.collapse') 656 | } 657 | 658 | if (!$.support.transition) return complete.call(this) 659 | 660 | this.$element 661 | [dimension](0) 662 | .one('bsTransitionEnd', $.proxy(complete, this)) 663 | .emulateTransitionEnd(Collapse.TRANSITION_DURATION) 664 | } 665 | 666 | Collapse.prototype.toggle = function () { 667 | this[this.$element.hasClass('in') ? 'hide' : 'show']() 668 | } 669 | 670 | Collapse.prototype.getParent = function () { 671 | return $(this.options.parent) 672 | .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') 673 | .each($.proxy(function (i, element) { 674 | var $element = $(element) 675 | this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) 676 | }, this)) 677 | .end() 678 | } 679 | 680 | Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { 681 | var isOpen = $element.hasClass('in') 682 | 683 | $element.attr('aria-expanded', isOpen) 684 | $trigger 685 | .toggleClass('collapsed', !isOpen) 686 | .attr('aria-expanded', isOpen) 687 | } 688 | 689 | function getTargetFromTrigger($trigger) { 690 | var href 691 | var target = $trigger.attr('data-target') 692 | || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 693 | 694 | return $(target) 695 | } 696 | 697 | 698 | // COLLAPSE PLUGIN DEFINITION 699 | // ========================== 700 | 701 | function Plugin(option) { 702 | return this.each(function () { 703 | var $this = $(this) 704 | var data = $this.data('bs.collapse') 705 | var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) 706 | 707 | if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false 708 | if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) 709 | if (typeof option == 'string') data[option]() 710 | }) 711 | } 712 | 713 | var old = $.fn.collapse 714 | 715 | $.fn.collapse = Plugin 716 | $.fn.collapse.Constructor = Collapse 717 | 718 | 719 | // COLLAPSE NO CONFLICT 720 | // ==================== 721 | 722 | $.fn.collapse.noConflict = function () { 723 | $.fn.collapse = old 724 | return this 725 | } 726 | 727 | 728 | // COLLAPSE DATA-API 729 | // ================= 730 | 731 | $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { 732 | var $this = $(this) 733 | 734 | if (!$this.attr('data-target')) e.preventDefault() 735 | 736 | var $target = getTargetFromTrigger($this) 737 | var data = $target.data('bs.collapse') 738 | var option = data ? 'toggle' : $this.data() 739 | 740 | Plugin.call($target, option) 741 | }) 742 | 743 | }(jQuery); 744 | 745 | /* ======================================================================== 746 | * Bootstrap: dropdown.js v3.3.6 747 | * http://getbootstrap.com/javascript/#dropdowns 748 | * ======================================================================== 749 | * Copyright 2011-2015 Twitter, Inc. 750 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 751 | * ======================================================================== */ 752 | 753 | 754 | +function ($) { 755 | 'use strict'; 756 | 757 | // DROPDOWN CLASS DEFINITION 758 | // ========================= 759 | 760 | var backdrop = '.dropdown-backdrop' 761 | var toggle = '[data-toggle="dropdown"]' 762 | var Dropdown = function (element) { 763 | $(element).on('click.bs.dropdown', this.toggle) 764 | } 765 | 766 | Dropdown.VERSION = '3.3.6' 767 | 768 | function getParent($this) { 769 | var selector = $this.attr('data-target') 770 | 771 | if (!selector) { 772 | selector = $this.attr('href') 773 | selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 774 | } 775 | 776 | var $parent = selector && $(selector) 777 | 778 | return $parent && $parent.length ? $parent : $this.parent() 779 | } 780 | 781 | function clearMenus(e) { 782 | if (e && e.which === 3) return 783 | $(backdrop).remove() 784 | $(toggle).each(function () { 785 | var $this = $(this) 786 | var $parent = getParent($this) 787 | var relatedTarget = { relatedTarget: this } 788 | 789 | if (!$parent.hasClass('open')) return 790 | 791 | if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return 792 | 793 | $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) 794 | 795 | if (e.isDefaultPrevented()) return 796 | 797 | $this.attr('aria-expanded', 'false') 798 | $parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget)) 799 | }) 800 | } 801 | 802 | Dropdown.prototype.toggle = function (e) { 803 | var $this = $(this) 804 | 805 | if ($this.is('.disabled, :disabled')) return 806 | 807 | var $parent = getParent($this) 808 | var isActive = $parent.hasClass('open') 809 | 810 | clearMenus() 811 | 812 | if (!isActive) { 813 | if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { 814 | // if mobile we use a backdrop because click events don't delegate 815 | $(document.createElement('div')) 816 | .addClass('dropdown-backdrop') 817 | .insertAfter($(this)) 818 | .on('click', clearMenus) 819 | } 820 | 821 | var relatedTarget = { relatedTarget: this } 822 | $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) 823 | 824 | if (e.isDefaultPrevented()) return 825 | 826 | $this 827 | .trigger('focus') 828 | .attr('aria-expanded', 'true') 829 | 830 | $parent 831 | .toggleClass('open') 832 | .trigger($.Event('shown.bs.dropdown', relatedTarget)) 833 | } 834 | 835 | return false 836 | } 837 | 838 | Dropdown.prototype.keydown = function (e) { 839 | if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return 840 | 841 | var $this = $(this) 842 | 843 | e.preventDefault() 844 | e.stopPropagation() 845 | 846 | if ($this.is('.disabled, :disabled')) return 847 | 848 | var $parent = getParent($this) 849 | var isActive = $parent.hasClass('open') 850 | 851 | if (!isActive && e.which != 27 || isActive && e.which == 27) { 852 | if (e.which == 27) $parent.find(toggle).trigger('focus') 853 | return $this.trigger('click') 854 | } 855 | 856 | var desc = ' li:not(.disabled):visible a' 857 | var $items = $parent.find('.dropdown-menu' + desc) 858 | 859 | if (!$items.length) return 860 | 861 | var index = $items.index(e.target) 862 | 863 | if (e.which == 38 && index > 0) index-- // up 864 | if (e.which == 40 && index < $items.length - 1) index++ // down 865 | if (!~index) index = 0 866 | 867 | $items.eq(index).trigger('focus') 868 | } 869 | 870 | 871 | // DROPDOWN PLUGIN DEFINITION 872 | // ========================== 873 | 874 | function Plugin(option) { 875 | return this.each(function () { 876 | var $this = $(this) 877 | var data = $this.data('bs.dropdown') 878 | 879 | if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) 880 | if (typeof option == 'string') data[option].call($this) 881 | }) 882 | } 883 | 884 | var old = $.fn.dropdown 885 | 886 | $.fn.dropdown = Plugin 887 | $.fn.dropdown.Constructor = Dropdown 888 | 889 | 890 | // DROPDOWN NO CONFLICT 891 | // ==================== 892 | 893 | $.fn.dropdown.noConflict = function () { 894 | $.fn.dropdown = old 895 | return this 896 | } 897 | 898 | 899 | // APPLY TO STANDARD DROPDOWN ELEMENTS 900 | // =================================== 901 | 902 | $(document) 903 | .on('click.bs.dropdown.data-api', clearMenus) 904 | .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) 905 | .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) 906 | .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) 907 | .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) 908 | 909 | }(jQuery); 910 | 911 | /* ======================================================================== 912 | * Bootstrap: modal.js v3.3.6 913 | * http://getbootstrap.com/javascript/#modals 914 | * ======================================================================== 915 | * Copyright 2011-2015 Twitter, Inc. 916 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 917 | * ======================================================================== */ 918 | 919 | 920 | +function ($) { 921 | 'use strict'; 922 | 923 | // MODAL CLASS DEFINITION 924 | // ====================== 925 | 926 | var Modal = function (element, options) { 927 | this.options = options 928 | this.$body = $(document.body) 929 | this.$element = $(element) 930 | this.$dialog = this.$element.find('.modal-dialog') 931 | this.$backdrop = null 932 | this.isShown = null 933 | this.originalBodyPad = null 934 | this.scrollbarWidth = 0 935 | this.ignoreBackdropClick = false 936 | 937 | if (this.options.remote) { 938 | this.$element 939 | .find('.modal-content') 940 | .load(this.options.remote, $.proxy(function () { 941 | this.$element.trigger('loaded.bs.modal') 942 | }, this)) 943 | } 944 | } 945 | 946 | Modal.VERSION = '3.3.6' 947 | 948 | Modal.TRANSITION_DURATION = 300 949 | Modal.BACKDROP_TRANSITION_DURATION = 150 950 | 951 | Modal.DEFAULTS = { 952 | backdrop: true, 953 | keyboard: true, 954 | show: true 955 | } 956 | 957 | Modal.prototype.toggle = function (_relatedTarget) { 958 | return this.isShown ? this.hide() : this.show(_relatedTarget) 959 | } 960 | 961 | Modal.prototype.show = function (_relatedTarget) { 962 | var that = this 963 | var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) 964 | 965 | this.$element.trigger(e) 966 | 967 | if (this.isShown || e.isDefaultPrevented()) return 968 | 969 | this.isShown = true 970 | 971 | this.checkScrollbar() 972 | this.setScrollbar() 973 | this.$body.addClass('modal-open') 974 | 975 | this.escape() 976 | this.resize() 977 | 978 | this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) 979 | 980 | this.$dialog.on('mousedown.dismiss.bs.modal', function () { 981 | that.$element.one('mouseup.dismiss.bs.modal', function (e) { 982 | if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true 983 | }) 984 | }) 985 | 986 | this.backdrop(function () { 987 | var transition = $.support.transition && that.$element.hasClass('fade') 988 | 989 | if (!that.$element.parent().length) { 990 | that.$element.appendTo(that.$body) // don't move modals dom position 991 | } 992 | 993 | that.$element 994 | .show() 995 | .scrollTop(0) 996 | 997 | that.adjustDialog() 998 | 999 | if (transition) { 1000 | that.$element[0].offsetWidth // force reflow 1001 | } 1002 | 1003 | that.$element.addClass('in') 1004 | 1005 | that.enforceFocus() 1006 | 1007 | var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) 1008 | 1009 | transition ? 1010 | that.$dialog // wait for modal to slide in 1011 | .one('bsTransitionEnd', function () { 1012 | that.$element.trigger('focus').trigger(e) 1013 | }) 1014 | .emulateTransitionEnd(Modal.TRANSITION_DURATION) : 1015 | that.$element.trigger('focus').trigger(e) 1016 | }) 1017 | } 1018 | 1019 | Modal.prototype.hide = function (e) { 1020 | if (e) e.preventDefault() 1021 | 1022 | e = $.Event('hide.bs.modal') 1023 | 1024 | this.$element.trigger(e) 1025 | 1026 | if (!this.isShown || e.isDefaultPrevented()) return 1027 | 1028 | this.isShown = false 1029 | 1030 | this.escape() 1031 | this.resize() 1032 | 1033 | $(document).off('focusin.bs.modal') 1034 | 1035 | this.$element 1036 | .removeClass('in') 1037 | .off('click.dismiss.bs.modal') 1038 | .off('mouseup.dismiss.bs.modal') 1039 | 1040 | this.$dialog.off('mousedown.dismiss.bs.modal') 1041 | 1042 | $.support.transition && this.$element.hasClass('fade') ? 1043 | this.$element 1044 | .one('bsTransitionEnd', $.proxy(this.hideModal, this)) 1045 | .emulateTransitionEnd(Modal.TRANSITION_DURATION) : 1046 | this.hideModal() 1047 | } 1048 | 1049 | Modal.prototype.enforceFocus = function () { 1050 | $(document) 1051 | .off('focusin.bs.modal') // guard against infinite focus loop 1052 | .on('focusin.bs.modal', $.proxy(function (e) { 1053 | if (this.$element[0] !== e.target && !this.$element.has(e.target).length) { 1054 | this.$element.trigger('focus') 1055 | } 1056 | }, this)) 1057 | } 1058 | 1059 | Modal.prototype.escape = function () { 1060 | if (this.isShown && this.options.keyboard) { 1061 | this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { 1062 | e.which == 27 && this.hide() 1063 | }, this)) 1064 | } else if (!this.isShown) { 1065 | this.$element.off('keydown.dismiss.bs.modal') 1066 | } 1067 | } 1068 | 1069 | Modal.prototype.resize = function () { 1070 | if (this.isShown) { 1071 | $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) 1072 | } else { 1073 | $(window).off('resize.bs.modal') 1074 | } 1075 | } 1076 | 1077 | Modal.prototype.hideModal = function () { 1078 | var that = this 1079 | this.$element.hide() 1080 | this.backdrop(function () { 1081 | that.$body.removeClass('modal-open') 1082 | that.resetAdjustments() 1083 | that.resetScrollbar() 1084 | that.$element.trigger('hidden.bs.modal') 1085 | }) 1086 | } 1087 | 1088 | Modal.prototype.removeBackdrop = function () { 1089 | this.$backdrop && this.$backdrop.remove() 1090 | this.$backdrop = null 1091 | } 1092 | 1093 | Modal.prototype.backdrop = function (callback) { 1094 | var that = this 1095 | var animate = this.$element.hasClass('fade') ? 'fade' : '' 1096 | 1097 | if (this.isShown && this.options.backdrop) { 1098 | var doAnimate = $.support.transition && animate 1099 | 1100 | this.$backdrop = $(document.createElement('div')) 1101 | .addClass('modal-backdrop ' + animate) 1102 | .appendTo(this.$body) 1103 | 1104 | this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { 1105 | if (this.ignoreBackdropClick) { 1106 | this.ignoreBackdropClick = false 1107 | return 1108 | } 1109 | if (e.target !== e.currentTarget) return 1110 | this.options.backdrop == 'static' 1111 | ? this.$element[0].focus() 1112 | : this.hide() 1113 | }, this)) 1114 | 1115 | if (doAnimate) this.$backdrop[0].offsetWidth // force reflow 1116 | 1117 | this.$backdrop.addClass('in') 1118 | 1119 | if (!callback) return 1120 | 1121 | doAnimate ? 1122 | this.$backdrop 1123 | .one('bsTransitionEnd', callback) 1124 | .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : 1125 | callback() 1126 | 1127 | } else if (!this.isShown && this.$backdrop) { 1128 | this.$backdrop.removeClass('in') 1129 | 1130 | var callbackRemove = function () { 1131 | that.removeBackdrop() 1132 | callback && callback() 1133 | } 1134 | $.support.transition && this.$element.hasClass('fade') ? 1135 | this.$backdrop 1136 | .one('bsTransitionEnd', callbackRemove) 1137 | .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : 1138 | callbackRemove() 1139 | 1140 | } else if (callback) { 1141 | callback() 1142 | } 1143 | } 1144 | 1145 | // these following methods are used to handle overflowing modals 1146 | 1147 | Modal.prototype.handleUpdate = function () { 1148 | this.adjustDialog() 1149 | } 1150 | 1151 | Modal.prototype.adjustDialog = function () { 1152 | var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight 1153 | 1154 | this.$element.css({ 1155 | paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', 1156 | paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' 1157 | }) 1158 | } 1159 | 1160 | Modal.prototype.resetAdjustments = function () { 1161 | this.$element.css({ 1162 | paddingLeft: '', 1163 | paddingRight: '' 1164 | }) 1165 | } 1166 | 1167 | Modal.prototype.checkScrollbar = function () { 1168 | var fullWindowWidth = window.innerWidth 1169 | if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 1170 | var documentElementRect = document.documentElement.getBoundingClientRect() 1171 | fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) 1172 | } 1173 | this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth 1174 | this.scrollbarWidth = this.measureScrollbar() 1175 | } 1176 | 1177 | Modal.prototype.setScrollbar = function () { 1178 | var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) 1179 | this.originalBodyPad = document.body.style.paddingRight || '' 1180 | if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth) 1181 | } 1182 | 1183 | Modal.prototype.resetScrollbar = function () { 1184 | this.$body.css('padding-right', this.originalBodyPad) 1185 | } 1186 | 1187 | Modal.prototype.measureScrollbar = function () { // thx walsh 1188 | var scrollDiv = document.createElement('div') 1189 | scrollDiv.className = 'modal-scrollbar-measure' 1190 | this.$body.append(scrollDiv) 1191 | var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth 1192 | this.$body[0].removeChild(scrollDiv) 1193 | return scrollbarWidth 1194 | } 1195 | 1196 | 1197 | // MODAL PLUGIN DEFINITION 1198 | // ======================= 1199 | 1200 | function Plugin(option, _relatedTarget) { 1201 | return this.each(function () { 1202 | var $this = $(this) 1203 | var data = $this.data('bs.modal') 1204 | var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) 1205 | 1206 | if (!data) $this.data('bs.modal', (data = new Modal(this, options))) 1207 | if (typeof option == 'string') data[option](_relatedTarget) 1208 | else if (options.show) data.show(_relatedTarget) 1209 | }) 1210 | } 1211 | 1212 | var old = $.fn.modal 1213 | 1214 | $.fn.modal = Plugin 1215 | $.fn.modal.Constructor = Modal 1216 | 1217 | 1218 | // MODAL NO CONFLICT 1219 | // ================= 1220 | 1221 | $.fn.modal.noConflict = function () { 1222 | $.fn.modal = old 1223 | return this 1224 | } 1225 | 1226 | 1227 | // MODAL DATA-API 1228 | // ============== 1229 | 1230 | $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { 1231 | var $this = $(this) 1232 | var href = $this.attr('href') 1233 | var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 1234 | var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) 1235 | 1236 | if ($this.is('a')) e.preventDefault() 1237 | 1238 | $target.one('show.bs.modal', function (showEvent) { 1239 | if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown 1240 | $target.one('hidden.bs.modal', function () { 1241 | $this.is(':visible') && $this.trigger('focus') 1242 | }) 1243 | }) 1244 | Plugin.call($target, option, this) 1245 | }) 1246 | 1247 | }(jQuery); 1248 | 1249 | /* ======================================================================== 1250 | * Bootstrap: tooltip.js v3.3.6 1251 | * http://getbootstrap.com/javascript/#tooltip 1252 | * Inspired by the original jQuery.tipsy by Jason Frame 1253 | * ======================================================================== 1254 | * Copyright 2011-2015 Twitter, Inc. 1255 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 1256 | * ======================================================================== */ 1257 | 1258 | 1259 | +function ($) { 1260 | 'use strict'; 1261 | 1262 | // TOOLTIP PUBLIC CLASS DEFINITION 1263 | // =============================== 1264 | 1265 | var Tooltip = function (element, options) { 1266 | this.type = null 1267 | this.options = null 1268 | this.enabled = null 1269 | this.timeout = null 1270 | this.hoverState = null 1271 | this.$element = null 1272 | this.inState = null 1273 | 1274 | this.init('tooltip', element, options) 1275 | } 1276 | 1277 | Tooltip.VERSION = '3.3.6' 1278 | 1279 | Tooltip.TRANSITION_DURATION = 150 1280 | 1281 | Tooltip.DEFAULTS = { 1282 | animation: true, 1283 | placement: 'top', 1284 | selector: false, 1285 | template: '', 1286 | trigger: 'hover focus', 1287 | title: '', 1288 | delay: 0, 1289 | html: false, 1290 | container: false, 1291 | viewport: { 1292 | selector: 'body', 1293 | padding: 0 1294 | } 1295 | } 1296 | 1297 | Tooltip.prototype.init = function (type, element, options) { 1298 | this.enabled = true 1299 | this.type = type 1300 | this.$element = $(element) 1301 | this.options = this.getOptions(options) 1302 | this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport)) 1303 | this.inState = { click: false, hover: false, focus: false } 1304 | 1305 | if (this.$element[0] instanceof document.constructor && !this.options.selector) { 1306 | throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!') 1307 | } 1308 | 1309 | var triggers = this.options.trigger.split(' ') 1310 | 1311 | for (var i = triggers.length; i--;) { 1312 | var trigger = triggers[i] 1313 | 1314 | if (trigger == 'click') { 1315 | this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) 1316 | } else if (trigger != 'manual') { 1317 | var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' 1318 | var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' 1319 | 1320 | this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) 1321 | this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) 1322 | } 1323 | } 1324 | 1325 | this.options.selector ? 1326 | (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : 1327 | this.fixTitle() 1328 | } 1329 | 1330 | Tooltip.prototype.getDefaults = function () { 1331 | return Tooltip.DEFAULTS 1332 | } 1333 | 1334 | Tooltip.prototype.getOptions = function (options) { 1335 | options = $.extend({}, this.getDefaults(), this.$element.data(), options) 1336 | 1337 | if (options.delay && typeof options.delay == 'number') { 1338 | options.delay = { 1339 | show: options.delay, 1340 | hide: options.delay 1341 | } 1342 | } 1343 | 1344 | return options 1345 | } 1346 | 1347 | Tooltip.prototype.getDelegateOptions = function () { 1348 | var options = {} 1349 | var defaults = this.getDefaults() 1350 | 1351 | this._options && $.each(this._options, function (key, value) { 1352 | if (defaults[key] != value) options[key] = value 1353 | }) 1354 | 1355 | return options 1356 | } 1357 | 1358 | Tooltip.prototype.enter = function (obj) { 1359 | var self = obj instanceof this.constructor ? 1360 | obj : $(obj.currentTarget).data('bs.' + this.type) 1361 | 1362 | if (!self) { 1363 | self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) 1364 | $(obj.currentTarget).data('bs.' + this.type, self) 1365 | } 1366 | 1367 | if (obj instanceof $.Event) { 1368 | self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true 1369 | } 1370 | 1371 | if (self.tip().hasClass('in') || self.hoverState == 'in') { 1372 | self.hoverState = 'in' 1373 | return 1374 | } 1375 | 1376 | clearTimeout(self.timeout) 1377 | 1378 | self.hoverState = 'in' 1379 | 1380 | if (!self.options.delay || !self.options.delay.show) return self.show() 1381 | 1382 | self.timeout = setTimeout(function () { 1383 | if (self.hoverState == 'in') self.show() 1384 | }, self.options.delay.show) 1385 | } 1386 | 1387 | Tooltip.prototype.isInStateTrue = function () { 1388 | for (var key in this.inState) { 1389 | if (this.inState[key]) return true 1390 | } 1391 | 1392 | return false 1393 | } 1394 | 1395 | Tooltip.prototype.leave = function (obj) { 1396 | var self = obj instanceof this.constructor ? 1397 | obj : $(obj.currentTarget).data('bs.' + this.type) 1398 | 1399 | if (!self) { 1400 | self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) 1401 | $(obj.currentTarget).data('bs.' + this.type, self) 1402 | } 1403 | 1404 | if (obj instanceof $.Event) { 1405 | self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false 1406 | } 1407 | 1408 | if (self.isInStateTrue()) return 1409 | 1410 | clearTimeout(self.timeout) 1411 | 1412 | self.hoverState = 'out' 1413 | 1414 | if (!self.options.delay || !self.options.delay.hide) return self.hide() 1415 | 1416 | self.timeout = setTimeout(function () { 1417 | if (self.hoverState == 'out') self.hide() 1418 | }, self.options.delay.hide) 1419 | } 1420 | 1421 | Tooltip.prototype.show = function () { 1422 | var e = $.Event('show.bs.' + this.type) 1423 | 1424 | if (this.hasContent() && this.enabled) { 1425 | this.$element.trigger(e) 1426 | 1427 | var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]) 1428 | if (e.isDefaultPrevented() || !inDom) return 1429 | var that = this 1430 | 1431 | var $tip = this.tip() 1432 | 1433 | var tipId = this.getUID(this.type) 1434 | 1435 | this.setContent() 1436 | $tip.attr('id', tipId) 1437 | this.$element.attr('aria-describedby', tipId) 1438 | 1439 | if (this.options.animation) $tip.addClass('fade') 1440 | 1441 | var placement = typeof this.options.placement == 'function' ? 1442 | this.options.placement.call(this, $tip[0], this.$element[0]) : 1443 | this.options.placement 1444 | 1445 | var autoToken = /\s?auto?\s?/i 1446 | var autoPlace = autoToken.test(placement) 1447 | if (autoPlace) placement = placement.replace(autoToken, '') || 'top' 1448 | 1449 | $tip 1450 | .detach() 1451 | .css({ top: 0, left: 0, display: 'block' }) 1452 | .addClass(placement) 1453 | .data('bs.' + this.type, this) 1454 | 1455 | this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) 1456 | this.$element.trigger('inserted.bs.' + this.type) 1457 | 1458 | var pos = this.getPosition() 1459 | var actualWidth = $tip[0].offsetWidth 1460 | var actualHeight = $tip[0].offsetHeight 1461 | 1462 | if (autoPlace) { 1463 | var orgPlacement = placement 1464 | var viewportDim = this.getPosition(this.$viewport) 1465 | 1466 | placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : 1467 | placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : 1468 | placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : 1469 | placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : 1470 | placement 1471 | 1472 | $tip 1473 | .removeClass(orgPlacement) 1474 | .addClass(placement) 1475 | } 1476 | 1477 | var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) 1478 | 1479 | this.applyPlacement(calculatedOffset, placement) 1480 | 1481 | var complete = function () { 1482 | var prevHoverState = that.hoverState 1483 | that.$element.trigger('shown.bs.' + that.type) 1484 | that.hoverState = null 1485 | 1486 | if (prevHoverState == 'out') that.leave(that) 1487 | } 1488 | 1489 | $.support.transition && this.$tip.hasClass('fade') ? 1490 | $tip 1491 | .one('bsTransitionEnd', complete) 1492 | .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : 1493 | complete() 1494 | } 1495 | } 1496 | 1497 | Tooltip.prototype.applyPlacement = function (offset, placement) { 1498 | var $tip = this.tip() 1499 | var width = $tip[0].offsetWidth 1500 | var height = $tip[0].offsetHeight 1501 | 1502 | // manually read margins because getBoundingClientRect includes difference 1503 | var marginTop = parseInt($tip.css('margin-top'), 10) 1504 | var marginLeft = parseInt($tip.css('margin-left'), 10) 1505 | 1506 | // we must check for NaN for ie 8/9 1507 | if (isNaN(marginTop)) marginTop = 0 1508 | if (isNaN(marginLeft)) marginLeft = 0 1509 | 1510 | offset.top += marginTop 1511 | offset.left += marginLeft 1512 | 1513 | // $.fn.offset doesn't round pixel values 1514 | // so we use setOffset directly with our own function B-0 1515 | $.offset.setOffset($tip[0], $.extend({ 1516 | using: function (props) { 1517 | $tip.css({ 1518 | top: Math.round(props.top), 1519 | left: Math.round(props.left) 1520 | }) 1521 | } 1522 | }, offset), 0) 1523 | 1524 | $tip.addClass('in') 1525 | 1526 | // check to see if placing tip in new offset caused the tip to resize itself 1527 | var actualWidth = $tip[0].offsetWidth 1528 | var actualHeight = $tip[0].offsetHeight 1529 | 1530 | if (placement == 'top' && actualHeight != height) { 1531 | offset.top = offset.top + height - actualHeight 1532 | } 1533 | 1534 | var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) 1535 | 1536 | if (delta.left) offset.left += delta.left 1537 | else offset.top += delta.top 1538 | 1539 | var isVertical = /top|bottom/.test(placement) 1540 | var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight 1541 | var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight' 1542 | 1543 | $tip.offset(offset) 1544 | this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical) 1545 | } 1546 | 1547 | Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) { 1548 | this.arrow() 1549 | .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') 1550 | .css(isVertical ? 'top' : 'left', '') 1551 | } 1552 | 1553 | Tooltip.prototype.setContent = function () { 1554 | var $tip = this.tip() 1555 | var title = this.getTitle() 1556 | 1557 | $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) 1558 | $tip.removeClass('fade in top bottom left right') 1559 | } 1560 | 1561 | Tooltip.prototype.hide = function (callback) { 1562 | var that = this 1563 | var $tip = $(this.$tip) 1564 | var e = $.Event('hide.bs.' + this.type) 1565 | 1566 | function complete() { 1567 | if (that.hoverState != 'in') $tip.detach() 1568 | that.$element 1569 | .removeAttr('aria-describedby') 1570 | .trigger('hidden.bs.' + that.type) 1571 | callback && callback() 1572 | } 1573 | 1574 | this.$element.trigger(e) 1575 | 1576 | if (e.isDefaultPrevented()) return 1577 | 1578 | $tip.removeClass('in') 1579 | 1580 | $.support.transition && $tip.hasClass('fade') ? 1581 | $tip 1582 | .one('bsTransitionEnd', complete) 1583 | .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : 1584 | complete() 1585 | 1586 | this.hoverState = null 1587 | 1588 | return this 1589 | } 1590 | 1591 | Tooltip.prototype.fixTitle = function () { 1592 | var $e = this.$element 1593 | if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') { 1594 | $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') 1595 | } 1596 | } 1597 | 1598 | Tooltip.prototype.hasContent = function () { 1599 | return this.getTitle() 1600 | } 1601 | 1602 | Tooltip.prototype.getPosition = function ($element) { 1603 | $element = $element || this.$element 1604 | 1605 | var el = $element[0] 1606 | var isBody = el.tagName == 'BODY' 1607 | 1608 | var elRect = el.getBoundingClientRect() 1609 | if (elRect.width == null) { 1610 | // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093 1611 | elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top }) 1612 | } 1613 | var elOffset = isBody ? { top: 0, left: 0 } : $element.offset() 1614 | var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() } 1615 | var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null 1616 | 1617 | return $.extend({}, elRect, scroll, outerDims, elOffset) 1618 | } 1619 | 1620 | Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { 1621 | return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : 1622 | placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : 1623 | placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : 1624 | /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } 1625 | 1626 | } 1627 | 1628 | Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { 1629 | var delta = { top: 0, left: 0 } 1630 | if (!this.$viewport) return delta 1631 | 1632 | var viewportPadding = this.options.viewport && this.options.viewport.padding || 0 1633 | var viewportDimensions = this.getPosition(this.$viewport) 1634 | 1635 | if (/right|left/.test(placement)) { 1636 | var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll 1637 | var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight 1638 | if (topEdgeOffset < viewportDimensions.top) { // top overflow 1639 | delta.top = viewportDimensions.top - topEdgeOffset 1640 | } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow 1641 | delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset 1642 | } 1643 | } else { 1644 | var leftEdgeOffset = pos.left - viewportPadding 1645 | var rightEdgeOffset = pos.left + viewportPadding + actualWidth 1646 | if (leftEdgeOffset < viewportDimensions.left) { // left overflow 1647 | delta.left = viewportDimensions.left - leftEdgeOffset 1648 | } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow 1649 | delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset 1650 | } 1651 | } 1652 | 1653 | return delta 1654 | } 1655 | 1656 | Tooltip.prototype.getTitle = function () { 1657 | var title 1658 | var $e = this.$element 1659 | var o = this.options 1660 | 1661 | title = $e.attr('data-original-title') 1662 | || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) 1663 | 1664 | return title 1665 | } 1666 | 1667 | Tooltip.prototype.getUID = function (prefix) { 1668 | do prefix += ~~(Math.random() * 1000000) 1669 | while (document.getElementById(prefix)) 1670 | return prefix 1671 | } 1672 | 1673 | Tooltip.prototype.tip = function () { 1674 | if (!this.$tip) { 1675 | this.$tip = $(this.options.template) 1676 | if (this.$tip.length != 1) { 1677 | throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!') 1678 | } 1679 | } 1680 | return this.$tip 1681 | } 1682 | 1683 | Tooltip.prototype.arrow = function () { 1684 | return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')) 1685 | } 1686 | 1687 | Tooltip.prototype.enable = function () { 1688 | this.enabled = true 1689 | } 1690 | 1691 | Tooltip.prototype.disable = function () { 1692 | this.enabled = false 1693 | } 1694 | 1695 | Tooltip.prototype.toggleEnabled = function () { 1696 | this.enabled = !this.enabled 1697 | } 1698 | 1699 | Tooltip.prototype.toggle = function (e) { 1700 | var self = this 1701 | if (e) { 1702 | self = $(e.currentTarget).data('bs.' + this.type) 1703 | if (!self) { 1704 | self = new this.constructor(e.currentTarget, this.getDelegateOptions()) 1705 | $(e.currentTarget).data('bs.' + this.type, self) 1706 | } 1707 | } 1708 | 1709 | if (e) { 1710 | self.inState.click = !self.inState.click 1711 | if (self.isInStateTrue()) self.enter(self) 1712 | else self.leave(self) 1713 | } else { 1714 | self.tip().hasClass('in') ? self.leave(self) : self.enter(self) 1715 | } 1716 | } 1717 | 1718 | Tooltip.prototype.destroy = function () { 1719 | var that = this 1720 | clearTimeout(this.timeout) 1721 | this.hide(function () { 1722 | that.$element.off('.' + that.type).removeData('bs.' + that.type) 1723 | if (that.$tip) { 1724 | that.$tip.detach() 1725 | } 1726 | that.$tip = null 1727 | that.$arrow = null 1728 | that.$viewport = null 1729 | }) 1730 | } 1731 | 1732 | 1733 | // TOOLTIP PLUGIN DEFINITION 1734 | // ========================= 1735 | 1736 | function Plugin(option) { 1737 | return this.each(function () { 1738 | var $this = $(this) 1739 | var data = $this.data('bs.tooltip') 1740 | var options = typeof option == 'object' && option 1741 | 1742 | if (!data && /destroy|hide/.test(option)) return 1743 | if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) 1744 | if (typeof option == 'string') data[option]() 1745 | }) 1746 | } 1747 | 1748 | var old = $.fn.tooltip 1749 | 1750 | $.fn.tooltip = Plugin 1751 | $.fn.tooltip.Constructor = Tooltip 1752 | 1753 | 1754 | // TOOLTIP NO CONFLICT 1755 | // =================== 1756 | 1757 | $.fn.tooltip.noConflict = function () { 1758 | $.fn.tooltip = old 1759 | return this 1760 | } 1761 | 1762 | }(jQuery); 1763 | 1764 | /* ======================================================================== 1765 | * Bootstrap: popover.js v3.3.6 1766 | * http://getbootstrap.com/javascript/#popovers 1767 | * ======================================================================== 1768 | * Copyright 2011-2015 Twitter, Inc. 1769 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 1770 | * ======================================================================== */ 1771 | 1772 | 1773 | +function ($) { 1774 | 'use strict'; 1775 | 1776 | // POPOVER PUBLIC CLASS DEFINITION 1777 | // =============================== 1778 | 1779 | var Popover = function (element, options) { 1780 | this.init('popover', element, options) 1781 | } 1782 | 1783 | if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') 1784 | 1785 | Popover.VERSION = '3.3.6' 1786 | 1787 | Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { 1788 | placement: 'right', 1789 | trigger: 'click', 1790 | content: '', 1791 | template: '' 1792 | }) 1793 | 1794 | 1795 | // NOTE: POPOVER EXTENDS tooltip.js 1796 | // ================================ 1797 | 1798 | Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) 1799 | 1800 | Popover.prototype.constructor = Popover 1801 | 1802 | Popover.prototype.getDefaults = function () { 1803 | return Popover.DEFAULTS 1804 | } 1805 | 1806 | Popover.prototype.setContent = function () { 1807 | var $tip = this.tip() 1808 | var title = this.getTitle() 1809 | var content = this.getContent() 1810 | 1811 | $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) 1812 | $tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events 1813 | this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' 1814 | ](content) 1815 | 1816 | $tip.removeClass('fade top bottom left right in') 1817 | 1818 | // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do 1819 | // this manually by checking the contents. 1820 | if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() 1821 | } 1822 | 1823 | Popover.prototype.hasContent = function () { 1824 | return this.getTitle() || this.getContent() 1825 | } 1826 | 1827 | Popover.prototype.getContent = function () { 1828 | var $e = this.$element 1829 | var o = this.options 1830 | 1831 | return $e.attr('data-content') 1832 | || (typeof o.content == 'function' ? 1833 | o.content.call($e[0]) : 1834 | o.content) 1835 | } 1836 | 1837 | Popover.prototype.arrow = function () { 1838 | return (this.$arrow = this.$arrow || this.tip().find('.arrow')) 1839 | } 1840 | 1841 | 1842 | // POPOVER PLUGIN DEFINITION 1843 | // ========================= 1844 | 1845 | function Plugin(option) { 1846 | return this.each(function () { 1847 | var $this = $(this) 1848 | var data = $this.data('bs.popover') 1849 | var options = typeof option == 'object' && option 1850 | 1851 | if (!data && /destroy|hide/.test(option)) return 1852 | if (!data) $this.data('bs.popover', (data = new Popover(this, options))) 1853 | if (typeof option == 'string') data[option]() 1854 | }) 1855 | } 1856 | 1857 | var old = $.fn.popover 1858 | 1859 | $.fn.popover = Plugin 1860 | $.fn.popover.Constructor = Popover 1861 | 1862 | 1863 | // POPOVER NO CONFLICT 1864 | // =================== 1865 | 1866 | $.fn.popover.noConflict = function () { 1867 | $.fn.popover = old 1868 | return this 1869 | } 1870 | 1871 | }(jQuery); 1872 | 1873 | /* ======================================================================== 1874 | * Bootstrap: scrollspy.js v3.3.6 1875 | * http://getbootstrap.com/javascript/#scrollspy 1876 | * ======================================================================== 1877 | * Copyright 2011-2015 Twitter, Inc. 1878 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 1879 | * ======================================================================== */ 1880 | 1881 | 1882 | +function ($) { 1883 | 'use strict'; 1884 | 1885 | // SCROLLSPY CLASS DEFINITION 1886 | // ========================== 1887 | 1888 | function ScrollSpy(element, options) { 1889 | this.$body = $(document.body) 1890 | this.$scrollElement = $(element).is(document.body) ? $(window) : $(element) 1891 | this.options = $.extend({}, ScrollSpy.DEFAULTS, options) 1892 | this.selector = (this.options.target || '') + ' .nav li > a' 1893 | this.offsets = [] 1894 | this.targets = [] 1895 | this.activeTarget = null 1896 | this.scrollHeight = 0 1897 | 1898 | this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this)) 1899 | this.refresh() 1900 | this.process() 1901 | } 1902 | 1903 | ScrollSpy.VERSION = '3.3.6' 1904 | 1905 | ScrollSpy.DEFAULTS = { 1906 | offset: 10 1907 | } 1908 | 1909 | ScrollSpy.prototype.getScrollHeight = function () { 1910 | return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight) 1911 | } 1912 | 1913 | ScrollSpy.prototype.refresh = function () { 1914 | var that = this 1915 | var offsetMethod = 'offset' 1916 | var offsetBase = 0 1917 | 1918 | this.offsets = [] 1919 | this.targets = [] 1920 | this.scrollHeight = this.getScrollHeight() 1921 | 1922 | if (!$.isWindow(this.$scrollElement[0])) { 1923 | offsetMethod = 'position' 1924 | offsetBase = this.$scrollElement.scrollTop() 1925 | } 1926 | 1927 | this.$body 1928 | .find(this.selector) 1929 | .map(function () { 1930 | var $el = $(this) 1931 | var href = $el.data('target') || $el.attr('href') 1932 | var $href = /^#./.test(href) && $(href) 1933 | 1934 | return ($href 1935 | && $href.length 1936 | && $href.is(':visible') 1937 | && [[$href[offsetMethod]().top + offsetBase, href]]) || null 1938 | }) 1939 | .sort(function (a, b) { return a[0] - b[0] }) 1940 | .each(function () { 1941 | that.offsets.push(this[0]) 1942 | that.targets.push(this[1]) 1943 | }) 1944 | } 1945 | 1946 | ScrollSpy.prototype.process = function () { 1947 | var scrollTop = this.$scrollElement.scrollTop() + this.options.offset 1948 | var scrollHeight = this.getScrollHeight() 1949 | var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height() 1950 | var offsets = this.offsets 1951 | var targets = this.targets 1952 | var activeTarget = this.activeTarget 1953 | var i 1954 | 1955 | if (this.scrollHeight != scrollHeight) { 1956 | this.refresh() 1957 | } 1958 | 1959 | if (scrollTop >= maxScroll) { 1960 | return activeTarget != (i = targets[targets.length - 1]) && this.activate(i) 1961 | } 1962 | 1963 | if (activeTarget && scrollTop < offsets[0]) { 1964 | this.activeTarget = null 1965 | return this.clear() 1966 | } 1967 | 1968 | for (i = offsets.length; i--;) { 1969 | activeTarget != targets[i] 1970 | && scrollTop >= offsets[i] 1971 | && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) 1972 | && this.activate(targets[i]) 1973 | } 1974 | } 1975 | 1976 | ScrollSpy.prototype.activate = function (target) { 1977 | this.activeTarget = target 1978 | 1979 | this.clear() 1980 | 1981 | var selector = this.selector + 1982 | '[data-target="' + target + '"],' + 1983 | this.selector + '[href="' + target + '"]' 1984 | 1985 | var active = $(selector) 1986 | .parents('li') 1987 | .addClass('active') 1988 | 1989 | if (active.parent('.dropdown-menu').length) { 1990 | active = active 1991 | .closest('li.dropdown') 1992 | .addClass('active') 1993 | } 1994 | 1995 | active.trigger('activate.bs.scrollspy') 1996 | } 1997 | 1998 | ScrollSpy.prototype.clear = function () { 1999 | $(this.selector) 2000 | .parentsUntil(this.options.target, '.active') 2001 | .removeClass('active') 2002 | } 2003 | 2004 | 2005 | // SCROLLSPY PLUGIN DEFINITION 2006 | // =========================== 2007 | 2008 | function Plugin(option) { 2009 | return this.each(function () { 2010 | var $this = $(this) 2011 | var data = $this.data('bs.scrollspy') 2012 | var options = typeof option == 'object' && option 2013 | 2014 | if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) 2015 | if (typeof option == 'string') data[option]() 2016 | }) 2017 | } 2018 | 2019 | var old = $.fn.scrollspy 2020 | 2021 | $.fn.scrollspy = Plugin 2022 | $.fn.scrollspy.Constructor = ScrollSpy 2023 | 2024 | 2025 | // SCROLLSPY NO CONFLICT 2026 | // ===================== 2027 | 2028 | $.fn.scrollspy.noConflict = function () { 2029 | $.fn.scrollspy = old 2030 | return this 2031 | } 2032 | 2033 | 2034 | // SCROLLSPY DATA-API 2035 | // ================== 2036 | 2037 | $(window).on('load.bs.scrollspy.data-api', function () { 2038 | $('[data-spy="scroll"]').each(function () { 2039 | var $spy = $(this) 2040 | Plugin.call($spy, $spy.data()) 2041 | }) 2042 | }) 2043 | 2044 | }(jQuery); 2045 | 2046 | /* ======================================================================== 2047 | * Bootstrap: tab.js v3.3.6 2048 | * http://getbootstrap.com/javascript/#tabs 2049 | * ======================================================================== 2050 | * Copyright 2011-2015 Twitter, Inc. 2051 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 2052 | * ======================================================================== */ 2053 | 2054 | 2055 | +function ($) { 2056 | 'use strict'; 2057 | 2058 | // TAB CLASS DEFINITION 2059 | // ==================== 2060 | 2061 | var Tab = function (element) { 2062 | // jscs:disable requireDollarBeforejQueryAssignment 2063 | this.element = $(element) 2064 | // jscs:enable requireDollarBeforejQueryAssignment 2065 | } 2066 | 2067 | Tab.VERSION = '3.3.6' 2068 | 2069 | Tab.TRANSITION_DURATION = 150 2070 | 2071 | Tab.prototype.show = function () { 2072 | var $this = this.element 2073 | var $ul = $this.closest('ul:not(.dropdown-menu)') 2074 | var selector = $this.data('target') 2075 | 2076 | if (!selector) { 2077 | selector = $this.attr('href') 2078 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 2079 | } 2080 | 2081 | if ($this.parent('li').hasClass('active')) return 2082 | 2083 | var $previous = $ul.find('.active:last a') 2084 | var hideEvent = $.Event('hide.bs.tab', { 2085 | relatedTarget: $this[0] 2086 | }) 2087 | var showEvent = $.Event('show.bs.tab', { 2088 | relatedTarget: $previous[0] 2089 | }) 2090 | 2091 | $previous.trigger(hideEvent) 2092 | $this.trigger(showEvent) 2093 | 2094 | if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return 2095 | 2096 | var $target = $(selector) 2097 | 2098 | this.activate($this.closest('li'), $ul) 2099 | this.activate($target, $target.parent(), function () { 2100 | $previous.trigger({ 2101 | type: 'hidden.bs.tab', 2102 | relatedTarget: $this[0] 2103 | }) 2104 | $this.trigger({ 2105 | type: 'shown.bs.tab', 2106 | relatedTarget: $previous[0] 2107 | }) 2108 | }) 2109 | } 2110 | 2111 | Tab.prototype.activate = function (element, container, callback) { 2112 | var $active = container.find('> .active') 2113 | var transition = callback 2114 | && $.support.transition 2115 | && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length) 2116 | 2117 | function next() { 2118 | $active 2119 | .removeClass('active') 2120 | .find('> .dropdown-menu > .active') 2121 | .removeClass('active') 2122 | .end() 2123 | .find('[data-toggle="tab"]') 2124 | .attr('aria-expanded', false) 2125 | 2126 | element 2127 | .addClass('active') 2128 | .find('[data-toggle="tab"]') 2129 | .attr('aria-expanded', true) 2130 | 2131 | if (transition) { 2132 | element[0].offsetWidth // reflow for transition 2133 | element.addClass('in') 2134 | } else { 2135 | element.removeClass('fade') 2136 | } 2137 | 2138 | if (element.parent('.dropdown-menu').length) { 2139 | element 2140 | .closest('li.dropdown') 2141 | .addClass('active') 2142 | .end() 2143 | .find('[data-toggle="tab"]') 2144 | .attr('aria-expanded', true) 2145 | } 2146 | 2147 | callback && callback() 2148 | } 2149 | 2150 | $active.length && transition ? 2151 | $active 2152 | .one('bsTransitionEnd', next) 2153 | .emulateTransitionEnd(Tab.TRANSITION_DURATION) : 2154 | next() 2155 | 2156 | $active.removeClass('in') 2157 | } 2158 | 2159 | 2160 | // TAB PLUGIN DEFINITION 2161 | // ===================== 2162 | 2163 | function Plugin(option) { 2164 | return this.each(function () { 2165 | var $this = $(this) 2166 | var data = $this.data('bs.tab') 2167 | 2168 | if (!data) $this.data('bs.tab', (data = new Tab(this))) 2169 | if (typeof option == 'string') data[option]() 2170 | }) 2171 | } 2172 | 2173 | var old = $.fn.tab 2174 | 2175 | $.fn.tab = Plugin 2176 | $.fn.tab.Constructor = Tab 2177 | 2178 | 2179 | // TAB NO CONFLICT 2180 | // =============== 2181 | 2182 | $.fn.tab.noConflict = function () { 2183 | $.fn.tab = old 2184 | return this 2185 | } 2186 | 2187 | 2188 | // TAB DATA-API 2189 | // ============ 2190 | 2191 | var clickHandler = function (e) { 2192 | e.preventDefault() 2193 | Plugin.call($(this), 'show') 2194 | } 2195 | 2196 | $(document) 2197 | .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler) 2198 | .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler) 2199 | 2200 | }(jQuery); 2201 | 2202 | /* ======================================================================== 2203 | * Bootstrap: affix.js v3.3.6 2204 | * http://getbootstrap.com/javascript/#affix 2205 | * ======================================================================== 2206 | * Copyright 2011-2015 Twitter, Inc. 2207 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 2208 | * ======================================================================== */ 2209 | 2210 | 2211 | +function ($) { 2212 | 'use strict'; 2213 | 2214 | // AFFIX CLASS DEFINITION 2215 | // ====================== 2216 | 2217 | var Affix = function (element, options) { 2218 | this.options = $.extend({}, Affix.DEFAULTS, options) 2219 | 2220 | this.$target = $(this.options.target) 2221 | .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) 2222 | .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) 2223 | 2224 | this.$element = $(element) 2225 | this.affixed = null 2226 | this.unpin = null 2227 | this.pinnedOffset = null 2228 | 2229 | this.checkPosition() 2230 | } 2231 | 2232 | Affix.VERSION = '3.3.6' 2233 | 2234 | Affix.RESET = 'affix affix-top affix-bottom' 2235 | 2236 | Affix.DEFAULTS = { 2237 | offset: 0, 2238 | target: window 2239 | } 2240 | 2241 | Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) { 2242 | var scrollTop = this.$target.scrollTop() 2243 | var position = this.$element.offset() 2244 | var targetHeight = this.$target.height() 2245 | 2246 | if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false 2247 | 2248 | if (this.affixed == 'bottom') { 2249 | if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom' 2250 | return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom' 2251 | } 2252 | 2253 | var initializing = this.affixed == null 2254 | var colliderTop = initializing ? scrollTop : position.top 2255 | var colliderHeight = initializing ? targetHeight : height 2256 | 2257 | if (offsetTop != null && scrollTop <= offsetTop) return 'top' 2258 | if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom' 2259 | 2260 | return false 2261 | } 2262 | 2263 | Affix.prototype.getPinnedOffset = function () { 2264 | if (this.pinnedOffset) return this.pinnedOffset 2265 | this.$element.removeClass(Affix.RESET).addClass('affix') 2266 | var scrollTop = this.$target.scrollTop() 2267 | var position = this.$element.offset() 2268 | return (this.pinnedOffset = position.top - scrollTop) 2269 | } 2270 | 2271 | Affix.prototype.checkPositionWithEventLoop = function () { 2272 | setTimeout($.proxy(this.checkPosition, this), 1) 2273 | } 2274 | 2275 | Affix.prototype.checkPosition = function () { 2276 | if (!this.$element.is(':visible')) return 2277 | 2278 | var height = this.$element.height() 2279 | var offset = this.options.offset 2280 | var offsetTop = offset.top 2281 | var offsetBottom = offset.bottom 2282 | var scrollHeight = Math.max($(document).height(), $(document.body).height()) 2283 | 2284 | if (typeof offset != 'object') offsetBottom = offsetTop = offset 2285 | if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) 2286 | if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) 2287 | 2288 | var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom) 2289 | 2290 | if (this.affixed != affix) { 2291 | if (this.unpin != null) this.$element.css('top', '') 2292 | 2293 | var affixType = 'affix' + (affix ? '-' + affix : '') 2294 | var e = $.Event(affixType + '.bs.affix') 2295 | 2296 | this.$element.trigger(e) 2297 | 2298 | if (e.isDefaultPrevented()) return 2299 | 2300 | this.affixed = affix 2301 | this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null 2302 | 2303 | this.$element 2304 | .removeClass(Affix.RESET) 2305 | .addClass(affixType) 2306 | .trigger(affixType.replace('affix', 'affixed') + '.bs.affix') 2307 | } 2308 | 2309 | if (affix == 'bottom') { 2310 | this.$element.offset({ 2311 | top: scrollHeight - height - offsetBottom 2312 | }) 2313 | } 2314 | } 2315 | 2316 | 2317 | // AFFIX PLUGIN DEFINITION 2318 | // ======================= 2319 | 2320 | function Plugin(option) { 2321 | return this.each(function () { 2322 | var $this = $(this) 2323 | var data = $this.data('bs.affix') 2324 | var options = typeof option == 'object' && option 2325 | 2326 | if (!data) $this.data('bs.affix', (data = new Affix(this, options))) 2327 | if (typeof option == 'string') data[option]() 2328 | }) 2329 | } 2330 | 2331 | var old = $.fn.affix 2332 | 2333 | $.fn.affix = Plugin 2334 | $.fn.affix.Constructor = Affix 2335 | 2336 | 2337 | // AFFIX NO CONFLICT 2338 | // ================= 2339 | 2340 | $.fn.affix.noConflict = function () { 2341 | $.fn.affix = old 2342 | return this 2343 | } 2344 | 2345 | 2346 | // AFFIX DATA-API 2347 | // ============== 2348 | 2349 | $(window).on('load', function () { 2350 | $('[data-spy="affix"]').each(function () { 2351 | var $spy = $(this) 2352 | var data = $spy.data() 2353 | 2354 | data.offset = data.offset || {} 2355 | 2356 | if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom 2357 | if (data.offsetTop != null) data.offset.top = data.offsetTop 2358 | 2359 | Plugin.call($spy, data) 2360 | }) 2361 | }) 2362 | 2363 | }(jQuery); 2364 | -------------------------------------------------------------------------------- /serverpanel/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.6 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>2)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 3")}(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]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",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(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.6",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.6",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),a(c.target).is('input[type="radio"]')||a(c.target).is('input[type="checkbox"]')||c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.6",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.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},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.6",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.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 in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.6",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),c.isInStateTrue()?void 0:(clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide())},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.6",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.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").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.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)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(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]()})}b.VERSION="3.3.6",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).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(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.6",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); -------------------------------------------------------------------------------- /serverpanel/templates/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flask-Server-Panel 5 | 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /serverpanel/utils/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /serverpanel/utils/jsonify.py: -------------------------------------------------------------------------------- 1 | from flask import Response 2 | from functools import wraps 3 | import json 4 | 5 | 6 | def jsonify(method): 7 | """ 8 | Benchmark decorator, a quick and convenient way to time a function 9 | :param method: function it wraps 10 | :return: decorated function 11 | """ 12 | @wraps(method) 13 | def jsonified(*args, **kw): 14 | result = method(*args, **kw) 15 | return Response(json.dumps(result, 16 | sort_keys=True, 17 | indent=4), 18 | mimetype='application/json') 19 | 20 | return jsonified -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from serverpanel import create_app 3 | from serverpanel.ext.serverinfo import ServerInfo 4 | 5 | from flask_testing import TestCase 6 | 7 | import unittest 8 | import json 9 | import unittest.mock as mock 10 | import tempfile 11 | import os 12 | 13 | from collections import namedtuple 14 | 15 | Request = namedtuple('Request', ['text']) 16 | valid_ip_data = Request(text='{"ip": "127.0.0.1", "country": "BE"}') 17 | valid_pihole_data = Request(text='{"domains_being_blocked": 1,"dns_queries_today":2, ' 18 | '"ads_blocked_today": 3, "ads_percentage_today": 50.0}') 19 | 20 | 21 | class MyTest(TestCase): 22 | 23 | def create_app(self): 24 | app = create_app('config') 25 | app.config['DEBUG'] = False 26 | 27 | return app 28 | 29 | def test_creation(self): 30 | self.app.config['ENABLE_PIHOLE'] = False 31 | server_info = ServerInfo(self.app) 32 | self.assertFalse(server_info.pihole_enabled) 33 | 34 | self.app.config['ENABLE_PIHOLE'] = True 35 | self.app.config['PIHOLE_API'] = None 36 | server_info = ServerInfo(self.app) 37 | self.assertTrue(server_info.pihole_enabled) 38 | 39 | def test_main(self): 40 | # check if route returns code 200 41 | response = self.client.get('/') 42 | self.assert_template_used('main.html') 43 | self.assert200(response) 44 | 45 | response = self.client.get('/network/') 46 | self.assert_template_used('main.html') 47 | self.assert200(response) 48 | 49 | def test_api(self): 50 | # check if route returns code 200 51 | response = self.client.get('/api/') 52 | self.assert200(response) 53 | data = json.loads(response.data.decode('utf-8')) 54 | self.assertTrue('version' in data.keys()) 55 | self.assertTrue('server' in data.keys()) 56 | self.assertTrue('system' in data.keys()) 57 | self.assertTrue('network' in data.keys()) 58 | self.assertTrue('pihole' in data.keys()) 59 | 60 | def test_api_details(self): 61 | # check if route returns code 200 62 | response = self.client.get('/api/version') 63 | self.assert200(response) 64 | data = json.loads(response.data.decode('utf-8')) 65 | self.assertTrue('name' in data.keys()) 66 | self.assertTrue('version' in data.keys()) 67 | 68 | def test_api_server(self): 69 | # check if route returns code 200 70 | response = self.client.get('/api/server') 71 | self.assert200(response) 72 | data = json.loads(response.data.decode('utf-8')) 73 | self.assertTrue('hostname' in data.keys()) 74 | self.assertTrue('os' in data.keys()) 75 | self.assertTrue('uptime' in data.keys()) 76 | 77 | def test_route_hostname(self): 78 | # check if route returns code 200 79 | response = self.client.get('/api/server/hostname') 80 | self.assert200(response) 81 | 82 | # check if object returned contains the desired data 83 | data = json.loads(response.data.decode('utf-8')) 84 | self.assertTrue('hostname' in data.keys()) 85 | 86 | def test_route_os(self): 87 | # check if route returns code 200 88 | response = self.client.get('/api/server/os') 89 | self.assert200(response) 90 | 91 | # check if object returned contains the desired data 92 | data = json.loads(response.data.decode('utf-8')) 93 | self.assertTrue('os_name' in data.keys()) 94 | 95 | def test_route_uptime(self): 96 | # check if route returns code 200 97 | response = self.client.get('/api/server/uptime') 98 | self.assert200(response) 99 | 100 | # check if object returned contains the desired data 101 | data = json.loads(response.data.decode('utf-8')) 102 | self.assertTrue('uptime' in data.keys()) 103 | 104 | def test_route_cpu_cores(self): 105 | # check if route returns code 200 106 | response = self.client.get('/api/system/cpu/cores') 107 | self.assert200(response) 108 | 109 | # check if object returned contains the desired data 110 | data = json.loads(response.data.decode('utf-8')) 111 | self.assertTrue('logical_cores' in data.keys()) 112 | self.assertTrue('physical_cores' in data.keys()) 113 | 114 | def test_route_cpu_load(self): 115 | # check if route returns code 200 116 | response = self.client.get('/api/system/cpu/load') 117 | self.assert200(response) 118 | 119 | # check if object returned contains the desired data 120 | data = json.loads(response.data.decode('utf-8')) 121 | self.assertTrue(len(data) > 0) 122 | 123 | def test_route_memory(self): 124 | # check if route returns code 200 125 | response = self.client.get('/api/system/memory') 126 | self.assert200(response) 127 | 128 | # check if object returned contains the desired data 129 | data = json.loads(response.data.decode('utf-8')) 130 | self.assertTrue('available' in data.keys()) 131 | self.assertTrue('free' in data.keys()) 132 | self.assertTrue('percent' in data.keys()) 133 | self.assertTrue('total' in data.keys()) 134 | self.assertTrue('used' in data.keys()) 135 | 136 | def test_route_swap(self): 137 | # check if route returns code 200 138 | response = self.client.get('/api/system/swap') 139 | self.assert200(response) 140 | 141 | # check if object returned contains the desired data 142 | data = json.loads(response.data.decode('utf-8')) 143 | self.assertTrue('sin' in data.keys()) 144 | self.assertTrue('sout' in data.keys()) 145 | self.assertTrue('free' in data.keys()) 146 | self.assertTrue('percent' in data.keys()) 147 | self.assertTrue('total' in data.keys()) 148 | self.assertTrue('used' in data.keys()) 149 | 150 | def test_route_disk_space(self): 151 | # check if route returns code 200 152 | response = self.client.get('/api/system/disk/space') 153 | self.assert200(response) 154 | 155 | # check if object returned contains the desired data 156 | data = json.loads(response.data.decode('utf-8')) 157 | for disk in data: 158 | self.assertTrue('device' in disk.keys()) 159 | self.assertTrue('fstype' in disk.keys()) 160 | self.assertTrue('mountpoint' in disk.keys()) 161 | self.assertTrue('opts' in disk.keys()) 162 | self.assertTrue('usage' in disk.keys()) 163 | self.assertTrue('free' in disk['usage'].keys()) 164 | self.assertTrue('percent' in disk['usage'].keys()) 165 | self.assertTrue('total' in disk['usage'].keys()) 166 | self.assertTrue('used' in disk['usage'].keys()) 167 | 168 | def test_route_disk_io(self): 169 | # check if route returns code 200 170 | response = self.client.get('/api/system/disk/io') 171 | self.assert200(response) 172 | 173 | # check if object returned contains the desired data 174 | data = json.loads(response.data.decode('utf-8')) 175 | for disk in data: 176 | self.assertTrue('device' in disk.keys()) 177 | self.assertTrue('io' in disk.keys()) 178 | self.assertTrue('read_bytes' in disk['io'].keys()) 179 | self.assertTrue('read_count' in disk['io'].keys()) 180 | self.assertTrue('read_time' in disk['io'].keys()) 181 | self.assertTrue('write_bytes' in disk['io'].keys()) 182 | self.assertTrue('write_count' in disk['io'].keys()) 183 | self.assertTrue('write_time' in disk['io'].keys()) 184 | 185 | def test_route_network_io(self): 186 | # check if route returns code 200 187 | response = self.client.get('/api/network/io') 188 | self.assert200(response) 189 | 190 | # check if object returned contains the desired data 191 | data = json.loads(response.data.decode('utf-8')) 192 | for network in data: 193 | self.assertTrue('device' in network.keys()) 194 | self.assertTrue('address' in network.keys()) 195 | self.assertTrue('io' in network.keys()) 196 | self.assertTrue('bytes_recv' in network['io'].keys()) 197 | self.assertTrue('bytes_sent' in network['io'].keys()) 198 | self.assertTrue('dropin' in network['io'].keys()) 199 | self.assertTrue('dropout' in network['io'].keys()) 200 | self.assertTrue('errin' in network['io'].keys()) 201 | self.assertTrue('errout' in network['io'].keys()) 202 | self.assertTrue('packets_recv' in network['io'].keys()) 203 | self.assertTrue('packets_sent' in network['io'].keys()) 204 | 205 | @mock.patch('serverpanel.ext.serverinfo.get', return_value=valid_ip_data) 206 | def test_route_network_external_success(self, mocked_get): 207 | # check if route returns code 200 208 | response = self.client.get('/api/network/external') 209 | self.assert200(response) 210 | 211 | # check if object returned contains the desired data 212 | data = json.loads(response.data.decode('utf-8')) 213 | self.assertTrue('ip' in data.keys()) 214 | self.assertTrue('country' in data.keys()) 215 | self.assertEqual(data['ip'], '127.0.0.1') 216 | self.assertEqual(data['country'], 'BE') 217 | 218 | @mock.patch('serverpanel.ext.serverinfo.get', return_value=None) 219 | def test_route_network_external_fail(self, mocked_get): 220 | # check if route returns code 200 221 | response = self.client.get('/api/network/external') 222 | self.assert200(response) 223 | 224 | # check if object returned contains the desired data 225 | data = json.loads(response.data.decode('utf-8')) 226 | self.assertTrue('ip' in data.keys()) 227 | self.assertTrue('country' in data.keys()) 228 | self.assertEqual(data['ip'], 'Unknown') 229 | self.assertEqual(data['country'], 'Unknown') 230 | 231 | def test_route_temperature_success(self): 232 | fd, path = tempfile.mkstemp() 233 | 234 | with open(path, "w") as f: 235 | f.write('100000') 236 | 237 | self.app.extensions['flask-serverinfo'].cpu_temp = path 238 | 239 | # check if route returns code 200 240 | response = self.client.get('/api/system/temp') 241 | self.assert200(response) 242 | 243 | # check if object returned contains the desired data 244 | data = json.loads(response.data.decode('utf-8')) 245 | self.assertTrue('cpu' in data.keys()) 246 | self.assertEqual(data['cpu'], 100) 247 | 248 | os.close(fd) 249 | 250 | def test_route_temperature_fail(self): 251 | fd, path = tempfile.mkstemp() 252 | 253 | with open(path, "w") as f: 254 | f.write('not a number') 255 | 256 | self.app.extensions['flask-serverinfo'].cpu_temp = path 257 | 258 | # check if route returns code 200 259 | response = self.client.get('/api/system/temp') 260 | self.assert200(response) 261 | 262 | # check if object returned contains the desired data 263 | data = json.loads(response.data.decode('utf-8')) 264 | self.assertTrue('cpu' in data.keys()) 265 | self.assertEqual(data['cpu'], -1) 266 | 267 | os.close(fd) 268 | 269 | def test_route_processes(self): 270 | # check if route returns code 200 271 | response = self.client.get('/api/system/processes') 272 | self.assert200(response) 273 | 274 | # check if object returned contains the desired data 275 | data = json.loads(response.data.decode('utf-8')) 276 | for proc in data: 277 | self.assertTrue('pid' in proc.keys()) 278 | self.assertTrue('name' in proc.keys()) 279 | self.assertTrue('cpu_percentage' in proc.keys()) 280 | 281 | def test_pihole_disabled(self): 282 | self.app.extensions['flask-serverinfo'].pihole_enabled = False 283 | response = self.client.get('/api/pihole/stats') 284 | self.assert200(response) 285 | 286 | data = json.loads(response.data.decode('utf-8')) 287 | self.assertTrue('enabled' in data.keys()) 288 | self.assertEqual(data['enabled'], False) 289 | self.assertEqual(data['error'], False) 290 | 291 | @mock.patch('serverpanel.ext.serverinfo.get', return_value=valid_pihole_data) 292 | def test_pihole_enabled_success(self, mocked_get): 293 | self.app.extensions['flask-serverinfo'].pihole_enabled = True 294 | self.app.extensions['flask-serverinfo'].pihole_api = '' 295 | 296 | response = self.client.get('/api/pihole/stats') 297 | self.assert200(response) 298 | 299 | data = json.loads(response.data.decode('utf-8')) 300 | self.assertTrue('enabled' in data.keys()) 301 | self.assertEqual(data['error'], False) 302 | 303 | @mock.patch('serverpanel.ext.serverinfo.get', return_value=None) 304 | def test_pihole_enabled_fail(self, mocked_get): 305 | self.app.extensions['flask-serverinfo'].pihole_enabled = True 306 | self.app.extensions['flask-serverinfo'].pihole_api = '' 307 | 308 | response = self.client.get('/api/pihole/stats') 309 | self.assert200(response) 310 | 311 | data = json.loads(response.data.decode('utf-8')) 312 | self.assertTrue('enabled' in data.keys()) 313 | self.assertEqual(data['error'], True) 314 | 315 | 316 | if __name__ == '__main__': 317 | unittest.main() 318 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 |   entry: './jsx/main.js', 6 |   output: { path: './serverpanel/static/js/', filename: 'bundle.js' }, 7 |   module: { 8 |     loaders: [ 9 | { 10 | test: /.jsx?$/, 11 | loader: 'babel-loader', 12 | exclude: /node_modules/, 13 | query: { 14 | presets: ['es2015', 'react'] 15 | } 16 | } 17 | ] 18 |   }, 19 | plugins:[ 20 | new webpack.DefinePlugin({ 21 | 'process.env':{ 22 | 'NODE_ENV': JSON.stringify('production') 23 | } 24 | }), 25 | new webpack.optimize.DedupePlugin(), //dedupe similar code 26 | new webpack.optimize.UglifyJsPlugin(), //minify everything 27 | new webpack.optimize.AggressiveMergingPlugin()//Merge chunks 28 | ] 29 | }; -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from serverpanel import create_app 3 | 4 | application = create_app('config') 5 | 6 | --------------------------------------------------------------------------------