├── .gitignore ├── LICENSE.md ├── README.md ├── app ├── index.html ├── linuxDash.min.css ├── linuxDash.min.js └── server │ ├── config │ └── ping_hosts │ ├── index.go │ ├── index.js │ ├── index.php │ ├── index.py │ └── linux_json_api.sh ├── bin └── linux-dash ├── demo.js ├── ecosystem.config.js ├── gulpfile.js ├── index.html ├── package.json └── src ├── css ├── README.md ├── buttons.css ├── main.css ├── plugins-cotnainer.css └── tables.css └── js ├── README.md ├── core ├── app.js ├── features │ ├── key-value-list │ │ ├── key-value-list.directive.js │ │ └── key-value-list.html │ ├── line-chart │ │ ├── line-chart-plugin.directive.js │ │ └── line-chart-plugin.html │ ├── loader │ │ ├── loader.css │ │ └── loader.directive.js │ ├── multi-line-chart │ │ ├── multi-line-chart-plugin.directive.js │ │ └── multi-line-chart-plugin.html │ ├── navbar │ │ ├── navbar.css │ │ └── navbar.directive.js │ ├── plugin │ │ ├── plugin.css │ │ ├── plugin.directive.js │ │ └── plugin.html │ ├── progress-bar │ │ ├── progress-bar-plugin.directive.js │ │ └── progress-bar.css │ ├── table-data │ │ ├── table-data.css │ │ ├── table-data.directive.js │ │ └── table-data.html │ └── top-bar │ │ ├── topbar.css │ │ └── topbar.directive.js ├── rootscope-event-handlers │ ├── hide-plugin.run.js │ └── make-plugins-draggable.run.js ├── routes.js └── server.service.js └── plugins ├── README.md ├── cpu-avg-load-chart.directive.js ├── cpu-temp.directive.js ├── cpu-utilization-chart.directive.js ├── disk-space ├── disk-space.directive.js └── disk-space.html ├── download-transfer-rate-chart.directive.js ├── ram-chart.directive.js ├── simple-table-data-plugins.directive.js └── upload-transfer-rate-chart.directive.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | */linuxDash.min.js.map 4 | temp 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 afaqurk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 |

8 | v2.0
9 | A simple & low-overhead web dashboard for linux systems 10 |

11 | 12 |

13 | 14 | Demo  |  15 | 16 | Docs 17 | 18 | 19 |

20 | 21 | 22 |

23 | 24 | linux-dash Gitter chat 27 | 28 |

