Compare commits

...

18 Commits

Author SHA1 Message Date
Arik Fraimovich
1b519269d8 Merge pull request #93 from EverythingMe/feature_env
Feature: better Heroku support - move configuration to environment variables & Procfile
2014-02-13 12:15:52 +02:00
Arik Fraimovich
5ffaf1aead Fix CircleCI configuration 2014-02-12 21:37:56 +02:00
Arik Fraimovich
b704406164 Example .env file. 2014-02-12 20:53:32 +02:00
Arik Fraimovich
5c9fe40702 Bump version. 2014-02-12 20:52:36 +02:00
Arik Fraimovich
fe7c4f96aa Fix: allow passing relative path for assets. 2014-02-12 20:52:19 +02:00
Arik Fraimovich
83909a07fa Read settings from environment variables instead of a settings file.
This is mostly done to make it easier to run re:dash on Heroku but should be convenient in other platforms too.
2014-02-12 20:43:41 +02:00
Arik Fraimovich
cd99927881 Add Honcho (foreman alternative in Python) file(s).
The reason we have both Procfile and Honchofile is to be able to run both the workers and the web server in a single dyno on Heroku.
2014-02-12 20:42:32 +02:00
Arik Fraimovich
8bbb485d5b Rename test files to test_. 2014-02-12 20:41:36 +02:00
Arik Fraimovich
b2ec77668e Merge pull request #89 from EverythingMe/feature_pie_chart
Feature: pie charts
2014-02-11 16:46:12 +02:00
Arik Fraimovich
f8302ab65a Better support for single series tooltips. 2014-02-11 16:30:41 +02:00
Arik Fraimovich
e632cf1c42 Support for pie charts. 2014-02-11 16:30:23 +02:00
Arik Fraimovich
640557df4f Merge pull request #88 from EverythingMe/feature_graphite_v2
Feature: graphite query runner
2014-02-11 11:47:11 +02:00
Arik Fraimovich
9b7227a88b Make the default newOptions apply to all but the chart vis 2014-02-11 11:42:37 +02:00
Arik Fraimovich
aabc912862 Graphite query runner support 2014-02-11 11:38:34 +02:00
Arik Fraimovich
02d6567347 Imrpove (?) line chart settings 2014-02-11 11:38:01 +02:00
Arik Fraimovich
6f8767d1fc Merge pull request #87 from EverythingMe/fix_viz
Some more visualizations UI updates
2014-02-10 21:11:13 +02:00
Arik Fraimovich
bc787efc86 Show delete/edit/create new visualization only to query owner.
This is a temporary solution until we have owners for visualizations.
2014-02-10 21:06:52 +02:00
Arik Fraimovich
e0d46c3942 When clicking on widget in dashboard, it should take to the correct visualization tab. 2014-02-10 19:38:23 +02:00
20 changed files with 199 additions and 61 deletions

9
.env.example Normal file
View File

@@ -0,0 +1,9 @@
REDASH_CONNECTION_ADAPTER=pg
REDASH_CONNECTION_STRING="dbname=data"
REDASH_STATIC_ASSETS_PATH=../rd_ui/app/
REDASH_GOOGLE_APPS_DOMAIN=
REDASH_ADMINS=
REDASH_WORKERS_COUNT=2
REDASH_COOKIE_SECRET=
REDASH_DATABASE_URL='postgresql://rd'
REDASH_LOG_LEVEL = "INFO"

2
.gitignore vendored
View File

@@ -2,7 +2,6 @@
.idea
*.pyc
.coverage
redash/settings.py
rd_ui/dist
.DS_Store
@@ -10,3 +9,4 @@ rd_ui/dist
.vagrant
Berksfile.lock
redash/dump.rdb
.env

2
Honchofile Normal file
View File

@@ -0,0 +1,2 @@
web: ./manage.py runserver -p $PORT
worker: ./manage.py runworkers

1
Procfile Normal file
View File

@@ -0,0 +1 @@
web: honcho start -f Honchofile -p $PORT

View File

@@ -10,7 +10,6 @@ dependencies:
- make deps
- pip install requests coverage nose
- pip install -r requirements.txt
- cp redash/settings_example.py redash/settings.py
cache_directories:
- rd_ui/node_modules/
- rd_ui/app/bower_components/

View File

