├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── app ├── assets │ ├── dashboards │ │ ├── default.json │ │ └── test.json │ └── images │ │ ├── favicon.ico │ │ └── logo.png ├── index.jade ├── scripts │ ├── components │ │ ├── dashboard │ │ │ ├── index.js │ │ │ └── template.jade │ │ ├── date-dialog │ │ │ ├── index.js │ │ │ └── template.jade │ │ ├── header │ │ │ ├── index.js │ │ │ └── template.jade │ │ └── panel │ │ │ └── line │ │ │ ├── index.js │ │ │ └── template.jade │ ├── initialize.js │ └── lib │ │ ├── dashboard.js │ │ ├── influxdb.js │ │ ├── time-series-chart.js │ │ └── utils.js └── styles │ ├── components │ ├── common.styl │ ├── header.styl │ └── panel.styl │ ├── main.styl │ └── vendors │ └── bootstrap-variables.styl ├── bin └── influga.js ├── bower.json ├── doc └── assets │ ├── Influga.ai │ ├── Influga_invert.ai │ ├── Influga_square.ai │ ├── influga-logo.ai │ ├── influga-logo.png │ ├── influga.png │ └── screenshot_01.png ├── gulpfile.js ├── package.json ├── release.sh ├── scripts ├── config.json ├── create_dummy_data.js └── dummy_access_log.js ├── server ├── src │ ├── app.js │ └── lib │ │ ├── dashboard.js │ │ └── influxdb.js └── test │ └── app_test.js └── vendor └── grapnel.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | public 4 | db 5 | influga-config.json 6 | test.db 7 | *.log 8 | 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | influga-config.json 3 | db 4 | node_modules 5 | bower_components 6 | scripts 7 | doc 8 | test.db 9 | *.log 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | 6 | before_install: 7 | - wget http://s3.amazonaws.com/influxdb/influxdb_latest_amd64.deb 8 | - sudo dpkg -i influxdb_latest_amd64.deb 9 | - sudo service influxdb start 10 | - npm install -g gulp 11 | - npm install -g bower 12 | - echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config 13 | - bower install -f 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Kazuyuki Honda 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 | #Influga is no longer maintained. Please consider to use alternative, if you are using InfluxDB 0.9+, my recommendation is [re:dash](http://redash.io/) 2 | 3 | # Influga [![Build Status](https://travis-ci.org/hakobera/influga.svg?branch=master)](https://travis-ci.org/hakobera/influga) 4 | 5 | ![logo](https://raw.githubusercontent.com/hakobera/influga/master/doc/assets/influga.png) 6 | 7 | A InfluxDB Dashboard and Graph Editor. 8 | 9 | ![screenshot](https://raw.githubusercontent.com/hakobera/influga/master/doc/assets/screenshot_01.png) 10 | 11 | ## Features 12 | 13 | - Create dashboard for InfluxDB 14 | - Influga can write raw query for InfluxDB 15 | - Drag and drop panel sorting 16 | - Mobile optimized layout 17 | 18 | ## How to use 19 | 20 | ### Install 21 | 22 | You can install influga via npm: 23 | 24 | ```sh 25 | $ npm install -g influga 26 | ``` 27 | 28 | ### Setup 29 | 30 | Move to working directory and type `influga init` 31 | 32 | ```sh 33 | $ influga init 34 | Config file template is created to influga-config.json 35 | Edit config for your environment 36 | 37 | { 38 | "dashboardDbPath": "./db/influga.db", 39 | "host": "localhost", 40 | "port": 8086, 41 | "database": "db", 42 | "username": "root", 43 | "password": "root" 44 | } 45 | ``` 46 | 47 | Config file template is created to `influga-config.json`. 48 | Open this file on your editor and edit values for your environment. 49 | 50 | | Name | Description | 51 | | --------------- | --------------------------------------- | 52 | | dashboardDbPath | Dashboard database file path | 53 | | host | InfluxDB hostname or IP | 54 | | port | InfluxDB HTTP API port. Default is 8086 | 55 | | database | InfluxDB database name | 56 | | username | InfluxDB username | 57 | | password | InfluxDB user's password | 58 | 59 | ### Run 60 | 61 | Run influga server using `start` command with config file, like this. 62 | 63 | ```sh 64 | $ influga start -c influga-config.json 65 | ``` 66 | 67 | Now you can access on your browser, `http://[server]:8089` 68 | 69 | ## How to develop 70 | 71 | ### Install 72 | 73 | ``` 74 | $ npm install -g gulp 75 | $ npm install -g bower 76 | $ npm install 77 | $ bower install 78 | ``` 79 | 80 | ### Build and run 81 | 82 | ``` 83 | $ gulp 84 | $ gulp watch 85 | ``` 86 | 87 | ### Build for production 88 | 89 | ``` 90 | $ gulp production 91 | ``` 92 | 93 | ## LICENSE 94 | 95 | MIT 96 | -------------------------------------------------------------------------------- /app/assets/dashboards/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default", 3 | "from": "now()-15m", 4 | "to": "now()", 5 | "presetFromToRanges": ["5m", "15m", "1h", "6h", "12h", "1d", "2d", "7d", "30d"], 6 | "presetAutoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "10m", "30m", "1h"], 7 | "panels": [ 8 | { 9 | "title": "Chart", 10 | "type": "raw", 11 | "query": "", 12 | "interval": "30s", 13 | "yAxisLabel": "", 14 | "refresh": false, 15 | "height": 200 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /app/assets/dashboards/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Samples", 3 | "from": "now()-15m", 4 | "to": "now()", 5 | "presetFromToRanges": ["5m", "15m", "1h", "6h", "12h", "1d", "2d", "7d", "30d"], 6 | "presetAutoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "10m", "30m", "7d", "1h"], 7 | "panels": [ 8 | { 9 | "title": "CPU Load", 10 | "type": "raw", 11 | "query": "select * from metrics", 12 | "interval": "5s", 13 | "yAxisLabel": "%", 14 | "refresh": false, 15 | "height": 200 16 | }, 17 | { 18 | "title": "MIN/MAX idl", 19 | "type": "raw", 20 | "query": "select max(idl), min(idl) from metrics group by time(5s)", 21 | "duration": "5m", 22 | "interval": "5s", 23 | "refresh": false, 24 | "height": 200 25 | }, 26 | { 27 | "title": "status count from access log", 28 | "type": "aggregate", 29 | "query": "select count(status) as z from access_log group by time(10s), status", 30 | "duration": "5m", 31 | "interval": "10s", 32 | "yAxisLabel": "count", 33 | "refresh": false, 34 | "height": 200 35 | }, 36 | { 37 | "title": "Resp Time Avg", 38 | "type": "raw", 39 | "query": "select mean(response_time) from access_log group by time(10s)", 40 | "duration": "5m", 41 | "interval": "10s", 42 | "yAxisLabel": "ms", 43 | "refresh": false, 44 | "height": 200 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakobera/influga/8244d8f099c550cf9e6d15b11adb0f5557372256/app/assets/images/favicon.ico -------------------------------------------------------------------------------- /app/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakobera/influga/8244d8f099c550cf9e6d15b11adb0f5557372256/app/assets/images/logo.png -------------------------------------------------------------------------------- /app/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(http-equiv='X-UA-Compatible', content='IE=edge,chrome=1') 5 | meta(charset='utf-8') 6 | title Influga 7 | meta(name='viewport' content='width=device-width, initial-scale=1') 8 | meta(name='format-detection' content='telephone=no') 9 | 10 | link(href="images/favicon.ico" type="image/vnd.microsoft.icon" rel="shortcut icon") 11 | link(href="images/favicon.ico" type="image/vnd.microsoft.icon" rel="icon") 12 | 13 | link(rel='stylesheet' href='./css/vendor.css' type='text/css') 14 | link(rel='stylesheet' href='./css/main.css' type='text/css') 15 | body 16 | #app 17 | div(v-component="dashboard", v-with="dashboard") 18 | 19 | script(src="./js/vendor.js") 20 | script(src="./js/index.js") 21 | -------------------------------------------------------------------------------- /app/scripts/components/dashboard/index.js: -------------------------------------------------------------------------------- 1 | var dashboard = require('../../lib/dashboard'); 2 | 3 | module.exports = Vue.extend({ 4 | template: require('./template')(), 5 | 6 | components: { 7 | header: require('../header'), 8 | line: require('../panel/line'), 9 | dateDialog: require('../date-dialog') 10 | }, 11 | 12 | created: function () { 13 | this.$on('delete-panel', function (panel) { 14 | this.deleteRow(panel.$parent); 15 | }); 16 | 17 | this.$on('show-date-dialog', function () { 18 | this.$.dateDialog.show(this.from, this.to); 19 | }); 20 | 21 | this.$on('set-from-to-date', function (from, to) { 22 | this.from = from; 23 | this.to = to; 24 | this.$broadcast('update', this); 25 | }); 26 | 27 | this.$on('add-panel', function () { 28 | var base = this._initialPanelData(); 29 | this.panels.push(base); 30 | }); 31 | }, 32 | 33 | ready: function () { 34 | var self = this; 35 | $('.panels').sortable({ 36 | update: function (e, ui) { 37 | self._updatePanels(this); 38 | } 39 | }); 40 | }, 41 | 42 | methods: { 43 | saveDashboard: function () { 44 | var data = _.clone(this.$data); 45 | dashboard 46 | .save(data) 47 | .done(function (data) { 48 | window.location.hash = '/dashboards/shared/' + data._id; 49 | }) 50 | .fail(function () { 51 | alert('Failed to save dashaboard'); 52 | }); 53 | }, 54 | 55 | deleteRow: function (panel) { 56 | this.panels.splice(panel.$index, 1); 57 | }, 58 | 59 | _updatePanels: function (target) { 60 | var self = this; 61 | var map = {}; 62 | var pos = []; 63 | var i = 0; 64 | 65 | $('.panel', $(target)).each(function () { 66 | var id = $(this).data('id'); 67 | pos.push({ 68 | index: i++, 69 | id: id 70 | }); 71 | }); 72 | 73 | _.each(this.$.panels, function (panel, i) { 74 | var id = panel.$.panel.$id; 75 | map[id] = self.panels[i]; 76 | }); 77 | 78 | while (self.panels.pop()) {} 79 | _.each(pos, function (p, i) { 80 | self.panels.push(map[p.id]); 81 | }); 82 | }, 83 | 84 | _initialPanelData: function () { 85 | return { 86 | "title": "Panel", 87 | "type": "raw", 88 | "query": "", 89 | "interval": "30s", 90 | "yAxisLabel": "", 91 | "refresh": false, 92 | "height": 200 93 | }; 94 | } 95 | }, 96 | 97 | computed: { 98 | fromTo: function () { 99 | if (this.from.substr(0,6) == 'now()-') { 100 | return this.from.substr(6) + ' ago to ' + moment().format('YYYY/MM/DD HH:MM:SS'); 101 | } else { 102 | return this.from + ' to ' + this.to; 103 | } 104 | } 105 | } 106 | }); 107 | -------------------------------------------------------------------------------- /app/scripts/components/dashboard/template.jade: -------------------------------------------------------------------------------- 1 | div(v-component="header" v-with="$data") 2 | .container 3 | p.bg-info.visible-xs 4 | div.panels 5 | div.col-xs-12.row(v-repeat="panel:panels" v-ref="panels") 6 | div.panel-container(v-component="line" v-with="panel" v-ref="panel") 7 | .date-dialog(v-component="dateDialog" v-ref="dateDialog") 8 | -------------------------------------------------------------------------------- /app/scripts/components/date-dialog/index.js: -------------------------------------------------------------------------------- 1 | module.exports = Vue.extend({ 2 | template: require('./template')(), 3 | 4 | created: function () { 5 | this.fromDateTime = null; 6 | this.toDateTime = null; 7 | }, 8 | 9 | ready: function () { 10 | this._initDataTimePicker(); 11 | this.$options.modal = $('.modal', this.$el); 12 | }, 13 | 14 | dateFormat: 'YYYY/MM/DD HH:MM:SS', 15 | 16 | methods: { 17 | show: function (from, to) { 18 | this.$options.modal.modal('show'); 19 | if (moment(from).isValid()) { 20 | _fromDateTime().setDate(moment(from)); 21 | } 22 | if (moment(to).isValid()) { 23 | _toDateTime().setDate(moment(to)); 24 | } 25 | }, 26 | 27 | close: function () { 28 | this.$options.modal.modal('hide'); 29 | }, 30 | 31 | change: function () { 32 | var from = this._fromDateTime().getDate(); 33 | var to = this._toDateTime().getDate(); 34 | 35 | if (from.isValid() && to.isValid()) { 36 | this.$dispatch('set-from-to-date', from.format(this.$options.dateFormat), to.format(this.$options.dateFormat)); 37 | } 38 | this.$options.modal.modal('hide'); 39 | }, 40 | 41 | _initDataTimePicker: function () { 42 | var fromDate = this.$options.fromDate = $('.from-date', this.$el); 43 | var toDate = this.$options.toDate = $('.to-date', this.$el); 44 | fromDate.datetimepicker({ pick12HourFormat: false }); 45 | toDate.datetimepicker({ pick12HourFormat: false }); 46 | }, 47 | 48 | _fromDateTime: function () { 49 | return this.$options.fromDate.data('DateTimePicker'); 50 | }, 51 | 52 | _toDateTime: function () { 53 | return this.$options.toDate.data('DateTimePicker'); 54 | } 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /app/scripts/components/date-dialog/template.jade: -------------------------------------------------------------------------------- 1 | .modal.fade 2 | .modal-dialog 3 | .modal-content 4 | .modal-body 5 | form 6 | .form-group 7 | label From 8 | .input-group.date.from-date(data-date-format="YYYY/MM/DD HH:MM:SS") 9 | input.form-control(type="text") 10 | span.input-group-addon 11 | span.glyphicon.glyphicon-calendar 12 | 13 | .form-group 14 | label To 15 | .input-group.date.to-date(data-date-format="YYYY/MM/DD HH:MM:SS") 16 | input.form-control(type="text") 17 | span.input-group-addon 18 | span.glyphicon.glyphicon-calendar 19 | 20 | .modal-footer 21 | button.btn.btn-default(v-on="click: close") Close 22 | button.btn.btn-primary(v-on="click: change") Save changes 23 | -------------------------------------------------------------------------------- /app/scripts/components/header/index.js: -------------------------------------------------------------------------------- 1 | var dashboard = require('../../lib/dashboard'); 2 | 3 | module.exports = Vue.extend({ 4 | template: require('./template')(), 5 | 6 | created: function () { 7 | this.dashboards = []; 8 | }, 9 | 10 | ready: function () { 11 | var self = this; 12 | $('.dashboard-dropdown', this.$el).on('show.bs.dropdown', this.loadDashboards.bind(this)); 13 | }, 14 | 15 | methods: { 16 | loadDashboards: function () { 17 | var self = this; 18 | dashboard.all().done(function (data) { 19 | self.dashboards = data; 20 | }); 21 | }, 22 | 23 | setFromToDate: function (from, to) { 24 | this.$dispatch('set-from-to-date', from, to); 25 | }, 26 | 27 | showDateDialog: function () { 28 | this.$dispatch('show-date-dialog'); 29 | }, 30 | 31 | addPanel: function () { 32 | this.$dispatch('add-panel'); 33 | }, 34 | 35 | deleteDashboard: function (data) { 36 | var self = this; 37 | var msg = 'Are you sure you want to remove "' + data.name + '" dashboard?'; 38 | if (confirm(msg)) { 39 | dashboard.delete(data._id).done(function () { 40 | alert('delete!'); 41 | }); 42 | } 43 | } 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /app/scripts/components/header/template.jade: -------------------------------------------------------------------------------- 1 | header 2 | nav.navbar.navbar-default.navbar-static-top(role="navigation") 3 | .container-fluid 4 | .navbar-header 5 | button(type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse") 6 | span.sr-only Toggle navigation 7 | span.icon-bar 8 | span.icon-bar 9 | span.icon-bar 10 | a.navbar-brand(href="#" id="dashboard-title") 11 | span {{ name }} 12 | 13 | ul.nav.navbar-nav.navbar-right 14 | li.dropdown.from-to-date-dropdown 15 | a.dropdown-toggle(data-toggle="dropdown" data-target="#" href="javascript:void(0)") {{ fromTo }} 16 | b.caret 17 | ul.dropdown-menu 18 | li(v-repeat="presetFromToRanges" href="javascript:void(0)") 19 | a(v-on="click: setFromToDate('now()-' + $value, 'now()')") Last {{ $value }} 20 | li 21 | a(v-on="click: showDateDialog" href="javascript:void(0)") Custom 22 | 23 | .navbar-collapse.collapse 24 | ul.nav.navbar-nav.navbar-right 25 | li.dropdown.dashboard-dropdown 26 | a.dropdown-toggle(data-toggle="dropdown" data-target="#") 27 | = "Edit" 28 | b.caret 29 | ul.dropdown-menu.dashboard-edit-menu 30 | li 31 | form.form-inline 32 | .form-group 33 | label Dahboard Name 34 | .input-group 35 | input.form-control(input type="text" placeholder="Dashboard name" v-model="name") 36 | span.input-group-btn 37 | button.btn.btn-default(type="button" v-on="click: saveDashboard") Save 38 | li.divider 39 | li 40 | a(href="javascript:void(0)" v-on="click: addPanel") Add Panel 41 | 42 | li.dropdown.dashboard-dropdown 43 | a.dropdown-toggle(data-toggle="dropdown" data-target="#") 44 | = "Dashboards" 45 | b.caret 46 | ul.dropdown-menu 47 | li.dashboard-list(v-repeat="dashboards") 48 | a(href="#/dashboards/shared/{{ _id }}") {{ name }} 49 | span.delete(v-on="click: deleteDashboard(this)") 50 | i.glyphicon.glyphicon-remove 51 | -------------------------------------------------------------------------------- /app/scripts/components/panel/line/index.js: -------------------------------------------------------------------------------- 1 | var Chart = require('../../../lib/time-series-chart'); 2 | var utils = require('../../../lib/utils'); 3 | 4 | module.exports = Vue.extend({ 5 | template: require('./template')(), 6 | timer: null, 7 | 8 | created: function () { 9 | this.$watch('refresh', this.timer.bind(this)); 10 | }, 11 | 12 | ready: function () { 13 | var dom = this.$el.querySelector('.chart'); 14 | this.chart = Chart.create(dom, this.$data); 15 | this.tick(); 16 | 17 | this.$on('update', function (dashboard) { 18 | this.tick(); 19 | }); 20 | 21 | this.$id = uuid.v1(); 22 | }, 23 | 24 | beforeDestroy: function () { 25 | this.stopTimer(); 26 | }, 27 | 28 | methods: { 29 | editPanel: function () { 30 | $('.collapse', this.$el).collapse('toggle'); 31 | }, 32 | 33 | deletePanel: function () { 34 | if (window.confirm(this.deleteConfirmationMessage)) { 35 | this.$dispatch('delete-panel', this); 36 | } 37 | }, 38 | 39 | onError: function (xhr, textStatus, error) { 40 | if (typeof xhr.responseText === 'string' && xhr.responseText.match(/^Error:/)) { 41 | this.chart.error(xhr.responseText); 42 | } else { 43 | this.chart.error(xhr.status + ':' + xhr.statusText); 44 | } 45 | }, 46 | 47 | timer: function (enable) { 48 | return enable ? this.startTimer() : this.stopTimer(); 49 | }, 50 | 51 | startTimer: function () { 52 | var self = this; 53 | this.stopTimer(); 54 | this.$options.timer = setTimeout(function tf() { 55 | self.tick(); 56 | if (self.refresh) { 57 | self.$options.timer = setTimeout(tf, utils.milisec(self.interval)); 58 | } 59 | }, utils.milisec(this.interval)); 60 | self.tick(); 61 | }, 62 | 63 | stopTimer: function () { 64 | if (this.$options.timer) { 65 | clearTimeout(this.$options.timer); 66 | this.$options.intervalTimer = null; 67 | } 68 | }, 69 | 70 | tick: function () { 71 | var self = this; 72 | this.chart.prepareUpdate(this); 73 | if (_.isEmpty(this.query)) { 74 | Vue.nextTick(function () { 75 | self.chart.error('Query is empty'); 76 | }); 77 | } else { 78 | var refresh = $('.refresh', this.$el); 79 | refresh.addClass('rotate'); 80 | 81 | influxdb 82 | .query(this.fullQuery) 83 | .done(function (data) { 84 | Vue.nextTick(function () { 85 | self.chart.update(data); 86 | }); 87 | }) 88 | .fail(this.onError.bind(this)) 89 | .always(function () { 90 | refresh.removeClass('rotate'); 91 | }); 92 | } 93 | } 94 | }, 95 | 96 | computed: { 97 | fullQuery: function () { 98 | var dashboard = this.$root.dashboard; 99 | var q = this.query.replace(/;\s*$/, ''); 100 | if (q.toLowerCase().indexOf('where') === -1) { 101 | return q + ' WHERE time > ' + dashboard.from + ' AND time < ' + dashboard.to + ' ORDER ASC'; 102 | } else { 103 | return q + ' AND time > ' + dashboard.from + ' AND time < ' + dashboard.to + ' ORDER ASC'; 104 | } 105 | }, 106 | 107 | deleteConfirmationMessage: function () { 108 | return 'Are you sure you want to remove this "' + this.title + '" panel?'; 109 | } 110 | } 111 | }); 112 | -------------------------------------------------------------------------------- /app/scripts/components/panel/line/template.jade: -------------------------------------------------------------------------------- 1 | .panel.panel-primary(v-attr="data-id: $id") 2 | .panel-heading 3 | h3.panel-title {{ title }} 4 | .panel-menu 5 | span(v-on="click: tick") 6 | i.refresh.glyphicon.glyphicon-refresh 7 | span(v-on="click: editPanel") 8 | i.glyphicon.glyphicon-cog 9 | span(v-on="click: deletePanel") 10 | i.glyphicon.glyphicon-remove 11 | 12 | .panel-body 13 | .settings.collapse 14 | form.form-horizontal(role="form") 15 | .form-group 16 | label.col-sm-2.control-label Title 17 | .col-sm-10 18 | input.form-control(type="text" v-model="title") 19 | 20 | .form-group 21 | label.col-sm-2.control-label Query 22 | .col-sm-10 23 | textarea.form-control(v-model="query") {{ query }} 24 | 25 | .form-group 26 | label.col-sm-2.control-label Type 27 | .col-sm-10 28 | select.form-control(v-model="type") 29 | option raw 30 | option aggregate 31 | 32 | .form-group 33 | label.col-sm-2.control-label 34 | input(type="checkbox" v-model="refresh") 35 | = " Auto Refresh" 36 | .col-sm-10 37 | select.form-control(v-model="interval" v-attr="disabled: !refresh") 38 | option(v-repeat="presetAutoRefreshIntervals" v-attr="value: $value, selected: $value == interval") Every {{ $value }} 39 | 40 | .form-group 41 | label.col-sm-2.control-label Y Axis Label 42 | .col-sm-10 43 | input.form-control(type="text" v-model="yAxisLabel") 44 | 45 | .form-group 46 | label.col-sm-2.control-label Height 47 | .col-sm-10 48 | input.form-control(type="number" v-model="height") 49 | 50 | .chart(v-style="height: height + 'px'") 51 | -------------------------------------------------------------------------------- /app/scripts/initialize.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | FastClick.attach(document.body); 3 | 4 | var InfluxDB = require('./lib/influxdb'); 5 | 6 | var router = new Grapnel(); 7 | router.get('', loadDefaultDashboard); 8 | router.get('/dashboards/files/:name', loadFileDashboard); 9 | router.get('/dashboards/shared/:id', loadSharedDashboard); 10 | 11 | function loadDefaultDashboard(req) { 12 | loadDashboard('/dashboards/default.json'); 13 | } 14 | 15 | function loadFileDashboard(req) { 16 | var name = req.params.name || 'default'; 17 | loadDashboard('/dashboards/' + encodeURIComponent(name) + '.json'); 18 | } 19 | 20 | function loadSharedDashboard(req) { 21 | var id = req.params.id; 22 | loadDashboard('/shared/dashboards/' + encodeURIComponent(id)); 23 | } 24 | 25 | function loadDashboard(path) { 26 | console.log('Load dashboard from ' + path); 27 | if (!window._app) { 28 | $.when( 29 | $.get('/config.json'), 30 | $.get(path) 31 | ).done(function (config, dashboard) { 32 | window.influxdb = new InfluxDB(config[0]); 33 | window._app = new Vue({ 34 | el: '#app', 35 | data: { 36 | dashboard: dashboard[0] 37 | }, 38 | components: { 39 | dashboard: require('./components/dashboard') 40 | }, 41 | created: function () { 42 | this.$watch('dashboard', function (newDashboard) { 43 | $('title').text(newDashboard.name + ' | Influga'); 44 | }); 45 | } 46 | }); 47 | }).fail(function () { 48 | alert('Failed to load dashboard'); 49 | }); 50 | } else { 51 | $.get(path).done(function(dashboard) { 52 | window._app.dashboard = dashboard; 53 | }); 54 | } 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /app/scripts/lib/dashboard.js: -------------------------------------------------------------------------------- 1 | exports.all = function () { 2 | return $.ajax('/shared/dashboards', { 3 | type: 'get', 4 | dataType: 'json', 5 | timeout: 10000 6 | }); 7 | }; 8 | 9 | exports.save = function (data) { 10 | return $.ajax('/shared/dashboards', { 11 | type: 'post', 12 | data: JSON.stringify(data), 13 | contentType: 'application/json', 14 | dataType: 'json', 15 | timeout: 10000 16 | }); 17 | }; 18 | 19 | exports.load = function (id) { 20 | return $.ajax('/shared/dashboards/' + encodeURIComponent(id), { 21 | type: 'get', 22 | dataType: 'json', 23 | timeout: 10000 24 | }); 25 | }; 26 | 27 | exports.delete = function (id) { 28 | return $.ajax('/shared/dashboards/' + encodeURIComponent(id), { 29 | type: 'delete' 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /app/scripts/lib/influxdb.js: -------------------------------------------------------------------------------- 1 | function InfluxDB(opts) { 2 | opts = opts || {}; 3 | this.host = opts.host || 'localhost'; 4 | this.hosts = opts.hosts || [this.host]; 5 | this.port = opts.port || 8086; 6 | this.username = opts.username || 'root'; 7 | this.password = opts.password || 'root'; 8 | this.database = opts.database; 9 | this.isCrossOrigin = (this.hosts.indexOf(window.location.host) !== -1); 10 | this.useProxy = opts.useProxy || false; 11 | } 12 | 13 | InfluxDB.prototype.query = function (query) { 14 | return this.get(this.path("db/" + this.database + "/series", {q: query})); 15 | }; 16 | 17 | InfluxDB.prototype.get = function (path) { 18 | return $.ajax({ 19 | url: this.url(path), 20 | method: 'get', 21 | contentType: 'application/json', 22 | dataType: 'json', 23 | crossDomain: this.isCrossOrigin 24 | }); 25 | }; 26 | 27 | InfluxDB.prototype.path = function (action, opts) { 28 | var path = action; 29 | if (this.useProxy) { 30 | if (opts && opts.q) { 31 | path += "?q=" + encodeURIComponent(opts.q); 32 | } 33 | } else { 34 | path += "?u=" + this.username + "&p=" + this.password; 35 | if (opts && opts.q) { 36 | path += "&q=" + encodeURIComponent(opts.q); 37 | } 38 | } 39 | return path; 40 | }; 41 | 42 | InfluxDB.prototype.url = function (path) { 43 | if (this.useProxy) { 44 | return "/" + path; 45 | } else { 46 | var host = this.hosts.shift(); 47 | this.hosts.push(host); 48 | return "//" + host + ":" + this.port + "/" + path; 49 | } 50 | }; 51 | 52 | module.exports = InfluxDB; 53 | -------------------------------------------------------------------------------- /app/scripts/lib/time-series-chart.js: -------------------------------------------------------------------------------- 1 | function Chart(svg, chart, opts) { 2 | this.svg = svg; 3 | this.chart = chart; 4 | this.opts = opts || {}; 5 | this.setConverter(opts.type); 6 | this.svg.datum([]).call(this.chart); 7 | nv.utils.windowResize(this.chart.update); 8 | } 9 | module.exports = Chart; 10 | 11 | Chart.prototype.update = function (data) { 12 | var series = this.converter(data); 13 | this.chart.noData('No Data Available'); 14 | this.svg.datum(series).call(this.chart); 15 | return this; 16 | }; 17 | 18 | Chart.prototype.prepareUpdate = function (opts) { 19 | if (opts.yAxisLabel) { 20 | this.chart.yAxis.axisLabel(opts.yAxisLabel).axisLabelDistance(50); 21 | } else { 22 | this.chart.yAxis.axisLabel(''); 23 | } 24 | return this; 25 | }; 26 | 27 | Chart.prototype.error = function (message) { 28 | this.chart.noData(message); 29 | this.svg.datum([]).call(this.chart); 30 | return this; 31 | }; 32 | 33 | Chart.prototype.setConverter = function (type) { 34 | switch (type) { 35 | case 'raw': 36 | this.converter = rawNumberConverter; 37 | break; 38 | 39 | case 'aggregate': 40 | this.converter = aggregateConverter; 41 | break; 42 | 43 | default: 44 | throw new Error('Invalid type: ' + type); 45 | } 46 | }; 47 | 48 | Chart.create = function (element, opts) { 49 | opts = opts || {}; 50 | 51 | var d3chart = lineChart(opts); 52 | d3chart.xAxis 53 | .showMaxMin(false) 54 | .tickFormat(function (d) { 55 | return d3.time.format('%X')(new Date(d)); 56 | }); 57 | 58 | d3chart.yAxis.showMaxMin(false); 59 | d3chart.yAxis.tickFormat(formatKMBT); 60 | 61 | if (opts.yAxisLabel) { 62 | d3chart.yAxis.axisLabel(opts.yAxisLabel).axisLabelDistance(50); 63 | } 64 | 65 | var chart = new Chart(d3.select(element).append('svg'), d3chart, opts); 66 | return chart; 67 | }; 68 | 69 | function lineChart(opts) { 70 | return nv.models 71 | .lineChart() 72 | .margin({left: 50, right: 20}) 73 | .useInteractiveGuideline(true) 74 | .transitionDuration(200) 75 | .showLegend(true) 76 | .showYAxis(true) 77 | .forceY([0]) 78 | .showXAxis(true); 79 | } 80 | 81 | function noData() { 82 | return []; 83 | } 84 | 85 | function rawNumberConverter(data) { 86 | if (!data || !data[0] || !data[0].columns) { 87 | return noData(); 88 | } 89 | 90 | var columns = data[0].columns; 91 | var points = data[0].points; 92 | var series = []; 93 | var c2i = _.reduce(columns, function (memo, column, index) { memo[column] = index; return memo; }, {}); 94 | 95 | _.each(columns, function (column, index, list) { 96 | if (_.include(['time', 'sequence_number'], column)) { 97 | return; 98 | } 99 | 100 | series.push({ 101 | values: points.map(function (pt) { return { x: pt[0], y: pt[c2i[column]] }; }), 102 | key: column 103 | }); 104 | }); 105 | 106 | return series; 107 | } 108 | 109 | function aggregateConverter(data) { 110 | if (!data || !data[0] || !data[0].columns) { 111 | return noData(); 112 | } 113 | 114 | var columns = data[0].columns; 115 | var points = data[0].points; 116 | var series = []; 117 | var c2i = _.reduce(columns, function (memo, column, index) { memo[column] = index; return memo; }, {}); 118 | 119 | var prevTime = -1; 120 | var items = {}; 121 | _.each(points, function (pt) { 122 | var time = pt[0]; 123 | var key = pt[2]; 124 | 125 | if (!items[key]) { 126 | items[key] = { 127 | values: [], 128 | key: key 129 | }; 130 | series.push(items[key]); 131 | } 132 | 133 | items[key].values.push({ x: time, y: pt[1] }); 134 | }); 135 | 136 | return series; 137 | } 138 | 139 | function formatKMBT(y) { 140 | var abs_y = Math.abs(y); 141 | if (abs_y >= 1000000000000) { return y / 1000000000000 + "T"; } 142 | else if (abs_y >= 1000000000) { return y / 1000000000 + "B"; } 143 | else if (abs_y >= 1000000) { return y / 1000000 + "M"; } 144 | else if (abs_y >= 1000) { return y / 1000 + "K"; } 145 | else if (abs_y < 1 && y > 0) { return y.toFixed(2); } 146 | else if (abs_y === 0) { return ''; } 147 | else { return y; } 148 | } 149 | 150 | function formatBase1024KMGTP(y) { 151 | var abs_y = Math.abs(y); 152 | if (abs_y >= 1125899906842624) { return y / 1125899906842624 + "P"; } 153 | else if (abs_y >= 1099511627776){ return y / 1099511627776 + "T"; } 154 | else if (abs_y >= 1073741824) { return y / 1073741824 + "G"; } 155 | else if (abs_y >= 1048576) { return y / 1048576 + "M"; } 156 | else if (abs_y >= 1024) { return y / 1024 + "K"; } 157 | else if (abs_y < 1 && y > 0) { return y.toFixed(2); } 158 | else if (abs_y === 0) { return ''; } 159 | else { return y; } 160 | } 161 | -------------------------------------------------------------------------------- /app/scripts/lib/utils.js: -------------------------------------------------------------------------------- 1 | exports.milisec = function milisec(str) { 2 | var ret = str.match(/(\d+)([s|m|h|d])/); 3 | if (!ret || !ret[1]) { 4 | return 0; 5 | } 6 | 7 | var base = parseInt(ret[1], 10); 8 | if (ret[2] === 's') { 9 | return base * 1000; 10 | } else if (ret[2] === 'm') { 11 | return base * 1000 * 60; 12 | } else if (ret[2] === 'h') { 13 | return base * 1000 * 60 * 60; 14 | } else if (ret[2] === 'd') { 15 | return base * 1000 * 60 * 60 * 24; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /app/styles/components/common.styl: -------------------------------------------------------------------------------- 1 | body 2 | font-family: "Lucida Grande", "Hiragino Kaku Gothic ProN", Meiryo, sans-serif 3 | background-color: #fff 4 | 5 | .container 6 | padding: 0 15px 7 | margin: 0 auto 8 | 9 | @keyframes rotate 10 | 0% 11 | transform-origin: center center 12 | transform: rotate(-200deg) 13 | 14 | 100% 15 | transform-origin: center center 16 | transform: rotate(0) 17 | 18 | .rotate 19 | animation-duration: 1s 20 | animation-fill-mode: both 21 | animation-name: rotate 22 | animation-iteration-count: infinite 23 | 24 | @media screen and (min-width: $screen-sm-min) 25 | .container 26 | padding: 0 15px 27 | margin: 0 auto 28 | -------------------------------------------------------------------------------- /app/styles/components/header.styl: -------------------------------------------------------------------------------- 1 | .navbar 2 | .navbar-brand 3 | padding-top: 5px 4 | padding-bottom: 5px 5 | padding-left: 50px 6 | line-height: 40px 7 | font-size: 18px 8 | background-image: url(../images/logo.png) 9 | background-repeat: no-repeat 10 | background-position: 5px 11 | 12 | .navbar-nav 13 | .open 14 | .dropdown-menu 15 | li 16 | a 17 | &:hover 18 | color: #fff 19 | background-color: $brand-primary 20 | 21 | .dashboard-edit-menu 22 | form 23 | padding: .5em 24 | min-width: 350px 25 | 26 | .dashboard-dropdown 27 | ul 28 | .dashboard-list 29 | position: relative 30 | 31 | .delete 32 | display: inline-block 33 | position: absolute 34 | top: 5px 35 | right: 10px 36 | width: 20px 37 | height: 20px 38 | text-align: center 39 | line-height: 20px 40 | font-size: 18px 41 | 42 | &:hover 43 | color: #f00 44 | 45 | @media screen and (min-width: $screen-sm-min) 46 | .navbar 47 | .navbar-brand 48 | font-size: 26px 49 | 50 | .navbar-form 51 | width: 350px; 52 | padding-left: 0; 53 | 54 | -------------------------------------------------------------------------------- /app/styles/components/panel.styl: -------------------------------------------------------------------------------- 1 | .panel 2 | .panel-heading 3 | padding: 10px 4 | position: relative 5 | 6 | h3 7 | display: inline-block 8 | text-align: left 9 | font-size: 12px 10 | 11 | .panel-menu 12 | position: absolute; 13 | top: 0 14 | right: 0; 15 | display: inline-block 16 | font-size: 22px 17 | 18 | span 19 | display: inline-block 20 | width: 42px 21 | height: 100% 22 | line-height: 42px 23 | text-align: center 24 | 25 | &:hover 26 | background-color: $panel-primary-text 27 | color: $panel-primary-heading-bg 28 | 29 | .panel-body 30 | padding: 5px 0 31 | 32 | .settings 33 | padding: 0 10px 10px 10px 34 | 35 | form 36 | input[type=checkbox] 37 | margin-right: 0.5em 38 | 39 | @media screen and (min-width: $screen-sm-min) 40 | .panel 41 | .panel-heading 42 | padding: 10px 15px 43 | 44 | h3 45 | font-size: 16px 46 | 47 | .panel-body 48 | padding: 15px 49 | 50 | -------------------------------------------------------------------------------- /app/styles/main.styl: -------------------------------------------------------------------------------- 1 | @import 'nib' 2 | @require './vendors/*' 3 | @require './components/*' 4 | -------------------------------------------------------------------------------- /app/styles/vendors/bootstrap-variables.styl: -------------------------------------------------------------------------------- 1 | // 2 | // Variables 3 | // -------------------------------------------------- 4 | 5 | 6 | //== Colors 7 | // 8 | //## Gray and brand colors for use across Bootstrap. 9 | 10 | $gray-darker ?= lighten(#000, 13.5%); // #222 11 | $gray-dark ?= lighten(#000, 20%); // #333 12 | $gray ?= lighten(#000, 33.5%); // #555 13 | $gray-light ?= lighten(#000, 60%); // #999 14 | $gray-lighter ?= lighten(#000, 93.5%); // #eee 15 | 16 | $brand-primary ?= #428bca; 17 | $brand-success ?= #5cb85c; 18 | $brand-info ?= #5bc0de; 19 | $brand-warning ?= #f0ad4e; 20 | $brand-danger ?= #d9534f; 21 | 22 | 23 | //== Scaffolding 24 | // 25 | // ## Settings for some of the most global styles. 26 | 27 | //** Background color for ``. 28 | $body-bg ?= #fff; 29 | //** Global text color on ``. 30 | $text-color ?= $gray-dark; 31 | 32 | //** Global textual link color. 33 | $link-color ?= $brand-primary; 34 | //** Link hover color set via `darken()` function. 35 | $link-hover-color ?= darken($link-color, 15%); 36 | 37 | 38 | //== Typography 39 | // 40 | //## Font, line-height, and color for body text, headings, and more. 41 | 42 | $font-family-sans-serif ?= "Helvetica Neue", Helvetica, Arial, sans-serif; 43 | $font-family-serif ?= Georgia, "Times New Roman", Times, serif; 44 | //** Default monospace fonts for ``, ``, and `
`.
 45 | $font-family-monospace ?=   Menlo, Monaco, Consolas, "Courier New", monospace;
 46 | $font-family-base ?=        $font-family-sans-serif;
 47 | 
 48 | $font-size-base ?=          14px;
 49 | $font-size-large ?=         ceil(($font-size-base * 1.25)); // ~18px
 50 | $font-size-small ?=         ceil(($font-size-base * 0.85)); // ~12px
 51 | 
 52 | $font-size-h1 ?=            floor(($font-size-base * 2.6)); // ~36px
 53 | $font-size-h2 ?=            floor(($font-size-base * 2.15)); // ~30px
 54 | $font-size-h3 ?=            ceil(($font-size-base * 1.7)); // ~24px
 55 | $font-size-h4 ?=            ceil(($font-size-base * 1.25)); // ~18px
 56 | $font-size-h5 ?=            $font-size-base;
 57 | $font-size-h6 ?=            ceil(($font-size-base * 0.85)); // ~12px
 58 | 
 59 | //** Unit-less `line-height` for use in components like buttons.
 60 | $line-height-base ?=        1.428571429; // 20/14
 61 | //** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
 62 | $line-height-computed ?=    floor(($font-size-base * $line-height-base)); // ~20px
 63 | 
 64 | //** By default, this inherits from the ``.
 65 | $headings-font-family ?=    inherit;
 66 | $headings-font-weight ?=    500;
 67 | $headings-line-height ?=    1.1;
 68 | $headings-color ?=          inherit;
 69 | 
 70 | 
 71 | //-- Iconography
 72 | //
 73 | //## Specify custom locations of the include Glyphicons icon font. Useful for those including Bootstrap via Bower.
 74 | 
 75 | $icon-font-path ?=          "../fonts/";
 76 | $icon-font-name ?=          "glyphicons-halflings-regular";
 77 | $icon-font-svg-id ?=        "glyphicons_halflingsregular";
 78 | 
 79 | //== Components
 80 | //
 81 | //## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
 82 | 
 83 | $padding-base-vertical ?=          6px;
 84 | $padding-base-horizontal ?=        12px;
 85 | 
 86 | $padding-large-vertical ?=         10px;
 87 | $padding-large-horizontal ?=       16px;
 88 | 
 89 | $padding-small-vertical ?=         5px;
 90 | $padding-small-horizontal ?=       10px;
 91 | 
 92 | $padding-xs-vertical ?=       1px;
 93 | $padding-xs-horizontal ?=     5px;
 94 | 
 95 | $line-height-large ?=         1.33;
 96 | $line-height-small ?=         1.5;
 97 | 
 98 | $border-radius-base ?=        4px;
 99 | $border-radius-large ?=       6px;
100 | $border-radius-small ?=       3px;
101 | 
102 | //** Global color for active items (e.g., navs or dropdowns).
103 | $component-active-color ?=    #fff;
104 | //** Global background color for active items (e.g., navs or dropdowns).
105 | $component-active-bg ?=       $brand-primary;
106 | 
107 | //** Width of the `border` for generating carets that indicator dropdowns.
108 | $caret-width-base ?=          4px;
109 | //** Carets increase slightly in size for larger components.
110 | $caret-width-large ?=         5px;
111 | 
112 | 
113 | //== Tables
114 | //
115 | //## Customizes the `.table` component with basic values, each used across all table variations.
116 | 
117 | //** Padding for ``s and ``s.
118 | $table-cell-padding ?=            8px;
119 | //** Padding for cells in `.table-condensed`.
120 | $table-condensed-cell-padding ?=  5px;
121 | 
122 | //** Default background color used for all tables.
123 | $table-bg ?=                      transparent;
124 | //** Background color used for `.table-striped`.
125 | $table-bg-accent ?=               #f9f9f9;
126 | //** Background color used for `.table-hover`.
127 | $table-bg-hover ?=                #f5f5f5;
128 | $table-bg-active ?=               $table-bg-hover;
129 | 
130 | //** Border color for table and cell borders.
131 | $table-border-color ?=            #ddd;
132 | 
133 | 
134 | //== Buttons
135 | //
136 | //## For each of Bootstrap's buttons, define text, background and border color.
137 | 
138 | $btn-font-weight ?=                normal;
139 | 
140 | $btn-default-color ?=              #333;
141 | $btn-default-bg ?=                 #fff;
142 | $btn-default-border ?=             #ccc;
143 | 
144 | $btn-primary-color ?=              #fff;
145 | $btn-primary-bg ?=                 $brand-primary;
146 | $btn-primary-border ?=             darken($btn-primary-bg, 5%);
147 | 
148 | $btn-success-color ?=              #fff;
149 | $btn-success-bg ?=                 $brand-success;
150 | $btn-success-border ?=             darken($btn-success-bg, 5%);
151 | 
152 | $btn-info-color ?=                 #fff;
153 | $btn-info-bg ?=                    $brand-info;
154 | $btn-info-border ?=                darken($btn-info-bg, 5%);
155 | 
156 | $btn-warning-color ?=              #fff;
157 | $btn-warning-bg ?=                 $brand-warning;
158 | $btn-warning-border ?=             darken($btn-warning-bg, 5%);
159 | 
160 | $btn-danger-color ?=               #fff;
161 | $btn-danger-bg ?=                  $brand-danger;
162 | $btn-danger-border ?=              darken($btn-danger-bg, 5%);
163 | 
164 | $btn-link-disabled-color ?=        $gray-light;
165 | 
166 | 
167 | //== Forms
168 | //
169 | //##
170 | 
171 | //** `` background color
172 | $input-bg ?=                       #fff;
173 | //** `` background color
174 | $input-bg-disabled ?=              $gray-lighter;
175 | 
176 | //** Text color for ``s
177 | $input-color ?=                    $gray;
178 | //** `` border color
179 | $input-border ?=                   #ccc;
180 | //** `` border radius
181 | $input-border-radius ?=            $border-radius-base;
182 | //** Border color for inputs on focus
183 | $input-border-focus ?=             #66afe9;
184 | 
185 | //** Placeholder text color
186 | $input-color-placeholder ?=        $gray-light;
187 | 
188 | //** Default `.form-control` height
189 | $input-height-base ?=              ($line-height-computed + ($padding-base-vertical * 2) + 2);
190 | //** Large `.form-control` height
191 | $input-height-large ?=             (ceil($font-size-large * $line-height-large) + ($padding-large-vertical * 2) + 2);
192 | //** Small `.form-control` height
193 | $input-height-small ?=             (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 2);
194 | 
195 | $legend-color ?=                   $gray-dark;
196 | $legend-border-color ?=            #e5e5e5;
197 | 
198 | //** Background color for textual input addons
199 | $input-group-addon-bg ?=           $gray-lighter;
200 | //** Border color for textual input addons
201 | $input-group-addon-border-color ?= $input-border;
202 | 
203 | 
204 | //== Dropdowns
205 | //
206 | //## Dropdown menu container and contents.
207 | 
208 | //** Background for the dropdown menu.
209 | $dropdown-bg ?=                    #fff;
210 | //** Dropdown menu `border-color`.
211 | $dropdown-border ?=                rgba(0,0,0,.15);
212 | //** Dropdown menu `border-color` **for IE8**.
213 | $dropdown-fallback-border ?=       #ccc;
214 | //** Divider color for between dropdown items.
215 | $dropdown-divider-bg ?=            #e5e5e5;
216 | 
217 | //** Dropdown link text color.
218 | $dropdown-link-color ?=            $gray-dark;
219 | //** Hover color for dropdown links.
220 | $dropdown-link-hover-color ?=      darken($gray-dark, 5%);
221 | //** Hover background for dropdown links.
222 | $dropdown-link-hover-bg ?=         #f5f5f5;
223 | 
224 | //** Active dropdown menu item text color.
225 | $dropdown-link-active-color ?=     $component-active-color;
226 | //** Active dropdown menu item background color.
227 | $dropdown-link-active-bg ?=        $component-active-bg;
228 | 
229 | //** Disabled dropdown menu item background color.
230 | $dropdown-link-disabled-color ?=   $gray-light;
231 | 
232 | //** Text color for headers within dropdown menus.
233 | $dropdown-header-color ?=          $gray-light;
234 | 
235 | // Note ?= Deprecated $dropdown-caret-color as of v3.1.0
236 | $dropdown-caret-color ?=           #000;
237 | 
238 | 
239 | //-- Z-index master list
240 | //
241 | // Warning: Avoid customizing these values. They're used for a bird's eye view
242 | // of components dependent on the z-axis and are designed to all work together.
243 | //
244 | // Note: These variables are not generated into the Customizer.
245 | 
246 | $zindex-navbar ?=            1000;
247 | $zindex-dropdown ?=          1000;
248 | $zindex-popover ?=           1010;
249 | $zindex-tooltip ?=           1030;
250 | $zindex-navbar-fixed ?=      1030;
251 | $zindex-modal-background ?=  1040;
252 | $zindex-modal ?=             1050;
253 | 
254 | 
255 | //== Media queries breakpoints
256 | //
257 | //## Define the breakpoints at which your layout will change, adapting to different screen sizes.
258 | 
259 | // Extra small screen / phone
260 | // Note: Deprecated @screen-xs and @screen-phone as of v3.0.1
261 | $screen-xs ?=                  480px;
262 | $screen-xs-min ?=              $screen-xs;
263 | $screen-phone ?=               $screen-xs-min;
264 | 
265 | // Small screen / tablet
266 | // Note: Deprecated $screen-sm and $screen-tablet as of v3.0.1
267 | $screen-sm ?=                  768px;
268 | $screen-sm-min ?=              $screen-sm;
269 | $screen-tablet ?=              $screen-sm-min;
270 | 
271 | // Medium screen / desktop
272 | // Note: Deprecated $screen-md and $screen-desktop as of v3.0.1
273 | $screen-md ?=                  992px;
274 | $screen-md-min ?=              $screen-md;
275 | $screen-desktop ?=             $screen-md-min;
276 | 
277 | // Large screen / wide desktop
278 | // Note: Deprecated $screen-lg and $screen-lg-desktop as of v3.0.1
279 | $screen-lg ?=                  1200px;
280 | $screen-lg-min ?=              $screen-lg;
281 | $screen-lg-desktop ?=          $screen-lg-min;
282 | 
283 | // So media queries don't overlap when required, provide a maximum
284 | $screen-xs-max ?=              ($screen-sm-min - 1);
285 | $screen-sm-max ?=              ($screen-md-min - 1);
286 | $screen-md-max ?=              ($screen-lg-min - 1);
287 | 
288 | 
289 | //== Grid system
290 | //
291 | //## Define your custom responsive grid.
292 | 
293 | //** Number of columns in the grid.
294 | $grid-columns ?=              12;
295 | //** Padding between columns. Gets divided in half for the left and right.
296 | $grid-gutter-width ?=         30px;
297 | // Navbar collapse
298 | //** Point at which the navbar becomes uncollapsed.
299 | $grid-float-breakpoint ?=     $screen-sm-min;
300 | //** Point at which the navbar begins collapsing.
301 | $grid-float-breakpoint-max ?= ($grid-float-breakpoint - 1);
302 | 
303 | 
304 | //== Container sizes
305 | //
306 | //## Define the maximum width of `.container` for different screen sizes.
307 | 
308 | // Small screen / tablet
309 | $container-tablet ?=             ((720px + $grid-gutter-width));
310 | //** For `$screen-sm-min` and up.
311 | $container-sm ?=                 $container-tablet;
312 | 
313 | // Medium screen / desktop
314 | $container-desktop ?=            ((940px + $grid-gutter-width));
315 | //** For `$screen-md-min` and up.
316 | $container-md ?=                 $container-desktop;
317 | 
318 | // Large screen / wide desktop
319 | $container-large-desktop ?=      ((1140px + $grid-gutter-width));
320 | //** For `$screen-lg-min` and up.
321 | $container-lg ?=                 $container-large-desktop;
322 | 
323 | 
324 | //== Navbar
325 | //
326 | //##
327 | 
328 | // Basics of a navbar
329 | $navbar-height ?=                    50px;
330 | $navbar-margin-bottom ?=             $line-height-computed;
331 | $navbar-border-radius ?=             $border-radius-base;
332 | $navbar-padding-horizontal ?=        floor(($grid-gutter-width / 2));
333 | $navbar-padding-vertical ?=          (($navbar-height - $line-height-computed) / 2);
334 | $navbar-collapse-max-height ?=       340px;
335 | 
336 | $navbar-default-color ?=             #777;
337 | $navbar-default-bg ?=                #f8f8f8;
338 | $navbar-default-border ?=            darken($navbar-default-bg, 6.5%);
339 | 
340 | // Navbar links
341 | $navbar-default-link-color ?=                #777;
342 | $navbar-default-link-hover-color ?=          #333;
343 | $navbar-default-link-hover-bg ?=             transparent;
344 | $navbar-default-link-active-color ?=         #555;
345 | $navbar-default-link-active-bg ?=            darken($navbar-default-bg, 6.5%);
346 | $navbar-default-link-disabled-color ?=       #ccc;
347 | $navbar-default-link-disabled-bg ?=          transparent;
348 | 
349 | // Navbar brand label
350 | $navbar-default-brand-color ?=               $navbar-default-link-color;
351 | $navbar-default-brand-hover-color ?=         darken($navbar-default-link-color, 10%);
352 | $navbar-default-brand-hover-bg ?=            transparent;
353 | 
354 | // Navbar toggle
355 | $navbar-default-toggle-hover-bg ?=           #ddd;
356 | $navbar-default-toggle-icon-bar-bg ?=        #ccc;
357 | $navbar-default-toggle-border-color ?=       #ddd;
358 | 
359 | 
360 | // Inverted navbar
361 | // Reset inverted navbar basics
362 | $navbar-inverse-color ?=                      $gray-light;
363 | $navbar-inverse-bg ?=                         #222;
364 | $navbar-inverse-border ?=                     darken($navbar-inverse-bg, 10%);
365 | 
366 | // Inverted navbar links
367 | $navbar-inverse-link-color ?=                 $gray-light;
368 | $navbar-inverse-link-hover-color ?=           #fff;
369 | $navbar-inverse-link-hover-bg ?=              transparent;
370 | $navbar-inverse-link-active-color ?=          $navbar-inverse-link-hover-color;
371 | $navbar-inverse-link-active-bg ?=             darken($navbar-inverse-bg, 10%);
372 | $navbar-inverse-link-disabled-color ?=        #444;
373 | $navbar-inverse-link-disabled-bg ?=           transparent;
374 | 
375 | // Inverted navbar brand label
376 | $navbar-inverse-brand-color ?=                $navbar-inverse-link-color;
377 | $navbar-inverse-brand-hover-color ?=          #fff;
378 | $navbar-inverse-brand-hover-bg ?=             transparent;
379 | 
380 | // Inverted navbar toggle
381 | $navbar-inverse-toggle-hover-bg ?=            #333;
382 | $navbar-inverse-toggle-icon-bar-bg ?=         #fff;
383 | $navbar-inverse-toggle-border-color ?=        #333;
384 | 
385 | 
386 | //== Navs
387 | //
388 | //##
389 | 
390 | //=== Shared nav styles
391 | $nav-link-padding ?=                          10px 15px;
392 | $nav-link-hover-bg ?=                         $gray-lighter;
393 | 
394 | $nav-disabled-link-color ?=                   $gray-light;
395 | $nav-disabled-link-hover-color ?=             $gray-light;
396 | 
397 | $nav-open-link-hover-color ?=                 #fff;
398 | 
399 | //== Tabs
400 | $nav-tabs-border-color ?=                     #ddd;
401 | 
402 | $nav-tabs-link-hover-border-color ?=          $gray-lighter;
403 | 
404 | $nav-tabs-active-link-hover-bg ?=             $body-bg;
405 | $nav-tabs-active-link-hover-color ?=          $gray;
406 | $nav-tabs-active-link-hover-border-color ?=   #ddd;
407 | 
408 | $nav-tabs-justified-link-border-color ?=            #ddd;
409 | $nav-tabs-justified-active-link-border-color ?=     $body-bg;
410 | 
411 | //== Pills
412 | $nav-pills-border-radius ?=                   $border-radius-base;
413 | $nav-pills-active-link-hover-bg ?=            $component-active-bg;
414 | $nav-pills-active-link-hover-color ?=         $component-active-color;
415 | 
416 | 
417 | //== Pagination
418 | //
419 | //##
420 | 
421 | $pagination-color ?=                     $link-color;
422 | $pagination-bg ?=                        #fff;
423 | $pagination-border ?=                    #ddd;
424 | 
425 | $pagination-hover-color ?=               $link-hover-color;
426 | $pagination-hover-bg ?=                  $gray-lighter;
427 | $pagination-hover-border ?=              #ddd;
428 | 
429 | $pagination-active-color ?=              #fff;
430 | $pagination-active-bg ?=                 $brand-primary;
431 | $pagination-active-border ?=             $brand-primary;
432 | 
433 | $pagination-disabled-color ?=            $gray-light;
434 | $pagination-disabled-bg ?=               #fff;
435 | $pagination-disabled-border ?=           #ddd;
436 | 
437 | 
438 | //== Pager
439 | //
440 | //##
441 | 
442 | $pager-bg ?=                             $pagination-bg;
443 | $pager-border ?=                         $pagination-border;
444 | $pager-border-radius ?=                  15px;
445 | 
446 | $pager-hover-bg ?=                       $pagination-hover-bg;
447 | 
448 | $pager-active-bg ?=                      $pagination-active-bg;
449 | $pager-active-color ?=                   $pagination-active-color;
450 | 
451 | $pager-disabled-color ?=                 $pagination-disabled-color;
452 | 
453 | 
454 | //== Jumbotron
455 | //
456 | //##
457 | 
458 | $jumbotron-padding ?=              30px;
459 | $jumbotron-color ?=                inherit;
460 | $jumbotron-bg ?=                   $gray-lighter;
461 | $jumbotron-heading-color ?=        inherit;
462 | $jumbotron-font-size ?=            ceil(($font-size-base * 1.5));
463 | 
464 | 
465 | //== Form states and alerts
466 | //
467 | //## Define colors for form feedback states and, by default, alerts.
468 | 
469 | $state-success-text ?=             #3c763d;
470 | $state-success-bg ?=               #dff0d8;
471 | $state-success-border ?=           darken(spin($state-success-bg, -10), 5%);
472 | 
473 | $state-info-text ?=                #31708f;
474 | $state-info-bg ?=                  #d9edf7;
475 | $state-info-border ?=              darken(spin($state-info-bg, -10), 7%);
476 | 
477 | $state-warning-text ?=             #8a6d3b;
478 | $state-warning-bg ?=               #fcf8e3;
479 | $state-warning-border ?=           darken(spin($state-warning-bg, -10), 5%);
480 | 
481 | $state-danger-text ?=              #a94442;
482 | $state-danger-bg ?=                #f2dede;
483 | $state-danger-border ?=            darken(spin($state-danger-bg, -10), 5%);
484 | 
485 | 
486 | //== Tooltips
487 | //
488 | //##
489 | 
490 | //** Tooltip max width
491 | $tooltip-max-width ?=           200px;
492 | //** Tooltip text color
493 | $tooltip-color ?=               #fff;
494 | //** Tooltip background color
495 | $tooltip-bg ?=                  #000;
496 | $tooltip-opacity ?=             .9;
497 | 
498 | //** Tooltip arrow width
499 | $tooltip-arrow-width ?=         5px;
500 | //** Tooltip arrow color
501 | $tooltip-arrow-color ?=         $tooltip-bg;
502 | 
503 | 
504 | //== Popovers
505 | //
506 | //##
507 | 
508 | //** Popover body background color
509 | $popover-bg ?=                          #fff;
510 | //** Popover maximum width
511 | $popover-max-width ?=                   276px;
512 | //** Popover border color
513 | $popover-border-color ?=                rgba(0,0,0,.2);
514 | //** Popover fallback border color
515 | $popover-fallback-border-color ?=       #ccc;
516 | 
517 | //** Popover title background color
518 | $popover-title-bg ?=                    darken($popover-bg, 3%);
519 | 
520 | //** Popover arrow width
521 | $popover-arrow-width ?=                 10px;
522 | //** Popover arrow color
523 | $popover-arrow-color ?=                 #fff;
524 | 
525 | //** Popover outer arrow width
526 | $popover-arrow-outer-width ?=           ($popover-arrow-width + 1);
527 | //** Popover outer arrow color
528 | $popover-arrow-outer-color ?=           rgba(0,0,0,.25);
529 | //** Popover outer arrow fallback color
530 | $popover-arrow-outer-fallback-color ?=  #999;
531 | 
532 | 
533 | //== Labels
534 | //
535 | //##
536 | 
537 | //** Default label background color
538 | $label-default-bg ?=            $gray-light;
539 | //** Primary label background color
540 | $label-primary-bg ?=            $brand-primary;
541 | //** Success label background color
542 | $label-success-bg ?=            $brand-success;
543 | //** Info label background color
544 | $label-info-bg ?=               $brand-info;
545 | //** Warning label background color
546 | $label-warning-bg ?=            $brand-warning;
547 | //** Danger label background color
548 | $label-danger-bg ?=             $brand-danger;
549 | 
550 | //** Default label text color
551 | $label-color ?=                 #fff;
552 | //** Default text color of a linked label
553 | $label-link-hover-color ?=      #fff;
554 | 
555 | 
556 | //== Modals
557 | //
558 | //##
559 | 
560 | //** Padding applied to the modal body
561 | $modal-inner-padding ?=         20px;
562 | 
563 | //** Padding applied to the modal title
564 | $modal-title-padding ?=         15px;
565 | //** Modal title line-height
566 | $modal-title-line-height ?=     $line-height-base;
567 | 
568 | //** Background color of modal content area
569 | $modal-content-bg ?=                             #fff;
570 | //** Modal content border color
571 | $modal-content-border-color ?=                   rgba(0,0,0,.2);
572 | //** Modal content border color **for IE8**
573 | $modal-content-fallback-border-color ?=          #999;
574 | 
575 | //** Modal backdrop background color
576 | $modal-backdrop-bg ?=           #000;
577 | //** Modal backdrop opacity
578 | $modal-backdrop-opacity ?=      .5;
579 | //** Modal header border color
580 | $modal-header-border-color ?=   #e5e5e5;
581 | //** Modal footer border color
582 | $modal-footer-border-color ?=   $modal-header-border-color;
583 | 
584 | $modal-lg ?=                    900px;
585 | $modal-md ?=                    600px;
586 | $modal-sm ?=                    300px;
587 | 
588 | 
589 | //== Alerts
590 | //
591 | //## Define alert colors, border radius, and padding.
592 | 
593 | $alert-padding ?=               15px;
594 | $alert-border-radius ?=         $border-radius-base;
595 | $alert-link-font-weight ?=      bold;
596 | 
597 | $alert-success-bg ?=            $state-success-bg;
598 | $alert-success-text ?=          $state-success-text;
599 | $alert-success-border ?=        $state-success-border;
600 | 
601 | $alert-info-bg ?=               $state-info-bg;
602 | $alert-info-text ?=             $state-info-text;
603 | $alert-info-border ?=           $state-info-border;
604 | 
605 | $alert-warning-bg ?=            $state-warning-bg;
606 | $alert-warning-text ?=          $state-warning-text;
607 | $alert-warning-border ?=        $state-warning-border;
608 | 
609 | $alert-danger-bg ?=             $state-danger-bg;
610 | $alert-danger-text ?=           $state-danger-text;
611 | $alert-danger-border ?=         $state-danger-border;
612 | 
613 | 
614 | //== Progress bars
615 | //
616 | //##
617 | 
618 | //** Background color of the whole progress component
619 | $progress-bg ?=                 #f5f5f5;
620 | //** Progress bar text color
621 | $progress-bar-color ?=          #fff;
622 | 
623 | //** Default progress bar color
624 | $progress-bar-bg ?=             $brand-primary;
625 | //** Success progress bar color
626 | $progress-bar-success-bg ?=     $brand-success;
627 | //** Warning progress bar color
628 | $progress-bar-warning-bg ?=     $brand-warning;
629 | //** Danger progress bar color
630 | $progress-bar-danger-bg ?=      $brand-danger;
631 | //** Info progress bar color
632 | $progress-bar-info-bg ?=        $brand-info;
633 | 
634 | 
635 | //== List group
636 | //
637 | //##
638 | 
639 | //** Background color on `.list-group-item`
640 | $list-group-bg ?=                 #fff;
641 | //** `.list-group-item` border color
642 | $list-group-border ?=             #ddd;
643 | //** List group border radius
644 | $list-group-border-radius ?=      $border-radius-base;
645 | 
646 | //** Background color of single list elements on hover
647 | $list-group-hover-bg ?=           #f5f5f5;
648 | //** Text color of active list elements
649 | $list-group-active-color ?=       $component-active-color;
650 | //** Background color of active list elements
651 | $list-group-active-bg ?=          $component-active-bg;
652 | //** Border color of active list elements
653 | $list-group-active-border ?=      $list-group-active-bg;
654 | $list-group-active-text-color ?=  lighten($list-group-active-bg, 40%);
655 | 
656 | $list-group-link-color ?=         #555;
657 | $list-group-link-heading-color ?= #333;
658 | 
659 | 
660 | //== Panels
661 | //
662 | //##
663 | 
664 | $panel-bg ?=                    #fff;
665 | $panel-body-padding ?=          15px;
666 | $panel-border-radius ?=         $border-radius-base;
667 | 
668 | //** Border color for elements within panels
669 | $panel-inner-border ?=          #ddd;
670 | $panel-footer-bg ?=             #f5f5f5;
671 | 
672 | $panel-default-text ?=          $gray-dark;
673 | $panel-default-border ?=        #ddd;
674 | $panel-default-heading-bg ?=    #f5f5f5;
675 | 
676 | $panel-primary-text ?=          #fff;
677 | $panel-primary-border ?=        $brand-primary;
678 | $panel-primary-heading-bg ?=    $brand-primary;
679 | 
680 | $panel-success-text ?=          $state-success-text;
681 | $panel-success-border ?=        $state-success-border;
682 | $panel-success-heading-bg ?=    $state-success-bg;
683 | 
684 | $panel-info-text ?=             $state-info-text;
685 | $panel-info-border ?=           $state-info-border;
686 | $panel-info-heading-bg ?=       $state-info-bg;
687 | 
688 | $panel-warning-text ?=          $state-warning-text;
689 | $panel-warning-border ?=        $state-warning-border;
690 | $panel-warning-heading-bg ?=    $state-warning-bg;
691 | 
692 | $panel-danger-text ?=           $state-danger-text;
693 | $panel-danger-border ?=         $state-danger-border;
694 | $panel-danger-heading-bg ?=     $state-danger-bg;
695 | 
696 | 
697 | //== Thumbnails
698 | //
699 | //##
700 | 
701 | //** Padding around the thumbnail image
702 | $thumbnail-padding ?=           4px;
703 | //** Thumbnail background color
704 | $thumbnail-bg ?=                $body-bg;
705 | //** Thumbnail border color
706 | $thumbnail-border ?=            #ddd;
707 | //** Thumbnail border radius
708 | $thumbnail-border-radius ?=     $border-radius-base;
709 | 
710 | //** Custom text color for thumbnail captions
711 | $thumbnail-caption-color ?=     $text-color;
712 | //** Padding around the thumbnail caption
713 | $thumbnail-caption-padding ?=   9px;
714 | 
715 | 
716 | //== Wells
717 | //
718 | //##
719 | 
720 | $well-bg ?=                     #f5f5f5;
721 | $well-border ?=                 darken($well-bg, 7%);
722 | 
723 | 
724 | //== Badges
725 | //
726 | //##
727 | 
728 | $badge-color ?=                 #fff;
729 | //** Linked badge text color on hover
730 | $badge-link-hover-color ?=      #fff;
731 | $badge-bg ?=                    $gray-light;
732 | 
733 | //** Badge text color in active nav link
734 | $badge-active-color ?=          $link-color;
735 | //** Badge background color in active nav link
736 | $badge-active-bg ?=             #fff;
737 | 
738 | $badge-font-weight ?=           bold;
739 | $badge-line-height ?=           1;
740 | $badge-border-radius ?=         10px;
741 | 
742 | 
743 | //== Breadcrumbs
744 | //
745 | //##
746 | 
747 | $breadcrumb-padding-vertical ?=   8px;
748 | $breadcrumb-padding-horizontal ?= 15px;
749 | //** Breadcrumb background color
750 | $breadcrumb-bg ?=                 #f5f5f5;
751 | //** Breadcrumb text color
752 | $breadcrumb-color ?=              #ccc;
753 | //** Text color of current page in the breadcrumb
754 | $breadcrumb-active-color ?=       $gray-light;
755 | //** Textual separator for between breadcrumb elements
756 | $breadcrumb-separator ?=          "/";
757 | 
758 | 
759 | //== Carousel
760 | //
761 | //##
762 | 
763 | $carousel-text-shadow ?=                        0 1px 2px rgba(0,0,0,.6);
764 | 
765 | $carousel-control-color ?=                      #fff;
766 | $carousel-control-width ?=                      15%;
767 | $carousel-control-opacity ?=                    .5;
768 | $carousel-control-font-size ?=                  20px;
769 | 
770 | $carousel-indicator-active-bg ?=                #fff;
771 | $carousel-indicator-border-color ?=             #fff;
772 | 
773 | $carousel-caption-color ?=                      #fff;
774 | 
775 | 
776 | //== Close
777 | //
778 | //##
779 | 
780 | $close-font-weight ?=           bold;
781 | $close-color ?=                 #000;
782 | $close-text-shadow ?=           0 1px 0 #fff;
783 | 
784 | 
785 | //== Code
786 | //
787 | //##
788 | 
789 | $code-color ?=                  #c7254e;
790 | $code-bg ?=                     #f9f2f4;
791 | 
792 | $kbd-color ?=                   #fff;
793 | $kbd-bg ?=                      #333;
794 | 
795 | $pre-bg ?=                      #f5f5f5;
796 | $pre-color ?=                   $gray-dark;
797 | $pre-border-color ?=            #ccc;
798 | $pre-scrollable-max-height ?=   340px;
799 | 
800 | 
801 | //== Type
802 | //
803 | //##
804 | 
805 | //** Text muted color
806 | $text-muted ?=                  $gray-light;
807 | //** Abbreviations and acronyms border color
808 | $abbr-border-color ?=           $gray-light;
809 | //** Headings small color
810 | $headings-small-color ?=        $gray-light;
811 | //** Blockquote small color
812 | $blockquote-small-color ?=      $gray-light;
813 | //** Blockquote border color
814 | $blockquote-border-color ?=     $gray-lighter;
815 | //** Page header border color
816 | $page-header-border-color ?=    $gray-lighter;
817 | 
818 | 
819 | //== Miscellaneous
820 | //
821 | //##
822 | 
823 | //** Horizontal line color.
824 | $hr-border ?=                   $gray-lighter;
825 | 
826 | //** Horizontal offset for forms and lists.
827 | $component-offset-horizontal ?= 180px;
828 | 
829 | // Stylus Extras
830 | // --------------------------------------------------
831 | 
832 | // Variables need to be passed after @media declarations
833 | // Media Helpers
834 | 
835 | minw(w)
836 |   '(min-width:' + w +')'
837 | 
838 | maxw(w)
839 |   '(max-width:' + w +')'
840 | 
841 | min-max(min-w, max-w)
842 |   minw(min-w) +  ' and ' + maxw(max-w)
843 | 
844 | screen-min(w)
845 |   'screen and ' + minw(w)
846 | 
847 | screen-max(w)
848 |   'screen and ' + maxw(w)
849 | 
850 | screen-min-max(min-w, max-w)
851 |   'screen and ' + min-max(min-w, max-w)
852 | 
853 | 
854 | // Commonly used @media variables
855 | // Min
856 | $media-min-phone ?= minw($screen-xs)
857 | $media-min-tablet ?= minw($screen-sm)
858 | $media-min-desktop ?= minw($screen-md)
859 | $media-min-lg-desktop ?= minw($screen-lg)
860 | $media-min-grid-float-breakpoint ?= minw($grid-float-breakpoint)
861 | 
862 | // Max
863 | $media-max-sm ?= maxw($screen-sm)
864 | 
865 | // Max -1
866 | $media-max-grid-float-breakpoint ?= maxw($grid-float-breakpoint-max)
867 | 
868 | $media-max-phone ?= maxw($screen-xs-max)
869 | $media-max-tablet ?= maxw($screen-sm-max)
870 | $media-max-desktop ?= maxw($screen-md-max)
871 | 
872 | 
873 | // Screen and min
874 | $media-screen-min-phone ?= screen-min($screen-xs)
875 | $media-screen-min-tablet ?= screen-min($screen-sm)
876 | $media-screen-min-desktop ?= screen-min($screen-md)
877 | $media-screen-min-lg-desktop ?= screen-min($screen-lg)
878 | 
879 | // Min Max
880 | $media-tablet ?= min-max($screen-sm, $screen-sm-max)
881 | $media-desktop ?= min-max($screen-md, $screen-md-max)
882 | 


--------------------------------------------------------------------------------
/bin/influga.js:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env node
 2 | 
 3 | var pkg = require('../package');
 4 | var program = require('commander');
 5 | var app = require('../server/src/app');
 6 | var _ = require('underscore');
 7 | var fs = require('fs');
 8 | var path = require('path');
 9 | 
10 | var COMMANDS = ['init', 'start'];
11 | 
12 | program
13 |   .version(pkg.version)
14 |   .usage('[options] {' + COMMANDS.join('|') + '}')
15 |   .option('-p, --port ', 'Influga port', 8089)
16 |   .option('-c, --config ', 'Influga config file path')
17 |   .parse(process.argv);
18 | 
19 | if (!_.isEmpty(program.args) && _.contains(COMMANDS, program.args[0])) {
20 |   var cmd = program.args[0];
21 |   switch (cmd) {
22 |   case 'start':
23 |     start();
24 |     break;
25 | 
26 |   case 'init':
27 |     init();
28 |     break;
29 |   }
30 | } else {
31 |   console.error('Invalid command');
32 |   program.help();
33 | }
34 | 
35 | function start() {
36 |   var configPath = program.config;
37 |   if (!configPath) {
38 |     console.error('config file is required');
39 |     return program.help();
40 |   }
41 | 
42 |   if (!fs.existsSync(configPath)) {
43 |     console.error('%s is not exists', configPath);
44 |     return process.exit(1);
45 |   }
46 | 
47 |   var configJson = fs.readFileSync(configPath);
48 |   var config;
49 |   try {
50 |     config = JSON.parse(configJson);
51 |   } catch (e) {
52 |     console.error('Config format error: %s', e.message);
53 |     return process.exit(1);
54 |   }
55 | 
56 |   config = _.extend({
57 |     host: 'localhost',
58 |     port: '8086',
59 |     username: 'root',
60 |     password: 'root',
61 |     useProxy: true
62 |   }, config);
63 | 
64 |   app.create(config).listen(program.port, function() {
65 |     console.log('Listening on port %d', program.port);
66 |   });
67 | }
68 | 
69 | function init() {
70 |   var tmpl = {
71 |     dashboardDbPath: './db/influga.db',
72 |     host: 'localhost',
73 |     port: 8086,
74 |     database: 'db',
75 |     username: 'root',
76 |     password: 'root'
77 |   };
78 |   var filename = 'influga-config.json';
79 |   var config = JSON.stringify(tmpl, null, 2);
80 |   fs.writeFileSync(filename, config);
81 |   console.log('Config file template is created to influga-config.json');
82 |   console.log('Edit config for your environment');
83 |   console.log('');
84 |   console.log(config);
85 | }
86 | 


--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "influga",
 3 |   "version": "0.0.1",
 4 |   "authors": [
 5 |     "Kazuyuki Honda "
 6 |   ],
 7 |   "main": "public/index.html",
 8 |   "moduleType": [
 9 |     "node"
10 |   ],
11 |   "license": "MIT",
12 |   "private": true,
13 |   "ignore": [
14 |     "**/.*",
15 |     "node_modules",
16 |     "bower_components",
17 |     "test",
18 |     "tests"
19 |   ],
20 |   "dependencies": {
21 |     "bootstrap": "~3.1.1",
22 |     "d3": "~3.4.6",
23 |     "moment": "~2.6.0",
24 |     "vue": "~0.10.4",
25 |     "jquery": "~2.1.1",
26 |     "lodash": "~2.4.1",
27 |     "nvd3": "~1.1.15-beta",
28 |     "eonasdan-bootstrap-datetimepicker": "~3.0.0",
29 |     "fastclick": "~1.0.1",
30 |     "jquery-ui": "~1.10.4",
31 |     "node-uuid": "~1.4.1"
32 |   },
33 |   "overrides": {
34 |     "d3": {
35 |       "main": "./d3.js"
36 |     },
37 |     "node-uuid": {
38 |       "main": "./uuid.js"
39 |     }
40 |   },
41 |   "resolutions": {
42 |     "d3": "~3.4.6"
43 |   }
44 | }
45 | 


--------------------------------------------------------------------------------
/doc/assets/Influga.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hakobera/influga/8244d8f099c550cf9e6d15b11adb0f5557372256/doc/assets/Influga.ai


--------------------------------------------------------------------------------
/doc/assets/Influga_invert.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hakobera/influga/8244d8f099c550cf9e6d15b11adb0f5557372256/doc/assets/Influga_invert.ai


--------------------------------------------------------------------------------
/doc/assets/Influga_square.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hakobera/influga/8244d8f099c550cf9e6d15b11adb0f5557372256/doc/assets/Influga_square.ai


--------------------------------------------------------------------------------
/doc/assets/influga-logo.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hakobera/influga/8244d8f099c550cf9e6d15b11adb0f5557372256/doc/assets/influga-logo.ai


--------------------------------------------------------------------------------
/doc/assets/influga-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hakobera/influga/8244d8f099c550cf9e6d15b11adb0f5557372256/doc/assets/influga-logo.png


--------------------------------------------------------------------------------
/doc/assets/influga.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hakobera/influga/8244d8f099c550cf9e6d15b11adb0f5557372256/doc/assets/influga.png


--------------------------------------------------------------------------------
/doc/assets/screenshot_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hakobera/influga/8244d8f099c550cf9e6d15b11adb0f5557372256/doc/assets/screenshot_01.png


--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
  1 | var gulp = require('gulp');
  2 | var jade = require('gulp-jade');
  3 | var livereload = require('gulp-livereload');
  4 | var plumber = require('gulp-plumber');
  5 | var stylus = require('gulp-stylus');
  6 | var nib = require('nib');
  7 | var browserify = require('browserify');
  8 | var concat = require('gulp-concat');
  9 | var uglify = require('gulp-uglify');
 10 | var minifyCSS = require('gulp-minify-css');
 11 | var gulpBowerFiles = require('gulp-bower-files');
 12 | var gulpFilter = require('gulp-filter');
 13 | var order = require("gulp-order");
 14 | var flatten = require('gulp-flatten');
 15 | var streamqueue = require('streamqueue');
 16 | var nodemon = require('gulp-nodemon');
 17 | var jshint = require('gulp-jshint');
 18 | var source = require('vinyl-source-stream');
 19 | var streamify = require('gulp-streamify');
 20 | var mocha = require('gulp-mocha');
 21 | 
 22 | var environment = 'development';
 23 | var paths = {
 24 |   src: './app/',
 25 |   dest: './public/',
 26 |   bower: './bower_components/',
 27 |   vendor: './vendor/',
 28 |   serverTest: './server/test/'
 29 | };
 30 | 
 31 | gulp.task('set-production', function() {
 32 |   environment = 'production';
 33 | });
 34 | 
 35 | gulp.task('assets', function() {
 36 |   gulpBowerFiles()
 37 |     .pipe(plumber())
 38 |     .pipe(gulpFilter('**/fonts/*'))
 39 |     .pipe(flatten())
 40 |     .pipe(gulp.dest(paths.dest + 'fonts/'));
 41 | 
 42 |   gulp.src(paths.src + 'assets/**/*')
 43 |     .pipe(plumber())
 44 |     .pipe(gulp.dest(paths.dest));
 45 | });
 46 | 
 47 | gulp.task('vendor-styles', function() {
 48 |   var stream = gulpBowerFiles()
 49 |     .pipe(plumber())
 50 |     .pipe(gulpFilter('**/*.css'))
 51 |     .pipe(flatten())
 52 |     .pipe(order([
 53 |       'bootstrap.css',
 54 |       '*.css'
 55 |     ]))
 56 |     .pipe(concat("vendor.css"));
 57 | 
 58 |   if (environment == 'production') {
 59 |     stream.pipe(minifyCSS());
 60 |   }
 61 | 
 62 |   stream.pipe(gulp.dest(paths.dest + 'css/'));
 63 | });
 64 | 
 65 | gulp.task('vendor-scripts', function() {
 66 |   var stream = streamqueue({ objectMode: true },
 67 |       gulpBowerFiles(),
 68 |       gulp.src(paths.vendor + '**/*.js')
 69 |     )
 70 |     .pipe(plumber())
 71 |     .pipe(gulpFilter('**/*.js'))
 72 |     .pipe(flatten())
 73 |     .pipe(order([
 74 |       'jquery.js',
 75 |       'd3.js',
 76 |       'moment.js',
 77 |       '*.js'
 78 |     ]))
 79 |     .pipe(concat("vendor.js"));
 80 | 
 81 |   if (environment == 'production') {
 82 |     stream.pipe(uglify());
 83 |   }
 84 | 
 85 |   stream.pipe(gulp.dest(paths.dest + 'js/'));
 86 | });
 87 | 
 88 | gulp.task('scripts', function() {
 89 |   var stream = browserify({
 90 |       entries: [paths.src + 'scripts/initialize.js'],
 91 |       extensions: ['.jade']
 92 |     })
 93 |     .bundle({ debug: true })
 94 |     .pipe(plumber())
 95 |     .pipe(source('index.js'));
 96 | 
 97 |   if (environment == 'production') {
 98 |     stream.pipe(streamify(uglify()));
 99 |   }
100 | 
101 |   stream.pipe(gulp.dest(paths.dest + 'js/'));
102 | });
103 | 
104 | gulp.task('html', function() {
105 |   gulp.src(paths.src + 'index.jade')
106 |     .pipe(plumber())
107 |     .pipe(jade({
108 |       pretty: environment == 'development'
109 |     }))
110 |     .pipe(gulp.dest(paths.dest));
111 | });
112 | 
113 | gulp.task('styles', function () {
114 |   var stream = gulp.src(paths.src + 'styles/main.styl')
115 |     .pipe(plumber())
116 |     .pipe(stylus({ use: nib(), errors: true }));
117 | 
118 |   if (environment == 'production') {
119 |     stream.pipe(minifyCSS());
120 |   }
121 | 
122 |   stream.pipe(gulp.dest(paths.dest + 'css/'));
123 | });
124 | 
125 | gulp.task('watch', function () {
126 |   var server = livereload();
127 | 
128 |   gulp.watch(paths.src + 'scripts/**/*', ['lint', 'scripts']);
129 |   gulp.watch(paths.src + 'styles/**/*.styl', ['styles']);
130 |   gulp.watch(paths.src + 'assets/**/*', ['assets']);
131 |   gulp.watch(paths.src + 'index.jade', ['html']);
132 | 
133 |   gulp.watch([
134 |       paths.dest + 'js/*.js',
135 |       paths.dest + 'css/*.css',
136 |       paths.dest + '**/*.html'
137 |     ], function(evt) {
138 |       server.changed(evt.path);
139 |     });
140 | 
141 |   nodemon({
142 |     script: 'bin/influga.js',
143 |     args: ['start', '--config', 'scripts/config.json'],
144 |     watch: 'server',
145 |     ext: 'js',
146 |     env: { 'NODE_ENV': 'development' }
147 |   })
148 |   .on('change', ['lint-server'])
149 |   .on('restart', function () {
150 |     console.log('restarted!');
151 |   });
152 | });
153 | 
154 | gulp.task('lint', function () {
155 |   gulp.src(paths.src + 'scripts/**/*.js')
156 |     .pipe(plumber())
157 |     .pipe(jshint())
158 |     .pipe(jshint.reporter('jshint-stylish'));
159 | });
160 | 
161 | gulp.task('lint-server', function () {
162 |   gulp.src('./server/src/app.js')
163 |     .pipe(plumber())
164 |     .pipe(jshint())
165 |     .pipe(jshint.reporter('jshint-stylish'));
166 | });
167 | 
168 | gulp.task('mocha', function () {
169 |   return gulp.src(paths.serverTest + '**/*.js').pipe(mocha());
170 | });
171 | 
172 | gulp.task('vendor', ['vendor-styles', 'vendor-scripts']);
173 | gulp.task('compile', ['html', 'styles', 'scripts']);
174 | 
175 | gulp.task('default', ['assets', 'vendor', 'compile']);
176 | gulp.task('production', ['set-production', 'default']);
177 | gulp.task('test', ['production', 'mocha']);
178 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "influga",
 3 |   "version": "0.1.4",
 4 |   "author": "Kazuyuki Honda ",
 5 |   "description": "A Dashboard for InfluxDB",
 6 |   "bin": "bin/influga.js",
 7 |   "repository": "git@github.com:hakobera/influga.git",
 8 |   "scripts": {
 9 |     "start": "node server/src/app.js",
10 |     "test": "./node_modules/gulp/bin/gulp.js test"
11 |   },
12 |   "browserify": {
13 |     "transform": [
14 |       "jadeify"
15 |     ]
16 |   },
17 |   "devDependencies": {
18 |     "async": "^0.9.0",
19 |     "browserify": "^4.1.5",
20 |     "connect-livereload": "^0.4.0",
21 |     "gulp": "^3.6.2",
22 |     "gulp-bower-files": "^0.2.2",
23 |     "gulp-concat": "^2.2.0",
24 |     "gulp-filter": "^0.4.1",
25 |     "gulp-flatten": "^0.0.2",
26 |     "gulp-jade": "^0.5.0",
27 |     "gulp-jshint": "^1.5.5",
28 |     "gulp-livereload": "^1.5.0",
29 |     "gulp-minify-css": "^0.3.3",
30 |     "gulp-mocha": "^0.4.1",
31 |     "gulp-nodemon": "^1.0.2",
32 |     "gulp-order": "^1.0.6",
33 |     "gulp-plumber": "^0.6.1",
34 |     "gulp-streamify": "0.0.5",
35 |     "gulp-stylus": "^1.0.0",
36 |     "gulp-uglify": "^0.3.0",
37 |     "jade": "^1.3.1",
38 |     "jadeify": "^2.1.1",
39 |     "jshint-stylish": "^0.2.0",
40 |     "mocha": "^1.19.0",
41 |     "nib": "^1.0.2",
42 |     "streamqueue": "^0.0.7",
43 |     "supertest": "^0.13.0",
44 |     "vinyl-source-stream": "^0.1.1"
45 |   },
46 |   "dependencies": {
47 |     "body-parser": "^1.2.0",
48 |     "commander": "^2.2.0",
49 |     "compression": "^1.0.2",
50 |     "errorhandler": "^1.0.1",
51 |     "express": "^4.2.0",
52 |     "influx": "^0.4.0",
53 |     "mkdirp": "^0.5.0",
54 |     "morgan": "^1.0.1",
55 |     "nedb": "^0.10.5",
56 |     "serve-static": "^1.1.0",
57 |     "underscore": "^1.6.0"
58 |   },
59 |   "engines": {
60 |     "node": ">= 0.10"
61 |   },
62 |   "linsence": "MIT"
63 | }
64 | 


--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
 1 | #!/bin/bash
 2 | 
 3 | echo 'clean'
 4 | rm -rf ./public
 5 | 
 6 | echo 'npm install'
 7 | npm install
 8 | 
 9 | echo 'bower install'
10 | bower install
11 | 
12 | echo 'build for production'
13 | gulp production
14 | 
15 | echo 'publish'
16 | npm publish
17 | 


--------------------------------------------------------------------------------
/scripts/config.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "dashboardDbPath": "./db/influga.db",
 3 |   "host": "localhost",
 4 |   "port": 8086,
 5 |   "database": "dummy_data",
 6 |   "username": "root",
 7 |   "password": "root",
 8 |   "useProxy": true
 9 | }
10 | 


--------------------------------------------------------------------------------
/scripts/create_dummy_data.js:
--------------------------------------------------------------------------------
 1 | var influx = require('influx');
 2 | var db = 'dummy_data';
 3 | var client = influx('localhost', 8086, 'root', 'root', db);
 4 | var seriesName = 'metrics';
 5 | var timer = null;
 6 | var _ = require('underscore');
 7 | var query = 'SELECT * FROM metrics WHERE time > now() - 5s';
 8 | 
 9 | function onExit() {
10 |   console.log('exiting ...');
11 |   clearInterval(timer);
12 | 
13 |   client.deleteDatabase(db, function (err) {
14 |     if(err) return console.error(err);
15 |     console.log('Delete ' + db);
16 |     process.exit(0);
17 |   });
18 | }
19 | 
20 | client.createDatabase(db, function(err) {
21 |   if(err && err.message !== 'database ' + db + ' exists') {
22 |     throw err;
23 |   }
24 |   console.log('Create ' + db);
25 | 
26 |   timer = setInterval(function () {
27 |     var points = [];
28 |     points.push({
29 |       usr: _.random(1, 20),
30 |       sys: _.random(20, 40),
31 |       idl: _.random(40, 60),
32 |       wai: _.random(60, 80)
33 |     });
34 | 
35 |     console.log('Write points ...');
36 |     client.writePoints(seriesName, points, function (err) {
37 |       if (err) return console.error(err);
38 | 
39 |       client.query(query, function (err, data) {
40 |         if (err) return console.error(err);
41 |         data.forEach(function (d) {
42 |           console.log(d.points);
43 |         });
44 |       });
45 |     });
46 | 
47 |   }, 1000);
48 | 
49 |   process.on('SIGINT', onExit);
50 |   process.on('uncaughtException', onExit);
51 | });
52 | 


--------------------------------------------------------------------------------
/scripts/dummy_access_log.js:
--------------------------------------------------------------------------------
 1 | var influx = require('influx');
 2 | var db = 'dummy_data';
 3 | var client = influx('localhost', 8086, 'root', 'root', db);
 4 | var seriesName = 'access_log';
 5 | var timer = null;
 6 | var _ = require('underscore');
 7 | var query = 'SELECT * FROM access_log WHERE time > now() - 5s';
 8 | 
 9 | function onExit() {
10 |   console.log('exiting ...');
11 |   clearInterval(timer);
12 | 
13 |   client.deleteDatabase(db, function (err) {
14 |     if(err) return console.error(err);
15 |     console.log('Delete ' + db);
16 |     process.exit(0);
17 |   });
18 | }
19 | 
20 | client.createDatabase(db, function(err) {
21 |   if(err && err.message !== 'database ' + db + ' exists') {
22 |     throw err;
23 |   }
24 |   console.log('Create ' + db);
25 | 
26 |   timer = setInterval(function () {
27 |     var points = [];
28 |     points.push({
29 |       method: _.sample(['GET', 'POST', 'PUT', 'DELETE', 'HEAD'], 1)[0],
30 |       status: _.sample([200, 301, 400, 500], 1)[0],
31 |       uri   : '/data/' + _.sample([1, 2, 3, 4], 1)[0],
32 |       response_time: _.random(100, 2000)
33 |     });
34 | 
35 |     console.log('Write points ...');
36 |     client.writePoints(seriesName, points, function (err) {
37 |       if (err) return console.error(err);
38 | 
39 |       client.query(query, function (err, data) {
40 |         if (err) return console.error(err);
41 |         data.forEach(function (d) {
42 |           console.log(d.points);
43 |         });
44 |       });
45 |     });
46 | 
47 |   }, 100);
48 | 
49 |   process.on('SIGINT', onExit);
50 |   process.on('uncaughtException', onExit);
51 | });
52 | 


--------------------------------------------------------------------------------
/server/src/app.js:
--------------------------------------------------------------------------------
 1 | var express = require('express');
 2 | var bodyParser = require('body-parser');
 3 | var morgan  = require('morgan');
 4 | var compress = require('compression');
 5 | var errorhandler = require('errorhandler');
 6 | var InfluxDB = require('./lib/influxdb');
 7 | var Dashboard = require('./lib/dashboard');
 8 | 
 9 | exports.create = function create(opts) {
10 |   var app = express();
11 |   var dashboard = new Dashboard({
12 |     path: opts.dashboardDbPath
13 |   });
14 | 
15 |   if (process.env.NODE_ENV === 'development') {
16 |     app.use(require('connect-livereload')({port: 35729}));
17 |     app.use(morgan());
18 |   }
19 | 
20 |   var influxdb = new InfluxDB(opts);
21 | 
22 |   app.use(bodyParser.json());
23 |   app.use(compress());
24 |   app.use(express.static(__dirname + '/../../public'));
25 |   app.use(errorhandler());
26 | 
27 |   app.route('/config.json')
28 |     .get(function (req, res, next) {
29 |       res.json(influxdb.config());
30 |     });
31 | 
32 |   app.route('/db/:db/series')
33 |     .get(function (req, res, next) {
34 |       var query = req.query.q;
35 |       influxdb.query(query, function (err, data) {
36 |         if (err) {
37 |           res.type('text/plain');
38 |           return res.send(500, err.toString());
39 |         }
40 |         res.json(data);
41 |       });
42 |     });
43 | 
44 |   app.route('/shared/dashboards')
45 |     .get(function (req, res, next) {
46 |       dashboard.all(function (err, docs) {
47 |         if (err) return res.json(400, err);
48 |         res.json(docs);
49 |       });
50 |     })
51 |     .post(function (req, res, next) {
52 |       dashboard.save(req.body, function (err, doc) {
53 |         if (err) return res.json(400, err);
54 |         res.json(doc);
55 |       });
56 |     });
57 | 
58 |   app.route('/shared/dashboards/:id')
59 |     .get(function (req, res, next) {
60 |       dashboard.find({ _id: req.params.id }, function (err, doc) {
61 |         if (err) return res.json(500, {});
62 |         if (!doc) return res.json(404, {});
63 |         res.json(doc);
64 |       });
65 |     })
66 |     .delete(function (req, res, next) {
67 |       dashboard.remove({ _id: req.params.id }, function (err, numRemoved) {
68 |         if (err) return res.json(500, {});
69 |         if (numRemoved !== 1) return res.json(404, {});
70 |         res.json({});
71 |       });
72 |     });
73 | 
74 |   return app;
75 | };
76 | 


--------------------------------------------------------------------------------
/server/src/lib/dashboard.js:
--------------------------------------------------------------------------------
 1 | var Datastore = require('nedb');
 2 | var _ = require('underscore');
 3 | var path = require('path');
 4 | var mkdirp = require('mkdirp');
 5 | 
 6 | function Dashboard(opts) {
 7 |   var dbPath = opts.path;
 8 |   var dir = path.dirname(dbPath);
 9 |   mkdirp.sync(dir);
10 | 
11 |   var db = new Datastore({ filename: dbPath, autoload: true });
12 |   db.ensureIndex({ fieldName: 'name', unique: true }, function (err) {
13 |     if (err) return console.error(err);
14 |   });
15 |   this.db = db;
16 | }
17 | 
18 | Dashboard.prototype.all = function (cb) {
19 |   this.db.find({ type: 'dashboard' }).sort({ name: 1 }).exec(cb);
20 | };
21 | 
22 | Dashboard.prototype.find = function (condition, cb) {
23 |   this.db.findOne(condition, cb);
24 | };
25 | 
26 | Dashboard.prototype.save = function (data, cb) {
27 |   var name = data.name;
28 |   var doc = cleanup(_.extend({ type: 'dashboard' }, data));
29 |   var self = this;
30 | 
31 |   this.db.update({ name: name }, cleanup(doc), { upsert: true }, function (err, numReplaced, upsert) {
32 |     if (err) return cb(err);
33 |     if (upsert) {
34 |       cb(null, upsert);
35 |     } else {
36 |       self.db.findOne({ name: name }, function (err, doc) {
37 |         if (err) return cb(err);
38 |         cb(null, doc);
39 |       });
40 |     }
41 |   });
42 | };
43 | 
44 | Dashboard.prototype.remove = function (condition, cb) {
45 |   this.db.remove(condition, cb);
46 | };
47 | 
48 | function cleanup(data) {
49 |   delete data._id;
50 |   delete data.dashboards;
51 |   if (data.panels) {
52 |     var panels = [];
53 |     data.panels.forEach(function (panel) {
54 |       Object.keys(panel).forEach(function (key) {
55 |         if (key[0] === '$') {
56 |           delete panel[key];
57 |         }
58 |       });
59 |     });
60 |   }
61 |   return data;
62 | }
63 | 
64 | module.exports = Dashboard;
65 | 


--------------------------------------------------------------------------------
/server/src/lib/influxdb.js:
--------------------------------------------------------------------------------
 1 | var influx = require('influx');
 2 | var _ = require('underscore');
 3 | 
 4 | function InfluxDB(opts) {
 5 |   var o = this.opts = {
 6 |     host: opts.host || 'localhost',
 7 |     port: opts.port || 8086,
 8 |     username: opts.username || 'root',
 9 |     password: opts.password || 'root',
10 |     useProxy: opts.useProxy || false,
11 |     database: opts.database
12 |   };
13 | 
14 |   if (o.useProxy) {
15 |     this.client = influx(o.host, o.port, o.username, o.password, o.database);
16 |   }
17 | }
18 | 
19 | InfluxDB.prototype.config = function () {
20 |   var o = this.opts;
21 |   var conf = {
22 |     database: o.database,
23 |     useProxy: o.useProxy
24 |   };
25 |   if (!o.useProxy) {
26 |     conf = _.extend(conf, {
27 |       host: o.host,
28 |       port: o.port,
29 |       username: o.username,
30 |       password: o.password
31 |     });
32 |   }
33 |   return conf;
34 | };
35 | 
36 | InfluxDB.prototype.query = function (query, cb) {
37 |   if (!this.opts.useProxy) {
38 |     return cb(new Error('Proxy access is not allowed'));
39 |   }
40 |   this.client.query(query, cb);
41 | };
42 | 
43 | module.exports = InfluxDB;
44 | 
45 | 


--------------------------------------------------------------------------------
/server/test/app_test.js:
--------------------------------------------------------------------------------
  1 | var request = require('supertest');
  2 | var influx = require('influx');
  3 | var async = require('async');
  4 | var fs = require('fs');
  5 | 
  6 | var app = require('../src/app');
  7 | 
  8 | var config = {
  9 |   "dashboardDbPath": "./test.db",
 10 |   "host": "localhost",
 11 |   "port": 8086,
 12 |   "database": "test",
 13 |   "username": "root",
 14 |   "password": "root",
 15 |   "useProxy": true
 16 | };
 17 | 
 18 | process.env.NODE_ENV = 'development';
 19 | 
 20 | var server = app.create(config);
 21 | var client = influx(config.host, config.port, config.username, config.password, config.database);
 22 | 
 23 | describe('app', function () {
 24 |   beforeEach(function (done) {
 25 |     client.createDatabase(config.database, done);
 26 |   });
 27 | 
 28 |   afterEach(function (done) {
 29 |     client.deleteDatabase(config.database, done);
 30 |   });
 31 | 
 32 |   describe('GET /config.json', function () {
 33 |     it('should return influxdb config', function (done) {
 34 |       request(server)
 35 |         .get('/config.json')
 36 |         .expect('Content-Type', /json/)
 37 |         .expect(200, { database: 'test', useProxy: true })
 38 |         .end(done);
 39 |     });
 40 |   });
 41 | 
 42 |   describe('GET /db/:db/series', function () {
 43 |     describe('when valid query', function () {
 44 |       it('should return 200', function (done) {
 45 |         var query = 'SELECT 1 FROM /.*/ LIMIT 1';
 46 | 
 47 |         request(server)
 48 |           .get('/db/' + config.database + '/series')
 49 |           .query({ q: query })
 50 |           .expect('Content-Type', /json/)
 51 |           .expect(200, [])
 52 |           .end(done);
 53 |       });
 54 |     });
 55 | 
 56 |     describe('when invalid query', function () {
 57 |       it('should return 500 with error reason', function (done) {
 58 |         var query = 'a';
 59 | 
 60 |         request(server)
 61 |           .get('/db/' + config.database + '/series')
 62 |           .query({ q: query })
 63 |           .expect('Content-Type', /text\/plain/)
 64 |           .expect(500, 'Error: syntax error, unexpected SIMPLE_NAME\na\n^')
 65 |           .end(done);
 66 |       });
 67 |     });
 68 |   });
 69 | 
 70 |   describe('/shared/dashboards', function () {
 71 |     afterEach(function () {
 72 |       if (fs.existsSync(config.dashboardDbPath)) {
 73 |         fs.unlinkSync(config.dashboardDbPath);
 74 |       }
 75 |     });
 76 | 
 77 |     it('should return empty array if now dashboards', function (done) {
 78 |       request(server)
 79 |         .get('/shared/dashboards')
 80 |         .expect('Content-Type', /json/)
 81 |         .expect(200, [])
 82 |         .end(done);
 83 |     });
 84 | 
 85 |     it('should return dashboards', function (done) {
 86 |       var dashboard1 = require('../../app/assets/dashboards/default.json');
 87 |       var dashboard2 = require('../../app/assets/dashboards/test.json');
 88 | 
 89 |       async.series([
 90 |         function (next) {
 91 |           // Return emtpy array if no dashboards
 92 |           request(server)
 93 |             .get('/shared/dashboards')
 94 |             .expect('Content-Type', /json/)
 95 |             .expect(200, [])
 96 |             .end(next);
 97 |         },
 98 |         function (next) {
 99 |           request(server)
100 |             .post('/shared/dashboards')
101 |             .type('json')
102 |             .send(dashboard1)
103 |             .expect('Content-Type', /json/)
104 |             .expect(200)
105 |             .expect(function (res) {
106 |               if (!('_id' in res.body)) return 'missing _id';
107 |               if (res.body.type !== 'dashboard') return 'type must be dashboard';
108 |               if (res.body.name !== dashboard1.name) return 'name is wrong';
109 |             })
110 |             .end(next);
111 |         },
112 |         function (next) {
113 |           request(server)
114 |             .get('/shared/dashboards')
115 |             .expect('Content-Type', /json/)
116 |             .expect(200)
117 |             .expect(function (res) {
118 |               var dashboards = res.body;
119 |               if (dashboards.length !== 1) return 'dashboards.length expects 1 but ' + dashboards.length;
120 |             })
121 |             .end(next);
122 |         },
123 |         function (next) {
124 |           request(server)
125 |             .post('/shared/dashboards')
126 |             .type('json')
127 |             .send(dashboard2)
128 |             .expect('Content-Type', /json/)
129 |             .expect(200)
130 |             .expect(function (res) {
131 |               if (!('_id' in res.body)) return 'missing _id';
132 |               if (res.body.type !== 'dashboard') return 'type must be dashboard';
133 |               if (res.body.name !== dashboard2.name) return 'name is wrong';
134 |             })
135 |             .end(next);
136 |         }
137 |       ], function (err) {
138 |         if (err) return done(err);
139 |         request(server)
140 |           .get('/shared/dashboards')
141 |           .expect('Content-Type', /json/)
142 |           .expect(200)
143 |           .expect(function (res) {
144 |             var dashboards = res.body;
145 |             if (dashboards.length !== 2) return 'dashboards.length expects 2 but ' + dashboards.length;
146 |           })
147 |           .end(done);
148 |       });
149 |     });
150 |   });
151 | 
152 |   describe('/shared/dashboards/:id', function () {
153 |     afterEach(function () {
154 |       if (fs.existsSync(config.dashboardDbPath)) {
155 |         fs.unlinkSync(config.dashboardDbPath);
156 |       }
157 |     });
158 | 
159 |     describe('GET', function () {
160 |       it('should return 404 if :id is not exists', function (done) {
161 |         request(server)
162 |           .get('/shared/dashboards/invalid')
163 |           .expect('Content-Type', /json/)
164 |           .expect(404)
165 |           .end(done);
166 |       });
167 | 
168 |       it('should return saved dashboards', function (done) {
169 |         var dashboard = require('../../app/assets/dashboards/default.json');
170 | 
171 |         async.waterfall([
172 |           function (next) {
173 |             request(server)
174 |               .post('/shared/dashboards')
175 |               .type('json')
176 |               .send(dashboard)
177 |               .end(function (err, res) {
178 |                 if (err) return next(err);
179 |                 next(null, res.body);
180 |               });
181 |           },
182 |           function (body, next) {
183 |             request(server)
184 |               .get('/shared/dashboards/' + body._id)
185 |               .expect('Content-Type', /json/)
186 |               .expect(200, body)
187 |               .end(function (err, res) {
188 |                 if (err) return next(err);
189 |                 next(null);
190 |               });
191 |           }
192 |         ], function (err) {
193 |           if (err) return done(err);
194 |           done();
195 |         });
196 |       });
197 |     });
198 | 
199 |     describe('DELETE', function () {
200 |       it('should delete dashboard', function (done) {
201 |         var dashboard = require('../../app/assets/dashboards/default.json');
202 | 
203 |         async.waterfall([
204 |           function (next) {
205 |             request(server)
206 |               .post('/shared/dashboards')
207 |               .type('json')
208 |               .send(dashboard)
209 |               .end(function (err, res) {
210 |                 if (err) return next(err);
211 |                 next(null, res.body);
212 |               });
213 |           },
214 |           function (body, next) {
215 |             request(server)
216 |               .get('/shared/dashboards/' + body._id)
217 |               .expect('Content-Type', /json/)
218 |               .expect(200, body)
219 |               .end(function (err, res) {
220 |                 if (err) return next(err);
221 |                 next(null, body);
222 |               });
223 |           },
224 |           function (body, next) {
225 |             request(server)
226 |               .delete('/shared/dashboards/' + body._id)
227 |               .expect('Content-Type', /json/)
228 |               .expect(200)
229 |               .end(function (err, res) {
230 |                 if (err) return next(err);
231 |                 next(null, body);
232 |               });
233 |           },
234 |           function (body, next) {
235 |             request(server)
236 |               .get('/shared/dashboards/' + body._id)
237 |               .expect('Content-Type', /json/)
238 |               .expect(404)
239 |               .end(next);
240 |           }
241 |         ], function (err) {
242 |           if (err) return done(err);
243 |           done();
244 |         });
245 |       });
246 |     });
247 |   });
248 | 
249 |   describe('static file server', function () {
250 |     it('should return index.html', function (done) {
251 |       request(server)
252 |         .get('/')
253 |         .expect('Content-Type', /html/)
254 |         .expect(200)
255 |         .expect(function (res) {
256 |           if (res.text.indexOf('Influga') < 0) return 'Invalid page';
257 |         })
258 |         .end(done);
259 |     });
260 | 
261 |     it('should return index.js', function (done) {
262 |       request(server)
263 |         .get('/js/index.js')
264 |         .expect('Content-Type', /javascript/)
265 |         .expect(200)
266 |         .end(done);
267 |     });
268 | 
269 |     it('should return vendor.js', function (done) {
270 |       request(server)
271 |         .get('/js/vendor.js')
272 |         .expect('Content-Type', /javascript/)
273 |         .expect(200)
274 |         .end(done);
275 |     });
276 | 
277 |     it('should return main.css', function (done) {
278 |       request(server)
279 |         .get('/css/main.css')
280 |         .expect('Content-Type', /css/)
281 |         .expect(200)
282 |         .end(done);
283 |     });
284 | 
285 |     it('should return vendor.css', function (done) {
286 |       request(server)
287 |         .get('/css/vendor.css')
288 |         .expect('Content-Type', /css/)
289 |         .expect(200)
290 |         .end(done);
291 |     });
292 | 
293 |     it('should return default.json', function (done) {
294 |       request(server)
295 |         .get('/dashboards/default.json')
296 |         .expect('Content-Type', /json/)
297 |         .expect(200)
298 |         .end(done);
299 |     });
300 |   });
301 | });
302 | 


--------------------------------------------------------------------------------
/vendor/grapnel.js:
--------------------------------------------------------------------------------
  1 | /****
  2 |  * Grapnel.js
  3 |  * https://github.com/EngineeringMode/Grapnel.js
  4 |  *
  5 |  * @author Greg Sabia Tucker
  6 |  * @link http://artificer.io
  7 |  * @version 0.4.1
  8 |  *
  9 |  * Released under MIT License. See LICENSE.txt or http://opensource.org/licenses/MIT
 10 | */
 11 | 
 12 | (function(root){
 13 | 
 14 |     function Grapnel(){
 15 |         "use strict";
 16 | 
 17 |         var self = this; // Scope reference
 18 |         this.events = {}; // Event Listeners
 19 |         this.params = []; // Named parameters
 20 |         this.state = null; // Event state
 21 |         this.version = '0.4.1'; // Version
 22 |         // Anchor
 23 |         this.anchor = {
 24 |             defaultHash : window.location.hash,
 25 |             get : function(){
 26 |                 return (window.location.hash) ? window.location.hash.split('#')[1] : '';
 27 |             },
 28 |             set : function(anchor){
 29 |                 window.location.hash = (!anchor) ? '' : anchor;
 30 |                 return self;
 31 |             },
 32 |             clear : function(){
 33 |                 return this.set(false);
 34 |             },
 35 |             reset : function(){
 36 |                 return this.set(this.defaultHash);
 37 |             }
 38 |         }
 39 |         /**
 40 |          * ForEach workaround
 41 |          *
 42 |          * @param {Array} to iterate
 43 |          * @param {Function} callback
 44 |         */
 45 |         this._forEach = function(a, callback){
 46 |             if(typeof Array.prototype.forEach === 'function') return Array.prototype.forEach.call(a, callback);
 47 |             // Replicate forEach()
 48 |             return function(c, next){
 49 |                 for(var i=0, n = this.length; i