29 | 30 |
31 | 32 | ## Features 33 | * **Small** ----- Under 400KB on disk _(with .git removed)!_ 34 | * **Simple** ---- A minimalist, beautiful dashboard 35 | * **Easy** ------ Drop-in installation 36 | * **Versatile** -- Choose your stack from Node.js, Go, Python, PHP 37 | 38 | ## Installation 39 | 40 | ### Step 1 41 | ```sh 42 | ## 1. clone the repo 43 | git clone --depth 1 https://github.com/afaqurk/linux-dash.git 44 | 45 | ## 2. go to the cloned directory 46 | cd linux-dash/app/server 47 | 48 | ``` 49 | OR, if you prefer to download manually: 50 | 51 | ```sh 52 | ## 1. Download the .zip 53 | curl -LOk https://github.com/afaqurk/linux-dash/archive/master.zip && unzip master.zip 54 | 55 | ## 2. navigate to downloaded & unzipped dir 56 | cd linux-dash-master/app/server 57 | 58 | ``` 59 | 60 | ### Step 2 61 | 62 | See instructions for preferred server linux-dash server _(all included)_: 63 | 64 | * [Node.js](#if-using-nodejs) _(recommended)_ 65 | * [Go](#if-using-go) 66 | * [Python](#if-using-python) 67 | * [PHP](#if-using-php) 68 | 69 | #### If Using Node.js 70 | ```sh 71 | ## install dependencies 72 | npm install --production 73 | 74 | ## start linux-dash (on port 80 by default; may require sudo) 75 | ## You may change this with the `LINUX_DASH_SERVER_PORT` environment variable (eg. `LINUX_DASH_SERVER_PORT=8080 node server`) 76 | ## or provide a --port flag to the command below 77 | ## Additionally, the server will listen on every network interface (`0.0.0.0`). 78 | ## You may change this with the `LINUX_DASH_SERVER_HOST` environment variable (eg. `LINUX_DASH_SERVER_HOST=127.0.0.1 node server`) 79 | ## or provide a --host flag to the command below 80 | node index.js 81 | 82 | ``` 83 | 84 | #### If Using Go 85 | ```sh 86 | ## start the server (on port 80 by default; may require sudo) 87 | go run index.go 88 | ``` 89 | 90 | To build a binary, run `go build && ./server -h`. See [@tehbilly](https://github.com/sergeifilippov)'s notes [here](https://github.com/afaqurk/linux-dash/pull/281) for binary usage options 91 | 92 | #### If Using Python 93 | ```sh 94 | # Start the server (on port 80 by default; may require sudo). 95 | python index.py 96 | ``` 97 | 98 | #### If Using PHP 99 | 100 | 1. Make sure you have the `exec`, `shell_exec`, and `escapeshellarg` functions enabled 101 | 2. Point your web server to `app/` directory under `linux-dash` 102 | 2. Restart your web server (Apache, nginx, etc.) 103 | - For PHP + Apache setup follow the [Digital Ocean tutorial](https://www.digitalocean.com/community/tutorials/how-to-install-linux-dash-on-ubuntu-14-04). 104 | - For help with nginx setup, see [this gist](https://gist.github.com/sergeifilippov/8909839) by [@sergeifilippov](https://github.com/sergeifilippov). 105 | 106 | ## Support 107 | 108 | For general help, please use the [Gitter chat room](https://gitter.im/afaqurk/linux-dash). 109 | 110 | ## Security 111 | 112 | **It is strongly recommended** that all linux-dash installations be protected via a security measure of your choice. 113 | 114 | Linux Dash does not provide any security or authentication features. 115 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Linux Dash : Simple, beautiful server monitoring web dashboard 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/linuxDash.min.css: -------------------------------------------------------------------------------- 1 | button{outline:0} 2 | html{margin-top:0;padding-top:0;-webkit-transition:all 1s ease;-moz-transition:all 1s ease;-ms-transition:all 1s ease;-o-transition:all 1s ease;transition:all 1s ease}body{letter-spacing:.1rem;font-family:Merriweather,Arial,sans-serif;padding:0;margin:0}.centered{margin:0 auto} 3 | #plugins{padding-top:0;margin-top:0;border:1px;margin-left:10%} 4 | table td,table th{border-bottom:1px solid #f1f1f1}table,table td{font-size:10px}table{width:100%;margin:0;border-collapse:collapse;text-align:left;table-layout:fixed}table td,table th{padding:2px;max-width:250px;word-wrap:break-word}table th{font-weight:600;text-transform:uppercase}table td{font-family:Arial,sans-serif;color:rgba(0,0,0,.65)}table tbody tr:hover td{background-color:#fafafa}table.metrics-table{text-align:center}table.metrics-table tr:last-child td{border:none} 5 | .spinner{margin:100px auto;width:50px;height:30px;text-align:center;font-size:10px}.spinner>div{background-color:#009587;height:100%;width:6px;display:inline-block;-webkit-animation:stretchdelay 1.2s infinite ease-in-out;animation:stretchdelay 1.2s infinite ease-in-out}.spinner .rect2{-webkit-animation-delay:-1.1s;animation-delay:-1.1s}.spinner .rect3{-webkit-animation-delay:-1s;animation-delay:-1s}.spinner .rect4{-webkit-animation-delay:-.9s;animation-delay:-.9s}.spinner .rect5{-webkit-animation-delay:-.8s;animation-delay:-.8s}@-webkit-keyframes stretchdelay{0%,100%,40%{-webkit-transform:scaleY(.4)}20%{-webkit-transform:scaleY(1)}}@keyframes stretchdelay{0%,100%,40%{transform:scaleY(.4);-webkit-transform:scaleY(.4)}20%{transform:scaleY(1);-webkit-transform:scaleY(1)}} 6 | nav-bar{display:block;background-color:#f6f8f8;height:40px;padding-left:10%;padding-top:3px;padding-right:5%}nav-bar ul{margin-left:10px;padding:0;list-style-type:none;display:inline}nav-bar ul li a{font-size:11px;text-transform:uppercase;text-decoration:none;line-height:2.3rem;color:grey}nav-bar ul li a:hover{color:#000}nav-bar ul li.active{border-bottom:2px solid #009587}nav-bar ul li.active a{color:#009587}nav-bar ul li{margin-left:20px;display:inline;padding-left:5px;padding-bottom:10px}nav-bar .title{color:#009587;font-weight:300;font-size:20px;letter-spacing:.1rem;float:left;padding-top:5px}nav-bar .right-content{float:right}nav-bar .right-content,nav-bar .right-content a{font-size:11px;color:grey;padding-top:10px}nav-bar .right-content a:hover{color:#000} 7 | .plugin{transition:all .1s linear;vertical-align:text-top;width:400px;display:inline-block;padding:0 0 15px;color:#000;text-align:center;border-radius:2px;box-shadow:0 1px 10px rgba(0,0,0,.13),0 5px 10px rgba(0,0,0,.16);margin:30px 15px 0 0;max-height:400px;overflow:hidden;resize:both;background-color:rgba(0,0,0,.015)}.plugin-hidden .top-bar .heading{color:grey;font-style:italic}.plugin-enlarged{width:50%}@media (max-width:768px){.plugin{max-width:80%;float:none;margin:0 auto 10px}.plugin-body{max-height:323px}.plugin-enlarged{width:80%}}@media (max-width:950px){.plugin{margin:15px 10px 0 0}.plugin-enlarged{width:90%}}.plugin.chart-plugin{resize:none}.plugin-body{background-color:#fff;height:323px;font-size:12px;padding:10px;line-height:30px;overflow:auto;border-top:1px solid #ececec}.plugin.chart-plugin,.plugin.chart-plugin .plugin-body{overflow:hidden;padding:0}.plugin-body-short{height:263px}.plugin last-update{font-size:11px;float:left}.plugin ::-webkit-scrollbar{width:8px;height:8px}.plugin ::-webkit-scrollbar-track{background:#eee;border:thin solid #d3d3d3;box-shadow:0 0 3px #dfdfdf inset}.plugin ::-webkit-scrollbar-thumb{background:rgba(0,0,0,.1);border:thin solid rgba(0,0,0,.1);border-radius:0}.plugin ::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.2)} 8 | .progress-bar{background-color:#eec;border-radius:10px;padding:0;clear:both;display:inline-block;overflow:hidden;white-space:nowrap}.progress-bar>div{background-color:#1EAEDB;width:0%;height:5px;border-radius:5px} 9 | .table-data-plugin .filter-container{padding-bottom:0;margin:0}.table-data-plugin .filter,.table-data-plugin .filter:active,.table-data-plugin .filter:focus{height:20px;padding:5px;border:none;outline-color:transparent;background:0 0;width:100%;margin:0;text-align:center;font-size:15px}.table-data-plugin .filter:focus{border-bottom:1px solid #ff5722}.table-data-plugin thead tr th a,.table-data-plugin thead tr th a:visited{color:#000;text-decoration:none}.table-data-plugin .column-sort-caret{font-size:10px;color:#1EAEDB} 10 | .top-bar{height:15px;padding:15px;font-size:13px;text-transform:capitalize;color:#009587;background-color:#f6f8f8}.top-bar .heading{float:left;cursor:grab;cursor:-moz-grab;cursor:-webkit-grab}.ld-top-bar-btn{float:right;font-size:17px;color:#009587;background:#fff;border:1px solid #eee;border-radius:50%;width:30px;height:30px;margin-top:-5px;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-ms-transition:all .5s ease;-o-transition:all .5s ease;cursor:pointer;transition:all .5s ease}.minimize-btn{font-size:19px}.ld-refresh-btn:hover{background-color:#ffeb3b;color:#000}.ld-refresh-btn:active{background-color:#0f9d58}.plugin-hidden .width-toggle-btn{display:none}.plugin-enlarged .width-toggle-btn,.plugin-hidden .minimize-btn{background:#eee;color:#000} -------------------------------------------------------------------------------- /app/server/config/ping_hosts: -------------------------------------------------------------------------------- 1 | google.com 2 | yahoo.com 3 | twitter.com 4 | -------------------------------------------------------------------------------- /app/server/index.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "os/exec" 10 | ) 11 | 12 | var ( 13 | listenAddress = flag.String("listen", "0.0.0.0:80", "Where the server listens for connections. [interface]:port") 14 | staticPath = flag.String("static", "../", "Location of static files.") 15 | ) 16 | 17 | func init() { 18 | flag.Parse() 19 | } 20 | 21 | func main() { 22 | http.Handle("/", http.FileServer(http.Dir(*staticPath))) 23 | http.HandleFunc("/server/", func(w http.ResponseWriter, r *http.Request) { 24 | module := r.URL.Query().Get("module") 25 | if module == "" { 26 | http.Error(w, "No module specified, or requested module doesn't exist.", 406) 27 | return 28 | } 29 | 30 | // Execute the command 31 | cmd := exec.Command("./linux_json_api.sh", module) 32 | var output bytes.Buffer 33 | cmd.Stdout = &output 34 | err := cmd.Run() 35 | if err != nil { 36 | fmt.Printf("Error executing '%s': %s\n\tScript output: %s\n", module, err.Error(), output.String()) 37 | http.Error(w, "Unable to execute module.", http.StatusInternalServerError) 38 | return 39 | } 40 | 41 | w.Write(output.Bytes()) 42 | }) 43 | 44 | fmt.Println("Starting http server at:", *listenAddress) 45 | err := http.ListenAndServe(*listenAddress, nil) 46 | if err != nil { 47 | fmt.Println("Error starting http server:", err) 48 | os.Exit(1) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/server/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var app = require('express')() 3 | var server = require('http').Server(app) 4 | var path = require('path') 5 | var spawn = require('child_process').spawn 6 | var fs = require('fs') 7 | var ws = require('websocket').server 8 | var args = require('yargs').argv 9 | var host = args.host || process.env.LINUX_DASH_SERVER_HOST || '0.0.0.0' 10 | var port = args.port || process.env.LINUX_DASH_SERVER_PORT || 80 11 | 12 | server.listen(port, host, function() { 13 | console.log('Linux Dash Server Started on ' + host + ':' + port + '!'); 14 | }) 15 | 16 | app.use(express.static(path.resolve(__dirname + '/../'))) 17 | 18 | app.get('/', function (req, res) { 19 | res.sendFile(path.resolve(__dirname + '/../index.html')) 20 | }) 21 | 22 | app.get('/websocket', function (req, res) { 23 | 24 | res.send({ 25 | websocket_support: true, 26 | }) 27 | 28 | }) 29 | 30 | wsServer = new ws({ 31 | httpServer: server 32 | }) 33 | 34 | var nixJsonAPIScript = __dirname + '/linux_json_api.sh' 35 | 36 | function getPluginData(pluginName, callback) { 37 | var command = spawn(nixJsonAPIScript, [ pluginName, '' ]) 38 | var output = [] 39 | 40 | command.stdout.on('data', function(chunk) { 41 | output.push(chunk.toString()) 42 | }) 43 | 44 | command.on('close', function (code) { 45 | callback(code, output) 46 | }) 47 | } 48 | 49 | wsServer.on('request', function(request) { 50 | 51 | var wsClient = request.accept('', request.origin) 52 | 53 | wsClient.on('message', function(wsReq) { 54 | 55 | var moduleName = wsReq.utf8Data 56 | var sendDataToClient = function(code, output) { 57 | if (code === 0) { 58 | var wsResponse = '{ "moduleName": "' + moduleName + '", "output": "'+ output.join('') +'" }' 59 | wsClient.sendUTF(wsResponse) 60 | } 61 | } 62 | 63 | getPluginData(moduleName, sendDataToClient) 64 | 65 | }) 66 | 67 | }) 68 | 69 | app.get('/server/', function (req, res) { 70 | 71 | var respondWithData = function(code, output) { 72 | if (code === 0) res.send(output.toString()) 73 | else res.sendStatus(500) 74 | } 75 | 76 | getPluginData(req.query.module, respondWithData) 77 | }) 78 | -------------------------------------------------------------------------------- /app/server/index.php: -------------------------------------------------------------------------------- 1 | to stop') 54 | server.serve_forever() 55 | -------------------------------------------------------------------------------- /app/server/linux_json_api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ECHO=$(type -P echo) 4 | SED=$(type -P sed) 5 | GREP=$(type -P grep) 6 | TR=$(type -P tr) 7 | AWK=$(type -P awk) 8 | CAT=$(type -P cat) 9 | HEAD=$(type -P head) 10 | CUT=$(type -P cut) 11 | PS=$(type -P ps) 12 | 13 | _parseAndPrint() { 14 | while read data; do 15 | $ECHO -n "$data" | $SED -r 's/\\//g' | $TR -d "\n"; 16 | done; 17 | } 18 | 19 | arp_cache() { 20 | local arpCommand=$(type -P arp) 21 | 22 | result=$($arpCommand | $AWK 'BEGIN {print "["} NR>1 \ 23 | {print "{ \"addr\": \"" $1 "\", " \ 24 | "\"hw_type\": \"" $2 "\", " \ 25 | "\"hw_addr.\": \"" $3 "\", " \ 26 | "\"mask\": \"" $5 "\" }, " \ 27 | } \ 28 | END {print "]"}' \ 29 | | $SED 'N;$s/},/}/;P;D') 30 | 31 | if [ -z "$result" ]; then 32 | $ECHO {} 33 | else 34 | $ECHO $result | _parseAndPrint 35 | fi 36 | } 37 | 38 | bandwidth() { 39 | 40 | $CAT /proc/net/dev \ 41 | | $AWK 'BEGIN {print "["} NR>2 {print "{ \"interface\": \"" $1 "\"," \ 42 | " \"tx\": " $2 "," \ 43 | " \"rx\": " $10 " }," } END {print "]"}' \ 44 | | $SED 'N;$s/,\n/\n/;P;D' \ 45 | | _parseAndPrint 46 | } 47 | 48 | common_applications() { 49 | result=$(whereis php node mysql mongo vim python ruby java apache2 nginx openssl vsftpd make \ 50 | | $AWK -F: '{if(length($2)==0) { installed="false"; } else { installed="true"; } \ 51 | print \ 52 | "{ \ 53 | \"binary\": \""$1"\", \ 54 | \"location\": \""$2"\", \ 55 | \"installed\": "installed" \ 56 | },"}') 57 | 58 | $ECHO "[" ${result%?} "]" | _parseAndPrint 59 | } 60 | 61 | cpu_info() { 62 | local lscpuCommand=$(type -P lscpu) 63 | 64 | result=$($lscpuCommand \ 65 | | $AWK -F: '{print "\""$1"\": \""$2"\"," } '\ 66 | ) 67 | 68 | $ECHO "{" ${result%?} "}" | _parseAndPrint 69 | } 70 | 71 | cpu_intensive_processes() { 72 | 73 | result=$($PS axo pid,user,pcpu,rss,vsz,comm --sort -pcpu,-rss,-vsz \ 74 | | $HEAD -n 15 \ 75 | | $AWK 'BEGIN{OFS=":"} NR>1 {print "{ \"pid\": " $1 \ 76 | ", \"user\": \"" $2 "\"" \ 77 | ", \"cpu%\": " $3 \ 78 | ", \"rss\": " $4 \ 79 | ", \"vsz\": " $5 \ 80 | ", \"cmd\": \"" $6 "\"" "},"\ 81 | }') 82 | 83 | $ECHO "[" ${result%?} "]" | _parseAndPrint 84 | } 85 | 86 | cpu_temp() { 87 | local ID=* 88 | [ -f /etc/os-release ] && source /etc/os-release 89 | case "$ID" in 90 | "raspbian") 91 | cpu=$(/dev/null; then 96 | returnString=`sensors` 97 | #amd 98 | if [[ "${returnString/"k10"}" != "${returnString}" ]] ; then 99 | $ECHO ${returnString##*k10} | $CUT -d ' ' -f 6 | $CUT -c 2- | $CUT -c 1-4 100 | #intel 101 | elif [[ "${returnString/"core"}" != "${returnString}" ]] ; then 102 | fromcore=${returnString##*"coretemp"} 103 | $ECHO ${fromcore##*Physical} | $CUT -d ' ' -f 3 | $CUT -c 2-5 | _parseAndPrint 104 | fi 105 | else 106 | $ECHO "[]" | _parseAndPrint 107 | fi 108 | ;; 109 | esac 110 | } 111 | 112 | # by Paul Colby (http://colby.id.au), no rights reserved ;) 113 | cpu_utilization() { 114 | 115 | PREV_TOTAL=0 116 | PREV_IDLE=0 117 | iteration=0 118 | 119 | while [[ iteration -lt 2 ]]; do 120 | # Get the total CPU statistics, discarding the 'cpu ' prefix. 121 | CPU=(`$SED -n 's/^cpu\s//p' /proc/stat`) 122 | IDLE=${CPU[3]} # Just the idle CPU time. 123 | 124 | # Calculate the total CPU time. 125 | TOTAL=0 126 | for VALUE in "${CPU[@]}"; do 127 | let "TOTAL=$TOTAL+$VALUE" 128 | done 129 | 130 | # Calculate the CPU usage since we last checked. 131 | let "DIFF_IDLE=$IDLE-$PREV_IDLE" 132 | let "DIFF_TOTAL=$TOTAL-$PREV_TOTAL" 133 | let "DIFF_USAGE=(1000*($DIFF_TOTAL-$DIFF_IDLE)/$DIFF_TOTAL+5)/10" 134 | #echo -en "\rCPU: $DIFF_USAGE% \b\b" 135 | 136 | # Remember the total and idle CPU times for the next check. 137 | PREV_TOTAL="$TOTAL" 138 | PREV_IDLE="$IDLE" 139 | 140 | # Wait before checking again. 141 | sleep 1 142 | iteration="$iteration+1" 143 | done 144 | $ECHO -en "$DIFF_USAGE" 145 | } 146 | 147 | cron_history() { 148 | 149 | local cronLog='/var/log/syslog' 150 | local numberOfLines='50' 151 | 152 | # Month, Day, Time, Hostname, tag, user, 153 | 154 | result=$($GREP -m $numberOfLines CRON $cronLog \ 155 | | $AWK '{ s = ""; for (i = 6; i <= NF; i++) s = s $i " "; \ 156 | print "{\"time\" : \"" $1" "$2" "$3 "\"," \ 157 | "\"user\" : \"" $6 "\"," \ 158 | "\"message\" : \"" $5" "gensub("\"", "\\\\\"", "g", s) "\"" \ 159 | "}," 160 | }' 161 | ) 162 | 163 | $ECHO [${result%?}] | _parseAndPrint 164 | } 165 | 166 | current_ram() { 167 | 168 | local memInfoFile="/proc/meminfo" 169 | 170 | # References: 171 | # Calculations: http://zcentric.com/2012/05/29/mapping-procmeminfo-to-output-of-free-command/ 172 | # Fields: https://www.kernel.org/doc/Documentation/filesystems/proc.txt 173 | 174 | memInfo=$($CAT $memInfoFile | $GREP 'MemTotal\|MemFree\|Buffers\|Cached') 175 | 176 | $ECHO $memInfo | $AWK '{print "{ \"total\": " ($2/1024) ", \"used\": " ( ($2-($5+$8+$11))/1024 ) ", \"available\": " (($5+$8+$11)/1024) " }" }' | _parseAndPrint 177 | } 178 | 179 | disk_partitions() { 180 | local dfCommand=$(type -P df) 181 | 182 | result=$($dfCommand -Ph | $AWK 'NR>1 {print "{\"file_system\": \"" $1 "\", \"size\": \"" $2 "\", \"used\": \"" $3 "\", \"avail\": \"" $4 "\", \"used%\": \"" $5 "\", \"mounted\": \"" $6 "\"},"}') 183 | 184 | $ECHO [ ${result%?} ] | _parseAndPrint 185 | } 186 | 187 | docker_processes() { 188 | 189 | local result="" 190 | local dockerCommand=$(type -P docker) 191 | local containers="$($dockerCommand ps | $AWK '{if(NR>1) print $NF}')" 192 | 193 | for i in $containers; do 194 | result="$result $($dockerCommand top $i axo pid,user,pcpu,pmem,comm --sort -pcpu,-pmem \ 195 | | $HEAD -n 15 \ 196 | | $AWK -v cnt="$i" 'BEGIN{OFS=":"} NR>1 {print "{ \"cname\": \"" cnt \ 197 | "\", \"pid\": " $1 \ 198 | ", \"user\": \"" $2 "\"" \ 199 | ", \"cpu%\": " $3 \ 200 | ", \"mem%\": " $4 \ 201 | ", \"cmd\": \"" $5 "\"" "},"\ 202 | }')" 203 | done 204 | 205 | $ECHO "[" ${result%?} "]" | _parseAndPrint 206 | } 207 | 208 | download_transfer_rate() { 209 | 210 | local files=(/sys/class/net/*) 211 | local pos=$(( ${#files[*]} - 1 )) 212 | local last=${files[$pos]} 213 | 214 | local json_output="{" 215 | 216 | for interface in "${files[@]}" 217 | do 218 | basename=$(basename "$interface") 219 | 220 | # find the number of bytes transfered for this interface 221 | in1=$($CAT /sys/class/net/"$basename"/statistics/rx_bytes) 222 | 223 | # wait a second 224 | sleep 1 225 | 226 | # check same interface again 227 | in2=$($CAT /sys/class/net/"$basename"/statistics/rx_bytes) 228 | 229 | # get the difference (transfer rate) 230 | in_bytes=$((in2 - in1)) 231 | 232 | # convert transfer rate to KB 233 | in_kbytes=$((in_bytes / 1024)) 234 | 235 | # convert transfer rate to KB 236 | json_output="$json_output \"$basename\": $in_kbytes" 237 | 238 | # if it is not the last line 239 | if [[ ! $interface == $last ]] 240 | then 241 | # add a comma to the line (JSON formatting) 242 | json_output="$json_output," 243 | fi 244 | done 245 | 246 | # close the JSON object & print to screen 247 | $ECHO "$json_output}" | _parseAndPrint 248 | } 249 | 250 | general_info() { 251 | local lsbRelease=$(type -P lsb_release) 252 | local uName=$(type -P uname) 253 | local hostName=$(type -P hostname) 254 | 255 | function displaytime { 256 | local T=$1 257 | local D=$((T/60/60/24)) 258 | local H=$((T/60/60%24)) 259 | local M=$((T/60%60)) 260 | local S=$((T%60)) 261 | [[ $D > 0 ]] && printf '%d days ' $D 262 | [[ $H > 0 ]] && printf '%d hours ' $H 263 | [[ $M > 0 ]] && printf '%d minutes ' $M 264 | [[ $D > 0 || $H > 0 || $M > 0 ]] && printf 'and ' 265 | printf '%d seconds\n' $S 266 | } 267 | 268 | local lsbRelease=$($lsbRelease -ds | $SED -e 's/^"//' -e 's/"$//') 269 | local uname=$($uName -r | $SED -e 's/^"//' -e 's/"$//') 270 | local os=$($ECHO $lsbRelease $uname) 271 | local hostname=$($hostName) 272 | local uptime_seconds=$($CAT /proc/uptime | awk '{print $1}') 273 | local server_time=$(date) 274 | 275 | $ECHO "{ \"OS\": \"$os\", \"Hostname\": \"$hostname\", \"Uptime\": \" $(displaytime ${uptime_seconds%.*}) \", \"Server Time\": \"$server_time\" }" | _parseAndPrint 276 | } 277 | 278 | io_stats() { 279 | 280 | result=$($CAT /proc/diskstats | $AWK \ 281 | '{ if($4==0 && $8==0 && $12==0 && $13==0) next } \ 282 | {print "{ \"device\": \"" $3 "\", \"reads\": \""$4"\", \"writes\": \"" $8 "\", \"in_prog.\": \"" $12 "\", \"time\": \"" $13 "\"},"}' 283 | ) 284 | 285 | $ECHO [ ${result%?} ] | _parseAndPrint 286 | } 287 | 288 | ip_addresses() { 289 | 290 | local ifconfigCmd=$(type -P ifconfig) 291 | local digCmd=$(type -P dig) 292 | 293 | externalIp=$($digCmd +short myip.opendns.com @resolver1.opendns.com) 294 | 295 | $ECHO -n "[" 296 | 297 | for item in $($ifconfigCmd | $GREP -oP "^[a-zA-Z0-9:]*(?=:)") 298 | do 299 | $ECHO -n "{\"interface\" : \""$item"\", \"ip\" : \"$( $ifconfigCmd $item | $GREP "inet" | $AWK '{match($0,"inet (addr:)?([0-9.]*)",a)}END{ if (NR != 0){print a[2]; exit}{print "none"}}')\"}, " 300 | done 301 | 302 | $ECHO "{ \"interface\": \"external\", \"ip\": \"$externalIp\" } ]" | _parseAndPrint 303 | } 304 | 305 | load_avg() { 306 | 307 | local numberOfCores=$($GREP -c 'processor' /proc/cpuinfo) 308 | 309 | if [ $numberOfCores -eq 0 ]; then 310 | numberOfCores=1 311 | fi 312 | 313 | result=$($CAT /proc/loadavg | $AWK '{print "{ \"1_min_avg\": " ($1*100)/'$numberOfCores' ", \"5_min_avg\": " ($2*100)/'$numberOfCores' ", \"15_min_avg\": " ($3*100)/'$numberOfCores' "}," }') 314 | 315 | $ECHO ${result%?} | _parseAndPrint 316 | } 317 | 318 | logged_in_users() { 319 | local whoCommand=$(type -P w) 320 | 321 | result=$(COLUMNS=300 $whoCommand -h | $AWK '{print "{\"user\": \"" $1 "\", \"from\": \"" $3 "\", \"when\": \"" $4 "\"},"}') 322 | 323 | $ECHO [ ${result%?} ] | _parseAndPrint 324 | } 325 | 326 | memcached() { 327 | local ncCommand=$(type -P nc) 328 | 329 | $ECHO "stats" \ 330 | | $ncCommand -w 1 127.0.0.1 11211 \ 331 | | $GREP 'bytes' \ 332 | | $AWK 'BEGIN {print "{"} {print "\"" $2 "\": " $3 } END {print "}"}' \ 333 | | $TR '\r' ',' \ 334 | | $SED 'N;$s/,\n/\n/;P;D' \ 335 | | _parseAndPrint 336 | } 337 | 338 | memory_info() { 339 | 340 | $CAT /proc/meminfo \ 341 | | $AWK -F: 'BEGIN {print "{"} {print "\"" $1 "\": \"" $2 "\"," } END {print "}"}' \ 342 | | $SED 'N;$s/,\n/\n/;P;D' \ 343 | | _parseAndPrint 344 | } 345 | 346 | network_connections() { 347 | 348 | local netstatCmd=$(type -P netstat) 349 | local sortCmd=$(type -P sort) 350 | local uniqCmd=$(type -P uniq) 351 | 352 | $netstatCmd -ntu \ 353 | | $AWK 'NR>2 {print $5}' \ 354 | | $sortCmd \ 355 | | $uniqCmd -c \ 356 | | $AWK 'BEGIN {print "["} {print "{ \"connections\": " $1 ", \"address\": \"" $2 "\" }," } END {print "]"}' \ 357 | | $SED 'N;$s/},/}/;P;D' \ 358 | | _parseAndPrint 359 | } 360 | 361 | number_of_cpu_cores() { 362 | 363 | local numberOfCPUCores=$($GREP -c 'model name' /proc/cpuinfo) 364 | 365 | if [ -z $numberOfCPUCores ]; then 366 | echo "cannot be found"; 367 | fi 368 | } 369 | 370 | # http://askubuntu.com/questions/413367/ping-multiple-ips-using-bash 371 | ping() { 372 | 373 | # get absolute path to config file 374 | local SCRIPTPATH=$(dirname $(readlink -f $0)) 375 | local CONFIG_PATH=$SCRIPTPATH"/config/ping_hosts" 376 | 377 | local pingCmd=$(type -P ping) 378 | local numOfLinesInConfig=$($SED -n '$=' $CONFIG_PATH) 379 | local result='[' 380 | 381 | $CAT $CONFIG_PATH \ 382 | | while read output 383 | do 384 | singlePing=$($pingCmd -qc 2 $output \ 385 | | $AWK -F/ 'BEGIN { endLine="}," } /^rtt/ { if ('$numOfLinesInConfig'==1){endLine="}"} print "{" "\"host\": \"'$output'\", \"ping\": " $5 " " endLine }' \ 386 | ) 387 | numOfLinesInConfig=$(($numOfLinesInConfig-1)) 388 | result=$result$singlePing 389 | if [ $numOfLinesInConfig -eq 0 ] 390 | then 391 | $ECHO $result"]" 392 | fi 393 | done \ 394 | | $SED 's/\},]/}]/g' \ 395 | | _parseAndPrint 396 | } 397 | 398 | pm2_stats() { 399 | 400 | #get data 401 | local data="$(pm2 list)" 402 | local tailCommand=$(type -P tail) 403 | 404 | #only process data if variable has a length 405 | #this should handle cases where pm2 is not installed 406 | if [ -n "$data" ]; then 407 | 408 | #start processing data on line 4 409 | #don't process last 2 lines 410 | json=$( $ECHO "$data" | $tailCommand -n +4 | $HEAD -n +2 \ 411 | | $AWK '{print "{"}\ 412 | {print "\"appName\":\"" $2 "\","} \ 413 | {print "\"id\":\"" $4 "\","} \ 414 | {print "\"mode\":\"" $6 "\","} \ 415 | {print "\"pid\":\"" $8 "\","}\ 416 | {print "\"status\":\"" $10 "\","}\ 417 | {print "\"restart\":\"" $12 "\","}\ 418 | {print "\"uptime\":\"" $14 "\","}\ 419 | {print "\"memory\":\"" $16 $17 "\","}\ 420 | {print "\"watching\":\"" $19 "\""}\ 421 | {print "},"}') 422 | #make sure to remove last comma and print in array 423 | $ECHO "[" ${json%?} "]" | _parseAndPrint 424 | else 425 | #no data found 426 | $ECHO "[]" | _parseAndPrint 427 | fi 428 | } 429 | 430 | ram_intensive_processes() { 431 | 432 | local psCommand=$(type -P ps) 433 | 434 | result=$($psCommand axo pid,user,pmem,rss,vsz,comm --sort -pmem,-rss,-vsz \ 435 | | $HEAD -n 15 \ 436 | | $AWK 'NR>1 {print "{ \"pid\": " $1 \ 437 | ", \"user\": \"" $2 \ 438 | "\", \"mem%\": " $3 \ 439 | ", \"rss\": " $4 \ 440 | ", \"vsz\": " $5 \ 441 | ", \"cmd\": \"" $6 \ 442 | "\"},"}') 443 | 444 | $ECHO [ ${result%?} ] | _parseAndPrint 445 | } 446 | 447 | recent_account_logins() { 448 | 449 | local lastLogCommand=$(type -p lastlog) 450 | 451 | result=$($lastLogCommand -t 365 \ 452 | | $AWK 'NR>1 {\ 453 | print "{ \ 454 | \"user\": \"" $1 "\", \ 455 | \"ip\": \"" $3 "\","" \ 456 | \"date\": \"" $5" "$6" "$7" "$8" "$9 "\"}," 457 | }' 458 | ) 459 | $ECHO [ ${result%?} ] | _parseAndPrint 460 | } 461 | 462 | redis() { 463 | 464 | ########### Enter Your Redis Password HERE ######### 465 | local redisPassword='' 466 | ########### Enter Your Redis Password HERE ######### 467 | 468 | local redisCommand=$(type -P redis-cli); 469 | 470 | if [ -n "$redisPassword" ]; then 471 | redisCommand="$redisCommand -a $redisPassword" 472 | fi 473 | 474 | result=$($redisCommand INFO \ 475 | | $GREP 'redis_version\|connected_clients\|connected_slaves\|used_memory_human\|total_connections_received\|total_commands_processed' \ 476 | | $AWK -F: '{print "\"" $1 "\":" "\"" $2 }' \ 477 | | $TR '\r' '"' | $TR '\n' ',' 478 | ) 479 | $ECHO { ${result%?} } | _parseAndPrint 480 | } 481 | 482 | scheduled_crons() { 483 | 484 | ###### 485 | # Credit: http://stackoverflow.com/questions/134906/how-do-i-list-all-cron-jobs-for-all-users#answer-137173 486 | ###### 487 | 488 | local egrepCmd=$(type -P egrep) 489 | local crontabCmd=$(type -P crontab) 490 | 491 | # System-wide crontab file and cron job directory. Change these for your system. 492 | CRONTAB='/etc/crontab' 493 | CRONDIR='/etc/cron.d' 494 | 495 | # Single tab character. Annoyingly necessary. 496 | tab=$($ECHO -en "\t") 497 | 498 | # Given a stream of crontab lines, exclude non-cron job lines, replace 499 | # whitespace characters with a single space, and remove any spaces from the 500 | # beginning of each line. 501 | function clean_cron_lines() { 502 | while read line ; do 503 | $ECHO "${line}" | 504 | $egrepCmd --invert-match '^($|\s*#|\s*[[:alnum:]_]+=)' | 505 | $SED --regexp-extended "s/\s+/ /g" | 506 | $SED --regexp-extended "s/^ //" 507 | done; 508 | } 509 | 510 | # Given a stream of cleaned crontab lines, $echoCmd any that don't include the 511 | # run-parts command, and for those that do, show each job file in the run-parts 512 | # directory as if it were scheduled explicitly. 513 | function lookup_run_parts() { 514 | while read line ; do 515 | match=$($ECHO "${line}" | $egrepCmd -o 'run-parts (-{1,2}\S+ )*\S+') 516 | 517 | if [[ -z "${match}" ]] ; then 518 | $echoCmd "${line}" 519 | else 520 | cron_fields=$($ECHO "${line}" | $CUT -f1-6 -d' ') 521 | cron_job_dir=$($ECHO "${match}" | $AWK '{print $NF}') 522 | 523 | if [[ -d "${cron_job_dir}" ]] ; then 524 | for cron_job_file in "${cron_job_dir}"/* ; do # */ 525 | [[ -f "${cron_job_file}" ]] && $ECHO "${cron_fields} ${cron_job_file}" 526 | done 527 | fi 528 | fi 529 | done; 530 | } 531 | 532 | # Temporary file for crontab lines. 533 | temp=$(mktemp) || exit 1 534 | 535 | # Add all of the jobs from the system-wide crontab file. 536 | $CAT "${CRONTAB}" | clean_cron_lines | lookup_run_parts >"${temp}" 537 | 538 | # Add all of the jobs from the system-wide cron directory. 539 | $CAT "${CRONDIR}"/* | clean_cron_lines >>"${temp}" # */ 540 | 541 | # Add each user's crontab (if it exists). Insert the user's name between the 542 | # five time fields and the command. 543 | while read user ; do 544 | $crontabCmd -l -u "${user}" 2>/dev/null | 545 | clean_cron_lines | 546 | $SED --regexp-extended "s/^((\S+ +){5})(.+)$/\1${user} \3/" >>"${temp}" 547 | done < <($CUT --fields=1 --delimiter=: /etc/passwd) 548 | 549 | # Output the collected crontab lines. 550 | 551 | ## Changes: Parses output into JSON 552 | 553 | $CAT "${temp}" \ 554 | | $AWK 'BEGIN {print "["} \ 555 | {print "{ \"min\": \"" $1 \ 556 | "\", \"hrs\": \"" $2 "\", " \ 557 | " \"day\": \"" $3 "\", " \ 558 | " \"month\": \"" $4 "\", " \ 559 | " \"wkday\": \"" $5 "\", " \ 560 | " \"user\": \"" $6 "\", " \ 561 | " \"CMD\": \""} \ 562 | {for(i=7;i<=NF;++i) printf("%s ", gensub("\"", "\\\\\"", "g", $i) ) } \ 563 | {print "\" " \ 564 | "}," } \ 565 | END {print "]"}' \ 566 | | $SED 'N;$s/,\n//;P;D' \ 567 | | _parseAndPrint 568 | 569 | rm --force "${temp}" 570 | } 571 | 572 | swap() { 573 | 574 | local wcCmd=$(which wc) 575 | 576 | local swapLineCount=$($CAT /proc/swaps | $wcCmd -l) 577 | 578 | if [ "$swapLineCount" -gt 1 ]; then 579 | 580 | result=$($CAT /proc/swaps \ 581 | | $AWK 'NR>1 {print "{ \"filename\": \"" $1"\", \"type\": \""$2"\", \"size\": \""$3"\", \"used\": \""$4"\", \"priority\": \""$5"\"}," }' 582 | ) 583 | 584 | $ECHO [ ${result%?} ] | _parseAndPrint 585 | 586 | else 587 | $ECHO [] | _parseAndPrint 588 | fi 589 | } 590 | 591 | upload_transfer_rate() { 592 | 593 | local files=(/sys/class/net/*) 594 | local pos=$(( ${#files[*]} - 1 )) 595 | local last=${files[$pos]} 596 | 597 | local json_output="{" 598 | 599 | for interface in "${files[@]}" 600 | do 601 | basename=$(basename "$interface") 602 | 603 | # find the number of bytes transfered for this interface 604 | out1=$($CAT /sys/class/net/"$basename"/statistics/tx_bytes) 605 | 606 | # wait a second 607 | sleep 1 608 | 609 | # check same interface again 610 | out2=$($CAT /sys/class/net/"$basename"/statistics/tx_bytes) 611 | 612 | # get the difference (transfer rate) 613 | out_bytes=$((out2 - out1)) 614 | 615 | # convert transfer rate to KB 616 | out_kbytes=$((out_bytes / 1024)) 617 | 618 | # convert transfer rate to KB 619 | json_output="$json_output \"$basename\": $out_kbytes" 620 | 621 | # if it is not the last line 622 | if [[ ! $interface == $last ]] 623 | then 624 | # add a comma to the line (JSON formatting) 625 | json_output="$json_output," 626 | fi 627 | done 628 | 629 | # close the JSON object & print to screen 630 | $ECHO "$json_output}" | _parseAndPrint 631 | } 632 | 633 | user_accounts() { 634 | result=$($AWK -F: '{ \ 635 | if ($3<=499){userType="system";} \ 636 | else {userType="user";} \ 637 | print "{ \"type\": \"" userType "\"" ", \"user\": \"" $1 "\", \"home\": \"" $6 "\" }," }' < /etc/passwd 638 | ) 639 | 640 | length=$($ECHO ${#result}) 641 | 642 | if [ $length -eq 0 ]; then 643 | result=$(getent passwd | $AWK -F: '{ if ($3<=499){userType="system";} else {userType="user";} print "{ \"type\": \"" userType "\"" ", \"user\": \"" $1 "\", \"home\": \"" $6 "\" }," }') 644 | fi 645 | 646 | $ECHO [ ${result%?} ] | _parseAndPrint 647 | } 648 | 649 | fnCalled="$1" 650 | 651 | # Check if the function call is indeed a function. 652 | if [ -n "$(type -t $fnCalled)" ] && [ "$(type -t $fnCalled)" = function ]; then 653 | ${fnCalled} 654 | else 655 | echo '{\"success\":false,\"status\":\"Invalid module\"}' 656 | fi 657 | 658 | -------------------------------------------------------------------------------- /bin/linux-dash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../app/server/index.js') 3 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('linuxDashDemo', ['linuxDash', 'ngMockE2E']) 3 | .run(function($httpBackend) { 4 | 5 | // signal to not use websockets for demo 6 | $httpBackend.whenGET('/websocket').respond(false) 7 | 8 | /** System Status */ 9 | $httpBackend.whenGET('server/?module=current_ram').respond(function () { 10 | return [200, {total: 512, used: 200, available: 312 }] 11 | }) 12 | 13 | $httpBackend.whenGET('server/?module=ram_intensive_processes').respond(function () { 14 | return [200, [ { 'pid': 2782, 'user': 'user1', 'mem%': 2.2, 'rss': 232332, 'vsz': 1118240, 'cmd': 'nginx'}, { 'pid': 2623, 'user': 'user1', 'mem%': 2.0, 'rss': 209732, 'vsz': 1321800, 'cmd': 'nginx'}, { 'pid': 1558, 'user': 'user1', 'mem%': 1.3, 'rss': 140796, 'vsz': 1321400, 'cmd': 'gnome-software'}, { 'pid': 5026, 'user': 'user1', 'mem%': 1.2, 'rss': 126312, 'vsz': 875072, 'cmd': 'nginx'}, { 'pid': 5051, 'user': 'user1', 'mem%': 1.1, 'rss': 112776, 'vsz': 872512, 'cmd': 'nginx'}, { 'pid': 1477, 'user': 'user1', 'mem%': 1.0, 'rss': 108248, 'vsz': 1471548, 'cmd': 'compiz'}, { 'pid': 2997, 'user': 'user1', 'mem%': 1.0, 'rss': 107208, 'vsz': 869076, 'cmd': 'nginx'}, { 'pid': 2298, 'user': 'root', 'mem%': 1.0, 'rss': 101356, 'vsz': 286772, 'cmd': 'aptd'}, { 'pid': 2700, 'user': 'user1', 'mem%': 0.9, 'rss': 95784, 'vsz': 460272, 'cmd': 'nginx'}, { 'pid': 2799, 'user': 'user1', 'mem%': 0.8, 'rss': 83940, 'vsz': 778512, 'cmd': 'nginx'}, { 'pid': 4248, 'user': 'user1', 'mem%': 0.8, 'rss': 82276, 'vsz': 1059420, 'cmd': 'redis'}, { 'pid': 1095, 'user': 'root', 'mem%': 0.7, 'rss': 78424, 'vsz': 457156, 'cmd': 'Xorg'}, { 'pid': 2810, 'user': 'user1', 'mem%': 0.7, 'rss': 72884, 'vsz': 750856, 'cmd': 'nginx'}, { 'pid': 2809, 'user': 'user1', 'mem%': 0.7, 'rss': 72872, 'vsz': 752908, 'cmd': 'nginx'} ]] 15 | }) 16 | 17 | $httpBackend.whenGET('server/?module=cpu_intensive_processes').respond(function () { 18 | return [200, [ { 'pid': 5026, 'user': 'user1', 'cpu%': 25.5, 'rss': 137944, 'vsz': 907236, 'cmd': 'nginx'}, { 'pid': 2623, 'user': 'user1', 'cpu%': 9.5, 'rss': 229016, 'vsz': 1327948, 'cmd': 'nginx'}, { 'pid': 2700, 'user': 'user1', 'cpu%': 8.8, 'rss': 106324, 'vsz': 474796, 'cmd': 'nginx'}, { 'pid': 4248, 'user': 'user1', 'cpu%': 8.8, 'rss': 96848, 'vsz': 1151460, 'cmd': 'sublime_text'}, { 'pid': 1477, 'user': 'user1', 'cpu%': 8.6, 'rss': 111252, 'vsz': 1476524, 'cmd': 'compiz'}, { 'pid': 1571, 'user': 'user1', 'cpu%': 7.7, 'rss': 62984, 'vsz': 551884, 'cmd': 'orca'}, { 'pid': 1095, 'user': 'root', 'cpu%': 6.2, 'rss': 86048, 'vsz': 457112, 'cmd': 'Xorg'}, { 'pid': 1535, 'user': 'user1', 'cpu%': 3.9, 'rss': 17152, 'vsz': 574624, 'cmd': 'pulseaudio'}, { 'pid': 5051, 'user': 'user1', 'cpu%': 1.6, 'rss': 131828, 'vsz': 888712, 'cmd': 'nginx'}, { 'pid': 2890, 'user': 'user1', 'cpu%': 1.1, 'rss': 52968, 'vsz': 657408, 'cmd': 'unity-panel-ser'}, { 'pid': 2782, 'user': 'user1', 'cpu%': 1.0, 'rss': 226536, 'vsz': 1110556, 'cmd': 'nginx'}, { 'pid': 1438, 'user': 'user1', 'cpu%': 0.9, 'rss': 45840, 'vsz': 660352, 'cmd': 'hud-service'}, { 'pid': 1631, 'user': 'user1', 'cpu%': 0.9, 'rss': 26208, 'vsz': 514684, 'cmd': 'indicator-multi'}, { 'pid': 1804, 'user': 'user1', 'cpu%': 0.9, 'rss': 9752, 'vsz': 523936, 'cmd': 'sd_espeak'} ]] 19 | }) 20 | 21 | $httpBackend.whenGET('server/?module=disk_partitions').respond(function () { 22 | return [200, [ {'file_system': 'udev', 'size': '4.9G', 'used': '0', 'avail': '4.9G', 'used%': '0%', 'mounted': '/dev'}, {'file_system': '/dev/sda1', 'size': '449G', 'used': '224.5G', 'avail': '224.5G', 'used%': '50%', 'mounted': '/'}, {'file_system': 'tmpfs', 'size': '5.0M', 'used': '4.0K', 'avail': '5.0M', 'used%': '1%', 'mounted': '/run/lock'} ]] 23 | }) 24 | 25 | $httpBackend.whenGET('server/?module=cpu_temp').respond(function () { 26 | return [200, [ ]] 27 | }) 28 | 29 | $httpBackend.whenGET('server/?module=cpu_temp').respond(function () { 30 | return [200, [ ]] 31 | }) 32 | 33 | $httpBackend.whenGET('server/?module=cpu_utilization').respond(function () { 34 | return [200, 25] 35 | }) 36 | 37 | $httpBackend.whenGET('server/?module=load_avg').respond(function () { 38 | return [200, { '1_min_avg': 40, '5_min_avg': 22, '15_min_avg': 10}] 39 | }) 40 | 41 | $httpBackend.whenGET('server/?module=docker_processes').respond(function () { 42 | return [200, []] 43 | }) 44 | 45 | $httpBackend.whenGET('server/?module=swap').respond(function () { 46 | return [200, [ { 'filename': '/dev/sda5', 'type': 'partition', 'size': '10364924', 'used': '0', 'priority': '-1'} ]] 47 | }) 48 | 49 | /** Basic Info **/ 50 | $httpBackend.whenGET('server/?module=cron_history').respond(function () { 51 | return [200, []] 52 | }) 53 | 54 | $httpBackend.whenGET('server/?module=io_stats').respond(function () { 55 | return [200, [ { 'device': 'sda', 'reads': '75406', 'writes': '119939', 'in_prog.': '0', 'time': '816604'}, { 'device': 'sda1', 'reads': '74587', 'writes': '81983', 'in_prog.': '0', 'time': '796964'}, { 'device': 'sda2', 'reads': '6', 'writes': '0', 'in_prog.': '0', 'time': '304'}, { 'device': 'sda5', 'reads': '73', 'writes': '0', 'in_prog.': '0', 'time': '3344'}, { 'device': 'sdb', 'reads': '276', 'writes': '0', 'in_prog.': '0', 'time': '72'}, { 'device': 'sdb1', 'reads': '234', 'writes': '0', 'in_prog.': '0', 'time': '64'} ]] 56 | }) 57 | 58 | $httpBackend.whenGET('server/?module=cpu_info').respond(function () { 59 | return [200, { "Architecture": " x86_64", "CPU op-mode(s)": " 32-bit, 64-bit", "Byte Order": " Little Endian", "CPU(s)": " 4", "On-line CPU(s) list": " 0-3", "Thread(s) per core": " 2", "Core(s) per socket": " 2", "Socket(s)": " 1", "NUMA node(s)": " 1", "Vendor ID": " GenuineIntel", "CPU family": " 6", "Model": " 69", "Model name": " Intel(R) Core(TM) i5-4200U CPU @ 1.60GHz", "Stepping": " 1", "CPU MHz": " 1402.101", "CPU max MHz": " 2600.0000", "CPU min MHz": " 800.0000", "BogoMIPS": " 4589.14", "Virtualization": " VT-x", "L1d cache": " 32K", "L1i cache": " 32K", "L2 cache": " 256K", "L3 cache": " 3072K", "NUMA node0 CPU(s)": " 0-3", "Flags": " fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm epb tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt dtherm ida arat pln pts" }] 60 | }) 61 | 62 | $httpBackend.whenGET('server/?module=scheduled_crons').respond(function () { 63 | return [200, [ ]] 64 | }) 65 | 66 | $httpBackend.whenGET('server/?module=memory_info').respond(function () { 67 | return [200, {'MemTotal': '10120412 kB','MemFree': '7257156 kB','MemAvailable': '8173888 kB','Buffers': ' 257824 kB','Cached': ' 1175940 kB','SwapCached': ' 0 kB','Active': ' 1815224 kB','Inactive': ' 783844 kB','Active(anon)': '1174832 kB','Inactive(anon)': ' 334816 kB','Active(file)': ' 640392 kB','Inactive(file)': ' 449028 kB','Unevictable': ' 384 kB','Mlocked': ' 384 kB','SwapTotal': ' 10364924 kB','SwapFree': '10364924 kB','Dirty': ' 156 kB','Writeback': ' 0 kB','AnonPages': '1165736 kB','Mapped': '420416 kB','Shmem': ' 344344 kB','Slab': ' 143556 kB','SReclaimable': ' 109592 kB','SUnreclaim': ' 33964 kB','KernelStack': ' 8560 kB','PageTables': ' 32700 kB','NFS_Unstable': ' 0 kB','Bounce': ' 0 kB','WritebackTmp': ' 0 kB','CommitLimit': '15425128 kB','Committed_AS': '5073516 kB','VmallocTotal': ' 34359738367 kB','VmallocUsed': '0 kB','VmallocChunk': ' 0 kB','HardwareCorrupted': ' 0 kB','AnonHugePages': '352256 kB','CmaTotal': '0 kB','CmaFree': ' 0 kB','HugePages_Total': '0','HugePages_Free': ' 0','HugePages_Rsvd': ' 0','HugePages_Surp': ' 0','Hugepagesize': '2048 kB','DirectMap4k': ' 130800 kB','DirectMap2M': ' 4993024 kB','DirectMap1G': ' 6291456 kB'}] 68 | }) 69 | 70 | $httpBackend.whenGET('server/?module=general_info').respond(function () { 71 | return [200, { 'OS': 'Ubuntu 16.04.2 LTS 4.4.0-77-generic', 'Hostname': 'webserver-prod-983742', 'Uptime': '5 hours and 32 minutes and 27 seconds ', 'Server Time': 'Sat May 13 00:11:39 EST 2017' }] 72 | }) 73 | 74 | /** Network **/ 75 | $httpBackend.whenGET('server/?module=download_transfer_rate').respond(function () { 76 | return [200, { "lo": 2048, "wlp3s0": 1024 }] 77 | }) 78 | 79 | $httpBackend.whenGET('server/?module=upload_transfer_rate').respond(function () { 80 | return [200, { "lo": 1024, "wlp3s0": 512 }] 81 | }) 82 | 83 | $httpBackend.whenGET('server/?module=bandwidth').respond(function () { 84 | return [200, [{ 'interface': 'wlp3s0:', 'tx': 226076758, 'rx': 9037438 },{ 'interface': 'lo:', 'tx': 12016995, 'rx': 12016995 }]] 85 | }) 86 | 87 | $httpBackend.whenGET('server/?module=ping').respond(function () { 88 | return [200, [{'host': 'google.com', 'ping': 23.234 },{'host': 'yahoo.com', 'ping': 67.412 },{'host': 'twitter.com', 'ping': 34.560 }]] 89 | }) 90 | 91 | $httpBackend.whenGET('server/?module=ip_addresses').respond(function () { 92 | return [200, [{ 'interface': 'external', 'ip': '70.113.122.2' } ]] 93 | }) 94 | 95 | $httpBackend.whenGET('server/?module=network_connections').respond(function () { 96 | return [200, [{ 'connections': 1, 'address': '127.0.0.1:48562' } 97 | ,{ 'connections': 1, 'address': '127.0.0.1:48564' } 98 | ,{ 'connections': 1, 'address': '127.0.0.1:48708' } 99 | ,{ 'connections': 3, 'address': '127.0.0.1:8080' } 100 | ,{ 'connections': 1, 'address': '192.241.178.140:443' } 101 | ,{ 'connections': 2, 'address': '2657:f9b0:9000:802::443' } 102 | ,{ 'connections': 1, 'address': '2657:f9b0:0000:80c::443' } 103 | ,{ 'connections': 1, 'address': '2657:f9b0:0000:80f:::80' } 104 | ,{ 'connections': 2, 'address': '2657:f9b0:0000:816:::80' } 105 | ,{ 'connections': 1, 'address': '2657:f9b0:0003:c09:5228' }]] 106 | }) 107 | 108 | $httpBackend.whenGET('server/?module=arp_cache').respond(function () { 109 | return [200, [ { 'addr': '192.168.0.1', 'hw_type': 'ether', 'hw_addr.': '15:db:45:eb:4d:6a', 'mask': 'wlp3s0' } ]] 110 | }) 111 | 112 | $httpBackend.whenGET('server/?module=logged_in_users').respond(function () { 113 | return [200, [ {'user': 'user1', 'from': ':0', 'when': 'Fri23'}, {'user': 'user1', 'from': 'webserver-prod-983742', 'when': 'Fri23'}, {'user': 'user1', 'from': 'webserver-prod-983742', 'when': 'Fri23'}, {'user': 'user1', 'from': 'webserver-prod-983742', 'when': '01:19'} ]] 114 | }) 115 | 116 | $httpBackend.whenGET('server/?module=user_accounts').respond(function () { 117 | return [200, [ {"type":"system", "user":"root", "home":"/root"}, {"type":"system", "user":"daemon", "home":"/usr/sbin"}, {"type":"system", "user":"bin", "home":"/bin"}, {"type":"system", "user":"sys", "home":"/dev"}, {"type":"system", "user":"sync", "home":"/bin"}, {"type":"system", "user":"games", "home":"/usr/games"}, {"type":"system", "user":"man", "home":"/var/cache/man"}, {"type":"system", "user":"lp", "home":"/var/spool/lpd"}, {"type":"system", "user":"mail", "home":"/var/mail"}, {"type":"system", "user":"news", "home":"/var/spool/news"}, {"type":"system", "user":"uucp", "home":"/var/spool/uucp"}, {"type":"system", "user":"proxy", "home":"/bin"}, {"type":"system", "user":"www-data", "home":"/var/www"}, {"type":"system", "user":"backup", "home":"/var/backups"}, {"type":"system", "user":"list", "home":"/var/list"}, {"type":"system", "user":"irc", "home":"/var/run/ircd"}, {"type":"system", "user":"gnats", "home":"/var/lib/gnats"}, {"type":"user", "user":"nobody", "home":"/nonexistent"}, {"type":"system", "user":"systemd-timesync", "home":"/run/systemd"} ]] 118 | }) 119 | 120 | $httpBackend.whenGET('server/?module=recent_account_logins').respond(function () { 121 | return [200, [ {"type":"system", "user":"root", "home":"/root"}, {"type":"system", "user":"daemon", "home":"/usr/sbin"}, {"type":"system", "user":"bin", "home":"/bin"}, {"type":"system", "user":"sys", "home":"/dev"}, {"type":"system", "user":"sync", "home":"/bin"}, {"type":"system", "user":"games", "home":"/usr/games"}, {"type":"system", "user":"man", "home":"/var/cache/man"}, {"type":"system", "user":"lp", "home":"/var/spool/lpd"}, {"type":"system", "user":"mail", "home":"/var/mail"}, {"type":"system", "user":"news", "home":"/var/spool/news"}, {"type":"system", "user":"uucp", "home":"/var/spool/uucp"}, {"type":"system", "user":"proxy", "home":"/bin"}, {"type":"system", "user":"www-data", "home":"/var/www"}, {"type":"system", "user":"backup", "home":"/var/backups"}, {"type":"system", "user":"list", "home":"/var/list"}, {"type":"system", "user":"irc", "home":"/var/run/ircd"}, {"type":"system", "user":"gnats", "home":"/var/lib/gnats"}, {"type":"user", "user":"nobody", "home":"/nonexistent"}, {"type":"system", "user":"systemd-timesync", "home":"/run/systemd"} ]] 122 | }) 123 | 124 | /** Applications **/ 125 | $httpBackend.whenGET('server/?module=common_applications').respond(function () { 126 | return [200, [ { "binary": "php", "location": "", "installed": "installed" }, { "binary": "node", "location": " /usr/bin/node /usr/include/node /usr/share/man/man1/node.1.gz", "installed": "installed" }, { "binary": "mysql", "location": "", "installed": "installed" }, { "binary": "mongo", "location": "", "installed": "installed" }, { "binary": "vim", "location": " /usr/bin/vim.basic /usr/bin/vim /usr/bin/vim.tiny /etc/vim /usr/share/vim /usr/share/man/man1/vim.1.gz", "installed": "installed" }, { "binary": "python", "location": " /usr/bin/python3.5 /usr/bin/python /usr/bin/python2.7-config /usr/bin/python3.5m /usr/bin/python2.7 /usr/lib/python3.5 /usr/lib/python2.7 /etc/python3.5 /etc/python /etc/python2.7 /usr/local/lib/python3.5 /usr/local/lib/python2.7 /usr/include/python3.5m /usr/include/python2.7 /usr/share/python /usr/share/man/man1/python.1.gz", "installed": "installed" }, { "binary": "ruby", "location": "", "installed": "installed" }, { "binary": "java", "location": " /usr/share/java", "installed": "installed" }, { "binary": "apache2", "location": "", "installed": "installed" }, { "binary": "nginx", "location": "", "installed": "installed" }, { "binary": "openssl", "location": " /usr/bin/openssl /usr/share/man/man1/openssl.1ssl.gz", "installed": "installed" }, { "binary": "vsftpd", "location": "", "installed": "installed" }, { "binary": "make", "location": " /usr/bin/make /usr/share/man/man1/make.1.gz", "installed": "installed" } ]] 127 | }) 128 | 129 | $httpBackend.whenGET('server/?module=memcached').respond(function () { return [200, {}] }) 130 | $httpBackend.whenGET('server/?module=redis').respond(function () { return [200, {}] }) 131 | $httpBackend.whenGET('server/?module=pm2_stats').respond(function () { return [200, []] }) 132 | 133 | }) 134 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps : [{ 3 | name: 'Linux Dash', 4 | script: './app/server/index.js', 5 | watch: false, 6 | env: { 7 | "LINUX_DASH_SERVER_PORT": 2800 8 | }, 9 | }], 10 | }; 11 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var g = require('gulp') 2 | var concat = require('gulp-concat') 3 | var uglify = require('gulp-uglify') 4 | var cssmin = require('gulp-cssmin') 5 | var gutil = require('gulp-util') 6 | var ngAnnotate = require('gulp-ng-annotate') 7 | var templateCache = require('gulp-angular-templatecache') 8 | 9 | g.task('template-cache', function () { 10 | return g.src('src/**/*.html') 11 | .pipe(templateCache('templates.js', { 12 | module: 'linuxDash', 13 | standAlone: false, 14 | root: 'src/' 15 | })) 16 | .pipe(g.dest('temp/')) 17 | }) 18 | 19 | g.task('generate-js-dist', ['template-cache'], function () { 20 | return g.src([ 21 | 'node_modules/angular/angular.min.js', 22 | 'node_modules/angular-route/angular-route.min.js', 23 | 'node_modules/smoothie/smoothie.js', 24 | 'node_modules/sortablejs/Sortable.min.js', 25 | 'src/js/**/*.js', 26 | 'temp/templates.js' 27 | ]) 28 | .pipe(concat('linuxDash.min.js')) 29 | .pipe(ngAnnotate()) 30 | // .pipe(uglify()) 31 | .on('error', gutil.log) 32 | .pipe(g.dest('app/')) 33 | }) 34 | 35 | g.task('generate-css-dist', function () { 36 | return g.src([ 'src/**/*.css' ]) 37 | .pipe(cssmin()) 38 | .pipe(concat('linuxDash.min.css')) 39 | .pipe(g.dest('app/')) 40 | }) 41 | 42 | g.task('build', [ 43 | 'generate-js-dist', 44 | 'generate-css-dist' 45 | ]) 46 | 47 | g.task('watch', function () { 48 | g.watch('src/**/*.css', ['generate-css-dist']) 49 | g.watch(['src/**/*.js', 'src/**/*.html'], ['generate-js-dist']) 50 | }) 51 | 52 | g.task('default', ['build', 'watch']) 53 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Linux Dash : Simple, beautiful server monitoring web dashboard 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linux-dash", 3 | "version": "2.0.0", 4 | "description": "A simple, low overhead web dashboard for linux.", 5 | "main": "app/server/index.js", 6 | "bin": "bin/linux-dash", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/afaqurk/linux-dash.git" 10 | }, 11 | "keywords": [ 12 | "linux", 13 | "gnu", 14 | "dashboard", 15 | "monitor", 16 | "server" 17 | ], 18 | "author": "Afaq Tariq", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/afaqurk/linux-dash/issues" 22 | }, 23 | "scripts": { 24 | "start": "gulp" 25 | }, 26 | "homepage": "https://github.com/afaqurk/linux-dash", 27 | "dependencies": { 28 | "express": "^4.11.1", 29 | "websocket": "^1.0.23", 30 | "yargs": "^8.0.1" 31 | }, 32 | "devDependencies": { 33 | "angular": "1.3.4", 34 | "angular-route": "1.3.4", 35 | "del": "^2.2.0", 36 | "gulp": "^3.9.1", 37 | "gulp-angular-templatecache": "^1.8.0", 38 | "gulp-concat": "^2.6.0", 39 | "gulp-cssmin": "^0.1.7", 40 | "gulp-ng-annotate": "^2.0.0", 41 | "gulp-uglify": "^1.5.3", 42 | "gulp-util": "^3.0.7", 43 | "smoothie": "^1.27.0", 44 | "sortablejs": "^1.4.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/css/README.md: -------------------------------------------------------------------------------- 1 | # Global CSS 2 | 3 | These are CSS files which set global & base element style rules. 4 | 5 | ## main.css 6 | 7 | Styles for body & html tags. 8 | 9 | ## plugins-container.css 10 | 11 | All plugins rendered on a page are inside of this element. 12 | 13 | ## tables.css 14 | 15 | Base table element styling. 16 | 17 | Also contains styling for the `metrics-table` class, which is used to display the tabular data underneath the line and multiline charts. 18 | 19 | -------------------------------------------------------------------------------- /src/css/buttons.css: -------------------------------------------------------------------------------- 1 | button { 2 | outline: none; 3 | } 4 | -------------------------------------------------------------------------------- /src/css/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | margin-top: 0px; 3 | padding-top: 0; 4 | 5 | -webkit-transition: all 1s ease; 6 | -moz-transition: all 1s ease; 7 | -ms-transition: all 1s ease; 8 | -o-transition: all 1s ease; 9 | transition: all 1s ease; 10 | } 11 | 12 | body { 13 | letter-spacing: .1rem; 14 | font-family: 'Merriweather', Arial, sans-serif; 15 | padding: 0; 16 | margin: 0; 17 | } 18 | 19 | .centered { 20 | margin: 0 auto; 21 | } 22 | -------------------------------------------------------------------------------- /src/css/plugins-cotnainer.css: -------------------------------------------------------------------------------- 1 | #plugins { 2 | padding-top: 0; 3 | margin-top: 0; 4 | border: 1px; 5 | margin-left: 10%; 6 | } 7 | 8 | @media (min-width: 1080px) { 9 | #plugins { 10 | } 11 | } 12 | 13 | @media (max-width: 1079px) { 14 | #plugins { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/css/tables.css: -------------------------------------------------------------------------------- 1 | table { 2 | width: 100%; 3 | font-size: 10px; 4 | margin: 0; 5 | border-collapse: collapse; 6 | text-align: left; 7 | table-layout:fixed; 8 | } 9 | 10 | table th, 11 | table td { 12 | padding: 2px; 13 | max-width: 250px; 14 | word-wrap: break-word; 15 | } 16 | 17 | table th{ 18 | font-weight: 600; 19 | text-transform: uppercase; 20 | border-bottom: 1px solid #f1f1f1; 21 | } 22 | 23 | table td { 24 | border-bottom: 1px solid #f1f1f1; 25 | font-family: Arial, sans-serif; 26 | font-size: 10px; 27 | color: rgba(0,0,0,.65); 28 | } 29 | 30 | table tbody tr:hover td { 31 | background-color: #fafafa; 32 | } 33 | 34 | table.metrics-table { 35 | text-align: center; 36 | } 37 | 38 | table.metrics-table tr:last-child td { 39 | border: none; 40 | } 41 | -------------------------------------------------------------------------------- /src/js/README.md: -------------------------------------------------------------------------------- 1 | # JS (UI) 2 | 3 | The root directory for _all_ of the UI JavaScript source files which power Linux Dash 4 | 5 | It is an Angular JS (v1.3) SPA 6 | 7 | ## core/ 8 | 9 | Contains all of the directives, services, and module declaration for the Angular SPA 10 | 11 | This is where you can find all of the base UI elements that power the UI 12 | 13 | For example, in this directory, you will find the base `line-chart` directive used by several plugins, the global `navbar` for Linux Dash pages, and other base UI elements. 14 | 15 | ## plugins/ 16 | 17 | Contains the code for the individual plugins the user interacts with on the Linux Dash UI. 18 | -------------------------------------------------------------------------------- /src/js/core/app.js: -------------------------------------------------------------------------------- 1 | function runFn(server, $location, $rootScope) { 2 | server.checkIfWebsocketsAreSupported() 3 | 4 | $rootScope.$on("$locationChangeSuccess", function(event, next, current) { 5 | var nextRoute = next.split('#')[1] 6 | if (nextRoute !== '/loading') { 7 | localStorage.setItem('currentTab', nextRoute) 8 | } 9 | }); 10 | 11 | $location.path('/loading') 12 | } 13 | 14 | angular 15 | .module('linuxDash', ['ngRoute']) 16 | .run([ 'server', '$location', '$rootScope', runFn]) 17 | .config(['$compileProvider', function ($compileProvider) { 18 | $compileProvider.debugInfoEnabled(false) 19 | }]) 20 | -------------------------------------------------------------------------------- /src/js/core/features/key-value-list/key-value-list.directive.js: -------------------------------------------------------------------------------- 1 | angular.module('linuxDash').directive('keyValueList', ['server', '$rootScope', function (server, $rootScope) { 2 | return { 3 | scope: { 4 | heading: '@', 5 | info: '@', 6 | moduleName: '@', 7 | }, 8 | templateUrl: 'src/js/core/features/key-value-list/key-value-list.html', 9 | link: function(scope, element) { 10 | 11 | scope.getData = function() { 12 | delete scope.tableRows 13 | 14 | server.get(scope.moduleName, function(serverResponseData) { 15 | scope.tableRows = serverResponseData 16 | scope.lastGet = new Date().getTime() 17 | 18 | if (Object.keys(serverResponseData).length === 0) { 19 | scope.emptyResult = true 20 | } 21 | 22 | if (!scope.$$phase && !$rootScope.$$phase) scope.$digest() 23 | }) 24 | } 25 | 26 | scope.getData() 27 | } 28 | } 29 | }]) 30 | -------------------------------------------------------------------------------- /src/js/core/features/key-value-list/key-value-list.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
{{ name }}{{ value }}
18 | 19 |
20 | 21 | No data 22 |
23 | -------------------------------------------------------------------------------- /src/js/core/features/line-chart/line-chart-plugin.directive.js: -------------------------------------------------------------------------------- 1 | angular.module('linuxDash').directive('lineChartPlugin', [ 2 | '$interval', '$compile', 'server', '$window', 3 | function ($interval, $compile, server, $window) { 4 | return { 5 | scope: { 6 | heading: '@', 7 | moduleName: '@', 8 | refreshRate: '=', 9 | maxValue: '=', 10 | minValue: '=', 11 | getDisplayValue: '=', 12 | metrics: '=', 13 | color: '@' 14 | }, 15 | templateUrl: 'src/js/core/features/line-chart/line-chart-plugin.html', 16 | link: function(scope, element) { 17 | 18 | scope.initializing = true 19 | 20 | // wrap the entire plugin into an initializing function 21 | var start_rendering_line_chart = function () { 22 | 23 | if (!scope.color) 24 | scope.color = '0, 255, 0' 25 | 26 | var series, w, h, canvas 27 | 28 | angular.element($window).bind('resize', function() { 29 | canvas.width = w 30 | canvas.height = h 31 | }) 32 | 33 | // smoothieJS - Create new chart 34 | var chart = new SmoothieChart({ 35 | borderVisible: false, 36 | sharpLines: true, 37 | grid: { 38 | fillStyle: '#ffffff', 39 | strokeStyle: 'rgba(232,230,230,0.93)', 40 | sharpLines: true, 41 | millisPerLine: 3000, 42 | borderVisible: false 43 | }, 44 | labels: { 45 | fontSize: 11, 46 | precision: 0, 47 | fillStyle: '#0f0e0e' 48 | }, 49 | maxValue: parseInt(scope.maxValue), 50 | minValue: parseInt(scope.minValue), 51 | horizontalLines: [{ 52 | value: 5, 53 | color: '#eff', 54 | lineWidth: 1 55 | }] 56 | }) 57 | 58 | var initializeChart = function () { 59 | // smoothieJS - set up canvas element for chart 60 | var checkForCanvasReadyState = $interval(function () { 61 | if (element.find('canvas')[0]) { 62 | canvas = element.find('canvas')[0] 63 | series = series || new TimeSeries() 64 | w = canvas.width 65 | h = canvas.height 66 | 67 | if (chart.seriesSet.length > 0) 68 | chart.removeTimeSeries(chart.seriesSet[0].timeSeries) 69 | 70 | chart.addTimeSeries(series, { 71 | strokeStyle: 'rgba(' + scope.color + ', 1)', 72 | fillStyle: 'rgba(' + scope.color + ', 0.2)', 73 | lineWidth: 2 74 | }) 75 | 76 | chart.streamTo(canvas, 1000) 77 | $interval.cancel(checkForCanvasReadyState) 78 | } 79 | }, 100) 80 | } 81 | 82 | scope.reInitializeChart = function () { 83 | initializeChart() 84 | } 85 | 86 | if (!scope.isHidden) 87 | initializeChart() 88 | 89 | var dataCallInProgress = false 90 | 91 | // update data on chart 92 | scope.getData = function() { 93 | 94 | if(scope.initializing) 95 | scope.initializing = false 96 | 97 | if (dataCallInProgress || !element.find('canvas')[0]) return 98 | 99 | dataCallInProgress = true 100 | 101 | server.get(scope.moduleName, function(serverResponseData) { 102 | 103 | if (serverResponseData.length < 1) { 104 | scope.emptyResult = true 105 | return 106 | } 107 | 108 | dataCallInProgress = false 109 | scope.lastGet = new Date().getTime() 110 | 111 | // change graph colour depending on usage 112 | if (scope.maxValue / 4 * 3 < scope.getDisplayValue(serverResponseData)) { 113 | chart.seriesSet[0].options.strokeStyle = 'rgba(255, 89, 0, 1)' 114 | chart.seriesSet[0].options.fillStyle = 'rgba(255, 89, 0, 0.2)' 115 | } else if (scope.maxValue / 3 < scope.getDisplayValue(serverResponseData)) { 116 | chart.seriesSet[0].options.strokeStyle = 'rgba(255, 238, 0, 1)' 117 | chart.seriesSet[0].options.fillStyle = 'rgba(255, 238, 0, 0.2)' 118 | } else { 119 | chart.seriesSet[0].options.strokeStyle = 'rgba(' + scope.color + ', 1)' 120 | chart.seriesSet[0].options.fillStyle = 'rgba(' + scope.color + ', 0.2)' 121 | } 122 | 123 | scope.newData = scope.getDisplayValue(serverResponseData) 124 | 125 | // update chart with this response 126 | series.append(scope.lastGet, scope.newData) 127 | 128 | // update the metrics for this chart 129 | scope.metrics.forEach(function(metricObj) { 130 | metricObj.data = metricObj.generate(serverResponseData) 131 | }) 132 | 133 | }) 134 | } 135 | 136 | // set the directive-provided interval 137 | // at which to run the chart update 138 | var intervalRef = $interval(scope.getData, scope.refreshRate) 139 | var removeInterval = function() { 140 | $interval.cancel(intervalRef) 141 | } 142 | 143 | element.on("$destroy", removeInterval) 144 | } 145 | 146 | // only start rendering plugin when we know the scale of max/min for the canvas chart (smoothie) 147 | var stopWatching = scope.$watch('maxValue', function (n, o) { 148 | if (n) { 149 | start_rendering_line_chart() 150 | stopWatching() 151 | } 152 | }) 153 | 154 | 155 | } 156 | } 157 | } 158 | ]) 159 | -------------------------------------------------------------------------------- /src/js/core/features/line-chart/line-chart-plugin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
{{ metric.name }}{{ metric.data }}
20 | 21 | No data 22 | 23 |
24 | -------------------------------------------------------------------------------- /src/js/core/features/loader/loader.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | margin: 100px auto; 3 | width: 50px; 4 | height: 30px; 5 | text-align: center; 6 | font-size: 10px; 7 | } 8 | 9 | .spinner > div { 10 | background-color: #009587; 11 | height: 100%; 12 | width: 6px; 13 | display: inline-block; 14 | 15 | -webkit-animation: stretchdelay 1.2s infinite ease-in-out; 16 | animation: stretchdelay 1.2s infinite ease-in-out; 17 | } 18 | 19 | .spinner .rect2 { 20 | -webkit-animation-delay: -1.1s; 21 | animation-delay: -1.1s; 22 | } 23 | 24 | .spinner .rect3 { 25 | -webkit-animation-delay: -1.0s; 26 | animation-delay: -1.0s; 27 | } 28 | 29 | .spinner .rect4 { 30 | -webkit-animation-delay: -0.9s; 31 | animation-delay: -0.9s; 32 | } 33 | 34 | .spinner .rect5 { 35 | -webkit-animation-delay: -0.8s; 36 | animation-delay: -0.8s; 37 | } 38 | 39 | @-webkit-keyframes stretchdelay { 40 | 0%, 40%, 100% { -webkit-transform: scaleY(0.4) } 41 | 20% { -webkit-transform: scaleY(1.0) } 42 | } 43 | 44 | @keyframes stretchdelay { 45 | 0%, 40%, 100% { 46 | transform: scaleY(0.4); 47 | -webkit-transform: scaleY(0.4); 48 | } 20% { 49 | transform: scaleY(1.0); 50 | -webkit-transform: scaleY(1.0); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/js/core/features/loader/loader.directive.js: -------------------------------------------------------------------------------- 1 | angular.module('linuxDash').directive('loader', function() { 2 | return { 3 | scope: { 4 | width: '@' 5 | }, 6 | template: '\ 7 |
\ 8 |
\ 9 |
\ 10 |
\ 11 |
\ 12 |
\ 13 |
\ 14 | ' 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /src/js/core/features/multi-line-chart/multi-line-chart-plugin.directive.js: -------------------------------------------------------------------------------- 1 | angular.module('linuxDash').directive('multiLineChartPlugin', [ 2 | '$interval', '$compile', 'server', '$window', 3 | function ($interval, $compile, server, $window) { 4 | return { 5 | scope: { 6 | heading: '@', 7 | moduleName: '@', 8 | refreshRate: '=', 9 | getDisplayValue: '=', 10 | units: '=', 11 | delay: '=' 12 | }, 13 | templateUrl: 'src/js/core/features/multi-line-chart/multi-line-chart-plugin.html', 14 | link: function(scope, element) { 15 | 16 | var w, h, canvas 17 | 18 | angular.element($window).bind('resize', function() { 19 | canvas.width = w 20 | canvas.height = h 21 | }) 22 | 23 | // smoothieJS - Create new chart 24 | var chart = new SmoothieChart({ 25 | borderVisible: false, 26 | sharpLines: true, 27 | grid: { 28 | fillStyle: '#ffffff', 29 | strokeStyle: 'rgba(232,230,230,0.93)', 30 | sharpLines: true, 31 | borderVisible: false 32 | }, 33 | labels: { 34 | fontSize: 12, 35 | precision: 0, 36 | fillStyle: '#0f0e0e' 37 | }, 38 | maxValue: 100, 39 | minValue: 0, 40 | horizontalLines: [{ 41 | value: 1, 42 | color: '#ecc', 43 | lineWidth: 1 44 | }] 45 | }) 46 | 47 | var seriesOptions = [ 48 | { 49 | strokeStyle: 'rgba(255, 0, 0, 1)', 50 | lineWidth: 2 51 | }, 52 | { 53 | strokeStyle: 'rgba(0, 255, 0, 1)', 54 | lineWidth: 2 55 | }, 56 | { 57 | strokeStyle: 'rgba(0, 0, 255, 1)', 58 | lineWidth: 2 59 | }, 60 | { 61 | strokeStyle: 'rgba(255, 255, 0, 1)', 62 | lineWidth: 1 63 | } 64 | ] 65 | 66 | // smoothieJS - set up canvas element for chart 67 | scope.seriesArray = [] 68 | scope.metricsArray = [] 69 | 70 | var delay = 1000 71 | 72 | if (angular.isDefined(scope.delay)) 73 | delay = scope.delay 74 | 75 | var initializeChart = function () { 76 | // smoothieJS - set up canvas element for chart 77 | var checkForCanvasReadyState = $interval(function () { 78 | if (element.find('canvas')[0]) { 79 | canvas = element.find('canvas')[0] 80 | w = canvas.width 81 | h = canvas.height 82 | 83 | // get the data once to set up # of lines on chart 84 | server.get(scope.moduleName, function(serverResponseData) { 85 | 86 | var numberOfLines = Object.keys(serverResponseData).length 87 | 88 | for (var x = 0; x < numberOfLines; x++) { 89 | 90 | var keyForThisLine = Object.keys(serverResponseData)[x]; 91 | 92 | scope.seriesArray[x] = new TimeSeries(); 93 | chart.addTimeSeries(scope.seriesArray[x], seriesOptions[x]); 94 | scope.metricsArray[x] = { 95 | name: keyForThisLine, 96 | color: seriesOptions[x].strokeStyle, 97 | } 98 | } 99 | 100 | }) 101 | 102 | chart.streamTo(canvas, delay) 103 | $interval.cancel(checkForCanvasReadyState) 104 | } 105 | }, 100) 106 | } 107 | 108 | scope.reInitializeChart = function () { 109 | chart.seriesSet.forEach(function (ts) { 110 | chart.removeTimeSeries(ts.timeSeries) 111 | }) 112 | 113 | initializeChart() 114 | } 115 | 116 | if (!scope.isHidden) 117 | initializeChart() 118 | 119 | var dataCallInProgress = false 120 | 121 | // update data on chart 122 | scope.getData = function() { 123 | 124 | if (dataCallInProgress) return 125 | 126 | if (!scope.seriesArray.length) return 127 | 128 | dataCallInProgress = true 129 | 130 | server.get(scope.moduleName, function(serverResponseData) { 131 | 132 | dataCallInProgress = false 133 | scope.lastGet = new Date().getTime() 134 | var keyCount = 0 135 | var maxAvg = 100 136 | 137 | // update chart with current response 138 | for (var key in serverResponseData) { 139 | scope.seriesArray[keyCount].append(scope.lastGet, serverResponseData[key]) 140 | keyCount++ 141 | maxAvg = Math.max(maxAvg, serverResponseData[key]) 142 | } 143 | 144 | // update the metrics for this chart 145 | scope.metricsArray.forEach(function(metricObj) { 146 | metricObj.data = serverResponseData[metricObj.name].toString() + ' ' + scope.units 147 | }) 148 | 149 | // round up the average and set the maximum scale 150 | var len = parseInt(Math.log(maxAvg) / Math.log(10)) 151 | var div = Math.pow(10, len) 152 | chart.options.maxValue = Math.ceil(maxAvg / div) * div 153 | 154 | }) 155 | 156 | } 157 | 158 | var refreshRate = (angular.isDefined(scope.refreshRate)) ? scope.refreshRate : 1000 159 | var intervalRef = $interval(scope.getData, refreshRate) 160 | var removeInterval = function() { 161 | $interval.cancel(intervalRef) 162 | } 163 | 164 | element.on("$destroy", removeInterval) 165 | } 166 | } 167 | }]) 168 | -------------------------------------------------------------------------------- /src/js/core/features/multi-line-chart/multi-line-chart-plugin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 14 | 15 | 16 | 17 | 18 |
9 |
12 |
13 |
{{ metric.name }}{{ metric.data }}
19 | 20 |
21 | -------------------------------------------------------------------------------- /src/js/core/features/navbar/navbar.css: -------------------------------------------------------------------------------- 1 | nav-bar { 2 | display: block; 3 | background-color: #f6f8f8; 4 | height: 40px; 5 | padding-left: 10%; 6 | padding-top: 3px; 7 | padding-right: 5%; 8 | } 9 | 10 | nav-bar ul { 11 | margin-left: 10px; 12 | padding: 0; 13 | list-style-type: none; 14 | display: inline; 15 | } 16 | 17 | nav-bar ul li a { 18 | font-size: 11px; 19 | text-transform: uppercase; 20 | text-decoration: none; 21 | line-height: 2.3rem; 22 | color: grey; 23 | } 24 | 25 | nav-bar ul li a:hover { 26 | color: rgb(0,0,0); 27 | } 28 | 29 | nav-bar ul li.active { 30 | border-bottom: 2px solid #009587; 31 | } 32 | 33 | nav-bar ul li.active a { 34 | color: #009587; 35 | } 36 | 37 | nav-bar ul li { 38 | margin-left: 20px; 39 | display: inline; 40 | padding-left: 5px; 41 | padding-bottom: 10px; 42 | } 43 | 44 | nav-bar .title { 45 | color: #009587; 46 | font-weight: 300; 47 | font-size: 20px; 48 | letter-spacing: .1rem; 49 | float: left; 50 | padding-top: 5px; 51 | } 52 | 53 | nav-bar .right-content { 54 | float: right; 55 | } 56 | 57 | nav-bar .right-content, 58 | nav-bar .right-content a { 59 | font-size: 11px; 60 | color: grey; 61 | padding-top: 10px; 62 | } 63 | 64 | nav-bar .right-content a:hover { 65 | color: #000; 66 | } 67 | -------------------------------------------------------------------------------- /src/js/core/features/navbar/navbar.directive.js: -------------------------------------------------------------------------------- 1 | angular.module('linuxDash').directive('navBar', ['$location', function($location) { 2 | return { 3 | template: '\ 4 | \ 5 | Linux Dash\ 6 | \ 7 | \ 12 | \ 13 | Resources:\ 14 | GitHub | \ 15 | Gitter Chat Room | \ 16 | Docs \ 17 | \ 18 | ', 19 | link: function(scope) { 20 | scope.items = [ 21 | 'system-status', 22 | 'basic-info', 23 | 'network', 24 | 'accounts', 25 | 'apps' 26 | ] 27 | 28 | scope.getNavItemName = function(url) { 29 | return url.replace('-', ' ') 30 | } 31 | 32 | scope.isActive = function(route) { 33 | return '/' + route === $location.path() 34 | } 35 | } 36 | } 37 | }]) 38 | -------------------------------------------------------------------------------- /src/js/core/features/plugin/plugin.css: -------------------------------------------------------------------------------- 1 | 2 | .plugin { 3 | transition: all .1s linear; 4 | 5 | vertical-align: text-top; 6 | width: 400px; 7 | display: inline-block; 8 | padding: 0; 9 | color: black; 10 | text-align: center; 11 | border-radius: 2px; 12 | box-shadow: 0 1px 10px rgba(0,0,0,.13),0px 5px 10px rgba(0,0,0,.16); 13 | margin: 30px 15px 0 0; 14 | max-height: 400px; 15 | overflow: hidden; 16 | 17 | resize: both; 18 | padding-bottom: 15px; 19 | background-color: rgba(0,0,0,0.015); 20 | } 21 | 22 | .plugin-hidden .top-bar .heading { 23 | color: grey; 24 | font-style: italic; 25 | } 26 | 27 | .plugin-enlarged { 28 | width: 50%; 29 | } 30 | 31 | @media (max-width: 768px) { 32 | .plugin { 33 | max-width: 80%; 34 | float: none; 35 | margin: 0 auto; 36 | margin-bottom: 10px; 37 | } 38 | 39 | .plugin-body { 40 | max-height: 323px; 41 | } 42 | 43 | .plugin-enlarged { 44 | width: 80%; 45 | } 46 | } 47 | 48 | @media (max-width: 950px) { 49 | .plugin { 50 | margin: 15px 10px 0 0; 51 | } 52 | .plugin-enlarged { 53 | width: 90%; 54 | } 55 | } 56 | 57 | .plugin.chart-plugin { 58 | padding: 0px; 59 | resize: none; 60 | padding-bottom: 0; 61 | } 62 | 63 | .plugin-body { 64 | background-color: #fff; 65 | height: 323px; 66 | font-size: 12px; 67 | padding: 10px; 68 | line-height: 30px; 69 | overflow: auto; 70 | border-top: 1px solid #ececec; 71 | } 72 | 73 | .plugin.chart-plugin, 74 | .plugin.chart-plugin .plugin-body { 75 | overflow: hidden; 76 | padding: 0; 77 | } 78 | 79 | .plugin-body-short { 80 | height: 263px; 81 | } 82 | 83 | .plugin last-update{ 84 | font-size: 11px; 85 | float: left; 86 | } 87 | 88 | .plugin ::-webkit-scrollbar { 89 | width: 8px; 90 | height: 8px; 91 | } 92 | .plugin ::-webkit-scrollbar-track { 93 | background: #eee; 94 | border: thin solid lightgray; 95 | box-shadow: 0px 0px 3px #dfdfdf inset; 96 | } 97 | .plugin ::-webkit-scrollbar-thumb { 98 | background: rgba(0,0,0,0.1); 99 | border: thin solid rgba(0,0,0,0.1); 100 | border-radius: 0; 101 | } 102 | .plugin ::-webkit-scrollbar-thumb:hover { 103 | background: rgba(0,0,0,0.2); 104 | } 105 | -------------------------------------------------------------------------------- /src/js/core/features/plugin/plugin.directive.js: -------------------------------------------------------------------------------- 1 | angular.module('linuxDash').directive('plugin', ['$rootScope', function($rootScope) { 2 | return { 3 | transclude: true, 4 | templateUrl: 'src/js/core/features/plugin/plugin.html', 5 | link: function (s, el, attr) { 6 | 7 | if (attr.hasOwnProperty('chartPlugin')) 8 | s.isChartPlugin = true 9 | 10 | if ($rootScope.hiddenPlugins.indexOf(s.moduleName) > -1) 11 | s.isHidden = true 12 | 13 | s.toggleWidth = function () { 14 | el.find('div')[0].removeAttribute('style') 15 | s.enlarged = !s.enlarged 16 | } 17 | 18 | var setPluginVisibility = function (shouldShow) { 19 | s.isHidden = !shouldShow 20 | 21 | if (shouldShow) { 22 | $rootScope.$emit('show-plugin', s.moduleName) 23 | if (s.isChartPlugin) s.reInitializeChart() 24 | } else { 25 | $rootScope.$emit('hide-plugin', s.moduleName) 26 | } 27 | } 28 | 29 | s.toggleVisibility = function () { 30 | setPluginVisibility(s.isHidden) 31 | } 32 | 33 | 34 | s.$watch('emptyResult', function (n, o) { 35 | if (n) { 36 | setPluginVisibility(false) 37 | } 38 | }) 39 | } 40 | } 41 | }]) 42 | -------------------------------------------------------------------------------- /src/js/core/features/plugin/plugin.html: -------------------------------------------------------------------------------- 1 |
9 | 10 | 19 | 20 | 21 |
26 |
27 | 28 |
29 | -------------------------------------------------------------------------------- /src/js/core/features/progress-bar/progress-bar-plugin.directive.js: -------------------------------------------------------------------------------- 1 | angular.module('linuxDash').directive('progressBarPlugin', function() { 2 | return { 3 | scope: { 4 | width: '@', 5 | moduleName: '@', 6 | name: '@', 7 | value: '@', 8 | max: '@' 9 | }, 10 | template: '\ 11 |
\ 12 |
\ 13 |
\ 14 |
\ 15 |
\ 16 | ' 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /src/js/core/features/progress-bar/progress-bar.css: -------------------------------------------------------------------------------- 1 | .progress-bar { 2 | background-color: #eec; 3 | border-radius: 10px; 4 | padding: 0px; 5 | clear: both; 6 | display: inline-block; 7 | overflow: hidden; 8 | white-space: nowrap; 9 | } 10 | 11 | .progress-bar > div { 12 | background-color: #1EAEDB; 13 | width: 0%; 14 | height: 5px; 15 | border-radius: 5px; 16 | } 17 | -------------------------------------------------------------------------------- /src/js/core/features/table-data/table-data.css: -------------------------------------------------------------------------------- 1 | .table-data-plugin .filter-container { 2 | padding-bottom: 0; 3 | margin: 0; 4 | } 5 | 6 | .table-data-plugin .filter, 7 | .table-data-plugin .filter:focus, 8 | .table-data-plugin .filter:active { 9 | height: 20px; 10 | padding: 5px; 11 | margin: 5px; 12 | border: none; 13 | outline-color: transparent; 14 | background: transparent; 15 | width: 100%; 16 | margin: 0; 17 | text-align: center; 18 | font-size: 15px; 19 | } 20 | 21 | .table-data-plugin .filter:focus { 22 | border-bottom: 1px solid #ff5722; 23 | } 24 | 25 | .table-data-plugin thead tr th a, 26 | .table-data-plugin thead tr th a:visited { 27 | color: black; 28 | text-decoration: none; 29 | } 30 | 31 | .table-data-plugin .column-sort-caret { 32 | font-size: 10px; 33 | color: #1EAEDB; 34 | } 35 | -------------------------------------------------------------------------------- /src/js/core/features/table-data/table-data.directive.js: -------------------------------------------------------------------------------- 1 | angular.module('linuxDash').directive('tableData', ['server', '$rootScope', function (server, $rootScope) { 2 | return { 3 | scope: { 4 | heading: '@', 5 | info: '@', 6 | moduleName: '@', 7 | width: '@', 8 | height: '@' 9 | }, 10 | templateUrl: 'src/js/core/features/table-data/table-data.html', 11 | link: function(scope, element) { 12 | 13 | scope.sortByColumn = null 14 | scope.sortReverse = null 15 | 16 | // set the column to sort by 17 | scope.setSortColumn = function(column) { 18 | 19 | // if the column is already being sorted 20 | // reverse the order 21 | if (column === scope.sortByColumn) { 22 | scope.sortReverse = !scope.sortReverse 23 | } else { 24 | scope.sortByColumn = column 25 | } 26 | 27 | scope.sortTableRows() 28 | } 29 | 30 | scope.sortTableRows = function() { 31 | scope.tableRows.sort(function(currentRow, nextRow) { 32 | 33 | var sortResult = 0 34 | 35 | if (currentRow[scope.sortByColumn] < nextRow[scope.sortByColumn]) { 36 | sortResult = -1 37 | } else if (currentRow[scope.sortByColumn] === nextRow[scope.sortByColumn]) { 38 | sortResult = 0 39 | } else { 40 | sortResult = 1 41 | } 42 | 43 | if (scope.sortReverse) { 44 | sortResult = -1 * sortResult 45 | } 46 | 47 | return sortResult 48 | }) 49 | } 50 | 51 | scope.getData = function() { 52 | delete scope.tableRows 53 | 54 | server.get(scope.moduleName, function(serverResponseData) { 55 | 56 | if (serverResponseData.length > 0) { 57 | scope.tableHeaders = Object.keys(serverResponseData[0]) 58 | } 59 | 60 | scope.tableRows = serverResponseData 61 | 62 | if (scope.sortByColumn) { 63 | scope.sortTableRows() 64 | } 65 | 66 | scope.lastGet = new Date().getTime() 67 | 68 | if (serverResponseData.length < 1) { 69 | scope.emptyResult = true 70 | } 71 | 72 | if (!scope.$$phase && !$rootScope.$$phase) scope.$digest() 73 | }) 74 | } 75 | 76 | scope.getData() 77 | } 78 | } 79 | }]) 80 | -------------------------------------------------------------------------------- /src/js/core/features/table-data/table-data.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 |
15 | 16 |
20 | {{ header }} 21 | 22 | {{ (header === sortByColumn && !sortReverse) ? '▲': ''; }} 23 | {{ (header === sortByColumn && sortReverse) ? '▼': ''; }} 24 | 25 |
31 | {{ row[header] }} 32 |
36 | 37 |
38 | 39 | No data 40 |
41 | -------------------------------------------------------------------------------- /src/js/core/features/top-bar/topbar.css: -------------------------------------------------------------------------------- 1 | .top-bar { 2 | 3 | height: 15px; 4 | padding: 15px; 5 | 6 | font-size: 13px; 7 | text-transform: capitalize; 8 | color: #009587; 9 | 10 | background-color: #f6f8f8; 11 | } 12 | 13 | .top-bar .heading { 14 | float: left; 15 | 16 | cursor: grab; 17 | cursor: -moz-grab; 18 | cursor: -webkit-grab; 19 | } 20 | 21 | .ld-top-bar-btn { 22 | 23 | float: right; 24 | font-size: 17px; 25 | 26 | color: #009587; 27 | background: #fff; 28 | 29 | border: 1px solid #eee; 30 | border-radius: 50%; 31 | 32 | width: 30px; 33 | height: 30px; 34 | margin-top: -5px; 35 | 36 | -webkit-transition: all 0.5s ease; 37 | -moz-transition: all 0.5s ease; 38 | -ms-transition: all 0.5s ease; 39 | -o-transition: all 0.5s ease; 40 | 41 | cursor: pointer; 42 | transition: all 0.5s ease; 43 | } 44 | 45 | .minimize-btn { 46 | font-size: 19px; 47 | } 48 | 49 | .minimize-btn.active { 50 | } 51 | 52 | .ld-refresh-btn:hover { 53 | background-color: #ffeb3b; 54 | color: black; 55 | } 56 | 57 | .ld-refresh-btn:active { 58 | background-color: #0f9d58; 59 | } 60 | 61 | .plugin-hidden .width-toggle-btn { 62 | display: none; 63 | } 64 | 65 | .plugin-hidden .minimize-btn, 66 | .plugin-enlarged .width-toggle-btn { 67 | background: #eee; 68 | color: #000; 69 | } 70 | -------------------------------------------------------------------------------- /src/js/core/features/top-bar/topbar.directive.js: -------------------------------------------------------------------------------- 1 | angular.module('linuxDash').directive('topBar', ['$rootScope', function($rootScope) { 2 | return { 3 | scope: { 4 | heading: '=', 5 | refresh: '&', 6 | lastUpdated: '=', 7 | toggleVisibility: '&', 8 | isHidden: '=', 9 | toggleWidth: '&', 10 | isChart: '=', 11 | info: '=', // not being used; needs a good ui solution 12 | }, 13 | template: '\ 14 |
\ 15 | ☰ {{ heading }} \ 16 | \ 17 | \ 21 | \ 22 | \ 23 | \ 24 | \ 25 |
\ 26 | ', 27 | } 28 | }]) 29 | -------------------------------------------------------------------------------- /src/js/core/rootscope-event-handlers/hide-plugin.run.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('linuxDash') 3 | .run(['$rootScope', '$location', function ($rootScope, $location) { 4 | 5 | var key = 'hiddenPlugins' 6 | 7 | var getHiddenPlugins = function () { 8 | var hiddenPluginsCSV = localStorage.getItem(key) || '' 9 | return hiddenPluginsCSV.split(',') 10 | } 11 | 12 | var updateHiddenPlugins = function (hiddenPlugins) { 13 | localStorage.setItem(key, hiddenPlugins.join(',')) 14 | } 15 | 16 | $rootScope.$on('hide-plugin', function (e, m) { 17 | var hiddenPlugins = getHiddenPlugins() 18 | 19 | if(hiddenPlugins.indexOf(m) < 0) 20 | hiddenPlugins.push(m) 21 | 22 | updateHiddenPlugins(hiddenPlugins) 23 | }) 24 | 25 | $rootScope.$on('show-plugin', function (e, m) { 26 | var hiddenPlugins = getHiddenPlugins() 27 | var indexOfPlugin = hiddenPlugins.indexOf(m) 28 | 29 | if(indexOfPlugin > -1) 30 | hiddenPlugins.splice(indexOfPlugin, 1) 31 | 32 | updateHiddenPlugins(hiddenPlugins) 33 | }) 34 | 35 | $rootScope.hiddenPlugins = getHiddenPlugins() 36 | 37 | }]) 38 | -------------------------------------------------------------------------------- /src/js/core/rootscope-event-handlers/make-plugins-draggable.run.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('linuxDash') 3 | .run(['$rootScope', '$location', function ($rootScope, $location) { 4 | 5 | $rootScope.$on('$routeChangeSuccess', function () { 6 | 7 | var intervalId = setInterval(function () { 8 | 9 | var el = document.getElementById('plugins') 10 | 11 | if (el) { 12 | 13 | var sortable = Sortable.create(el, { 14 | group: 'plugin-order-' + $location.path().replace('/', ''), 15 | handle: '.heading', 16 | ghostClass: 'ld-ghost', 17 | chosenClass: 'ld-chosen', 18 | dataIdAttr: 'sortablejs-id', 19 | animation: 1050, 20 | store: { 21 | get: function (sortable) { 22 | var order = localStorage.getItem(sortable.options.group.name); 23 | return order ? order.split('|') : []; 24 | }, 25 | set: function (sortable) { 26 | var order = sortable.toArray(); 27 | localStorage.setItem(sortable.options.group.name, order.join('|')); 28 | } 29 | } 30 | }) 31 | 32 | clearInterval(intervalId) 33 | } 34 | }) 35 | }) 36 | 37 | }]) 38 | -------------------------------------------------------------------------------- /src/js/core/routes.js: -------------------------------------------------------------------------------- 1 | function appLoadController($scope, $location, $rootScope) { 2 | var loadUrl = localStorage.getItem('currentTab') || 'system-status' 3 | var loadLinuxDash = function () { 4 | $location.path(loadUrl) 5 | } 6 | 7 | $rootScope.$on('start-linux-dash', loadLinuxDash) 8 | } 9 | 10 | function routesFn($routeProvider) { 11 | 12 | $routeProvider 13 | 14 | .when('/loading', { 15 | template: [ 16 | '
', 17 | '', 18 | 'Loading...', 19 | '
', 20 | ].join(''), 21 | controller: ['$scope', '$location', '$rootScope', appLoadController], 22 | }) 23 | 24 | .when('/system-status', { 25 | template: [ 26 | ' ', 27 | ' ', 28 | ' ', 29 | ' ', 30 | ' ', 31 | ' ', 32 | ' ', 33 | ' ', 34 | ' ', 35 | ].join(''), 36 | }) 37 | 38 | .when('/basic-info', { 39 | template: [ 40 | '', 41 | '', 42 | '', 43 | '', 44 | '', 45 | '', 46 | ].join(''), 47 | }) 48 | 49 | .when('/network', { 50 | template: [ 51 | ' ', 52 | ' ', 53 | ' ', 54 | ' ', 55 | ' ', 56 | ' ', 57 | ' ', 58 | ].join(''), 59 | }) 60 | 61 | .when('/accounts', { 62 | template: [ 63 | ' ', 64 | ' ', 65 | ' ', 66 | ].join(''), 67 | }) 68 | 69 | .when('/apps', { 70 | template: [ 71 | '', 72 | '', 73 | '', 74 | '', 75 | ].join(''), 76 | }) 77 | .otherwise({ 78 | redirectTo: '/loading' 79 | }) 80 | } 81 | 82 | angular.module('linuxDash').config(['$routeProvider', routesFn]) 83 | -------------------------------------------------------------------------------- /src/js/core/server.service.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('linuxDash') 3 | .service('server', [ 4 | '$http', '$rootScope', '$location', 5 | function($http, $rootScope, $location) { 6 | 7 | var websocket = { 8 | connection: null, 9 | onMessageEventHandlers: {} 10 | }; 11 | 12 | /** 13 | * @description: 14 | * Establish a websocket connection with server 15 | * 16 | * @return Null 17 | */ 18 | var establishWebsocketConnection = function() { 19 | 20 | var websocketUrl = (location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.hostname + ':' + window.location.port; 21 | 22 | if (websocket.connection === null) { 23 | 24 | websocket.connection = new WebSocket(websocketUrl); 25 | 26 | websocket.connection.onopen = function() { 27 | $rootScope.$broadcast("start-linux-dash", {}); 28 | $rootScope.$apply(); 29 | console.info('Websocket connection is open'); 30 | }; 31 | 32 | websocket.connection.onmessage = function(event) { 33 | 34 | var response = JSON.parse(event.data); 35 | var moduleName = response.moduleName; 36 | var moduleData = JSON.parse(response.output); 37 | 38 | if (!!websocket.onMessageEventHandlers[moduleName]) { 39 | websocket.onMessageEventHandlers[moduleName](moduleData); 40 | } else { 41 | console.info("Websocket could not find module", moduleName, "in:", websocket.onMessageEventHandlers); 42 | } 43 | 44 | }; 45 | 46 | websocket.connection.onclose = function() { 47 | websocket.connection = null; 48 | } 49 | } 50 | 51 | }; 52 | 53 | /** 54 | * @description: 55 | * Check if websockets are supported 56 | * If so, call establishWebsocketConnection() 57 | * 58 | * @return Null 59 | */ 60 | this.checkIfWebsocketsAreSupported = function() { 61 | 62 | var websocketSupport = { 63 | browser: null, 64 | server: null, 65 | }; 66 | 67 | // does browser support websockets? 68 | if (window.WebSocket) { 69 | 70 | websocketSupport.browser = true; 71 | 72 | // does backend support websockets? 73 | $http.get("/websocket").then(function(response) { 74 | 75 | // if websocket_support property exists and is trurthy 76 | // websocketSupport.server will equal true. 77 | websocketSupport.server = !!response.data["websocket_support"]; 78 | 79 | }).catch(function websocketNotSupportedByServer() { 80 | 81 | websocketSupport.server = false; 82 | $rootScope.$broadcast("start-linux-dash", {}); 83 | 84 | }).then(function finalDecisionOnWebsocket() { 85 | 86 | if (websocketSupport.browser && websocketSupport.server) { 87 | 88 | establishWebsocketConnection(); 89 | 90 | } else { 91 | $rootScope.$broadcast("start-linux-dash", {}); 92 | } 93 | 94 | }); 95 | 96 | } 97 | 98 | }; 99 | 100 | /** 101 | * Handles requests from modules for data from server 102 | * 103 | * @param {String} moduleName 104 | * @param {Function} callback 105 | * @return {[ Null || callback(server response) ]} 106 | */ 107 | this.get = function(moduleName, callback) { 108 | 109 | // if we have a websocket connection 110 | if (websocket.connection) { 111 | 112 | // and the connection is ready 113 | if (websocket.connection.readyState === 1) { 114 | 115 | // set the callback as the event handler 116 | // for server response. 117 | // 118 | // Callback instance needs to be overwritten 119 | // each time for this to work. Not sure why. 120 | websocket.onMessageEventHandlers[moduleName] = callback; 121 | 122 | // 123 | websocket.connection.send(moduleName); 124 | 125 | } else { 126 | console.log("Websocket not ready yet.", moduleName); 127 | } 128 | 129 | } 130 | // otherwise 131 | else { 132 | 133 | var moduleAddress = 'server/?module=' + moduleName; 134 | 135 | return $http.get(moduleAddress).then(function(response) { 136 | return callback(response.data); 137 | }); 138 | 139 | } 140 | 141 | }; 142 | 143 | } 144 | ]) 145 | -------------------------------------------------------------------------------- /src/js/plugins/README.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | These are the individual plugins which display server data to the user in the UI. 4 | 5 | Majority of the implementations of plugins are very simple since they leverage the Linux Dash core (`src/js/core/features`) 6 | 7 | ### disk-space 8 | 9 | Shows the disk 10 | -------------------------------------------------------------------------------- /src/js/plugins/cpu-avg-load-chart.directive.js: -------------------------------------------------------------------------------- 1 | angular.module('linuxDash').directive('cpuAvgLoadChart', ['server', function(server) { 2 | return { 3 | restrict: 'E', 4 | scope: {}, 5 | template: '\ 6 | \ 10 | \ 11 | ', 12 | link: function(scope) { 13 | scope.units = '%' 14 | } 15 | } 16 | }]) 17 | -------------------------------------------------------------------------------- /src/js/plugins/cpu-temp.directive.js: -------------------------------------------------------------------------------- 1 | angular.module('linuxDash').directive('cpuTemp', ['server', function(server) { 2 | return { 3 | restrict: 'E', 4 | scope: {}, 5 | template: ' \ 6 | \ 18 | \ 19 | ', 20 | link: function(scope) { 21 | scope.min = 0 22 | scope.max = 100 23 | 24 | scope.displayValue = function (serverResponseData) { 25 | return serverResponseData 26 | } 27 | 28 | scope.utilMetrics = [{ 29 | name: 'Temprature', 30 | generate: function (serverResponseData) { 31 | return serverResponseData + ' °C' 32 | } 33 | }] 34 | 35 | } 36 | } 37 | }]) 38 | -------------------------------------------------------------------------------- /src/js/plugins/cpu-utilization-chart.directive.js: -------------------------------------------------------------------------------- 1 | angular.module('linuxDash').directive('cpuUtilizationChart', ['server', function(server) { 2 | return { 3 | restrict: 'E', 4 | scope: {}, 5 | template: ' \ 6 | \ 18 | \ 19 | ', 20 | link: function(scope) { 21 | scope.min = 0 22 | scope.max = 100 23 | 24 | scope.displayValue = function(serverResponseData) { 25 | return serverResponseData 26 | } 27 | 28 | scope.utilMetrics = [{ 29 | name: 'Usage', 30 | generate: function(serverResponseData) { 31 | return serverResponseData + ' %' 32 | } 33 | }] 34 | 35 | } 36 | } 37 | }]) 38 | -------------------------------------------------------------------------------- /src/js/plugins/disk-space/disk-space.directive.js: -------------------------------------------------------------------------------- 1 | angular.module('linuxDash').directive('diskSpace', ['server', function(server) { 2 | return { 3 | restrict: 'E', 4 | scope: {}, 5 | templateUrl: 'src/js/plugins/disk-space/disk-space.html', 6 | link: function(scope) { 7 | 8 | var getKBMultiplierFn = function (size, power) { 9 | return function () { 10 | return size * Math.pow(1024, power) 11 | } 12 | } 13 | 14 | var kbDictionary = { 15 | 'M': function () { return getKBMultiplierFn(size, 1) }, 16 | 'G': function () { return getKBMultiplierFn(size, 2) }, 17 | 'T': function () { return getKBMultiplierFn(size, 3) }, 18 | 'P': function () { return getKBMultiplierFn(size, 4) }, 19 | 'E': function () { return getKBMultiplierFn(size, 5) }, 20 | 'Z': function () { return getKBMultiplierFn(size, 6) }, 21 | 'Y': function () { return getKBMultiplierFn(size, 7) }, 22 | } 23 | 24 | scope.heading = "Disk Partitions" 25 | scope.moduleName = 'disk_partitions' 26 | 27 | scope.getData = function() { 28 | server.get(scope.moduleName, function(serverResponseData) { 29 | scope.diskSpaceData = serverResponseData 30 | }) 31 | 32 | scope.lastGet = new Date().getTime() 33 | } 34 | 35 | scope.getData() 36 | 37 | scope.getKB = function(stringSize) { 38 | 39 | var lastChar = stringSize.slice(-1) 40 | var size = parseFloat(stringSize.replace(",", ".")) 41 | 42 | try { 43 | return kbDictionary[lastChar](size) 44 | } catch (err) { 45 | return size 46 | } 47 | } 48 | 49 | } 50 | } 51 | }]) 52 | -------------------------------------------------------------------------------- /src/js/plugins/disk-space/disk-space.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 31 | 34 | 35 | 36 | 37 |
NameStatsUsedMount
{{partition['file_system']}} 22 | 26 | 27 | 29 | {{ partition['used'] }} / {{ partition['size'] }} 30 | 32 | {{ partition['used%'] }} 33 | {{ partition['mounted'] }}
38 | 39 |
40 | -------------------------------------------------------------------------------- /src/js/plugins/download-transfer-rate-chart.directive.js: -------------------------------------------------------------------------------- 1 | angular.module('linuxDash').directive('downloadTransferRateChart', ['server', function(server) { 2 | return { 3 | restrict: 'E', 4 | scope: {}, 5 | template: ' \ 6 | \ 10 | \ 11 | ', 12 | link: function(scope) { 13 | scope.delay = 2000 14 | scope.units = 'KB/s' 15 | } 16 | } 17 | }]) 18 | -------------------------------------------------------------------------------- /src/js/plugins/ram-chart.directive.js: -------------------------------------------------------------------------------- 1 | angular.module('linuxDash').directive('ramChart', ['server', function (server) { 2 | return { 3 | restrict: 'E', 4 | scope: {}, 5 | template: '\ 6 | \ 18 | \ 19 | ', 20 | link: function(scope) { 21 | 22 | // get max ram available on machine before we 23 | // can start charting 24 | server.get('current_ram', function(resp) { 25 | scope.maxRam = resp.total 26 | scope.minRam = 0 27 | }) 28 | 29 | scope.ramToDisplay = function(serverResponseData) { 30 | return serverResponseData.used 31 | } 32 | 33 | var humanizeRam = function (ramInMB) { 34 | var ram = { 35 | value: parseInt(ramInMB, 10), 36 | unit: 'MB', 37 | } 38 | 39 | // if ram > 1,000 MB, use GB 40 | if (ram.value > 1000) { 41 | ram = { 42 | value: (ramInMB/1024).toFixed(2), 43 | unit: 'GB', 44 | } 45 | } 46 | 47 | return ram.value + ' ' + ram.unit 48 | } 49 | 50 | scope.ramMetrics = [{ 51 | name: 'Used', 52 | generate: function(serverResponseData) { 53 | var ratio = serverResponseData.used / serverResponseData.total 54 | var percentage = parseInt(ratio * 100) 55 | 56 | var usedRam = humanizeRam(serverResponseData.used) 57 | return usedRam + ' (' + percentage.toString() + '%)' 58 | } 59 | }, 60 | { 61 | name: 'Available', 62 | generate: function(serverResponseData) { 63 | 64 | var availableRam = humanizeRam(serverResponseData.available) 65 | var totalRam = humanizeRam(serverResponseData.total) 66 | return availableRam + ' of ' + totalRam 67 | } 68 | }] 69 | } 70 | } 71 | }]) 72 | -------------------------------------------------------------------------------- /src/js/plugins/simple-table-data-plugins.directive.js: -------------------------------------------------------------------------------- 1 | var simpleTableModules = [ 2 | { 3 | name: 'machineInfo', 4 | template: '' 5 | }, 6 | { 7 | name: 'ipAddresses', 8 | template: '' 9 | }, 10 | { 11 | name: 'ramIntensiveProcesses', 12 | template: '' 13 | }, 14 | { 15 | name: 'cpuIntensiveProcesses', 16 | template: '' 17 | }, 18 | { 19 | name: 'dockerProcesses', 20 | template: '' 21 | }, 22 | { 23 | name: 'networkConnections', 24 | template: '' 25 | }, 26 | { 27 | name: 'serverAccounts', 28 | template: '' 29 | }, 30 | { 31 | name: 'loggedInAccounts', 32 | template: '' 33 | }, 34 | { 35 | name: 'recentLogins', 36 | template: '' 37 | }, 38 | { 39 | name: 'arpCacheTable', 40 | template: '' 41 | }, 42 | { 43 | name: 'commonApplications', 44 | template: '' 45 | }, 46 | { 47 | name: 'pingSpeeds', 48 | template: '' 49 | }, 50 | { 51 | name: 'bandwidth', 52 | template: '' 53 | }, 54 | { 55 | name: 'swapUsage', 56 | template: '' 57 | }, 58 | { 59 | name: 'internetSpeed', 60 | template: '' 61 | }, 62 | { 63 | name: 'memcached', 64 | template: '' 65 | }, 66 | { 67 | name: 'redis', 68 | template: '' 69 | }, 70 | { 71 | name: 'pm2', 72 | template: '' 73 | }, 74 | { 75 | name: 'memoryInfo', 76 | template: '' 77 | }, 78 | { 79 | name: 'cpuInfo', 80 | template: '' 81 | }, 82 | { 83 | name: 'ioStats', 84 | template: '' 85 | }, 86 | { 87 | name: 'scheduledCrons', 88 | template: '' 89 | }, 90 | { 91 | name: 'cronHistory', 92 | template: '' 93 | } 94 | ] 95 | 96 | simpleTableModules.forEach(function(module, key) { 97 | 98 | angular.module('linuxDash').directive(module.name, ['server', function(server) { 99 | 100 | var moduleDirective = { 101 | restrict: 'E', 102 | scope: {} 103 | } 104 | 105 | moduleDirective['template'] = module.template 106 | 107 | return moduleDirective 108 | }]) 109 | 110 | }) 111 | -------------------------------------------------------------------------------- /src/js/plugins/upload-transfer-rate-chart.directive.js: -------------------------------------------------------------------------------- 1 | angular.module('linuxDash').directive('uploadTransferRateChart', ['server', function(server) { 2 | return { 3 | restrict: 'E', 4 | scope: {}, 5 | template: ' \ 6 | \ 10 | \ 11 | ', 12 | link: function(scope) { 13 | scope.delay = 2000 14 | scope.units = 'KB/s' 15 | } 16 | } 17 | }]) 18 | --------------------------------------------------------------------------------