@@ -20,8 +20,9 @@
});
};
$scope.open = function(query) {
$scope.open = function(query, visualization) {
$location.path('/queries/' + query.id);
$location.hash(visualization.id);
}
$scope.query = new Query($scope.widget.visualization.query);

View File

@@ -65,7 +65,8 @@
'Line': 'line',
'Column': 'column',
'Area': 'area',
'Scatter': 'scatter'
'Scatter': 'scatter',
'Pie': 'pie'
};
if (!scope.vis) {
@@ -79,24 +80,22 @@
'type': Visualization.prototype.TYPES.CHART,
'name': '',
'description': q.description || '',
'options': newOptions()
'options': newOptions(Visualization.prototype.TYPES.CHART)
};
}
}, true);
}
function newOptions(chartType) {
if (chartType === Visualization.prototype.TYPES.COHORT) {
// empty config at the moment
return {};
if (chartType === Visualization.prototype.TYPES.CHART) {
return {
'series': {
'type': 'column'
}
};
}
// Chart
return {
'series': {
'type': scope.seriesTypes[0]
}
};
return {};
}
scope.$watch('vis.type', function(type) {

View File

@@ -33,10 +33,20 @@
}
});
} else {
var s = "<b>" + this.points[0].key + "</b>";
$.each(this.points, function (i, point) {
s += '<br/><span style="color:' + point.series.color + '">' + point.series.name + '</span>: ' +
Highcharts.numberFormat(point.y);
var points = this.points;
var name = points[0].key || points[0].name;
var s = "<b>" + name + "</b>";
$.each(points, function (i, point) {
if (points.length > 1) {
s += '<br/><span style="color:' + point.series.color + '">' + point.series.name + '</span>: ' + Highcharts.numberFormat(point.y);
} else {
s += ": " + Highcharts.numberFormat(point.y);
if (point.percentage < 100) {
s += ' (' +Highcharts.numberFormat(point.percentage) + '%)';
}
}
});
}
@@ -95,15 +105,28 @@
},
line: {
marker: {
radius: 3,
radius: 1
},
lineWidth: 1,
lineWidth: 2,
states: {
hover: {
lineWidth: 2
lineWidth: 2,
marker: {
radius: 3
}
}
}
},
pie: {
allowPointSelect: true,
cursor: 'pointer',
dataLabels: {
enabled: true,
color: '#000000',
connectorColor: '#000000',
format: '<b>{point.name}</b>: {point.percentage:.1f} %'
}
},
scatter: {
marker: {
radius: 5,

View File

@@ -21,7 +21,7 @@
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title" style="cursor: pointer;" ng-click="open(query)">
<h3 class="panel-title" style="cursor: pointer;" ng-click="open(query, widget.visualization)">
<p>
<span ng-bind="query.name"></span>
</p>
@@ -37,7 +37,7 @@
tooltip-placement="bottom">Updated: <span am-time-ago="queryResult.getUpdatedAt()"></span></span>
<span class="pull-right">
<a class="btn btn-default btn-xs" ng-href="/queries/{{query.id}}"><span class="glyphicon glyphicon-link"></span></a>
<a class="btn btn-default btn-xs" ng-href="/queries/{{query.id}}#{{widget.visualization.id}}"><span class="glyphicon glyphicon-link"></span></a>
<button type="button" class="btn btn-default btn-xs" ng-show="dashboard.canEdit()" ng-click="deleteWidget()" title="Remove Widget"><span class="glyphicon glyphicon-trash"></span></button>
</span>

View File

@@ -56,9 +56,9 @@
<rd-tab id="pivot" name="Pivot Table"></rd-tab>
<!-- hide the table visualization -->
<rd-tab id="{{vis.id}}" name="{{vis.name}}" ng-hide="vis.type=='TABLE'" ng-repeat="vis in query.visualizations">
<span class="remove" ng-click="deleteVisualization($event, vis)"> &times;</span>
<span class="remove" ng-click="deleteVisualization($event, vis)" ng-show="currentUser.canEdit(query)"> &times;</span>
</rd-tab>
<rd-tab id="add" name="&plus;New" removeable="true"></rd-tab>
<rd-tab id="add" name="&plus;New" removeable="true" ng-show="currentUser.canEdit(query)"></rd-tab>
</ul>
<div class="col-lg-12" ng-show="selectedTab == 'table'">
@@ -70,12 +70,16 @@
</div>
<div class="col-lg-12" ng-show="selectedTab == vis.id" ng-repeat="vis in query.visualizations">
<div class="row">
<div class="row" ng-show="currentUser.canEdit(query)">
<p>
<div class="col-lg-6">
<div class="col-lg-12">
<edit-visulatization-form vis="vis" query="query"></edit-visulatization-form>
</div>
<div class="col-lg-6">
</p>
</div>
<div class="row">
<p>
<div class="col-lg-12">
<visualization-renderer visualization="vis" query-result="queryResult"></visualization-renderer>
</div>
</p>

View File

@@ -7,7 +7,7 @@ from flask_peewee.db import Database
import redis
from redash import settings, utils
__version__ = '0.3.1'
__version__ = '0.3.2'
app = Flask(__name__,
template_folder=settings.STATIC_ASSETS_PATH,

View File

@@ -153,9 +153,12 @@ class Manager(object):
if self.workers:
return self.workers
if getattr(settings, 'CONNECTION_ADAPTER', None) == "mysql":
if connection_type == 'mysql':
from redash.data import query_runner_mysql
runner = query_runner_mysql.mysql(connection_string)
elif connection_type == 'graphite':
from redash.data import query_runner_graphite
runner = query_runner_graphite.graphite(connection_string)
else:
from redash.data import query_runner
runner = query_runner.redshift(connection_string)

View File

@@ -0,0 +1,46 @@
"""
QueryRunner for Graphite.
"""
import json
import datetime
import requests
from redash.utils import JSONEncoder
def graphite(connection_params):
def transform_result(response):
columns = [{'name': 'Time::x'}, {'name': 'value::y'}, {'name': 'name::series'}]
rows = []
for series in response.json():
for values in series['datapoints']:
timestamp = datetime.datetime.fromtimestamp(int(values[1]))
rows.append({'Time::x': timestamp, 'name::series': series['target'], 'value::y': values[0]})
data = {'columns': columns, 'rows': rows}
return json.dumps(data, cls=JSONEncoder)
def query_runner(query):
base_url = "%s/render?format=json&" % connection_params['url']
url = "%s%s" % (base_url, "&".join(query.split("\n")))
error = None
data = None
try:
response = requests.get(url, auth=connection_params['auth'],
verify=connection_params['verify'])
if response.status_code == 200:
data = transform_result(response)
else:
error = "Failed getting results (%d)" % response.status_code
except Exception, ex:
data = None
error = ex.message
return data, error
query_runner.annotate_query = False
return query_runner

View File

@@ -234,8 +234,11 @@ class Worker(threading.Thread):
start_time = time.time()
self.set_title("running query %s" % job_id)
annotated_query = "/* Pid: %s, Job Id: %s, Query hash: %s, Priority: %s */ %s" % \
(pid, job.id, job.query_hash, job.priority, job.query)
if getattr(self.query_runner, 'annotate_query', True):
annotated_query = "/* Pid: %s, Job Id: %s, Query hash: %s, Priority: %s */ %s" % \
(pid, job.id, job.query_hash, job.priority, job.query)
else:
annotated_query = job.query
# TODO: here's the part that needs to be forked, not all of the worker process...
data, error = self.query_runner(annotated_query)

49
redash/settings.py Normal file
View File

@@ -0,0 +1,49 @@
import os
import urlparse
def parse_db_url(url):
url_parts = urlparse.urlparse(url)
connection = {
'engine': 'peewee.PostgresqlDatabase',
}
if url_parts.hostname and not url_parts.path:
connection['name'] = url_parts.hostname
else:
connection['name'] = url_parts.path[1:]
connection['host'] = url_parts.hostname
connection['port'] = url_parts.port
connection['user'] = url_parts.username
connection['password'] = url_parts.password
return connection
def fix_assets_path(path):
fullpath = os.path.join(os.path.dirname(__file__), path)
return fullpath
REDIS_URL = os.environ.get('REDASH_REDIS_URL', "redis://localhost:6379")
# "pg", "graphite" or "mysql"
CONNECTION_ADAPTER = os.environ.get("REDASH_CONNECTION_ADAPTER", "pg")
# Connection string for the database that is used to run queries against. Examples:
# -- mysql: CONNECTION_STRING = "Server=;User=;Pwd=;Database="
# -- pg: CONNECTION_STRING = "user= password= host= port=5439 dbname="
# -- graphite: CONNECTION_STRING = {'url': 'https://graphite.yourcompany.com', 'auth': ('user', 'password'), 'verify': True}
CONNECTION_STRING = os.environ.get("REDASH_CONNECTION_STRING", "user= password= host= port=5439 dbname=")
# Connection settings for re:dash's own database (where we store the queries, results, etc)
DATABASE_CONFIG = parse_db_url(os.environ.get("REDASH_DATABASE_URL", "postgresql://postgres"))
# Google Apps domain to allow access from; any user with email in this Google Apps will be allowed
# access
GOOGLE_APPS_DOMAIN = os.environ.get("REDASH_GOOGLE_APPS_DOMAIN", "")
# Email addresses of admin users (comma separated)
ADMINS = os.environ.get("REDASH_ADMINS", '').split(',')
STATIC_ASSETS_PATH = fix_assets_path(os.environ.get("REDASH_STATIC_ASSETS_PATH", "../rd_ui/dist/"))
WORKERS_COUNT = int(os.environ.get("REDASH_WORKERS_COUNT", "2"))
COOKIE_SECRET = os.environ.get("REDASH_COOKIE_SECRET", "c292a0a3aa32397cdb050e233733900f")
LOG_LEVEL = os.environ.get("REDASH_LOG_LEVEL", "INFO")
ANALYTICS = os.environ.get("REDASH_ANALYTICS", "")

View File

@@ -1,29 +0,0 @@
"""
Example settings module. You should make your own copy as settings.py and enter the real settings.
"""
REDIS_URL = "redis://localhost:6379"
# Either "pg" or "mysql"
CONNECTION_ADAPTER = "pg"
# Connection string for the database that is used to run queries against
# -- example mysql CONNECTION_STRING = "Server=;User=;Pwd=;Database="
# -- example pg CONNECTION_STRING = "user= password= host= port=5439 dbname="
CONNECTION_STRING = "user= password= host= port=5439 dbname="
# Connection settings for re:dash's own database (where we store the queries, results, etc)
DATABASE_CONFIG = {
'name': 'postgres',
'engine': 'peewee.PostgresqlDatabase',
}
# Google Apps domain to allow access from; any user with email in this Google Apps will be allowed
# access
GOOGLE_APPS_DOMAIN = ""
# Email addresses of specific users not from the above set Google Apps Domain, that you want to
# allow access to re:dash
ALLOWED_USERS = []
# Email addresses of admin users
ADMINS = []
STATIC_ASSETS_PATH = "../rd_ui/dist/"
WORKERS_COUNT = 2
COOKIE_SECRET = "c292a0a3aa32397cdb050e233733900f"
LOG_LEVEL = "INFO"
ANALYTICS = ""

View File

@@ -23,3 +23,4 @@ sqlparse==0.1.8
wsgiref==0.1.2
wtf-peewee==0.2.2
Flask-Script==0.6.6
honcho==0.5.0

27
tests/test_settings.py Normal file
View File

@@ -0,0 +1,27 @@
from redash import settings as settings
from unittest import TestCase
class TestDatabaseUrlParser(TestCase):
def test_only_database_name(self):
config = settings.parse_db_url("postgresql://postgres")
self.assertEquals(config['name'], 'postgres')
def test_host_and_database_name(self):
config = settings.parse_db_url("postgresql://localhost/postgres")
self.assertEquals(config['name'], 'postgres')
self.assertEquals(config['host'], 'localhost')
def test_host_with_port_and_database_name(self):
config = settings.parse_db_url("postgresql://localhost:5432/postgres")
self.assertEquals(config['name'], 'postgres')
self.assertEquals(config['host'], 'localhost')
self.assertEquals(config['port'], 5432)
def test_full_url(self):
config = settings.parse_db_url("postgresql://user:pass@localhost:5432/postgres")
self.assertEquals(config['name'], 'postgres')
self.assertEquals(config['host'], 'localhost')
self.assertEquals(config['port'], 5432)
self.assertEquals(config['user'], 'user')
self.assertEquals(config['password'], 'pass')