mirror of
https://github.com/getredash/redash.git
synced 2025-12-25 10:00:45 -05:00
Compare commits
18 Commits
v0.3.1+b91
...
v0.3.2+b10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b519269d8 | ||
|
|
5ffaf1aead | ||
|
|
b704406164 | ||
|
|
5c9fe40702 | ||
|
|
fe7c4f96aa | ||
|
|
83909a07fa | ||
|
|
cd99927881 | ||
|
|
8bbb485d5b | ||
|
|
b2ec77668e | ||
|
|
f8302ab65a | ||
|
|
e632cf1c42 | ||
|
|
640557df4f | ||
|
|
9b7227a88b | ||
|
|
aabc912862 | ||
|
|
02d6567347 | ||
|
|
6f8767d1fc | ||
|
|
bc787efc86 | ||
|
|
e0d46c3942 |
9
.env.example
Normal file
9
.env.example
Normal 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
2
.gitignore
vendored
@@ -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
2
Honchofile
Normal file
@@ -0,0 +1,2 @@
|
||||
web: ./manage.py runserver -p $PORT
|
||||
worker: ./manage.py runworkers
|
||||
@@ -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/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)"> ×</span>
|
||||
<span class="remove" ng-click="deleteVisualization($event, vis)" ng-show="currentUser.canEdit(query)"> ×</span>
|
||||
</rd-tab>
|
||||
<rd-tab id="add" name="+New" removeable="true"></rd-tab>
|
||||
<rd-tab id="add" name="+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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
46
redash/data/query_runner_graphite.py
Normal file
46
redash/data/query_runner_graphite.py
Normal 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
|
||||
@@ -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
49
redash/settings.py
Normal 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", "")
|
||||
@@ -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 = ""
|
||||
@@ -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
27
tests/test_settings.py
Normal 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')
|
||||
Reference in New Issue
Block a user