├── .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 [](https://travis-ci.org/hakobera/influga) 4 | 5 |  6 | 7 | A InfluxDB Dashboard and Graph Editor. 8 | 9 |  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