Compare commits

..

756 Commits

Author SHA1 Message Date
Arik Fraimovich
bac4025eff Merge pull request #882 from tsibley/new-data-source-via-cli
CLI: Restore ability to pass JSON options string for a new data source
2016-03-06 12:01:23 +02:00
Thomas Sibley
d07bf7e0aa CLI: Restore ability to pass JSON options string for a new data source
Commit "Encapsulate data source/query runner configuration in an
object." (ed99b84) accidentally removed that functionality by not
inflating the passed in JSON into a ConfigurationContainer object.

This led to errors of the form if you passed -o:

    Traceback (most recent call last):
      ...
      File "/opt/redash/redash-git/redash/models.py", line 321, in db_value
	return value.to_json()
    AttributeError: 'unicode' object has no attribute 'to_json'
2016-03-04 09:22:42 -08:00
Arik Fraimovich
8f937b7a35 Merge pull request #850 from toyama0919/master
Kibana: add support for limiting # of results returned
2016-03-01 23:37:24 +02:00
Arik Fraimovich
8747d0e724 Merge pull request #872 from erans/master
MongoDB: support for count queries
2016-03-01 23:36:24 +02:00
Arik Fraimovich
4a280eea29 Merge pull request #877 from ink-adavison/ink-adavison-ubuntubasepathfix
Correct Ubuntu Bootstrap.sh to a working Base URL
2016-03-01 13:36:25 +02:00
ink-adavison
194e9f4d7e Correct Ubuntu Bootstrap.sh to a working Base URL
The redash/docker/setup/... path is returning 404, but redash/master/setup/... works
2016-03-01 11:17:11 +00:00
Arik Fraimovich
4c5d2f14bf Merge pull request #866 from jeffwidman/fix_end_of_file_spacing
Fix whitespace issues
2016-02-29 22:39:15 +02:00
Jeff Widman
b86cf6ea4d Check should be 'x not in y' rather than 'not x in y' 2016-02-29 12:34:50 -08:00
Jeff Widman
dd72faaa77 Fix docstring spacing per PEP 257 2016-02-29 12:34:50 -08:00
Jeff Widman
c1b33939d0 A few misc whitespaces fixes 2016-02-29 12:34:50 -08:00
Jeff Widman
1aad95986b Add spaces around arithmetic operators per PEP 8 2016-02-29 12:34:50 -08:00
Jeff Widman
80209defc9 Remove extraneous spaces at end of lines 2016-02-29 12:34:46 -08:00
Jeff Widman
c198d22691 Format files to end in a single newline per PEP 8 2016-02-29 12:00:03 -08:00
Arik Fraimovich
43ac5600e5 Merge pull request #873 from getredash/feature/print_layout
Feature: print layout for dashboards
2016-02-29 21:11:41 +02:00
Arik Fraimovich
863365a412 Feature: print layout for dashboards 2016-02-29 21:07:07 +02:00
Eran Sandler
50c6bca421 added support for count queries 2016-02-29 21:05:50 +02:00
Arik Fraimovich
30b97e37f0 Merge pull request #854 from erans/master
Minors fixes for MongoDB and Python query runners
2016-02-29 21:04:14 +02:00
Eran Sandler
7f96de8b22 updated reverted change 2016-02-29 21:02:05 +02:00
Arik Fraimovich
dec30549f6 Merge pull request #867 from jeffwidman/fix_weird_indenting
Fix non-standard indentation to conventional four spaces
2016-02-29 15:25:11 +02:00
Arik Fraimovich
1a9059f1cc Merge pull request #865 from jeffwidman/fix_flask_ext
Replace deprecated flask.ext.* with flask_*
2016-02-29 15:22:13 +02:00
Jeff Widman
5208abd072 Make lines indented by four spaces instead of three 2016-02-29 01:58:19 -08:00
Jeff Widman
0ccbb24b3f Fix non-standard indentation to conventional four spaces
Not sure what happened in this section of code, but it was incorrectly indented by two spaces rather than four in several places.
2016-02-29 01:09:43 -08:00
Jeff Widman
09ccec59f5 Replace deprecated flask.ext.* with flask_*
Importing flask extensions using flask.ext.* is deprecated in favor of flask_*
For background, see: https://github.com/mitsuhiko/flask/issues/1135
2016-02-29 00:39:50 -08:00
Arik Fraimovich
8688b1c432 Merge pull request #864 from getredash/fix/datasource_show
Fix: data source loaded without properties
2016-02-28 18:56:10 +02:00
Arik Fraimovich
d06d1ada28 Fix: data source loaded without properties 2016-02-28 18:54:20 +02:00
Arik Fraimovich
3328de3462 Merge pull request #863 from getredash/fix/filters_datetime
Fix: properly show date/time in filters
2016-02-28 11:06:02 +02:00
Arik Fraimovich
84f71d1837 Fix: properly show date/time in filters 2016-02-28 11:04:08 +02:00
Arik Fraimovich
f219d20299 Merge pull request #857 from jeffwidman/patch-1
Fix typo: completly => completely
2016-02-27 21:55:56 +02:00
Eran Sandler
bdd2e0c418 fixed the query runner actually running code 2016-02-26 09:59:49 +02:00
Jeff Widman
d0cdf53b33 Fix typo: completly => completely 2016-02-25 18:00:15 -08:00
Arik Fraimovich
27faf8f88a Merge pull request #849 from sortable/presto-column-names
Fix: Presto - deduplicate column names
2016-02-25 21:39:29 +02:00
Arik Fraimovich
caf0734bac Merge pull request #855 from sreynen/patch-2
Docs: document settings.py
2016-02-25 21:37:57 +02:00
Eran Sandler
5f501b9df6 added JSON serialization support for internal bson timestamp which sometimes gets return in newer pymongo versions 2016-02-25 09:25:26 +02:00
Scott Reynen
caaf180d13 Create settings.rst
This is mostly just a template so far, as I'm only able to describe the settings I already understand, which isn't many.
2016-02-24 16:28:24 -07:00
Eran Sandler
68220a0d67 Added 'additionalModulesPaths' to the config allowing import modules from an external verified path. You'd still need to whitelist the module name in 'allowedImportModules' 2016-02-25 00:35:00 +02:00
Colin Dellow
0ebb53994b presto: use the disambiguated column name 2016-02-24 15:40:22 -05:00
Colin Dellow
177b62ea40 presto: use existing disambiguation path 2016-02-24 15:23:50 -05:00
Arik Fraimovich
a26da3aed3 Merge pull request #846 from sreynen/patch-1
Add link to dev environment documentation.
2016-02-24 22:07:35 +02:00
Arik Fraimovich
3a27955d24 Merge pull request #853 from getredash/arikfr-patch-1
Fix #851: embed doesn't load due to missing module.
2016-02-24 21:59:29 +02:00
Arik Fraimovich
86f2a0172f Fix #851: embed doesn't load due to missing module. 2016-02-24 21:53:12 +02:00
Arik Fraimovich
db59b34bda Merge pull request #852 from hudl/ConfigurationFixes
Small fixes for new ConfigurationContainer use
2016-02-24 21:49:57 +02:00
Alex DeBrie
51e92e0c71 Small fixes for new ConfigurationContainer use 2016-02-24 17:52:49 +00:00
toyama0919
246ce10a7f fix bug: Kibana not working limit, all select results. 2016-02-24 13:02:28 +09:00
Colin Dellow
cde54cec8b presto: don't merge columns with the same name
Fixes #847
2016-02-23 18:40:49 -05:00
Scott Reynen
21dc36b506 Add link to dev environment documentation. 2016-02-23 14:16:47 -07:00
Arik Fraimovich
d74442184e Merge pull request #844 from getredash/kms
Encapsulate data sources configuration logic in an object
2016-02-23 15:06:29 +02:00
Arik Fraimovich
db3e689e68 Update query runners for new config class 2016-02-23 15:02:49 +02:00
Arik Fraimovich
491e2e10d1 Fix test 2016-02-23 15:02:49 +02:00
Arik Fraimovich
ed99b8452c Encapsulate data source/query runner configuration in an object.
This is a step towards adding more complex logic in configuration
handling, like encryption of secrets.
2016-02-23 15:02:49 +02:00
Arik Fraimovich
f1e90fde31 Merge pull request #843 from getredash/design/download_links
Fix: dashboard query results links broken
2016-02-23 11:35:06 +02:00
Arik Fraimovich
954e63a41f Fix: dashboard query results links broken 2016-02-23 11:33:13 +02:00
Arik Fraimovich
6ec4c4c19c Merge pull request #840 from getredash/design/download_links
Improve layout of download links
2016-02-22 11:53:43 +02:00
Arik Fraimovich
553c6ac8d7 Improve layout of download links 2016-02-22 11:50:42 +02:00
Arik Fraimovich
b462869be7 Merge pull request #833 from toyama0919/feature/download-excel-from-dashboard
Feature: download Excel file link from dashboard.
2016-02-22 10:55:20 +02:00
Arik Fraimovich
3a5d59cf69 Merge pull request #839 from getredash/feature/api_params
Feature: add API to trigger query refresh and support for parameters.
2016-02-22 10:43:42 +02:00
Arik Fraimovich
c12b059d10 Add API to trigger query refresh and support for parameters. 2016-02-22 10:40:46 +02:00
Arik Fraimovich
e705ede3b7 Merge pull request #838 from erans/master
Python query runner -- added access to sorted and reversed functions
2016-02-21 13:03:05 +02:00
Eran Sandler
3b5aafa8e1 Added access to sorted and reversed functions 2016-02-21 11:13:46 +02:00
Arik Fraimovich
2440a83e46 Merge pull request #835 from benmanns/ubuntu-bootstrap-update-reorder
Ubuntu bootstrap script - move update before upgrade
2016-02-18 10:56:00 +02:00
Benjamin Manns
2b5a36cb3f Move update before upgrade
Running update before upgrade will fetch the latest sources, so we
can be sure that the upgrades will bring the box to the latest
versions of everything. Otherwise, this is often a no-op because
the box's sources will be cached at time of generation, meaning
there is nothing to upgrade.
2016-02-17 16:11:16 -05:00
toyama0919
78511fd0ce add feature, Excel download from dashboard. 2016-02-17 14:48:23 +09:00
Arik Fraimovich
a50ae19236 Merge pull request #823 from mobiledefense/add-widescreen-toggle
Feature: Button toggle to display dashboard in at full screen width
2016-02-16 10:23:16 +02:00
Arik Fraimovich
65f81c4d93 Merge pull request #831 from ninneko/801-download-excel
Feature: download results in Excel (XSLX) format (closes #801)
2016-02-16 09:19:53 +02:00
yohei.naruse
0afca7321a #801 fix test case.
schedule = "{:02d}:00".format(now.hour - 3) maybe be negative value when now.hour < 3.
I've fixed it.
2016-02-16 13:16:31 +09:00
yohei.naruse
32824f7575 apply reviews 2016-02-16 10:56:06 +09:00
yohei.naruse
2f16c8ae5f #801 Download DataSheets as Excel file 2016-02-15 23:47:31 +09:00
Arik Fraimovich
868263315b Merge pull request #829 from getredash/fix/embed
Fix: Plot.ly was given wrong timestamp
2016-02-14 20:40:43 +02:00
Arik Fraimovich
1ceddc9e91 Fix: we were sending wrong timestamp to Plotly 2016-02-14 20:38:32 +02:00
Arik Fraimovich
a96d135a4f Merge pull request #828 from getredash/fix/embed
Fix #797: user redirected to homepage when changing permission type
2016-02-14 20:11:51 +02:00
Arik Fraimovich
cec4e71d99 Fix #797: user redirected to homepage when changing permission type 2016-02-14 16:01:36 +02:00
Arik Fraimovich
0730ed8ed4 Merge pull request #827 from getredash/fix/embed
Feature: pivot tables are now regular visualizations that can be *saved*
2016-02-14 15:21:45 +02:00
Arik Fraimovich
e3420acd4b Feature: pivots are now regular visualizations that can be *saved*. 2016-02-14 15:17:52 +02:00
Arik Fraimovich
d21e2a79cc Close #772: upgrade to latest PivotTable.js lib 2016-02-14 14:40:01 +02:00
Arik Fraimovich
d1cf376ab3 Merge pull request #826 from getredash/fix/embed
Fix #802: switching to/from query source view resets chart colors
2016-02-14 14:13:24 +02:00
Arik Fraimovich
0ea0ba3fbe Fix #802: switching to/from query source view resets chart colors 2016-02-14 13:52:53 +02:00
Arik Fraimovich
0c93fe12ba Amend jshint settings 2016-02-14 13:51:32 +02:00
Arik Fraimovich
dad7b22cba Merge pull request #825 from getredash/fix/embed
Fix: sorting X values in charts had no effect
2016-02-14 13:26:32 +02:00
Arik Fraimovich
19766cf4ce Fix: sorting X values had no effect. 2016-02-14 13:24:14 +02:00
Arik Fraimovich
5e2727cfdf Use unminified d3.js in development. 2016-02-14 12:25:16 +02:00
Arik Fraimovich
3da326009b Bump Plot.ly version to a more recent one. 2016-02-14 12:24:56 +02:00
Matt Sochor
240739a445 Add dashboard toggle to display dashboard in at full screen width 2016-02-11 16:39:40 -05:00
Arik Fraimovich
253c4fd0a6 Merge pull request #821 from getredash/fix/embed
Fix embed URL & move logic into a directive
2016-02-10 15:41:05 +02:00
Arik Fraimovich
cda1068ff1 Show logo in embdes 2016-02-10 15:39:02 +02:00
Arik Fraimovich
eb324a4067 Limit the amount of information we return for embeds 2016-02-10 15:34:48 +02:00
Arik Fraimovich
8cf7314dc0 Fix embed URL & move logic into a directive 2016-02-10 15:34:31 +02:00
Arik Fraimovich
32b928d247 BSD 2-Clause
Updated copyright holder & removed last paragraph that doesn't belong to BSD-2 clause.
2016-02-10 11:32:00 +02:00
Arik Fraimovich
a5168ecc80 Update bootstrap.sh to more recent release 2016-02-09 15:18:58 +02:00
Arik Fraimovich
262ebb3bf1 Merge pull request #820 from getredash/gulp
Switch to Gulp from Grunt for faster builds
2016-02-09 15:15:38 +02:00
Arik Fraimovich
3e58d8798a Copy additional files on gulp build 2016-02-09 15:09:31 +02:00
Arik Fraimovich
bab536aaea Support for embeds in multi-org 2016-02-09 14:59:38 +02:00
Arik Fraimovich
bab4080430 Switch to Gulp from Grunt 2016-02-09 14:59:19 +02:00
Arik Fraimovich
a894f035dd Merge pull request #815 from getredash/fix/cli
Fix CLI issues with recent version
2016-02-09 00:56:16 +02:00
Arik Fraimovich
d4a83e29d4 Fix: delete data source CLI failing when data source has references 2016-02-09 00:03:35 +02:00
Arik Fraimovich
ded4761c8a If start_time not found skip metric collections (probably not running in real request context) 2016-02-09 00:01:44 +02:00
Arik Fraimovich
3fa143cfb1 Merge pull request #813 from ojarva/shellcheck-fixes
Fix shellcheck issues in bootstrap.sh scripts
2016-02-07 08:49:06 +02:00
Olli Jarva
de01184bbd Small shellcheck updates
Fix shellcheck complaints. These changes are not particularly important,
but spotting new/real issues is easier when checker output is empty by
default.
2016-02-06 16:42:24 +02:00
Arik Fraimovich
635bcc3e9f Pull latest docker image before building 2016-02-03 11:06:09 +02:00
Arik Fraimovich
b6b8daced6 Update Circle: build deps on master branch 2016-02-03 10:20:07 +02:00
Arik Fraimovich
b222f85d88 Add freetds-dev to Dockerfile 2016-02-03 09:06:01 +02:00
Arik Fraimovich
27c3fee345 Merge pull request #808 from joeharris76/master
Feature: Microsoft SQL Server query runner
2016-02-03 08:48:05 +02:00
Joe Harris
8c48ec5508 Cleanup of issues with the SQL Server feature PR 2016-02-02 16:18:21 -05:00
Joe Harris
cc176f5cba Add error handling to the pymssql import 2016-02-02 09:24:58 -05:00
Joe Harris
3a970a00c4 Add Microsoft SQL Server as a data source
Uses `pymssql` which in turn uses `FreeTDS`. Note that the data type
support is somewhat limited (see “datasources” page in docs).
2016-02-01 16:53:52 -05:00
Joe Harris
3b395a05b8 Merge pull request #1 from getredash/master
Pull from origin
2016-01-29 15:55:50 -05:00
Arik Fraimovich
9fa249a519 Update screenshots. 2016-01-27 12:55:52 +02:00
Arik Fraimovich
4e9b60ac82 Merge pull request #794 from getredash/hotfixes
Fixes for #792, #785, #733 and additional logging for execute_query
2016-01-24 12:03:34 +02:00
Arik Fraimovich
7a7e5be166 Fix #733: update migrations to work with new code 2016-01-24 11:57:05 +02:00
Arik Fraimovich
a1eec8490a Add more logging to execute_query 2016-01-24 11:32:44 +02:00
Arik Fraimovich
197bbde788 Fix #785: remove admin check box and direct users to use the groups
admin.
2016-01-24 11:08:02 +02:00
Arik Fraimovich
fed9d80fdb Fix #792: can't grant admin with CLI 2016-01-24 10:58:05 +02:00
Arik Fraimovich
78ba6f2739 Merge pull request #781 from woei66/master
Amazon Linux bootstrap script: check nginx default directory
2016-01-23 16:52:39 +02:00
Arik Fraimovich
cbb84ae3d3 Merge pull request #786 from JohnConnell/patch-1
Docs: instructions for compressed backup.
2016-01-23 16:51:21 +02:00
Arik Fraimovich
8120158119 Merge pull request #782 from shyamgopal/bug-768
Fix: Empty cells in google sheets displayed as datetime values #768
2016-01-23 16:50:44 +02:00
Arik Fraimovich
bd7b60d859 Merge pull request #784 from bobrik/fix-isoformat
Fix json serialization for datetime.timedelta, closes #783
2016-01-23 16:49:46 +02:00
Arik Fraimovich
80c03a5900 Merge pull request #787 from JohnConnell/patch-2
Docs: Updated links to Google's documentation about creating a service account
2016-01-23 16:46:04 +02:00
Arik Fraimovich
77e2d5db9b Merge pull request #790 from tknzk/fix_typo_on_doc
Docs: fix a typo in backup instructions
2016-01-23 16:44:35 +02:00
tknzk
7174dd856e fix a typo. 2016-01-22 17:45:40 +09:00
John Connell
6b5efc9e16 Update datasources.rst
Updated links to Google's documentation about creating a service account.
2016-01-21 13:05:02 -07:00
John Connell
4f95205795 Update maintenance.rst
Added: How to create a compressed backup.
2016-01-21 12:51:13 -07:00
Ivan Babrou
e26ea40c9b Fix json serialization for datetime.timedelta, closes #783 2016-01-21 14:37:36 +00:00
Arik Fraimovich
24137e87fd Update cloud images references 2016-01-21 14:47:51 +02:00
Shyamgopal Kundapurkar
221ec3a2a1 Fix of #768 2016-01-21 16:35:03 +05:30
David Lin
7081e25fa3 add -y to expect package, check nginx default directory and install to the right directory 2016-01-21 06:37:12 +00:00
Arik Fraimovich
8d126331cf Fix #778: update docs with correct CLI command. 2016-01-20 22:19:43 +02:00
Arik Fraimovich
33ffb2158b Update __init__.py 2016-01-20 10:21:45 +02:00
Arik Fraimovich
76ee88608d Merge pull request #777 from getredash/hotfix
Treat query errors as expected errors
2016-01-20 10:21:08 +02:00
Arik Fraimovich
a1ac289fb4 Treat query errors as expected errors 2016-01-20 10:17:10 +02:00
Arik Fraimovich
4f0b18b44e Merge pull request #776 from getredash/hotfix
Friendlier error messages for BigQuery query errors
2016-01-20 10:16:36 +02:00
Arik Fraimovich
44595cc930 Friendlier error messages for BigQuery 2016-01-20 10:01:08 +02:00
Arik Fraimovich
fcd478c93c Merge pull request #774 from getredash/hotfix
Fix: don't fail refresh_schema if one of the refresh ops fails
2016-01-19 18:44:47 +02:00
Arik Fraimovich
9971496401 Fix: don't fail refresh_schema if one of the refresh ops fails 2016-01-19 18:37:47 +02:00
Arik Fraimovich
8473783b0b Merge pull request #773 from getredash/hotfix
0.9.0 Hot Fixes
2016-01-19 18:34:19 +02:00
Arik Fraimovich
a9ae3c9ea3 Don't use DataSource.all in old migrations 2016-01-19 18:31:08 +02:00
Arik Fraimovich
505166455d Fix: show each data source only once 2016-01-19 18:26:51 +02:00
Arik Fraimovich
c6a06bd40a Remove debugging text 2016-01-19 18:01:35 +02:00
Arik Fraimovich
ed9e27019f Remove references to activity_log table 2016-01-19 18:00:42 +02:00
Arik Fraimovich
5b1abaaa52 Bump version. 2016-01-18 10:14:15 +02:00
Arik Fraimovich
c1da2579a3 Test for embed handler 2016-01-16 21:25:19 +02:00
Arik Fraimovich
1b36a62b91 Add conversion to int for Organization 2016-01-16 21:25:09 +02:00
Arik Fraimovich
ed2e06a787 Fix: counter visualization doesn't update when editing 2016-01-16 21:17:23 +02:00
Arik Fraimovich
47d3faae92 Fix: dashboard editor doesn't include last added widget 2016-01-16 21:11:25 +02:00
Olga Kogan
ff49321056 Update supervisor configs to recycle Gunicorn/Celery workers
This helps with avoiding memory leaks.
2016-01-15 17:57:09 +02:00
Arik Fraimovich
ee98b5a5c6 Improve the migration for unique data source name 2016-01-15 17:53:24 +02:00
Arik Fraimovich
245a4b5a3f Merge pull request #765 from nakechi/master
Feature: support HipChat Server
2016-01-15 17:30:48 +02:00
Arik Fraimovich
0546528b2c Merge pull request #762 from JohnConnell/master
Fix: typos and formatting issues in letsencrypt SSL cert documentation
2016-01-15 17:29:46 +02:00
Arik Fraimovich
d8d925c297 Merge pull request #764 from JohnConnell/master
Documentation: How to backup & restore redash db
2016-01-15 17:28:43 +02:00
nao-akechi
fac0af548b Feature: support HipChat Server 2016-01-15 18:36:07 +09:00
John Connell
5deca9bd60 Documentation: How to backup & restore redash db 2016-01-14 19:48:04 -07:00
John Connell
b1e0620f85 Update backup_restore.rst 2016-01-14 18:50:20 -07:00
John Connell
0a35f70a27 Update backup_restore.rst 2016-01-14 18:49:13 -07:00
John Connell
bd1551fb9d Rename backup_restore to backup_restore.rst 2016-01-14 18:47:18 -07:00
John Connell
f6a8a9975f How To: Backup re:dash database & restore to different server
Short guide explaining how to backup your re:dash database and restore it on a different server.
2016-01-14 18:46:31 -07:00
John Connell
179649d422 Update letsencrypt.rst 2016-01-14 01:32:10 -07:00
John Connell
1c584f65ba Update letsencrypt.rst
Fix various typos and formatting issues, including the commands for step 5 not being displayed.
2016-01-14 01:30:56 -07:00
John Connell
b62c75ac66 Update letsencrypt.rst
Fix two small typos. The first prevented the commands for step 5 from appearing and the second was a typo in the SSLLabs test URL.
2016-01-14 01:19:26 -07:00
Arik Fraimovich
f4096c0356 Update README.md 2016-01-14 09:56:17 +02:00
Arik Fraimovich
419fe389a4 Update README.md 2016-01-14 09:56:07 +02:00
Arik Fraimovich
031cb63f67 Rename peronal.html -> index.html 2016-01-13 10:03:52 +02:00
Arik Fraimovich
a62c5b5b24 Merge pull request #759 from getredash/fix/new_ds
Remove unused client side code
2016-01-13 10:02:25 +02:00
Arik Fraimovich
3befab7244 Remove client side performance collection 2016-01-13 10:00:06 +02:00
Arik Fraimovich
8c006238c5 Remove old IndexCtrl 2016-01-13 09:56:58 +02:00
Arik Fraimovich
03d897886e Merge pull request #758 from getredash/fix/new_ds
Fix: update dashboard after layout change
2016-01-12 21:43:10 +02:00
Arik Fraimovich
ebe032070e Fix: update dashboard after layout change 2016-01-12 16:25:10 +02:00
Arik Fraimovich
4a29f41ab3 Merge pull request #757 from getredash/fix/new_ds
Fix: infinite digest loop in coutner visualization
2016-01-12 15:14:05 +02:00
Arik Fraimovich
566cda359e Fix: infinite digest loop in coutner visualization 2016-01-12 15:13:23 +02:00
Arik Fraimovich
5a1736ad31 Merge pull request #756 from getredash/fix/new_ds
Fix: new data source should be assigned to default group
2016-01-12 15:13:04 +02:00
Arik Fraimovich
eed3d50372 create data source with default group specific method 2016-01-12 15:10:03 +02:00
Arik Fraimovich
901cf6f017 Fix: new data source should be assigned to default group? 2016-01-12 13:39:54 +02:00
Arik Fraimovich
83458ab25e increase opacity of overlay 2016-01-12 12:08:02 +02:00
Arik Fraimovich
9ab4e0e888 Merge pull request #754 from getredash/proxy_fix
Make groups listing only available for users with list_users permission
2016-01-12 09:22:06 +02:00
Arik Fraimovich
89ac67555e Make groups listing only available for users with list_users permission 2016-01-11 15:46:41 +02:00
Arik Fraimovich
4d7e58c8d7 Merge pull request #753 from getredash/proxy_fix
Show meaningful message when no data sources defined yet
2016-01-11 12:48:29 +02:00
Arik Fraimovich
14c4203593 Show meaningful message when no data sources defined yet 2016-01-11 12:47:17 +02:00
Arik Fraimovich
ccec964c24 Merge pull request #752 from getredash/proxy_fix
Fix: creating new user w/ Google Auth was broken.
2016-01-11 12:46:53 +02:00
Arik Fraimovich
d65e1a799a Fix: creating new user w/ Google Auth was broken. 2016-01-11 12:46:19 +02:00
Arik Fraimovich
451f216c31 Merge pull request #750 from JohnConnell/master
Docs: how to setup SSL using Let's Encrypt SSL certs
2016-01-11 12:07:21 +02:00
Arik Fraimovich
270afad6cf Merge pull request #751 from getredash/proxy_fix
Feature: ability to set # of proxies for the ProxyFix & fix the unique data source name migration
2016-01-11 12:07:14 +02:00
Arik Fraimovich
ccae8bcc69 Add option to override # of proxies 2016-01-11 12:02:18 +02:00
Arik Fraimovich
07f96a22af Update data source unique name migration to support another name of constraint 2016-01-11 11:30:26 +02:00
John Connell
3f6cf95307 Update letsencrypt.rst 2016-01-10 14:41:32 -07:00
John Connell
6f2d5090e6 Add documentation on using Let's Encrypt SSL certs 2016-01-09 16:00:09 -07:00
Arik Fraimovich
9cedb3bb66 Merge pull request #749 from getredash/unique_ds
Data sources should have unique names per organization
2016-01-09 22:37:40 +02:00
Arik Fraimovich
9751d3584b Remove forgotten console.log 2016-01-08 20:45:13 +02:00
Arik Fraimovich
13ced12cc9 Change data source index to be (org, name) 2016-01-08 20:44:11 +02:00
Arik Fraimovich
fdd60b364f Merge pull request #746 from Xangis/master
Feature: add an option to update a query every 30 days
2016-01-07 21:47:14 +02:00
Arik Fraimovich
dde63d1e96 Fix #745: when creating user from CLI, use default org. 2016-01-07 21:46:46 +02:00
=
174f7c0b1a Add an option to update a query every 30 days for use with things like monthly reports. 2016-01-07 08:24:34 -08:00
Arik Fraimovich
887d7179c4 Merge pull request #744 from getredash/feature/permissions
Run make deps only if rd_ui/app exists
2016-01-07 14:56:52 +02:00
Arik Fraimovich
fc84cf39fc Run make deps only if rd_ui/app exists 2016-01-07 14:56:28 +02:00
Arik Fraimovich
849c11b5f4 Merge pull request #743 from getredash/feature/permissions
Explicitly add httplib2 to requirements
2016-01-07 14:20:07 +02:00
Arik Fraimovich
66b4fe8e32 Explicitly add httplib2 to requirements 2016-01-07 14:18:12 +02:00
Arik Fraimovich
9d1823426c Fix SSLify skip list. 2016-01-07 13:09:41 +02:00
Arik Fraimovich
c004274108 Merge pull request #742 from getredash/feature/permissions
Add option to enforce HTTPs at the "Flask level"
2016-01-07 12:25:01 +02:00
Arik Fraimovich
0b89ee4653 Add option to enforce HTTPs at the Flask level 2016-01-07 12:22:32 +02:00
Arik Fraimovich
caff2e5caa Fix logo URL for multi-org 2016-01-07 12:03:28 +02:00
Arik Fraimovich
aa98f22a04 Merge pull request #741 from getredash/feature/permissions
Upgrade gunicorn version to latest.
2016-01-07 11:55:52 +02:00
Arik Fraimovich
db8915f154 Upgrade gunicorn 2016-01-07 11:52:50 +02:00
Arik Fraimovich
ce9a5c05fb Merge pull request #740 from getredash/feature/permissions
Fix #738: alert code was referencing non existing attribute
2016-01-07 11:48:27 +02:00
Arik Fraimovich
246725515d Fix #738: alert code was referencing non existing attribute 2016-01-07 11:46:35 +02:00
Arik Fraimovich
be4c59e73d Merge pull request #739 from toyama0919/master
Fix: Alert: when Alert.name is multibyte character, occur UnicodeEncodeError
2016-01-07 11:44:58 +02:00
toyama0919
40e047a47c Fix: Alert: when Alert.name is multibyte character, occur UnicodeEncodeError. 2016-01-07 11:03:33 +09:00
Arik Fraimovich
048ef7234c Merge pull request #737 from getredash/feature/permissions
Fix: user created without groups (+2 more)
2016-01-07 00:38:27 +02:00
Arik Fraimovich
bd29bdbb2e Fix: datasource refresh schemas was broken 2016-01-07 00:36:09 +02:00
Arik Fraimovich
13252bb0af Fix #736: user missing groups & events missing ord_id 2016-01-07 00:34:23 +02:00
Arik Fraimovich
07a709d59a Upgrade Sentry client to support new flask-login 2016-01-07 00:24:34 +02:00
Arik Fraimovich
55f80695b0 Merge pull request #707 from ryotarai/bower-in-dockerfile
Build dependencies during building Docker image
2016-01-06 23:11:14 +02:00
Arik Fraimovich
991512bc17 Merge pull request #735 from getredash/feature/permissions
Fix migration issue and CLI
2016-01-06 22:28:33 +02:00
Arik Fraimovich
5e58818043 Fix CLI to work with organizations 2016-01-06 15:14:09 +02:00
Arik Fraimovich
224998c62a Fix #733: merge migration #20 into #18, to avoid errors. 2016-01-06 14:59:18 +02:00
Arik Fraimovich
9a31077a99 Merge pull request #732 from getredash/feature/permissions
Fix #730: migration failing when no Google Apps domain set
2016-01-05 12:48:08 +02:00
Arik Fraimovich
ab39ed2898 Fix #730: migration failing when no Google Apps domain set 2016-01-05 12:46:00 +02:00
Arik Fraimovich
cb4fbf81a2 Merge pull request #724 from getredash/feature/permissions
Feature: new permission model
2016-01-04 17:27:01 +02:00
Arik Fraimovich
7c6b95e71d Change multi-org implementation:
To avoid complications with how Google Auth works, when enabling organization
multi-tenancy on a single instance, each organization becomes a "sub folder"
instead of a sub-domain.
2016-01-04 00:03:49 +02:00
Arik Fraimovich
f7b57fa580 Feature: new permissions system
This is one huge change for the permissions system and related:

* (Backward incompatible:) Remove the table based permissions in favour of the new model.
* Manage permission to view or query datasources based on groups.
* Add the concept of Organization. It's irrelevant for most deployments, but allows for
  multi-tenant support in re:dash.
* Replace ActivityLog with Event based rows (old data in activity_log table is retained).
* Enforce permissions on the server-side. There were some permissions that were only enforced
  on the client side. This is no more. All permissions are enforced by the server.
* Added new permission: 'super-admin' to access the status and Flask-Admin interface.
* Make sure that html is never cached by the browser - this is to make sure that the browser
  will always ask for the new Javascript/CSS resources (if such are available).
2015-12-31 10:43:33 +02:00
Arik Fraimovich
6e32f5b9f2 Merge pull request #726 from getredash/fix/lazy_load_oauth_app
Fix: lazy load the oauth app
2015-12-28 15:15:43 +02:00
Arik Fraimovich
1a748c2141 Fix expected path in test 2015-12-28 15:10:42 +02:00
Arik Fraimovich
99ed076c0c To speed up builds, install npm & pack only on master branch. 2015-12-28 15:06:12 +02:00
Arik Fraimovich
8a7dd3b46a Fix: lazy load the oauth app 2015-12-28 14:52:33 +02:00
Arik Fraimovich
6e28f949fb Merge pull request #725 from akariv/master
Fix: Google OAuth - support for next
2015-12-28 12:07:34 +02:00
Adam Kariv
a9ccfb8b42 Fix next for Google oauth 2015-12-27 13:48:59 +02:00
Arik Fraimovich
1aba777b61 Change output path for junit.xml. 2015-12-27 10:12:19 +02:00
Arik Fraimovich
1894df49fa Use XUnit reports in CircleCI tests. 2015-12-27 09:46:45 +02:00
Arik Fraimovich
200131bb45 Silence metrics collection in tests. 2015-12-27 09:43:36 +02:00
Arik Fraimovich
5e25ba0cf6 Merge pull request #722 from ninneko/721-chart-right-axis
Fix: use second y axis for line charts while stacking
2015-12-24 17:39:52 +02:00
Arik Fraimovich
184d208020 Merge pull request #723 from getredash/feature/metrics
Feature: collect metrics on query time & request time
2015-12-24 17:33:18 +02:00
Arik Fraimovich
610fe2a8a2 Feature: collect metrics on query time & request time 2015-12-24 16:35:41 +02:00
yohei.naruse
068ce57b24 make right axis enabled if there are stacked bars on right axis and lines on right axis. 2015-12-24 17:34:29 +09:00
Arik Fraimovich
af61784a28 Merge pull request #664 from akariv/master
Feature: ability to embed visualizations in external sites
2015-12-21 22:07:32 +02:00
Arik Fraimovich
871d8d6b6a Merge pull request #716 from getredash/fix/perf
Fix #708: dashboard breaks when removing widgets and adding again
2015-12-21 16:34:13 +02:00
Adam Kariv
ea1fac76a3 Adapt to changes in upstream 2015-12-21 09:01:44 +02:00
Adam Kariv
ed380fefaa CR fixes 2015-12-21 09:01:44 +02:00
Adam Kariv
cc9e89bb69 Fix allowAllToEditQueries not bound to settings 2015-12-21 09:01:44 +02:00
Adam Kariv
e9aeb11685 Embedding of visualizations in external sites 2015-12-21 09:01:44 +02:00
Arik Fraimovich
cc2dcb25b6 Merge pull request #714 from erans/mongodb-schema-support
Feature: load schema for MongoDB data source
2015-12-20 15:38:36 +02:00
Arik Fraimovich
bfb73166c6 Merge pull request #713 from alexanderlz/master
Fix: don't add "Copy of" when saving a query
2015-12-20 14:36:46 +02:00
Arik Fraimovich
30adfccd79 Fix #708: dashboard breaks when removing widgets and adding again 2015-12-20 13:15:58 +02:00
Eran Sandler
c3b6de55c0 added an extra check when a collection is empty and there are no documents to merge to show as fields 2015-12-20 09:58:29 +02:00
Eran Sandler
fa2cae1753 added schema support for MongoDB. Collections will be shown as tables and we merge the first and last documents (sorted by Natural order) to show the properties of the document. Since MongoDB is document based it might miss a few fields but it should give a good enough reference 2015-12-20 09:55:26 +02:00
Alexander Leibzon
b337a50fcc fix queryname when forking, add forked query_id to the name 2015-12-20 01:24:24 +02:00
Arik Fraimovich
3d178f9a60 Merge pull request #711 from alexanderlz/master
Feature: update forked query name
2015-12-16 20:56:06 +02:00
Arik Fraimovich
a0219bf354 Merge pull request #706 from alonho/fix/692_3
#692: Enable scrolling for pie charts with long legend
2015-12-16 17:41:02 +02:00
Ryota Arai
ec41077dc1 Run apt-get clean to reduce image size. 2015-12-17 00:07:56 +09:00
Ryota Arai
15f9a063ae Install nodejs, build assets and uninstall it in one instruction. 2015-12-17 00:07:56 +09:00
Ryota Arai
a15085dc93 Run supervisord as root. 2015-12-17 00:07:56 +09:00
Ryota Arai
78ae9ac647 Build dependencies during building Docker image. 2015-12-17 00:07:56 +09:00
Ryota Arai
f31ec7b1dd Stop to install bower and grunt-cli globally. 2015-12-17 00:07:56 +09:00
Arik Fraimovich
85916efa81 Merge pull request #710 from ryotarai/bq-max-mb-processed
Feature: BigQuery: limit amount of MB processed per query
2015-12-16 16:30:47 +02:00
Alexander Leibzon
31b6e6ff0f Merge remote-tracking branch 'upstream/master' 2015-12-16 15:17:58 +02:00
Ryota Arai
f20774b6c2 Rename maximumTotalMBytesProcessed to totalMBytesProcessedLimit. 2015-12-16 20:25:18 +09:00
Ryota Arai
dac6cabd1e Extract code into a method _get_query_result. 2015-12-16 20:19:35 +09:00
Ryota Arai
51949230d6 Extract code into a method _get_total_bytes_processed. 2015-12-16 20:19:31 +09:00
Ryota Arai
81386bcf37 If maximumTotalMBytesProcessed is set, do dryrun and check data size. 2015-12-16 20:04:33 +09:00
Alexander Leibzon
67118ee1aa add 'Copy of' to forked query 2015-12-15 01:15:03 +02:00
Alon Horev
e863d83bf4 #692: Enable scrolling for pie charts with long legend 2015-12-14 11:24:56 +02:00
Arik Fraimovich
d958817b10 Update 0014_add_alert_rearm_seconds.py 2015-12-14 10:47:46 +02:00
Arik Fraimovich
450631d6ce Merge pull request #680 from alexanderlz/master
Feature: show rows count per table
2015-12-14 10:31:08 +02:00
Arik Fraimovich
8b5a0206c2 Merge pull request #705 from alonho/fix/692_2
#692: Fix scrolling issue with plotly charts (didn't always work)
2015-12-13 17:25:02 +02:00
Alon Horev
49848a193a #692: Fix scrolling issue with plotly charts (didn't always work) 2015-12-13 17:06:07 +02:00
Alexander Leibzon
0f9d5219ef add setting for global enable/disable of table size estimations for schema 2015-12-13 15:13:14 +02:00
Alexander Leibzon
3cb14786f5 Bug 704: fix 2015-12-13 12:22:58 +02:00
Alexander Leibzon
8e432200aa Merge remote-tracking branch 'upstream/master' 2015-12-12 12:10:35 +02:00
Arik Fraimovich
30dd030a9d Merge pull request #703 from alonho/fix/area_stacking_hover
Chart: regular area stacking (not percent) now shows both the value and sum per point.
2015-12-12 07:46:22 +02:00
Alon Horev
fc3fc0e84a Chart: pie chart colors should use our custom palette and not the default plotly palette 2015-12-12 01:02:22 +02:00
Alon Horev
24b70e66af Chart: regular area stacking (not percent) now shows both the value and sum per point. 2015-12-11 23:22:22 +02:00
Arik Fraimovich
76a1b9fdbe Merge pull request #701 from alonho/fix/694_2
Fix: #694: When stacking is enabled show both the relative value (in %) and the absolute value (attempt #2)
2015-12-11 17:02:34 +02:00
Arik Fraimovich
e310f9d522 Merge pull request #700 from alonho/fix/692
Fix: #692: Chart legend was cut off with a large number of series. The wrapping div now scrolls to make it visible.
2015-12-11 14:18:19 +02:00
Alon Horev
86a0e74db8 #694: When stacking is enabled show both the relative value (in %) and the absolute value 2015-12-10 23:00:49 +02:00
Alon Horev
30a70338ba #692: Chart legend was cut off with a large number of series. The wrapping div now scrolls to make it visible. 2015-12-10 22:16:58 +02:00
Arik Fraimovich
b242dbb531 Merge pull request #698 from alonho/fix/694
Fix: When stacking is enabled show both the relative value (in %) and the absolute value
2015-12-10 21:59:38 +02:00
Arik Fraimovich
ca47b0e6f7 Merge pull request #699 from alonho/fix/695
Fix: Charts: when stacking is enabled we should use one yaxis otherwise they overlap
2015-12-10 18:57:14 +02:00
Alon Horev
7c992c53eb #694: When stacking is enabled show both the relative value (in %) and the absolute value 2015-12-10 17:28:47 +02:00
Alon Horev
4deb150a89 #695: Charts: when stacking is enabled we should use one yaxis otherwise they overlap 2015-12-10 16:18:32 +02:00
Arik Fraimovich
63f0a8cc20 Merge pull request #631 from brickx/master
Feature: alert rearm setting which allows periodic resending of alert messages.
2015-12-10 09:19:58 +02:00
Arik Fraimovich
7e4f5e1e03 Merge pull request #687 from alonho/feature/plotly
Feature: replace HighCharts with Plotly
2015-12-09 10:17:40 +02:00
Arik Fraimovich
6f1fed47b3 Merge pull request #691 from VirtualPaul/patch-1
Docs: add TreasureData to the list of datasources
2015-12-09 10:17:26 +02:00
Arik Fraimovich
4505437097 Bump version. 2015-12-09 10:16:15 +02:00
Paul Lacey
2ea2df5943 Update datasources.rst
Add Treasure Data to list of supported data sources
2015-12-08 15:31:58 -08:00
Alon Horev
135ffd693a Add an option to disable chart legend.
A user can disable it if he has tons of series.
Now that we explicitly enable it, it's also visible for a single series.
2015-12-07 19:07:04 +02:00
Alon Horev
0f82d4e17b Remove highcharts as it's not used anymore 2015-12-06 21:05:35 +02:00
Arik Fraimovich
32c0d3eb3d Merge pull request #688 from Xangis/patch-1
Docs: Add Greenplum to Postgresql section since it works with same settings.
2015-12-06 09:22:49 +02:00
Jason Champion
1bee22a578 Add Greenplum to Postgresql section since it works with same settings. 2015-12-05 15:24:55 -08:00
Arik Fraimovich
6bb57508e1 Merge pull request #686 from scottkrager/patch-1
Docs: Update bootstrap.sh link to getredash repo
2015-12-05 22:15:32 +02:00
Alon Horev
b7a43feeca #273: Replace highcharts with plotly (it's free!) 2015-12-05 03:01:44 +02:00
Arik Fraimovich
2d34bf1c54 Typo fix in task name. 2015-12-04 17:09:01 +02:00
Arik Fraimovich
7e3856b4f5 Unify deployment sections in CirlceCI config. 2015-12-04 16:18:58 +02:00
Scott Krager
189e105c68 Update bootstrap.sh link to getredash repo 2015-12-03 16:30:06 -08:00
Arik Fraimovich
378459d64f Merge pull request #685 from getredash/fix/alert_sub_migration
Feature: add settings to query results cleanup
2015-12-03 11:20:51 +02:00
Arik Fraimovich
ab72531889 Add settings to query results cleanup (closes #683) 2015-12-03 11:10:02 +02:00
Arik Fraimovich
51deb8f75d Merge pull request #684 from getredash/fix/alert_sub_migration
Fix: add migration for AlertSubscriber table
2015-12-03 11:04:31 +02:00
Arik Fraimovich
68f6e9b5e5 Add migration for AlertSubscriber table 2015-12-03 11:03:38 +02:00
Arik Fraimovich
fbfa76f4d6 Merge pull request #682 from alonho/master
Fix: bug with new version of ui-select and 'track by ' on choices
2015-12-02 20:12:42 +02:00
Alon Horev
28e8e049eb fix bug with new version of ui-select and 'track by ' on choices 2015-12-02 20:10:19 +02:00
Alon Horev
47dcead383 #273: as a preparation for adding plotly, remove date range picker in the chart (plotly supports it within the chart) 2015-12-02 11:08:25 +02:00
Arik Fraimovich
f1f9597998 Bump version. 2015-12-02 11:03:50 +02:00
Alexander Leibzon
0da39edf1a Merge branch 'master' of github.com:alexanderlz/redash
Conflicts:
	redash/models.py
	redash/query_runner/__init__.py
2015-12-01 16:32:03 +02:00
Alexander Leibzon
7845ad5ff7 refresh param 2015-12-01 16:27:35 +02:00
Alexander Leibzon
3808b451c6 add param to allow skipping table row count 2015-12-01 16:27:34 +02:00
Alexander Leibzon
c78789a670 modify hive/impala/oracle to use BaseSQLQueryRunner 2015-12-01 16:27:34 +02:00
Alexander Leibzon
2cd08d25a0 improve code, create BaseSQLQueryRunner class, adapt postgres/mysql 2015-12-01 16:27:34 +02:00
Alexander Leibzon
09ed4d5ede feature #674 2015-12-01 16:23:28 +02:00
Alexander Leibzon
1e97a0ce9f add param to allow skipping table row count 2015-12-01 15:18:25 +02:00
Alexander Leibzon
61cb203ce7 modify hive/impala/oracle to use BaseSQLQueryRunner 2015-12-01 13:38:17 +02:00
Alexander Leibzon
58c0c5c099 improve code, create BaseSQLQueryRunner class, adapt postgres/mysql 2015-12-01 13:30:39 +02:00
blu35ky
8072b06246 Merge with upstream/master. 2015-12-01 20:50:42 +11:00
Niels Draaisma
65f2c2136b Added handling of empty rearm settings 2015-12-01 20:47:42 +11:00
Niels Draaisma
8b9a9e9ac4 Added alert rearm setting 2015-12-01 20:43:49 +11:00
Arik Fraimovich
0b389d51aa Merge pull request #644 from toyama0919/feature/alert-to-hipchat
Feature: send alert notifications to HipChat or web hook
2015-12-01 10:50:53 +02:00
toyama0919
46f3e82571 Apply reviews. fix redash.utils instead of bson. 2015-12-01 10:36:21 +09:00
toyama0919
5b64918379 Apply reviews. fix, post json nested data for webhook. 2015-12-01 10:36:21 +09:00
toyama0919
7549f32d9a Apply reviews. fix http client library httplib2 to requests. 2015-12-01 10:36:21 +09:00
toyama0919
6f51776cbb fix, basic auth for webhook. 2015-12-01 10:36:21 +09:00
toyama0919
ad0afd8f3e add, alert notification to webhook. 2015-12-01 10:36:21 +09:00
toyama0919
8863282e58 Apply reviews from arikfr 2015-12-01 10:34:56 +09:00
toyama0919
9c1fda488c fix, alert notification to hipchat. 2015-12-01 10:33:01 +09:00
blu35ky
30a494dab0 Changes based on PR 2015-12-01 11:22:19 +11:00
Arik Fraimovich
995659ee0d Merge pull request #679 from alonho/table_pagination
Improve table widget pagination UI
2015-11-30 23:39:06 +02:00
Alon Horev
ad2642e9e5 Improve table widget pagination UI 2015-11-30 23:37:56 +02:00
Arik Fraimovich
740b305910 Merge pull request #676 from getredash/feature/version_check
Feature: re:dash version check
2015-11-30 22:37:20 +02:00
Arik Fraimovich
ca8cca0a8c Merge pull request #678 from alonho/655
Fix: Dashboard shouldn't crash with empty queries
2015-11-30 22:34:00 +02:00
Arik Fraimovich
7c4410ac63 Use ng-cloak to hide the new version message until relevant 2015-11-30 22:31:06 +02:00
Alon Horev
91a209ae82 #655: Dashboard shouldn't crash with empty queries 2015-11-30 18:17:37 +02:00
Arik Fraimovich
60cdb85cc4 Move all version check logic into a module of its own 2015-11-30 17:06:21 +02:00
Arik Fraimovich
becb4decf1 Show in UI if new version available 2015-11-30 16:38:42 +02:00
Arik Fraimovich
5f33e7ea18 Perform daily version check 2015-11-30 16:31:49 +02:00
Arik Fraimovich
7675de4ec7 Merge pull request #675 from alonho/redash_link
Add link to redash.io
2015-11-30 16:16:10 +02:00
Alon Horev
fe2aa71349 Add link to redash.io 2015-11-30 16:10:33 +02:00
Arik Fraimovich
b7720f7001 Merge pull request #672 from alonho/chart_editor
Feature: Improved chart editor UI/UX
2015-11-30 12:38:01 +02:00
Alon Horev
3b24f56eba #671: Improve chart editor UI/UX 2015-11-30 12:37:00 +02:00
Alexander Leibzon
06065badd4 feature #674 2015-11-29 01:16:27 +02:00
Arik Fraimovich
52b8e98b1a Merge pull request #620 from getredash/docker
Reorganize setup files & update Docker configurations
2015-11-26 11:27:52 +02:00
Arik Fraimovich
5fe9c2fcf0 Update Ubuntu with docker readme 2015-11-26 10:39:42 +02:00
Arik Fraimovich
816142aa54 Update evn files 2015-11-26 10:38:06 +02:00
Arik Fraimovich
f737be272f Update GitHub repo url (EverythingMe -> GetRedash) 2015-11-26 10:34:16 +02:00
Arik Fraimovich
0343fa7980 Merge pull request #661 from hudl/fix-cancelquery
Fix cancelling queries for Redshift/Postgres
2015-11-24 15:18:58 +02:00
Arik Fraimovich
0f9f9a24a0 Remove spaces in export command. 2015-11-24 15:10:27 +02:00
Alex DeBrie
5b9b18639b Move signal handler 2015-11-23 14:02:09 +00:00
Arik Fraimovich
ce46295dd3 Update location of config files 2015-11-23 15:46:00 +02:00
Arik Fraimovich
3781b0758e Fix nginx conf mounting 2015-11-23 15:39:48 +02:00
Arik Fraimovich
8d20180d40 Update mail setup guide. 2015-11-23 14:24:43 +02:00
Arik Fraimovich
a7b41327c6 Update docker hub organization 2015-11-23 11:41:45 +02:00
Arik Fraimovich
4d415c0246 WIP: bootstrap for docker 2015-11-23 11:38:17 +02:00
Arik Fraimovich
5331008e78 add docker-compose.yml 2015-11-23 11:38:17 +02:00
Arik Fraimovich
80783feda6 Bootstrap files for Docker image 2015-11-23 11:38:17 +02:00
Arik Fraimovich
2f308c3fa6 Remove test file 2015-11-23 11:38:17 +02:00
Arik Fraimovich
a63055f7f0 Fix build step 2015-11-23 11:38:17 +02:00
Arik Fraimovich
ce884ba6d3 Update CircleCI config to build images 2015-11-23 11:38:17 +02:00
Arik Fraimovich
63765281fe Fix path in bootstrap script 2015-11-23 11:38:16 +02:00
Arik Fraimovich
47e79003e5 Update packer config 2015-11-23 11:38:16 +02:00
Arik Fraimovich
541060c62e Remove latest_release_url.py - docker images will be created with current code base as context 2015-11-23 11:38:16 +02:00
Arik Fraimovich
3ba19fa80f update readme for ubuntu bootstrap 2015-11-23 11:38:16 +02:00
Arik Fraimovich
f3ec0448f5 Updates to Dockerfile:
- No need to pg client anymore.
- Fix path to supervisord.conf.
2015-11-23 11:38:16 +02:00
Arik Fraimovich
654349a7ae Better arrangement of setup directory 2015-11-23 11:38:16 +02:00
Arik Fraimovich
2b32de184e Change suffix of docker-compose file to .yml as suggested by docker-compose 2015-11-23 11:38:15 +02:00
Arik Fraimovich
1fb57edd1f Remove old Vagrant file 2015-11-23 11:38:15 +02:00
Arik Fraimovich
f6c65d139a Move Amazon Linux bootstrap into folder of its own 2015-11-23 11:38:15 +02:00
Arik Fraimovich
4e59472238 Fix .dockerignore file:
Allow sending rd_ui/dist, remove rd_ui/nodemodules.
2015-11-23 11:38:15 +02:00
Arik Fraimovich
feabc46da4 Merge pull request #668 from cou929/fix-all_models
Fix: AlertSubscription missing in all_models
2015-11-23 11:13:49 +02:00
Kosei Moriyama
51a10e5a20 Add AlertSubscription to all_models 2015-11-23 02:06:39 +09:00
Arik Fraimovich
5bf370d0f0 Merge pull request #660 from hudl/fix-regexanchors
Fix: strings that start with a date wrongly treated as date fields
2015-11-21 20:41:56 +02:00
Arik Fraimovich
5beec581d8 Merge pull request #667 from getredash/docs_alerts
Docs: add instructions on setting up email server
2015-11-20 21:32:20 +02:00
Arik Fraimovich
70080df534 Add instructions on setting up email server 2015-11-20 21:31:50 +02:00
Arik Fraimovich
0d4c3c329e Merge pull request #666 from alonho/patch-1
Fix: Specifying field type in the field name using __ didn't work
2015-11-20 16:39:06 +02:00
Alon Horev
76dfbad971 Specifying field type in the field name using __ didn't work
It works for '::' but probably didn't work for '__' due to a a copy-paste
2015-11-20 14:20:26 +02:00
Alex DeBrie
45a85c110f Add SIGINT signal 2015-11-18 18:30:54 +00:00
Alex DeBrie
f77c0aeb1d Add InterruptException to __all__ 2015-11-18 18:07:47 +00:00
Alex DeBrie
b23e328f69 Add sigint signal handler to BaseQueryRunner 2015-11-18 17:20:39 +00:00
Alex DeBrie
165d782b98 Add end of string anchor to date parsing regex 2015-11-18 16:15:10 +00:00
Arik Fraimovich
1bdc1bef73 Merge pull request #653 from hakobera/fix-date-range-selector
Fix date range selector does not show data of last day when user timezone is not UTC
2015-11-18 17:58:20 +02:00
Arik Fraimovich
e3b41b15d7 Update links in README. 2015-11-18 17:49:11 +02:00
Arik Fraimovich
7a95dec33b Merge pull request #659 from getredash/fixes_151118
Add footer to the layout, to have links to docs & GitHub
2015-11-18 17:45:55 +02:00
Arik Fraimovich
a3d059041c Add footer 2015-11-18 17:36:24 +02:00
Arik Fraimovich
3a6c1599f3 Update index.rst 2015-11-18 17:35:06 +02:00
Arik Fraimovich
f92aa7b15f Merge pull request #658 from getredash/fixes_151118
Charts: remove "Show Total %" menu option and the yellow color
2015-11-18 16:51:41 +02:00
Arik Fraimovich
d823506e5b Remove menu option and yellow color 2015-11-18 16:50:59 +02:00
Arik Fraimovich
fc93de7aa2 Merge pull request #657 from getredash/fixes_151118
Fix: Change user create button from Save to Create
2015-11-18 16:49:19 +02:00
Arik Fraimovich
a0cc25d174 Change user create button from Save to Create 2015-11-18 16:45:15 +02:00
Arik Fraimovich
df24bc3aae Merge pull request #656 from enriquesaid/header-gravatar-src
Fix: load user avatar image with ng-src
2015-11-17 23:19:23 +02:00
Enrique Marques Junior
60c2cb0a75 using ng-src 2015-11-17 14:53:43 -02:00
Kazuyuki Honda
ad19f2d304 Treat dateRange as UTC 2015-11-17 11:03:07 +09:00
Arik Fraimovich
3aa59a8152 Update README links. 2015-11-16 16:12:41 +02:00
Arik Fraimovich
32638aebed Merge pull request #650 from alonho/mql
Feature: MQL query runner
2015-11-15 17:21:44 +02:00
Arik Fraimovich
346ea66c9d Merge pull request #651 from alonho/datasource_defaults
Support default values in data source creation forms
2015-11-15 17:17:26 +02:00
Arik Fraimovich
d14b74b683 Merge pull request #654 from EverythingMe/fix-graphite-verify
Fix: verify is optional value of Graphite's config
2015-11-15 17:10:46 +02:00
Arik Fraimovich
5d879ce358 Update circle.yml 2015-11-15 17:02:43 +02:00
Arik Fraimovich
b4da4359a8 Fix: verify is optional value of Graphite's config 2015-11-14 23:35:37 +02:00
Kazuyuki Honda
7e08518a31 Fix date range selector when user timezone is not UTC 2015-11-14 13:03:14 +09:00
Alon Horev
bea0e9aad0 Add support for MQL (a propietery SQL implementation for MongoDB by digdata.io) 2015-11-13 23:35:34 +02:00
Alon Horev
a87179b68b Support default values in data source creation forms 2015-11-13 23:28:33 +02:00
Arik Fraimovich
91806eda44 Merge pull request #647 from runa/patch-3
Fix: bind Redis to localhost
2015-11-11 06:04:58 +02:00
martin sarsale
d1fe3d63fd bind redis to localhost
Having it bound to the public addresses is a security problem.
See http://antirez.com/news/96
2015-11-10 23:03:53 -03:00
Arik Fraimovich
8408409ce2 Merge pull request #642 from tjwudi/patch-3
Docs: make migrating Vagrant box command a one-liner
2015-11-10 20:43:59 +02:00
John Wu
6bbdd5eb44 Make migrating command one-liner 2015-11-09 14:54:45 -08:00
Arik Fraimovich
34ba54397d Merge pull request #638 from underdogio/dev/show.db.select.mobile.sqwished
Removed `rd-hidden-xs` to make everything visible on mobile
2015-11-08 22:59:14 +02:00
Arik Fraimovich
ec79ce74d0 Merge pull request #639 from hudl/Feature-ScheduleQueryPermission
Feature: permission to schedule query
2015-11-07 23:11:38 +02:00
Alex DeBrie
f324f1bf6f Add schedule_query permission 2015-11-07 17:52:32 +00:00
Todd Wolfson
47cfb7d620 Removed rd-hidden-xs to make everything visible on mobile 2015-11-05 18:55:40 -06:00
Arik Fraimovich
dab1a21b40 Merge pull request #637 from underdogio/dev/explore.regression.sqwished
Reverted pivottable upgrade to remove XSS vulnerability
2015-11-05 20:36:05 +02:00
Arik Fraimovich
aa04a6e4a5 Merge pull request #630 from gissehel/sqlite_query_runner
Feature: SQLite query runner
2015-11-05 09:19:13 +02:00
gissehel
e0a43a32ab * Removed commented lines
* Renamed "Database Name"/dbname to "Database Path"/dbpath
2015-11-04 07:17:58 +01:00
gissehel
68001ae0f1 sqlite support 2015-11-04 07:17:58 +01:00
Todd Wolfson
9d9501b158 Reverted pivottable upgrade to remove XSS vulnerability 2015-11-03 16:49:30 -06:00
Arik Fraimovich
67aecc0201 Merge pull request #594 from tjwudi/diwu/feature/date-range-selector
Feature: date range selector support for charts
2015-11-03 23:16:04 +02:00
Arik Fraimovich
0bc9fc1ed5 Merge pull request #575 from Wondermall/feature/support_for_basic_auth_on_elastic_queries
Feature: new ElasticSearch datasource, and rename previous one to Kibana
2015-11-03 22:01:51 +02:00
Arik Fraimovich
b548cb1d8f Merge pull request #625 from essence-tech/oracle-support
Feature: Oracle query runner
2015-11-03 21:56:38 +02:00
Arik Fraimovich
eb5c4dd5f3 Merge pull request #623 from stanhu/support-mysql-ssl
Feature: support MySQL over SSL
2015-11-03 21:54:58 +02:00
Stan Hu
a07a9b9390 Normalize SSL titles 2015-11-03 10:16:48 -08:00
Arik Fraimovich
56ade4735c Merge pull request #634 from tjwudi/patch-1
Document APIs exposed to Python scripts
2015-11-03 10:17:52 +02:00
John Wu
b8a9f1048a Document APIs exposed to Python scripts 2015-11-02 13:52:39 -08:00
Niels Draaisma
3dc62e3c85 Added handling of empty rearm settings 2015-10-30 16:18:15 +11:00
Niels Draaisma
73b2c5d38e Added alert rearm setting 2015-10-30 16:01:21 +11:00
Arik Fraimovich
5b3bcff4f5 Update README.md 2015-10-26 12:54:21 +02:00
Arik Fraimovich
b41b21c69e Update README.md 2015-10-26 12:53:32 +02:00
Arik Fraimovich
172d57e82c Update README.md 2015-10-26 12:51:58 +02:00
Arik Fraimovich
f507da9df7 Update README about re:dash future. 2015-10-26 12:51:20 +02:00
Stan Hu
2e27e43357 Support MySQL over SSL 2015-10-21 16:21:17 -07:00
Josh Fyne
8a0c287d05 Updated datasources docs 2015-10-21 12:06:47 -04:00
Josh Fyne
664a1806bc Better number handling 2015-10-21 10:05:38 -04:00
Josh Fyne
9a0ccd1bb5 Added cx_Oracle requirement 2015-10-20 15:40:18 -04:00
Josh Fyne
076fca0c5a Initial Oracle pass 2015-10-20 15:27:07 -04:00
Arik Fraimovich
59f099418a Merge pull request #617 from EverythingMe/fix/timezone
Improve timezone handling:
2015-10-20 16:29:47 +03:00
Arik Fraimovich
b9a0760d7e Improve timezone handling:
1. Load all date/datetime values with moment.utc() which doesn't apply
   current timezone to them.
2. Don't use toLocaleString to format strings (which was converting them
   to current timezone as well).

Ref #411.
2015-10-20 16:17:57 +03:00
Arik Fraimovich
a0c26c64f0 Bump version. 2015-10-20 15:50:27 +03:00
Arik Fraimovich
5f47689553 Update AWS/GCE image links. 2015-10-19 23:01:43 +03:00
Arik Fraimovich
a5bc90c816 Merge pull request #615 from EverythingMe/fix_y_axis
Fix: y axis settings should take into account two axes
2015-10-19 11:33:12 +03:00
Arik Fraimovich
39b8f40ad4 Fix: y axis settings should take into account two axes 2015-10-19 11:32:47 +03:00
Arik Fraimovich
070caa6976 Gruntfile.js: copy image files. 2015-10-18 23:45:21 +03:00
Arik Fraimovich
56b51f68bc Merge pull request #614 from EverythingMe/fix/caching
Fix: don't cache /results API endpoint
2015-10-18 14:20:52 +03:00
Arik Fraimovich
799ce3e718 Fix: don't cache /results API endpoint 2015-10-16 23:11:19 +03:00
Arik Fraimovich
9b47f0d08a Fix: test shouldn't depend on currnet time 2015-10-16 23:10:50 +03:00
Arik Fraimovich
4f4dc135f5 Merge pull request #607 from tlpham/master
Docs: Remove trailing spaces
2015-10-14 13:26:48 +03:00
Lior Rozner
4eb490a839 Code review fix.
Added migration to change all existing elasticsearch datasource to kibana datasource.
2015-10-13 20:14:58 -07:00
John Wu
410c5671f0 Revert: python data source in setting 2015-10-13 11:42:42 -07:00
John Wu
fad8bd47e8 Remove commented code
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-13 11:35:56 -07:00
John Wu
89f5074054 Prevent unneccesary call to setDateRangeToExtreme
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-13 11:33:20 -07:00
John Wu
5826fbd05f Use moment.min and moment.max 2015-10-13 11:20:36 -07:00
John Wu
ddab1c9493 Update angular-moment and moment 2015-10-13 11:19:19 -07:00
John Wu
f9d5fe235b Always use _addPointToSeriesIfInDateRange 2015-10-13 11:01:21 -07:00
tlpham
afe64fe981 Update upgrade.rst
Trailing spaces.
2015-10-13 12:25:55 +08:00
tlpham
99efe497ee Update results_format.rst
Trailing spaces.
2015-10-13 12:25:11 +08:00
Arik Fraimovich
9e183f1500 Merge pull request #588 from tjwudi/diwu/feature/docker-deployment
Docker deployment support
2015-10-11 23:13:22 +03:00
Arik Fraimovich
4b17b9869e Merge pull request #551 from ElJoche:hidden_widgets
Feature: allow adding hidden text box widgets.
2015-10-11 22:56:41 +03:00
Arik Fraimovich
872d58688f Update the hidden widgets code (only use for textbox, ng-hide) 2015-10-11 22:54:24 +03:00
Arik Fraimovich
37272dc2d9 Capitalize logout link 2015-10-11 15:54:08 +03:00
Arik Fraimovich
1a3df37940 Merge pull request #605 from EverythingMe/small_fixes_11_10_2015
Feature: allow setting HighChart's turbo threshold value
2015-10-11 15:30:58 +03:00
Arik Fraimovich
ddbf264020 Close #572: allow setting the HighCharts turbo threshold value 2015-10-11 15:29:50 +03:00
Arik Fraimovich
e93b71af85 Don't sanitize non string values 2015-10-11 15:16:23 +03:00
Arik Fraimovich
13184519c3 Merge pull request #604 from EverythingMe/small_fixes_11_10_2015
Fix #597: MongoDB date parsing logic improvement
2015-10-11 15:00:23 +03:00
Arik Fraimovich
0f8da884f9 Fix #597: MongoDB date parsing logic improvement 2015-10-11 14:44:12 +03:00
Arik Fraimovich
21de1d90e3 Merge pull request #599 from EverythingMe/fix/passwords
Fix: don't send passwords back to the UI
2015-10-11 12:33:33 +03:00
Arik Fraimovich
ed9eb691c1 Merge pull request #603 from EverythingMe/small_fixes_11_10_2015
Feature: allow setting only the additional query runners you need
2015-10-11 12:29:46 +03:00
Arik Fraimovich
d6c229759f Update docs 2015-10-11 12:20:59 +03:00
Arik Fraimovich
f0b8dfb449 Allow setting only the additional query runners instead of overriding whole list 2015-10-11 12:17:28 +03:00
Arik Fraimovich
6f335d34b9 Merge pull request #602 from EverythingMe/small_fixes_11_10_2015
Close #564: support setting API key with headers
2015-10-11 12:10:04 +03:00
Arik Fraimovich
bed63083a7 Close #564: support setting API key in headers 2015-10-11 11:54:21 +03:00
Arik Fraimovich
9886f5b13b Merge pull request #601 from EverythingMe/small_fixes_11_10_2015
Fix #581: execute_query permission ignored by UI
2015-10-11 11:26:50 +03:00
Arik Fraimovich
f0ee7a67d2 Fix #581: execute_query permission ignored by UI 2015-10-11 11:26:11 +03:00
Arik Fraimovich
9c43e1540e Merge pull request #600 from EverythingMe/small_fixes_11_10_2015
Fix: cohort visulization had infinte digest loop
2015-10-11 11:24:37 +03:00
Arik Fraimovich
b0cb2d3f1c Fix: cohort visulization had infinte digest loop 2015-10-11 11:16:49 +03:00
Arik Fraimovich
b525ad0622 Fix: don't require uploading file again when editing BQ/GS data source 2015-10-11 10:29:05 +03:00
Arik Fraimovich
602b9128a7 Stop sending passwords to the UI 2015-10-11 09:27:51 +03:00
Arik Fraimovich
45d3b18c0c Update comment 2015-10-11 08:26:57 +03:00
Arik Fraimovich
b1918743f2 Merge pull request #596 from Oneross/master
Docs: added notes about Python query runner configuration
2015-10-10 11:41:26 +03:00
qjo744
716f36ef9c updated python datasources note to reflect preference for setting environ variable over editing settings.py 2015-10-09 07:54:25 -04:00
qjo744
62aa21cdc8 updated python datasources note to reflect preference for setting environ variable over editing settings.py 2015-10-09 07:53:36 -04:00
qjo744
4e30fc1054 updated python datasources note to reflect preference for setting environ variable over editing settings.py 2015-10-09 07:52:48 -04:00
Arik Fraimovich
5a1d38c572 Merge pull request #593 from hudl/chartColourPalette
Chart colour palette updated
2015-10-09 06:43:14 +03:00
qjo744
360b0da159 added notes for python query runner configuration to docs #595 2015-10-08 19:09:03 -04:00
John Wu
cc91981845 Naming stuff
`seriesCollection` -> `allSeries` (shorter)
`s` -> `series`

Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-08 14:49:45 -07:00
John Wu
e19962d4e3 Remove unnecessary ENV line
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-08 13:40:03 -07:00
John Wu
99b6f8955e Add some mandatory nginx directives
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-08 13:25:44 -07:00
John Wu
cf6ce0599b Use volume to store postgres data
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-08 12:14:07 -07:00
John Wu
a699c04ee1 Download and build from latest source instead 2015-10-08 12:09:24 -07:00
John Wu
a8d7547dc7 Rename folder 2015-10-08 12:09:07 -07:00
John Wu
72804e6d80 Add redash-nginx repo content
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-08 11:57:44 -07:00
John Wu
e51db087c5 Remove unnecessarily exposed ports
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-08 11:46:16 -07:00
John Wu
0e9607205b Add nginx frontend in docker-compose
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-08 11:43:33 -07:00
John Wu
9f799f4bfe Use built image in docker-compose
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-08 11:38:17 -07:00
atharva.inamdar
17e0bd4cd2 hudl/fulla#140 new chart colours added to palette 2015-10-08 16:27:44 +01:00
atharva.inamdar
102038b129 hudl/fulla#140 new chart colours added to palette 2015-10-08 16:21:51 +01:00
Arik Fraimovich
c01d88cbea Merge pull request #591 from hudl/master
Feature: export pivot table as TSV
2015-10-08 08:35:28 +03:00
John Wu
9d6d88ebff Remove "export" in **.env**
Since we add `export` using `sed` in `bin/run`, there is no need to add
`export` in **.env** anymore. Also, docker-compose's `env_file` option
does not agree with `export`.

Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-07 17:26:14 -07:00
John Wu
3f429ebcb7 Don't use bin/run in docker
`bin/run` exports environment variables, which can override environment
variables provided by docker-compose.

Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-07 17:22:58 -07:00
John Wu
c854ce3c10 Remove postgres user
Also changed **docker_init_postgres.sh**. Since we don't have postgres
user now, then we cannot use `sudo -u postgres`. The alternative will be
running `psql --username=blahblah`.

Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-07 17:04:27 -07:00
John Wu
ab6cc3f146 Run celery using redash user 2015-10-07 16:21:37 -07:00
John Wu
97d0035f4a Group supervisord installation commands
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-07 11:16:49 -07:00
John Wu
8108bc7cb1 Group relevant commands
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-07 11:07:33 -07:00
John Wu
690cb2fccd Group all apt-get commands
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-07 10:58:52 -07:00
Arik Fraimovich
515c45776e Merge pull request #590 from underdogio/dev/select.last.data.source.sqwished
Added "Select last used data source" to query view
2015-10-07 19:50:15 +03:00
Todd Wolfson
fc44dba2ef Added "Select last used data source" to query view 2015-10-07 11:43:28 -05:00
Ben Cook
5329fe547c Merge pull request #1 from hudl/ExportPivotTable
Upgrade PivotTable.js and enable TSV export
2015-10-07 09:02:19 -05:00
John Wu
d6bb6d33a3 Expose ports in Dockerfile
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-06 16:30:18 -07:00
John Wu
9832b7f72a Use more descriptive name for series collection
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-06 15:04:34 -07:00
John Wu
2a6ed3ca52 Use bind(this) instead of creating that
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-06 15:04:02 -07:00
John Wu
2e78ef0128 Use more descriptive method name
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-06 14:50:15 -07:00
John Wu
d2d52d44f7 Postgres&Redis version consistency
Use Postgres 9.3 and Redis 2.8 images. This is to keep it consistent
with the version we use in provision script.

Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-06 14:38:24 -07:00
John Wu
987f4bd356 Use .env file through Dockefile
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-06 14:38:05 -07:00
John Wu
0c8c196d65 Group apt-get instructions
Given how docker caching works, it is better the group multiple
`apt-get` instructions into one when possible because it prevents docker
from building too many layers.

Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-06 14:13:53 -07:00
John Wu
9d703b44de Create postgres user
Create postgres user because we are now using `postgres-client` packages
which does not create postgres user by default. We need this user when
running `docker_init_postgres.sh`, so let's create it by hand.

Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-06 14:12:48 -07:00
John Wu
fb00350c58 Migrate stuff in bootstrap_docker.sh into Dockerfile
By using Dockerfile `RUN` command, we can enable docker to cache our
build. Also, much more easier to maintain.

Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-06 13:21:09 -07:00
jbencook
6cccd30553 Upgrade PivotTable.js and enable TSV export 2015-10-06 20:14:05 +00:00
John Wu
0bbcb69197 Remove redis build + use postgres-client package instead of postgres
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-06 11:43:35 -07:00
John Wu
b0eaffdf6c tag postgres & redis version in docker-compose.yaml
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-06 11:42:36 -07:00
John Wu
407a649d17 Use ubuntu instead
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-06 11:42:19 -07:00
John Wu
73bd83a527 Revert TCP listening address
Instead of binding to `0.0.0.0`, use `127.0.0.1` instead for security
concerns. "The Python Web server is more
vulnerable than nginx that proxies it."

Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-06 11:17:48 -07:00
John Wu
72e48a191b Remove Node.js infra 2015-10-06 11:12:13 -07:00
John Wu
11682b3779 Remove redundant database migration scripts
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-06 10:35:54 -07:00
Arik Fraimovich
a15d7964fa Merge pull request #585 from matthew-sochor/add-d3js-boxplot
Feautre: d3.js based Box Plot visualization.
2015-10-06 17:14:33 +03:00
Matt Sochor
2feb8b81f5 fixup! Removed unused function and options 2015-10-06 10:11:19 -04:00
Arik Fraimovich
6286024350 Merge pull request #589 from shyamgopal/master
Fix: Google spreadsheet data source: cast values to their actual type from string
2015-10-06 17:08:53 +03:00
Matt Sochor
0b5dce0ebf Removed unused function and options 2015-10-06 09:26:33 -04:00
Arik Fraimovich
32311c55e6 Merge pull request #587 from matthew-sochor/add-logarithmic-scale-to-chart
Feature: logarithmic scale support in chart
2015-10-06 15:26:46 +03:00
Shyamgopal Kundapurkar
2ac795d6f7 Fixed non-plotting of charts for Google spreadsheet data source 2015-10-06 10:00:32 +05:30
John Wu
d50af7dec9 Use dateRangeEnabled to decided whether we should display the data range selector
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-05 17:35:41 -07:00
John Wu
20159a1c2a Separate setDateRangeToExtreme function
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-05 17:32:46 -07:00
John Wu
06400ed840 Refactor addPointToSeries
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-05 17:13:36 -07:00
John Wu
0ddc6cf135 Use null to state empty object instead
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-05 17:13:04 -07:00
John Wu
46a008346f Use standalone supervisord.conf for docker deployment
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-05 15:00:52 -07:00
John Wu
21c413f699 Add CMD to start service since docker doesn't support init scripts
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-05 14:59:52 -07:00
Matt Sochor
e7222944a5 Added logarithmic option to chart x axis type 2015-10-05 17:47:23 -04:00
Matt Sochor
f49839eadf Add logarithmic y-axis option to chart 2015-10-05 17:42:16 -04:00
John Wu
aa1b72908b Do not ignore .env file
Signed-off-by: John Wu <webmaster@leapoahead.com>
2015-10-05 14:33:20 -07:00
Matt Sochor
5dd457e5f1 fixup! Added d3 box plot visualization 2015-10-05 15:55:07 -04:00
Matt Sochor
a471134e07 Added X and Y axis labels 2015-10-05 15:53:06 -04:00
Matt Sochor
8a8f91ee8f Added ggplot style gridlines 2015-10-05 14:39:54 -04:00
Matt Sochor
59aa218b24 Added axes to boxplot 2015-10-05 10:44:59 -04:00
Matt Sochor
5fd8dbe523 fixup! Added variable width for box plots 2015-10-04 23:35:13 -04:00
Matt Sochor
a08f3c7cd0 fixup! Added variable width for box plots 2015-10-04 23:30:49 -04:00
Matt Sochor
824d053ddd Added variable width for box plots 2015-10-04 23:10:49 -04:00
Matt Sochor
b6e61deb24 Added d3 box plot visualization 2015-10-04 21:43:28 -04:00
Matt Sochor
4f40b28120 Added d3js 2015-10-04 21:43:28 -04:00
Arik Fraimovich
5d1c75df1c Merge pull request #576 from joaofraga/fix/deduplicate-column-names
Fix: support for duplicate columns for MySQL query runner
2015-10-01 19:01:49 +03:00
John Wu
28ccaedfff Ignore .env file 2015-09-30 17:11:56 -07:00
John Wu
1ee05e12fd Docker support 2015-09-30 14:19:22 -07:00
John Wu
6f91849419 Bind to 0.0.0.0 instead of 127.0.0.1 2015-09-30 10:38:51 -07:00
Joao Fraga
65cc67d1dd Changed duplicated column name formating 2015-09-30 10:47:49 -03:00
Joao Fraga
a8f6d9e45b Moved columns full data to BaseQueryRunner to avoid unnecessary loops 2015-09-29 14:56:18 -03:00
Joao Fraga
2c39a2faae Improved column name formater 2015-09-29 14:53:39 -03:00
Arik Fraimovich
1052528a5f Merge pull request #577 from EverythingMe/fix/get_key
Fix: getKeyFromObject was failing if key had dot in name
2015-09-29 09:38:06 +03:00
Arik Fraimovich
92cd2f1367 Fix: getKeyFromObject was failing if key had dot in name
This is due to incorrect use of `_.include` instead of `_.has`.

#571
2015-09-29 09:30:00 +03:00
Joao Fraga
990717a43d run_query now uses fetch_column to get column names 2015-09-29 01:29:00 -03:00
Joao Fraga
a2608d6a44 Added fetch_columns method to avoid columns duplications 2015-09-29 01:28:39 -03:00
John Wu
dedae03c8c Remove imagemin grunt task
grunt-contrib-imagemin seems to be broken because several dependencies
are quite obsolete and cannot be downloaded.
2015-09-28 17:14:49 -07:00
John Wu
61f2be02b7 Redundant filter removed 2015-09-28 15:06:09 -07:00
John Wu
9eca43801a Fix: date range does not update in dashboard
Replace the whole dateRange object in scope instead of changing min and max properties one-by-one. Given how angular `$watch` works with Moment.js object, I wrote some comment to clarify the right way to update dateRange.
2015-09-28 15:00:55 -07:00
John Wu
bcaefda600 Clearfix date-range-selector 2015-09-28 14:58:26 -07:00
Lior Rozner
42b0430866 Added support for ElasticSearch with basic auth.
Initial support for full blown ElasticSearch Search API (https://www.elastic.co/guide/en/elasticsearch/reference/current/search.html)
2015-09-28 08:56:38 -07:00
Arik Fraimovich
445dbb5ade Merge pull request #573 from easytaxibr/PR/feature/logout_link_for_admin_UI
Feature: Add logout link to Admin UI
2015-09-25 08:18:36 +03:00
John Wu
40ee0d8a6e Add date-range-selector to chart 2015-09-24 15:06:35 -07:00
wesleybatista
a5b738a035 Feature: Add logout link to Admin UI 2015-09-24 18:24:48 -03:00
Arik Fraimovich
e893ab4519 Merge pull request #556 from nathanlubchenco/paramaterized_cohorts
Feature: options for Cohort visualization
2015-09-24 21:44:30 +03:00
Arik Fraimovich
8b569379bc Merge pull request #570 from toru-takahashi/feature/treasuredata
Add TreasureData query runner
2015-09-21 14:23:08 +03:00
toru-takahashi
bff3e7c3b2 Add TreasureData query runner 2015-09-21 16:12:34 +09:00
Arik Fraimovich
3fbd0d9579 Merge pull request #560 from stanhu/add-yaxis-label
Feature: add ability to configure y-Axis title
2015-09-20 15:27:39 +03:00
Arik Fraimovich
00f4ec16f8 Merge pull request #569 from EverythingMe/fix/remove_warnings
Remove import warnings from query runners
2015-09-20 12:39:46 +03:00
Arik Fraimovich
6f24b31858 Update setup.rst 2015-09-20 12:39:29 +03:00
Arik Fraimovich
7a8844180b Updated cloud images to latest version. 2015-09-20 12:38:06 +03:00
Arik Fraimovich
aefaf204a3 Merge pull request #568 from EverythingMe/fix/bq_timeout
Add timeout setting for BigQuery query runner
2015-09-20 12:32:18 +03:00
Arik Fraimovich
1527ea36b1 Remove import warnings from query runners 2015-09-20 12:32:04 +03:00
Arik Fraimovich
a71b83d98a Add timeout setting for BigQuery query runner 2015-09-20 12:27:58 +03:00
Arik Fraimovich
7add6287dc Merge pull request #567 from EverythingMe/fixes
Remove page title from navbar & limit # of recent entries to 20
2015-09-20 12:02:16 +03:00
Arik Fraimovich
d37b5ed075 Remove title from navbar 2015-09-20 11:18:43 +03:00
Arik Fraimovich
23b8b77feb Don't send log entries to Sentry. 2015-09-20 11:13:35 +03:00
Arik Fraimovich
46f1478e0d Make sure only 20 dashboards/queries returned in recent call. 2015-09-20 11:12:44 +03:00
Arik Fraimovich
ec46312bf6 Bump version. 2015-09-20 11:05:56 +03:00
Arik Fraimovich
7c308bee09 Merge pull request #563 from stanhu/allow-admin-to-edit-all
Fix: allow admins to edit everything.
2015-09-20 09:46:05 +03:00
Arik Fraimovich
5f656f3868 Fix: upload assets url changed 2015-09-20 09:22:41 +03:00
Arik Fraimovich
4e27331d56 Move vertica requirement to correct requirements file. 2015-09-20 08:52:33 +03:00
Stan Hu
8f28c52b8d Allow admins to edit everything
Closes #562
2015-09-17 17:24:12 -07:00
Stan Hu
47e6960b83 Add ability to configure y-Axis title 2015-09-17 15:57:44 -07:00
nathanlubchenco
0990d93b03 allow defaults for existing visualizations, link time label to time interval 2015-09-16 10:34:22 -06:00
nathanlubchenco
bf88d8b578 explicitly pass in just timeInterval and timeLabel from visualization.options to be watched 2015-09-15 14:57:27 -06:00
nathanlubchenco
384e756817 don't pass data as an argument to scope 2015-09-15 14:52:10 -06:00
nathanlubchenco
d2c46c99eb actually pass in visualization options 2015-09-15 14:46:40 -06:00
nathanlubchenco
9c2858191f typo fix 2015-09-15 14:28:51 -06:00
nathanlubchenco
0473de7392 add editor directive 2015-09-15 14:23:33 -06:00
nathanlubchenco
faece4f2c4 fixing mistakes, adding editTemplate and defaultOptions to visualization registration 2015-09-14 16:40:25 -06:00
Arik Fraimovich
c9e74104b1 Merge pull request #549 from shinjiikeda/master
Feature: Vertica query runner
2015-09-14 22:58:39 +03:00
nathanlubchenco
d100c915f4 changed text to select and replaced original cohort directive 2015-09-11 13:29:40 -06:00
nathanlubchenco
ef3636145c Revert "weekly cohort visualization modeled after chort.js"
This reverts commit 6210d6ab80.
2015-09-10 15:29:17 -06:00
nathanlubchenco
6bd7dc9237 paramaterized cohort visualization 2015-09-10 15:10:31 -06:00
nathanlubchenco
6210d6ab80 weekly cohort visualization modeled after chort.js 2015-09-10 14:34:15 -06:00
ike_s
864a12a3be Merge https://github.com/EverythingMe/redash 2015-09-10 15:40:52 +09:00
Arik Fraimovich
f48c47712d Merge pull request #553 from EverythingMe/sentry
Feature: optional Sentry support
2015-09-10 08:30:44 +03:00
Arik Fraimovich
2c90fb3fa9 Sentry support in Celery 2015-09-10 07:29:44 +03:00
jvasquez
176fd16e95 Adding behavior into the controller. 2015-09-09 16:33:51 -03:00
jvasquez
75d3a63070 Removing extra class from dashboard.html 2015-09-09 14:56:50 -03:00
jvasquez
8c4a5a644e Clean up branch. 2015-09-09 14:55:14 -03:00
jvasquez
5b024a3518 Hidden widgets 2015-09-09 14:39:43 -03:00
jvasquez
d474267934 Adding hidden options for widgets. 2015-09-09 14:14:27 -03:00
ike_s
9429314b6e update 2015-09-10 00:57:41 +09:00
EC2 Default User
7cd132b47d update 2015-09-09 12:17:17 +00:00
EC2 Default User
89661990e7 add vertica query runner 2015-09-09 10:59:37 +00:00
EC2 Default User
01564d7e10 add vertica query runner 2015-09-09 10:59:21 +00:00
Arik Fraimovich
98307aec0d Make the counter migration safer. 2015-09-09 09:57:54 +03:00
Arik Fraimovich
5de3de12f0 Remove details about datasources, as they're managed through the web UI now. 2015-09-08 12:19:13 +03:00
Arik Fraimovich
dea64734d6 Update instructions on how to flush Redis. 2015-09-08 12:17:30 +03:00
Arik Fraimovich
98857ea64c Feature: Support for Sentry 2015-09-07 10:51:51 +03:00
Arik Fraimovich
3181f28509 Merge pull request #547 from EverythingMe/fix/counter_vis
Fix: allow to control if to render script tags from user input
2015-09-07 09:14:35 +03:00
Arik Fraimovich
37745ad1c0 Fix: allow to control if to render script tags 2015-09-07 08:59:20 +03:00
Arik Fraimovich
5fe5c94b3d Merge pull request #546 from EverythingMe/fix/counter_vis
Improvements to counter visualization:
2015-09-07 08:58:55 +03:00
Arik Fraimovich
59cbafa724 Fix migration - always set target row number 2015-09-07 08:49:04 +03:00
Arik Fraimovich
1d99da5a32 Improvements to counter visualization:
1. Ability to take target value from different row.
2. Ability to clear target column name.
3. Use explicit default configuration values instead of "magic" defaults.
2015-09-06 23:30:02 +03:00
Arik Fraimovich
8dfa1ca7bd Merge pull request #543 from quaninte/fix_gte_mongo
Fix $gte mongodb query fail
2015-09-06 11:20:02 +03:00
Arik Fraimovich
1fb6860ee2 Merge pull request #545 from EverythingMe/feature/users_admin
Fix: logout wasn't working in Angluar context
2015-09-06 11:18:47 +03:00
Arik Fraimovich
99c50c1f64 Fix: logout wasn't working in Angluar context 2015-09-06 11:18:20 +03:00
Arik Fraimovich
b1576b5a91 Merge pull request #531 from EverythingMe/feature/users_admin
Feature: users admin in the web interface
2015-09-06 10:25:18 +03:00
Arik Fraimovich
6f2ee2c0bb Update vagrant instructions 2015-09-06 10:18:33 +03:00
Arik Fraimovich
eec5e3290b Return dashboard after archive 2015-09-06 10:17:38 +03:00
Arik Fraimovich
aaac5928c4 Fix: tests w/ celery breaking 2015-09-06 10:15:26 +03:00
Arik Fraimovich
b97b35d9b5 Update icons size in navbar 2015-09-06 10:15:26 +03:00
Arik Fraimovich
6955514ec3 Update documentation re. users admin 2015-09-06 10:15:26 +03:00
Arik Fraimovich
c8d5267bc7 Bump version 2015-09-06 10:15:25 +03:00
Arik Fraimovich
993a861c78 Users UI. 2015-09-06 10:15:25 +03:00
Arik Fraimovich
a11e100050 Tests for users API 2015-09-06 10:15:25 +03:00
Arik Fraimovich
470ec4924c Remove redash.cache module 2015-09-06 10:15:25 +03:00
Arik Fraimovich
cdb6aaac6e Split the giant redash.controllers module into a package 2015-09-06 10:15:25 +03:00
Arik Fraimovich
580d33a6f8 API for users resource 2015-09-06 10:14:56 +03:00
Arik Fraimovich
8686694be9 Merge pull request #544 from EverythingMe/fix/archived_dashboard_in_recent
Fix: archived dashboards were shown in recent list (and move archive button)
2015-09-03 08:44:10 +03:00
Arik Fraimovich
795a9fe011 Move dashboard archive to dashboard page. 2015-09-03 08:31:51 +03:00
Arik Fraimovich
4b08a3a5f2 Fix #541: archived dashboards appear in recent list. 2015-09-03 08:31:17 +03:00
Quan MT
d9e8a81655 Fix $gte mongodb query fail 2015-08-28 10:06:18 +07:00
Arik Fraimovich
7000547419 Merge pull request #537 from ekampf/patch-3
Docs: Missing --source-uri in command
2015-08-25 23:46:45 +03:00
Arik Fraimovich
e0100543cd Merge pull request #538 from ekampf/patch-4
Docs: Space-separated list of scopes is deprecated. Need to use comma-seper…
2015-08-25 23:46:07 +03:00
Arik Fraimovich
7ea640927f Merge pull request #539 from ekampf/patch-5
Docs: Misc. UI fixes because Google changed their console
2015-08-25 23:45:48 +03:00
Eran Kampf
db26cafc41 Misc. UI fixes because Google changed their console 2015-08-24 17:07:11 -07:00
Eran Kampf
100b9e7c71 Space-separated list of scopes is deprecated. Need to use comma-seperated 2015-08-24 16:53:29 -07:00
Eran Kampf
d3391db8f0 Missing --source-uri in command 2015-08-24 16:50:36 -07:00
Arik Fraimovich
1ad01d8394 Warn about using the bootstrap script with existing machines. 2015-08-19 21:25:36 +03:00
Arik Fraimovich
3ef3f2c01b Merge pull request #529 from rm420/fix/multifilters_for_bq
Enables multi-filtering for big-query.
2015-08-16 11:07:02 +03:00
Ryan McClarnon
371422a9ae Merge multi-filter check into one statement 2015-08-10 23:58:46 +01:00
Ryan McClarnon
f4af650292 Enables multi-filtering for big-query. Before had issue with hyphen in field name 2015-08-10 16:17:39 +01:00
Arik Fraimovich
5f38e87f01 Fix images links. 2015-08-09 13:26:23 +03:00
Arik Fraimovich
b98e4a27ce Update images. 2015-08-09 13:25:25 +03:00
Arik Fraimovich
9ff8db31d2 Merge pull request #528 from EverythingMe/chore/packer_update
Cleanup packer.json (no longer building GCE image with it).
2015-08-09 11:40:07 +03:00
Arik Fraimovich
446148d07f Run dist-upgrade before apt-get install 2015-08-09 11:29:01 +03:00
Arik Fraimovich
2d6ca50568 Remove google compute 2015-08-09 11:29:01 +03:00
Arik Fraimovich
650ccac501 Merge pull request #527 from EverythingMe/fix/filters_for_bq
Fix: support for filters in BigQuery.
2015-08-09 11:09:21 +03:00
Arik Fraimovich
ab507f0fd5 Update path to tarball. 2015-08-09 11:09:05 +03:00
Arik Fraimovich
7187b5ffee Fix: support for filters in BigQuery.
BigQuery doesn't support :: in column names, so using __ to split instead of ::.
2015-08-09 11:00:35 +03:00
Arik Fraimovich
5e73da1df4 Fix link to tarball (it changes on every build). 2015-08-07 19:34:00 +03:00
Arik Fraimovich
244d25b12c Fix #524: use v0.7.1 in bootstrap.sh. 2015-08-07 19:18:18 +03:00
Arik Fraimovich
2dcf676cf2 Fix #525: make sure we're in right path for requirements_all_ds.txt 2015-08-07 19:14:46 +03:00
Arik Fraimovich
e07af676a5 Fix #526: install latest setuptools in bootstrap.sh. 2015-08-07 19:12:17 +03:00
Arik Fraimovich
3dea6302de Merge pull request #523 from EverythingMe/feature/new_home
Fix: global_recent isnt set if user has enough recents
2015-08-06 16:58:44 +03:00
Arik Fraimovich
b1ceb60360 Fix: global_recent isnt set if user has enough recents 2015-08-06 16:58:15 +03:00
Arik Fraimovich
1ef94b77e9 Merge pull request #522 from EverythingMe/feature/new_home
Feature: "personalized" homepage with recent queries and dashboards
2015-08-06 16:48:29 +03:00
Arik Fraimovich
292d31e490 Improve /personal and use it as default home 2015-08-06 16:42:29 +03:00
Arik Fraimovich
6f0ac1e730 Merge pull request #521 from kataring/update-doc
Update docs about data sources for Presto
2015-08-06 10:16:39 +03:00
Noriaki Katayama
9f82e5850d Update docs about data sources for Presto 2015-08-06 16:02:18 +09:00
Arik Fraimovich
4a18fa07ec Merge pull request #518 from massaru129/feature/bootstrap_amazon_linux
Other: created bootstrap script for amazon linux
2015-08-04 16:17:18 +03:00
Arik Fraimovich
05d1886467 Merge pull request #520 from EverythingMe/docs-datasources
Docs: update documentation about data sources
2015-08-02 10:49:57 +03:00
Arik Fraimovich
6e45706825 Update docs about data sources 2015-08-02 10:15:11 +03:00
Arik Fraimovich
464402a233 Merge pull request #519 from EverythingMe/feature/disable_collaborative_editing
Feature: ability to disable the ability to edit anyone's query by everyone
2015-08-01 16:41:08 +03:00
Arik Fraimovich
3a56b9ded7 Don't set last_modified_by if only changing ref to last result 2015-08-01 16:36:56 +03:00
Arik Fraimovich
142295671b Feature flag to control if everyone can edit queries 2015-08-01 16:30:03 +03:00
masaru
0e46a24112 fixed config file pathes 2015-07-31 19:21:36 +09:00
masaru
a3cb698be0 fixed file path
the fixed pathes will work if my original files are uploaded
2015-07-31 14:34:25 +09:00
masaru
08730ad113 created bootstrap script for amazon linux 2015-07-31 14:05:48 +09:00
Arik Fraimovich
d155f166d7 Merge pull request #517 from EverythingMe/cleanup
Fix: URL query runner was failing without base URL
2015-07-30 21:27:00 +03:00
Arik Fraimovich
ca95e9252f Fix: URL query runner was failing without base URL 2015-07-30 21:26:14 +03:00
Arik Fraimovich
d078e80e79 Grammar fix. 2015-07-30 14:50:52 +03:00
Arik Fraimovich
8ad1d2672c Clarify about -i. 2015-07-30 14:49:55 +03:00
Arik Fraimovich
735130efc9 Merge pull request #510 from rghose/patch-1
for aws based and password less logins to boxes
2015-07-30 14:46:44 +03:00
Arik Fraimovich
7e6b7398a4 Remove confusing exception logging 2015-07-30 14:36:27 +03:00
Arik Fraimovich
edf8f5b1fd Fix tests post field name change in #515 2015-07-30 14:36:01 +03:00
Arik Fraimovich
08c09d896a Merge pull request #516 from EverythingMe/cleanup
Fix: multi-filter was broken in newer version of angular-ui-select
2015-07-30 13:36:50 +03:00
Arik Fraimovich
58403634cf Fix: multi-filter was broken in newer version of angular-ui-select 2015-07-30 13:35:50 +03:00
Arik Fraimovich
2eb171e40d Merge pull request #515 from moyomot/fix/only_email_authentication
Fix: change wording on login screen "username or email" -> "email"
2015-07-30 12:25:37 +03:00
moyomot
3753f58980 authentication are allowed e-mail only. 2015-07-30 17:56:57 +09:00
Arik Fraimovich
fe1cc78ab3 Merge pull request #514 from alexanderlz/master
Feature: Support Hive as datasource
2015-07-30 08:48:26 +03:00
Alexander Leibzon
c140668648 minor fixes. working version of Hive datasource. 2015-07-29 23:39:39 +03:00
Arik Fraimovich
41ca1321cf Merge pull request #513 from EverythingMe/cleanup
Cleanup: remove select2 and use ui-select.
2015-07-29 09:36:13 +03:00
Alexander Leibzon
d88340158a add Hive as datasource 2015-07-29 02:01:22 +03:00
Arik Fraimovich
52f335edd5 Cleanup: remove select2 and use ui-select. 2015-07-28 10:03:56 +03:00
Arik Fraimovich
22200ec7b2 Merge pull request #511 from stanhu/add-primary-key-to-flask-admin
Add the primary key to Flask admin to make it possible to lookup queries
2015-07-27 23:40:15 +03:00
Arik Fraimovich
e458ed03c8 Bump version. 2015-07-27 23:38:10 +03:00
Arik Fraimovich
e9f1e3a189 Merge pull request #512 from johnkearney/tidier-requirements
Move datasource requirements from bootstrap to own requirements file
2015-07-27 23:32:44 +03:00
John Kearney
d202570b0d Move datasource requirements from bootstrap to own requirements file 2015-07-27 11:46:53 -07:00
Stan Hu
9b6edde5c8 Add the primary key to Flask admin to make it possible to lookup queries 2015-07-27 10:24:34 -07:00
Rahul Ghose
975c92d40d for aws based and password less logins to boxes 2015-07-27 13:23:38 +05:30
Arik Fraimovich
27639f83c7 Update index.rst 2015-07-26 22:38:37 +03:00
Arik Fraimovich
c08e6791df Remove version info from conf.py -- rtd doesn't use it 2015-07-26 15:54:56 +03:00
Arik Fraimovich
5c7158b6ae Update vagrant instructions 2015-07-26 15:46:26 +03:00
Arik Fraimovich
b886067a9f Merge pull request #509 from EverythingMe/docs
Moving documentation to ReatTheDocs
2015-07-26 15:25:29 +03:00
Arik Fraimovich
2421de8819 Add Sphinx based documentation to the project. 2015-07-26 15:24:16 +03:00
Arik Fraimovich
9e87e42400 Merge pull request #508 from EverythingMe/cleanup
Some cleanup (updated settings and bootstrap script)
2015-07-26 12:30:31 +03:00
Arik Fraimovich
8c750826e3 Install dependencies for new sources 2015-07-26 11:49:23 +03:00
Arik Fraimovich
b14b6d1773 Give permission to read user(id, name) to redash_reader 2015-07-26 11:45:58 +03:00
Arik Fraimovich
76cb73f4ce Add description to the server param of elastic search 2015-07-26 11:45:25 +03:00
Arik Fraimovich
8854a45598 Update to settings:
1. Enable API key auth by default.
2. Enable query results cleanup by default.
3. Add ElasticSearch to the enabled query runners list.
2015-07-26 11:44:11 +03:00
Arik Fraimovich
228b8c7614 Merge pull request #507 from EverythingMe/cleanup
Fix: when editing alerts show correct column
2015-07-26 11:39:55 +03:00
Arik Fraimovich
5de79213ae Fix: when editing alerts show correct column 2015-07-26 11:39:22 +03:00
Arik Fraimovich
c7d30c8b87 Merge pull request #498 from EverythingMe/feature/ds_admin
Feature: datasources web admin (closes #193)
2015-07-26 11:35:52 +03:00
Arik Fraimovich
076710f0c6 Bump version 2015-07-26 10:24:09 +03:00
Arik Fraimovich
a9172dac00 Fix: if connection fails connection isn't set 2015-07-26 10:24:08 +03:00
Arik Fraimovich
accca51f39 Feature: web interface to edit datasources
* Web interface to add and delete data sources, without the need to ssh
into the server.
* Ability to safely delete datasources -- query results from this data sources
are deleted, while queries get assigned null datasource.
* Updated the BigQuery datasource to use the JSON key file from Google Developer
console. Also both BigQuery and the Google Spreadsheets datasource no longer store
their key on the filesystem, but rather in the DB.
* Minor updates to the Flask Admin.
2015-07-26 10:24:08 +03:00
Arik Fraimovich
5f5774d01b Merge pull request #506 from EverythingMe/small_fixes
Fix: makes sure the totals series is sorted
2015-07-23 15:03:36 +03:00
Arik Fraimovich
00e99d858c Fix: makes sure the totals series is sorted 2015-07-23 15:03:18 +03:00
Arik Fraimovich
da56dc883f Merge pull request #505 from EverythingMe/small_fixes
Fix: Update URL after creating an alert
2015-07-22 20:38:52 +03:00
Arik Fraimovich
02582cab65 Update URL after creating an alert 2015-07-22 20:38:22 +03:00
Arik Fraimovich
bff4d31ada Read HOST from env. 2015-07-22 18:19:27 +03:00
Arik Fraimovich
83554207e1 Merge pull request #504 from EverythingMe/fix/python_result_set
Fix: cohort was wrong if values were not sorted
2015-07-22 18:06:41 +03:00
Arik Fraimovich
1c0c3e0b93 Fix: cohort was wrong if values were not sorted 2015-07-22 18:05:54 +03:00
Arik Fraimovich
5feb563dc9 Merge pull request #503 from EverythingMe/fix/python_result_set
Fix: if you change the result object, python runner wouldn't return any results
2015-07-22 18:03:29 +03:00
Arik Fraimovich
07b88d0b53 Fix: log results were lost 2015-07-22 17:56:49 +03:00
Arik Fraimovich
21f33462d5 Anoter try in removing optipng from build 2015-07-22 17:43:05 +03:00
Arik Fraimovich
6a9d95f1ac Fix: if you change the result object, python runner wouldn't return any results 2015-07-22 17:36:46 +03:00
Arik Fraimovich
36b80fc4ef Remove optipng from build artifacts 2015-07-22 17:21:50 +03:00
Arik Fraimovich
d89dd2c9af Merge pull request #502 from EverythingMe/feature/alerts
Feature: alerts on query results
2015-07-22 17:14:29 +03:00
Arik Fraimovich
658af526c7 Add alerts to menu 2015-07-22 17:05:31 +03:00
Arik Fraimovich
3d859ec5f3 Feature: alerts for query results.
This is basic implementation for alerts feature, where you can
define a simple rule on the last query result to send an alert.

As part of the implementation added Flask-Mail to the project,
to send emails. Should be useful to make re:dash more "self aware"
(notify users about potential issues, when queries done executing
and more).
2015-07-22 17:05:31 +03:00
Arik Fraimovich
fdff799d23 ng_smart_table: support for inline templates 2015-07-22 17:05:09 +03:00
Arik Fraimovich
5fc0b88b23 ng_smart_table: support for nested objects 2015-07-22 17:05:09 +03:00
Alexander Leibzon
63de247478 add datasources 2015-07-22 14:55:26 +03:00
Arik Fraimovich
5d3caac1b5 Merge pull request #499 from alexanderlz/master
Feature: Support Impala as DataSource
2015-07-22 14:23:55 +03:00
Alexander Leibzon
e4b9d23dfe minor fixes 2015-07-22 14:21:40 +03:00
Alexander Leibzon
890f59a4c9 add get_schema ability to impala 2015-07-22 13:54:00 +03:00
Arik Fraimovich
d4a18ba611 Merge pull request #501 from johnkearney/all_pg_queries_with_no_results
All pg queries with no results
2015-07-21 06:47:33 +03:00
John Kearney
c4502b2925 Add a more use-friendly message when redshift returns no rows 2015-07-20 14:17:51 -07:00
Alexander Leibzon
1d5efdd93f fixes in accordance with pull req spec 2015-07-20 23:21:02 +03:00
John Kearney
2b95da102e Remove unused exports for env examples 2015-07-20 12:36:10 -07:00
Arik Fraimovich
d512cd0c1d Merge pull request #500 from EverythingMe/feature/login_events
Feature: add event for users logining in
2015-07-20 18:30:57 +03:00
Arik Fraimovich
3dc9c84a98 Feature: add event for users logining in 2015-07-20 18:26:45 +03:00
Alexander Leibzon
4a33b987b8 datasource rename 2015-07-20 02:07:17 +03:00
Alexander Leibzon
f7041977d5 impala datasource fixes 2015-07-20 02:06:15 +03:00
Alexander Leibzon
83bc38579e impala data source 2015-07-19 01:44:48 +03:00
Arik Fraimovich
4b8a94e795 Merge pull request #495 from EverythingMe/fix/bq_param
Fix: Update BigQuery configuration parameter name to avoid confusion.
2015-07-15 19:49:56 +03:00
Arik Fraimovich
406010a7a6 Fix: Update BigQuery configuration parameter name to avoid confusion. 2015-07-15 19:45:55 +03:00
Arik Fraimovich
4f11f28efa Merge pull request #494 from erans/master
MongoDB query runner: cleanup
2015-07-15 18:24:28 +03:00
Eran Sandler
c919602b20 cleanups and shit. 2015-07-15 18:17:55 +03:00
Arik Fraimovich
7702b05635 Merge pull request #493 from erans/master
Fix: a generic way to parse all the input JSON and make sure we replace ISODate to Python date times.
2015-07-15 17:50:21 +03:00
Eran Sandler
5fc7c499a3 stupid auto merge. 2015-07-15 17:48:03 +03:00
Eran Sandler
628240906e Fix: a generic way to parse all the input JSON and make sure we replace ISODate to Python date times. 2015-07-15 17:44:33 +03:00
Arik Fraimovich
41b9b21a20 Merge pull request #492 from erans/master
Fix: MongoDB: Date parsing and dates in aggregation $match
2015-07-15 17:03:17 +03:00
Eran Sandler
dbd3f754ba - Fixed parsing dates in the format of YYYY-MM-DDTHH:mm
- Added handling dates in the aggregate $match
2015-07-15 16:58:10 +03:00
Arik Fraimovich
4ef3c27fe6 Merge pull request #489 from kataring/suport-presto
Feature: Support Presto
2015-07-15 16:50:51 +03:00
Arik Fraimovich
58a005c71b Merge pull request #484 from alexanderlz/master
Feature: Google Spreadsheets support (alpha)
2015-07-14 12:14:57 +03:00
Alexander Leibzon
9d7ff31178 replace camelcase vars 2015-07-14 12:13:04 +03:00
Noriaki Katayama
93d6b01fbf add bigint 2015-07-14 16:59:25 +09:00
Arik Fraimovich
7d57f9d0f1 Merge pull request #488 from EverythingMe/fix/mongo-support-sandbox
Fix: Drop database name check in MongoDB queries  to support sandboxed environments
2015-07-14 08:56:47 +03:00
Arik Fraimovich
e80f470255 Mongo: Drop database name check to support sandboxed environments 2015-07-14 08:51:31 +03:00
Arik Fraimovich
5636cec0eb Merge pull request #487 from erans/master
Fix: Support newer as well as older PyMongo versions
2015-07-14 08:23:58 +03:00
Eran Sandler
912bbc1a4a Added backwards compatibility mode with older versions of PyMongo.
It appears that older versions would return a dictionary from an aggregate operation that had the cursor inside the "result" key.
Newer versions return a new type of cursor called CommandCursor.
2015-07-14 08:19:25 +03:00
Arik Fraimovich
d3bb58167e Merge pull request #486 from EverythingMe/fix/mysql-noerror-onconnect
Fix: no error when failing to connect to a MySQL data source
2015-07-13 19:04:16 +03:00
Arik Fraimovich
4503c6af66 Move the MySQL connect to the try/except block 2015-07-13 18:31:00 +03:00
Alexander Leibzon
b850da52a2 remove .nojekyll, naming convention 2015-07-13 09:56:11 +03:00
Alexander Leibzon
079fbf33f4 don't execute query if the query string is empty (when changing datasource) 2015-07-12 22:53:08 +03:00
Alexander Leibzon
43edb009d6 safer handling of worksheet num 2015-07-12 01:28:52 +03:00
Alexander Leibzon
81978c5049 jekyll disable 2015-07-11 22:51:31 +03:00
Alexander Leibzon
239813e195 modify google spreadsheed datasource params, only the credentials file needed 2015-07-11 22:43:07 +03:00
Alexander Leibzon
28dd571a03 google spreadsheets working version 2015-07-10 21:27:16 +03:00
Alexander Leibzon
808126cf91 forgot type 2015-07-09 01:22:08 +03:00
Alexander Leibzon
69a8295f4c forgot type 2015-07-09 01:18:31 +03:00
Noriaki Katayama
6338be3811 modified response 2015-07-08 10:33:55 +09:00
Alexander Leibzon
3ee6371250 initial work on google spreadsheets 2015-07-07 01:39:59 +03:00
Noriaki Katayama
4f38d42182 add presto 2015-07-06 18:22:23 +09:00
268 changed files with 14750 additions and 5341 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
rd_ui/.tmp/
rd_ui/node_modules/
.git/
.vagrant/

View File

@@ -1,9 +1,6 @@
REDASH_CONNECTION_ADAPTER=pg
REDASH_CONNECTION_STRING="dbname=data"
REDASH_STATIC_ASSETS_PATH=../rd_ui/app/
REDASH_STATIC_ASSETS_PATH="../rd_ui/app/"
REDASH_LOG_LEVEL="INFO"
REDASH_REDIS_URL=redis://localhost:6379/1
REDASH_DATABASE_URL="postgresql://redash"
REDASH_COOKIE_SECRET=veryverysecret
REDASH_GOOGLE_APPS_DOMAIN=
REDASH_ADMINS=
REDASH_WORKERS_COUNT=2
REDASH_COOKIE_SECRET=
REDASH_DATABASE_URL='postgresql://rd'
REDASH_LOG_LEVEL = "INFO"

4
.gitignore vendored
View File

@@ -8,6 +8,7 @@ celerybeat-schedule*
.#*
\#*#
*~
_build
# Vagrant related
.vagrant
@@ -18,3 +19,6 @@ redash/dump.rdb
venv
dump.rdb
# Docker related
docker-compose.yml

51
Dockerfile Normal file
View File

@@ -0,0 +1,51 @@
FROM ubuntu:trusty
# Ubuntu packages
RUN apt-get update && \
apt-get install -y python-pip python-dev curl build-essential pwgen libffi-dev sudo git-core wget \
# Postgres client
libpq-dev \
# Additional packages required for data sources:
libssl-dev libmysqlclient-dev freetds-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Users creation
RUN useradd --system --comment " " --create-home redash
# Pip requirements for all data source types
RUN pip install -U setuptools && \
pip install supervisor==3.1.2
COPY . /opt/redash/current
RUN chown -R redash /opt/redash/current
# Setting working directory
WORKDIR /opt/redash/current
# Install project specific dependencies
RUN pip install -r requirements_all_ds.txt && \
pip install -r requirements.txt
RUN curl https://deb.nodesource.com/setup_4.x | bash - && \
apt-get install -y nodejs && \
sudo -u redash -H make deps && \
rm -rf rd_ui/node_modules /home/redash/.npm /home/redash/.cache && \
apt-get purge -y nodejs && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Setup supervisord
RUN mkdir -p /opt/redash/supervisord && \
mkdir -p /opt/redash/logs && \
cp /opt/redash/current/setup/docker/supervisord/supervisord.conf /opt/redash/supervisord/supervisord.conf
# Fix permissions
RUN chown -R redash /opt/redash
# Expose ports
EXPOSE 5000
EXPOSE 9001
# Startup script
CMD ["supervisord", "-c", "/opt/redash/supervisord/supervisord.conf"]

19
LICENSE
View File

@@ -1,4 +1,5 @@
Copyright 2013 DoAT. All rights reserved.
Copyright (c) 2013-2016, Arik Fraimovich.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@@ -10,17 +11,13 @@ are permitted provided that the following conditions are met:
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED “AS IS” WITHOUT ANY WARRANTIES WHATSOEVER.
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF NON INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE HEREBY DISCLAIMED. IN NO EVENT SHALL DoAT OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are those of
the authors and should not be interpreted as representing official policies,
either expressed or implied, of DoAT.

View File

@@ -6,18 +6,17 @@ BASE_VERSION=$(shell python ./manage.py version | cut -d + -f 1)
FILENAME=$(CIRCLE_ARTIFACTS)/$(NAME).$(VERSION).tar.gz
deps:
cd rd_ui && npm install
cd rd_ui && npm install -g bower grunt-cli
cd rd_ui && bower install
cd rd_ui && grunt build
if [ -d "./rd_ui/app" ]; then cd rd_ui && npm install; fi
if [ -d "./rd_ui/app" ]; then cd rd_ui && npm run bower install; fi
if [ -d "./rd_ui/app" ]; then cd rd_ui && npm run build; fi
pack:
sed -ri "s/^__version__ = '([0-9.]*)'/__version__ = '$(FULL_VERSION)'/" redash/__init__.py
tar -zcv -f $(FILENAME) --exclude=".git*" --exclude="*.pyc" --exclude="*.pyo" --exclude="venv" --exclude="rd_ui/node_modules" --exclude="rd_ui/dist/bower_components" --exclude="rd_ui/app" *
tar -zcv -f $(FILENAME) --exclude="optipng*" --exclude=".git*" --exclude="*.pyc" --exclude="*.pyo" --exclude="venv" --exclude="rd_ui/node_modules" --exclude="rd_ui/dist/bower_components" --exclude="rd_ui/app" *
upload:
python bin/release_manager.py $(CIRCLE_SHA1) $(BASE_VERSION) $(FILENAME)
test:
nosetests --with-coverage --cover-package=redash tests/*.py
nosetests --with-coverage --cover-package=redash tests/
#cd rd_ui && grunt test

View File

@@ -1,8 +1,12 @@
More details about the future of re:dash : http://bit.ly/journey-first-step
---
<p align="center">
<img title="re:dash" src='http://redash.io/static/img/redash_logo.png' width="200px"/>
<img title="re:dash" src='http://redash.io/static/old_img/redash_logo.png' width="200px"/>
</p>
<p align="center">
<img title="Build Status" src='https://circleci.com/gh/EverythingMe/redash.png?circle-token=8a695aa5ec2cbfa89b48c275aea298318016f040'/>
<img title="Build Status" src='https://circleci.com/gh/getredash/redash.png?circle-token=8a695aa5ec2cbfa89b48c275aea298318016f040'/>
</p>
**_re:dash_** is our take on freeing the data within our company in a way that will better fit our culture and usage patterns.
@@ -10,7 +14,8 @@
Prior to **_re:dash_**, we tried to use traditional BI suites and discovered a set of bloated, technically challenged and slow tools/flows. What we were looking for was a more hacker'ish way to look at data, so we built one.
**_re:dash_** was built to allow fast and easy access to billions of records, that we process and collect using Amazon Redshift ("petabyte scale data warehouse" that "speaks" PostgreSQL).
Today **_re:dash_** has support for querying multiple databases, including: Redshift, Google BigQuery, PostgreSQL, MySQL, Graphite and custom scripts.
Today **_re:dash_** has support for querying multiple databases, including: Redshift, Google BigQuery, PostgreSQL, MySQL, Graphite,
Presto, Google Spreadsheets, Cloudera Impala, Hive and custom scripts.
**_re:dash_** consists of two parts:
@@ -21,31 +26,27 @@ Today **_re:dash_** has support for querying multiple databases, including: Reds
## Demo
![Screenshots](https://raw.github.com/EverythingMe/redash/screenshots/screenshots.gif)
<img src="https://cloud.githubusercontent.com/assets/71468/12611424/1faf4d6a-c4f5-11e5-89b5-31efc1155d2c.gif" width="60%"/>
You can try out the demo instance: http://demo.redash.io/ (login with any Google account).
## Getting Started
* [Setting up re:dash instance](http://redash.io/deployment/setup.html) (includes links to ready made AWS/GCE images).
* Additional documentation in the [Wiki](https://github.com/everythingme/redash/wiki).
* [Documentation](http://docs.redash.io).
## Getting help
* [Google Group (mailing list)](https://groups.google.com/forum/#!forum/redash-users): the best place to get updates about new releases or ask general questions.
* Find us [on gitter](https://gitter.im/EverythingMe/redash#) (chat).
* Contact Arik, the maintainer directly: arik@everything.me.
## Roadmap
TBD.
* Find us [on gitter](https://gitter.im/getredash/redash#) (chat).
* Contact Arik, the maintainer directly: arik@redash.io.
## Reporting Bugs and Contributing Code
* Want to report a bug or request a feature? Please open [an issue](https://github.com/everythingme/redash/issues/new).
* Want to help us build **_re:dash_**? Fork the project and make a pull request. We need all the help we can get!
* Want to report a bug or request a feature? Please open [an issue](https://github.com/getredash/redash/issues/new).
* Want to help us build **_re:dash_**? Fork the project, edit in a [dev environment](http://docs.redash.io/en/latest/dev/vagrant.html), and make a pull request. We need all the help we can get!
## License
See [LICENSE](https://github.com/EverythingMe/redash/blob/master/LICENSE) file.
See [LICENSE](https://github.com/getredash/redash/blob/master/LICENSE) file.

View File

@@ -7,7 +7,7 @@ import requests
github_token = os.environ['GITHUB_TOKEN']
auth = (github_token, 'x-oauth-basic')
repo = 'EverythingMe/redash'
repo = 'getredash/redash'
def _github_request(method, path, params=None, headers={}):
if not path.startswith('https://api.github.com'):
@@ -56,14 +56,14 @@ def create_release(version, commit_sha):
return response.json()
def upload_asset(release, filepath):
upload_url = release['upload_url'].replace('{?name}', '')
upload_url = release['upload_url'].replace('{?name,label}', '')
filename = filepath.split('/')[-1]
with open(filepath) as file_content:
headers = {'Content-Type': 'application/gzip'}
response = requests.post(upload_url, file_content, params={'name': filename}, headers=headers, auth=auth, verify=False)
if response.status_code != 201: # not 200/201/...
if response.status_code != 201: # not 200/201/...
raise exception_from_error('Failed uploading asset', response)
return response

View File

@@ -1,63 +0,0 @@
"""
Script to test concurrency (multithreading/multiprocess) issues with the workers. Use with caution.
"""
import json
import atfork
atfork.monkeypatch_os_fork_functions()
import atfork.stdlib_fixer
atfork.stdlib_fixer.fix_logging_module()
import time
from redash.data import worker
from redash import models, data_manager, redis_connection
if __name__ == '__main__':
models.create_db(True, False)
print "Creating data source..."
data_source = models.DataSource.create(name="Concurrency", type="pg", options="dbname=postgres")
print "Clear jobs/hashes:"
redis_connection.delete("jobs")
query_hashes = redis_connection.keys("query_hash_*")
if query_hashes:
redis_connection.delete(*query_hashes)
starting_query_results_count = models.QueryResult.select().count()
jobs_count = 5000
workers_count = 10
print "Creating jobs..."
for i in xrange(jobs_count):
query = "SELECT {}".format(i)
print "Inserting: {}".format(query)
data_manager.add_job(query=query, priority=worker.Job.LOW_PRIORITY,
data_source=data_source)
print "Starting workers..."
workers = data_manager.start_workers(workers_count)
print "Waiting for jobs to be done..."
keep_waiting = True
while keep_waiting:
results_count = models.QueryResult.select().count() - starting_query_results_count
print "QueryResults: {}".format(results_count)
time.sleep(5)
if results_count == jobs_count:
print "Yay done..."
keep_waiting = False
data_manager.stop_workers()
qr_count = 0
for qr in models.QueryResult.select():
number = int(qr.query.split()[1])
data_number = json.loads(qr.data)['rows'][0].values()[0]
if number != data_number:
print "Oops? {} != {} ({})".format(number, data_number, qr.id)
qr_count += 1
print "Verified {} query results.".format(qr_count)
print "Done."

View File

@@ -1,31 +1,34 @@
machine:
services:
- docker
node:
version:
0.10.24
0.12.4
python:
version:
2.7.3
dependencies:
pre:
- wget http://downloads.sourceforge.net/project/optipng/OptiPNG/optipng-0.7.5/optipng-0.7.5.tar.gz
- tar xvf optipng-0.7.5.tar.gz
- cd optipng-0.7.5; ./configure; make; sudo checkinstall -y;
- make deps
- pip install -r dev_requirements.txt
- pip install -r requirements_dev.txt
- pip install -r requirements.txt
- if [ "$CIRCLE_BRANCH" = "master" ]; then make deps; fi
cache_directories:
- rd_ui/node_modules/
- rd_ui/app/bower_components/
test:
override:
- make test
post:
- make pack
- nosetests --with-xunit --xunit-file=$CIRCLE_TEST_REPORTS/junit.xml --with-coverage --cover-package=redash tests/
deployment:
github:
github_and_docker:
branch: master
commands:
- make pack
- make upload
- echo "rd_ui/app" >> .dockerignore
- docker pull redash/redash:latest
- docker build -t redash/redash:$(./manage.py version | sed -e "s/\+/./") .
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
- docker push redash/redash:$(./manage.py version | sed -e "s/\+/./")
notify:
webhooks:
- url: https://webhooks.gitter.im/e/895d09c3165a0913ac2f

View File

@@ -0,0 +1,28 @@
redash:
image: redash
ports:
- "5000:5000"
links:
- redis
- postgres
environment:
REDASH_STATIC_ASSETS_PATH:"../rd_ui/app/"
REDASH_LOG_LEVEL:"INFO"
REDASH_REDIS_URL:redis://localhost:6379/0
REDASH_DATABASE_URL:"postgresql://redash"
REDASH_COOKIE_SECRET:veryverysecret
REDASH_GOOGLE_APPS_DOMAIN:
redis:
image: redis:2.8
postgres:
image: postgres:9.3
volumes:
- /opt/postgres-data:/var/lib/postgresql/data
redash-nginx:
image: redash-nginx:1.0
ports:
- "80:80"
volumes:
- "../redash-nginx/nginx.conf:/etc/nginx/nginx.conf"
links:
- redash

192
docs/Makefile Normal file
View File

@@ -0,0 +1,192 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/redash.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/redash.qhc"
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/redash"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/redash"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

110
docs/conf.py Normal file
View File

@@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
#
# re:dash documentation build configuration file, created by
# sphinx-quickstart on Mon Jul 20 22:40:24 2015.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import os
import shlex
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = []
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u're:dash'
copyright = u'2015, EverythingMe'
author = u'EverythingMe'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
exclude_patterns = ['_build']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
import sphinx_rtd_theme
html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If true, links to the reST sources are added to the pages.
html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
html_show_sphinx = False
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
html_show_copyright = False
# Output file base name for HTML help builder.
htmlhelp_basename = 'redashdoc'
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'redash', u're:dash Documentation',
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'redash', u're:dash Documentation',
author, 'redash', 'One line description of project.',
'Miscellaneous'),
]

261
docs/datasources.rst Normal file
View File

@@ -0,0 +1,261 @@
Supported Data Sources
######################
re:dash supports several types of data sources, and if you set it up using the provided images, it should already have
the needed dependencies to use them all. Starting from version 0.7 and newer, you can manage data sources from the UI
by browsing to ``/data_sources`` on your instance.
If one of the listed data source types isn't available when trying to create a new data source, make sure that:
1. You installed required dependencies.
2. If you've set custom value for the ``REDASH_ENABLED_QUERY_RUNNERS`` setting, it's included in the list.
PostgreSQL / Redshift / Greenplum
---------------------------------
- **Options**:
- Database name (mandatory)
- User
- Password
- Host
- Port
- **Additional requirements**:
- None
MySQL
-----
- **Options**:
- Database name (mandatory)
- User
- Password
- Host
- Port
- **Additional requirements**:
- ``MySQL-python`` python package
Google BigQuery
---------------
- **Options**:
- Project ID (mandatory)
- JSON key file, generated when creating a service account (see `instructions <https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount>`__).
- **Additional requirements**:
- ``google-api-python-client``, ``oauth2client`` and ``pyopenssl`` python packages (on Ubuntu it might require installing ``libffi-dev`` and ``libssl-dev`` as well).
Graphite
--------
- **Options**:
- Url (mandatory)
- User
- Password
- Verify SSL certificate
MongoDB
-------
- **Options**:
- Connection String (mandatory)
- Database name
- Replica set name
- **Additional requirements**:
- ``pymongo`` python package.
For information on how to write MongoDB queries, see :doc:`documentation </usage/mongodb_querying>`.
ElasticSearch
-------------
...
InfluxDB
--------
...
Presto
------
- **Options**:
- Host (mandatory)
- Address to a Presto coordinator.
- Port
- Port to a Presto coordinator. `8080` is the default port.
- Schema
- Default schema name of Presto. You can read other schemas by qualified name like `FROM myschema.table1`.
- Catalog
- Catalog (connector) name of Presto such as `hive-cdh4`, `hive-hadoop1`, etc.
- Username
- User name to connect to a Presto.
- **Additional requirements**:
- ``pyhive`` python package.
Hive
----
...
Impala
------
...
URL
---
A URL based data source which requests URLs that return the :doc:`results JSON
format </dev/results_format>`.
Very useful in situations where you want to expose the data without
connecting directly to the database.
The query itself inside re:dash will simply contain the URL to be
executed (i.e. http://myserver/path/myquery)
- **Options**:
- Url - set this if you want to limit queries to certain base path.
Google Spreadsheets
-------------------
- **Options**:
- JSON key file, generated when creating a service account (see `instructions <https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount>`__).
- **Additional requirements**:
- ``gspread`` and ``oauth2client`` python packages.
Notes:
1. To be able to load the spreadsheet in re:dash - share your it with
your ServiceAccount's email (it can be found in the credentials json
file, for example
43242343247-fjdfakljr3r2@developer.gserviceaccount.com).
2. The query format is "DOC\_UUID\|SHEET\_NUM" (for example
"kjsdfhkjh4rsEFSDFEWR232jkddsfh\|0")
Python
------
**Execute other queries, manipulate and compute with Python code**
This is a special query runner, that will execute provided Python code as the query. Useful for various scenarios such as
merging data from different data sources, doing data transformation/manipulation that isn't trivial with SQL, merging
with remote data or using data analysis libraries such as Pandas (see `example query <https://gist.github.com/arikfr/be7c2888520c44cf4f0f>`__).
While the Python query runner uses a sandbox (RestrictedPython), it's not 100% secure and the security depends on the
modules you allow to import. We recommend enabling the Python query runner only in a trusted environment (meaning: behind
VPN and with users you trust).
- **Options**:
- Allowed Modules in a comma separated list (optional). **NOTE:**
You MUST make sure these modules are installed on the machine
running the Celery workers.
Notes:
- For security, the python query runner is disabled by default.
To enable, add ``redash.query_runner.python`` to the ``REDASH_ADDITIONAL_QUERY_RUNNERS`` environmental variable. If you used
the bootstrap script, or one of the provided images, add to ``/opt/redash/.env`` file the line: ``export REDASH_ADDITIONAL_QUERY_RUNNERS=redash.query_runner.python``.
Vertica
-----
- **Options**:
- Database (mandatory)
- User
- Password
- Host
- Port
- **Additional requirements**:
- ``vertica-python`` python package
Oracle
------
- **Options**
- DSN Service name
- User
- Password
- Host
- Port
- **Additional requirements**
- ``cx_Oracle`` python package. This requires the installation of the Oracle `instant client <http://www.oracle.com/technetwork/database/features/instant-client/index-097480.html>`__.
Treasure Data
------
- **Options**
- Type (TreasureData)
- API Key
- Database Name
- Type (Presto/Hive[default])
- **Additional requirements**
- Must have account on https://console.treasuredata.com
Documentation: https://docs.treasuredata.com/articles/redash
Microsoft SQL Server
-----
- **Options**:
- Database (mandatory)
- User #TODO: DB users only? What about domain users?
- Password
- Server
- Port
- **Notes**:
- Data type support is currently quite limited.
- Complex and new types are converted to strings in ``re:dash``
- Coerce into simpler types if needed using ``CAST()``
- Known conversion issues for:
- DATE
- TIME
- DATETIMEOFFSET
- **Additional requirements**:
- ``freetds-dev`` C library
- ``pymsssql`` python package, requires FreeTDS to be installed first

11
docs/dev.rst Normal file
View File

@@ -0,0 +1,11 @@
Developer Information
=====================
.. toctree::
:maxdepth: 2
:glob:
dev/vagrant
dev/*

View File

@@ -0,0 +1,94 @@
Query Execution Model
#####################
Introduction
============
The first datasource which was used with re:dash was Redshift. Because
we had billions of records in Redshift, and some queries were costly to
re-run, from the get go there was the idea of caching query results in
re:dash.
This was to relieve stress from the Redshift cluster and also to improve
user experience.
How queries get executed and cached in re:dash?
===============================================
Server
------
To make sure each query is executed only once at any giving time, we
translate the query to a ``query hash``, using the following code:
.. code:: python
COMMENTS_REGEX = re.compile("/\*.*?\*/")
def gen_query_hash(sql):
sql = COMMENTS_REGEX.sub("", sql)
sql = "".join(sql.split()).lower()
return hashlib.md5(sql.encode('utf-8')).hexdigest()
When query execution is done, the result gets stored to
``query_results`` table. Also we check for all queries in the
``queries`` table that have the same query hash and update their
reference to the query result we just saved
(`code <https://github.com/getredash/redash/blob/master/redash/models.py#L235>`__).
Client
------
The client (UI) will execute queries in two scenarios:
1. (automatically) When opening a query page of a query that doesn't
have a result yet.
2. (manually) When the user clicks on "Execute".
In each case the client does a POST request to ``/api/query_results``
with the following parameters: ``query`` (the query text),
``data_source_id`` (data source to execute the query with) and ``ttl``.
When loading a cached result, ``ttl`` will be the one set to the query
(if it was set). This is a relic from previous versions, and I'm not
sure if it's really used anymore, as usually we will fetch query result
using its id.
When loading a non cached result, ``ttl`` will be 0 which will "force"
the server to execute the query.
As a response to ``/api/query_results`` the server will send either the
query results (in case of a cached query) or job id of the currently
executing query. When job id received the client will start polling on
this id, until a query result received (this is encapsulated in
``Query`` and ``QueryResult`` services).
Ideas on how to implement query parameters
==========================================
Client side only implementation
-------------------------------
(This was actually implemented in. See pull request `#363 <https://github.com/getredash/redash/pull/363>`__ for details.)
The basic idea of how to implement parametized queries is to treat the
query as a template and merge it with parameters taken from query string
or UI (or both).
When the caching facility isn't required (with queries that return in a
reasonable time frame) the implementation can be completely client side
and the backend can be "blind" to the parameters - it just receives the
final query to execute and returns result.
As one improvement over this, we can let the UI/user specify the TTL
value when making the request to ``/api/query_results``, in which case
caching will be availble too, while not having to make the server aware
of the parameters.
Hybrid
------
Another option, will be to store the list of possible parameters for a
query, with their default/optional values. In such case, the server can
prefetch all the options and cache them to provide faster results to the
client.

View File

@@ -0,0 +1,30 @@
Data Source Results Format
==========================
All data sources in re:dash return the following results in JSON format:
.. code:: javascript
{
"columns" : [
{
// Required: a unique identifier of the column name in this result
"name" : "COLUMN_NAME",
// Required: friendly name of the column that will appear in the results
"friendly_name" : "FRIENDLY_NAME",
// Optional: If not specified sort might not work well.
// Supported types: integer, float, boolean, string (default), datetime (ISO-8601 text format)
"type" : "VALUE_TYPE"
},
...
],
"rows" : [
{
// name is the column name as it appears in the columns above.
// VALUE is a valid JSON value. For dates its an ISO-8601 string.
"name" : VALUE,
"name2" : VALUE2
},
...
]
}

38
docs/dev/vagrant.rst Normal file
View File

@@ -0,0 +1,38 @@
Setting up development environment (using Vagrant)
==================================================
To simplify contribution there is a `Vagrant
box <https://vagrantcloud.com/redash/boxes/dev>`__ available with all
the needed software to run re:dash for development (use it only for
development, for demo purposes there is
`redash/demo <https://vagrantcloud.com/redash/boxes/demo>`__ box and the
AWS/GCE images).
To get started with this box:
1. Make sure you have recent version of
`Vagrant <https://www.vagrantup.com/>`__ installed.
2. Clone the re:dash repository:
``git clone https://github.com/getredash/redash.git``.
3. Change dir into the repository (``cd redash``) and run run
``vagrant up``. This might take some time the first time you run it,
as it downloads the Vagrant virtual box.
4. Once Vagrant is ready, ssh into the instance (``vagrant ssh``), and
change dir to ``/opt/redash/current`` -- this is where your local
repository copy synced to.
5. Copy ``.env`` file into this directory (``cp ../.env ./``).
6. From ``/opt/redash/current/rd_ui`` run ``bower install`` to install
frontend packages. This can be done from your host machine as well,
if you have bower installed.
7. Go back to ``/opt/redash/current`` and install python dependencies
``sudo pip install -r requirements.txt``
8. Apply migrations
::
export PYTHONPATH=. && find migrations/ -type f | grep 00 --null | xargs -I file bin/run python file
9. Start the server and background workers with
``bin/run honcho start -f Procfile.dev``.
10. Now the server should be available on your host on port 9001 and you
can login with username admin and password admin.

57
docs/index.rst Normal file
View File

@@ -0,0 +1,57 @@
.. image:: http://redash.io/static/old_img/redash_logo.png
:width: 200px
Open Source Data Collaboration and Visualization Platform
===================================
**re:dash** is our take on freeing the data within our company in a way that will better fit our culture and usage patterns.
Prior to **re:dash**, we tried to use traditional BI suites and discovered a set of bloated, technically challenged and slow tools/flows. What we were looking for was a more hacker'ish way to look at data, so we built one.
**re:dash** was built to allow fast and easy access to billions of records, that we process and collect using Amazon Redshift ("petabyte scale data warehouse" that "speaks" PostgreSQL).
Today **_re:dash_** has support for querying multiple databases, including: Redshift, Google BigQuery,Google Spreadsheets, PostgreSQL, MySQL, Graphite and custom scripts.
Features
########
1. **Query Editor**: think of `JS Fiddle`_ for SQL queries. It's your way to share data in the organization in an open way, by sharing both the dataset and the query that generated it. This way everyone can peer review not only the resulting dataset but also the process that generated it.
2. **Visualizations**: once you have a dataset, you can create different visualizations out of it. Currently it supports charts, pivot table and cohorts.
3. **Dashboards**: combine several visualizations into a single dashboard.
Demo
####
.. figure:: https://raw.github.com/getredash/redash/screenshots/screenshots.gif
:alt: Screenshots
You can try out the demo instance: `http://demo.redash.io`_ (login with any Google account).
.. _http://demo.redash.io: http://demo.redash.io
.. _JS Fiddle: http://jsfiddle.net
Getting Started
###############
:doc:`Setting up re:dash instance </setup>` (includes links to ready made AWS/GCE images).
Getting Help
############
* Source: https://github.com/getredash/redash
* Issues: https://github.com/getredash/redash/issues
* Mailing List: https://groups.google.com/forum/#!forum/redash-users
* Gitter (chat): https://gitter.im/getredash/redash
* Contact Arik, the maintainer directly: arik@redash.io.
TOC
###
.. toctree::
:maxdepth: 2
setup
upgrade
datasources
usage
dev
misc

10
docs/misc.rst Normal file
View File

@@ -0,0 +1,10 @@
Miscellaneous
=============
.. toctree::
:maxdepth: 2
:glob:
misc/*

View File

@@ -0,0 +1,74 @@
How To: Backup your re:dash database and restore it on a different server
=================
**Note:** This guide assumes that the default database name (redash) has not been changed.
1. Check the size of your redash database. This can be done by creating a query within redash itself against the 're:dash metadata' data source.
.. code::
select t1.datname AS db_name, pg_size_pretty(pg_database_size(t1.datname)) as db_size
from pg_database t1
where t1.datname = 'redash'
2. Check the amount of available disk space on your existing server.
.. code::
df -hT
3. Backup the existing redash database.
.. code::
sudo -u redash pg_dump redash | gzip > redash_backup.gz
4. Transfer the backup to the new server.
5. `Perform a clean install of re:dash <http://docs.redash.io/en/latest/setup.html>`__ on the new server.
6. Check the amount of available disk space on the new server.
.. code::
df -hT
7. Login as postgres user on the new server.
.. code::
sudo -u postgres -i
8. drop the current redash database, create a new database named redash, and then restore the backup into the new database.
.. code::
dropdb redash
createdb -T template0 redash
gunzip -c redash_backup.gz | psql redash
9. Set a new password of your choosing for the 'redash_reader' user (since the new installation generated a random password).
.. code::
psql -c "ALTER ROLE redash_reader WITH PASSWORD 'yourpasswordgoeshere';"
**Note:** Then you must navigate to the 're:dash metadata' data source (/data_sources/1) in the new re:dash installation and change the password to match the one entered above.
10. Grant permissions on the redash database to the redash_reader user.
.. code::
psql -c "grant select(id,name,type) ON data_sources to redash_reader;" redash
psql -c "grant select(id,name) ON users to redash_reader;" redash
psql -c "grant select on events, queries, dashboards, widgets, visualizations, query_results to redash_reader;" redash
Create a new query in redash (using re:dash metadata as the data source) to test that everything is working as expected.

View File

@@ -0,0 +1,50 @@
How To: Create a Google Developers Project
==========================================
1. Go to the `Google Developers
Console <https://console.developers.google.com/>`__.
2. Select a project, or create a new one by clicking Create Project:
1. In the Project name field, type in a name for your project.
2. In the Project ID field, optionally type in a project ID for your
project or use the one that the console has created for you. This
ID must be unique world-wide.
3. Click the **Create** button and wait for the project to be
created.
4. Click on the new project name in the list to start editing the
project.
3. In the left sidebar, select the **APIs** item below "APIs & auth". A
list of Google web services appears.
4. Find the **Google+ API** service and set its status to **ON**—notice
that this action moves the service to the top of the list.
5. In the sidebar under "APIs & auth", select **Credentials** and in that screen choose the **OAuth consent screen** tab
- Choose an Email Address and specify a Product Name.
6. In the sidebar under "APIs & auth", select **Credentials**.
7. Click **Add Credentials** button and choose **OAuth 20 Client ID**.
- In the **Application type** section of the dialog, select **Web
application**.
- In the **Authorized JavaScript origins** field, enter the origin
for your app. You can enter multiple origins to use with multiple
re:dash instance. Wildcards are not allowed. In the example below,
we assume your re:dash instance address is *redash.example.com*:
::
http://redash.example.com
https://redash.example.com
- In the Authorized redirect URI field, enter the redirect URI
callback:
::
http://redash.example.com/oauth/google_callback
- Click the ``Create`` button.
8. In the resulting **Client ID for web application** section, copy the
**Client ID** and **Client secret** to your ``.env`` file.

141
docs/misc/letsencrypt.rst Normal file
View File

@@ -0,0 +1,141 @@
How To: Encrypt your re:dash installation with a free SSL certificate from Let's Encrypt
=================
**Note:** This below steps were tested on Ubuntu 14.04, but *should* work with any Debian-based distro.
`Let's Encrypt <https://letsencrypt.org/>`__ is a new certificate authority sponsored by major tech companies including Mozilla, Google, Cisco, and Facebook. Unlike traditional CA authorities, Let's Encrypt allows you to generate and renew an SSL certificate quickly and **at no cost**.
1. Open port 443 in your security group (if using AWS or GCE).
2. Update package lists, install git, and clone the letsencrypt repository.
.. code::
sudo apt-get update
sudo apt-get install git
sudo git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt
3. Stop nginx and redash, then ensure that no processes are still listening on port 80.
.. code::
sudo supervisorctl stop redash_server
sudo service nginx stop
netstat -na | grep ':80.*LISTEN'
4. Generate your letsencrypt certificate.
.. code::
cd /opt/letsencrypt
sudo pip install urllib3[secure] --upgrade
./letsencrypt-auto certonly --standalone
In most cases you'll want to enter 'example.com www.example.com' when prompted for your domain so that you can use the certificate on http://example.com and http://www.example.com.
5. Optionally generate a stronger Diffie-Hellman ephemeral parameter. Without this step, you will not achieve higher than a B score on `SSLLabs <https://www.ssllabs.com/ssltest/>`__. Please note that on a low-end server (VPS or micro/small GCE instance) this step can take approximately 20-30 minutes.
.. code::
cd /etc/ssl/certs
sudo openssl dhparam -out dhparam.pem 3072
6. Backup the existing nginx redash config, delete it, and then create a new version with the code supplied below.
.. code::
sudo cp /etc/nginx/sites-available/redash /etc/nginx/sites-available/redash.bak
sudo rm /etc/nginx/sites-available/redash
sudo nano /etc/nginx/sites-available/redash
.. code:: nginx
upstream redash_servers {
server 127.0.0.1:5000;
}
server {
listen 80;
# Allow accessing /ping without https. Useful when placing behind load balancer.
location /ping {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://redash_servers;
}
location / {
# Enforce SSL.
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
ssl on;
# Make sure to set paths to your certificate .pem and .key files.
ssl_certificate /etc/letsencrypt/live/YOURDOMAIN.TLD/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/YOURDOMAIN.TLD/privkey.pem;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
# Use secure protocols and ciphers which are compatible with modern browsers
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers AES256+EECDH:AES256+EDH;
ssl_session_cache shared:SSL:20m;
# Enforce strict transport security
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";
access_log /var/log/nginx/redash.access.log;
gzip on;
gzip_types *;
gzip_proxied any;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://redash_servers;
proxy_redirect off;
}
}
7. Start the nginx and redash servers again.
.. code::
sudo service nginx start
sudo supervisorctl start redash_server
8. Verify the installation by running a `SSLLabs test <https://www.ssllabs.com/ssltest/>`__. This guide *should* yield an A+ score. If everything is working as expected, optionally delete the old redash nginx config:
.. code::
sudo rm /etc/nginx/sites-available/redash.bak
**Important Note:** letsencrypt certificates only remain valid for 90 days. To renew your certificate, simply follow steps 3 and 4 again:
.. code::
sudo supervisorctl stop redash_server
sudo service nginx stop
netstat -na | grep ':80.*LISTEN'
cd /opt/letsencrypt
./letsencrypt-auto certonly --standalone
sudo service nginx start
sudo supervisorctl start redash_server

59
docs/misc/ssl.rst Normal file
View File

@@ -0,0 +1,59 @@
SSL (HTTPS) Setup
=================
If you used the provided images or the bootstrap script, to start using
SSL with your instance you need to:
1. Update the nginx config file (``/etc/nginx/sites-available/redash``)
with SSL configuration (see below an example). Make sure to upload
the certificate to the server, and set the paths correctly in the new
config.
2. Open port 443 in your security group (if using AWS or GCE).
.. code:: nginx
upstream redash_servers {
server 127.0.0.1:5000;
}
server {
listen 80;
# Allow accessing /ping without https. Useful when placing behind load balancer.
location /ping {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://redash_servers;
}
location / {
# Enforce SSL.
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
# Make sure to set paths to your certificate .pem and .key files.
ssl on;
ssl_certificate /path-to/cert.pem; # or crt
ssl_certificate_key /path-to/cert.key;
access_log /var/log/nginx/redash.access.log;
gzip on;
gzip_types *;
gzip_proxied any;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://redash_servers;
proxy_redirect off;
}
}

3
docs/requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
sphinx
sphinx-autobuild
sphinx_rtd_theme

62
docs/settings.rst Normal file
View File

@@ -0,0 +1,62 @@
Settings
########
Much of the functionality of re:dash can be changes with settings. Settings are read by `/redash/settings.py` from environment variables which (for most installs) can be set in `/opt/redash/current/.env`
The follow is a list of settings and what they control:
- **REDASH_NAME**: name of the site, used in page titles, *default "re:dash"*
- **REDASH_REDIS_URL**: *default "redis://localhost:6379/0"*
- **REDASH_PROXIES_COUNT**: *default "1"*
- **REDASH_STATSD_HOST**: *default "127.0.0.1"*
- **REDASH_STATSD_PORT**: *default "8125"*
- **REDASH_STATSD_PREFIX**: *default "redash"*
- **REDASH_DATABASE_URL**: *default "postgresql://postgres"*
- **REDASH_CELERY_BROKER**: *default REDIS_URL*
- **REDASH_CELERY_BACKEND**: *default CELERY_BROKER*
- **REDASH_QUERY_RESULTS_CLEANUP_ENABLED**: *default "true"*
- **REDASH_QUERY_RESULTS_CLEANUP_COUNT**: *default "100"*
- **REDASH_QUERY_RESULTS_CLEANUP_MAX_AGE**: *default "7"*
- **REDASH_AUTH_TYPE**: *default "api_key"*
- **REDASH_PASSWORD_LOGIN_ENABLED**: *default "true"*
- **REDASH_ENFORCE_HTTPS**: *default "false"*
- **REDASH_MULTI_ORG**: *default "false"*
- **REDASH_GOOGLE_APPS_DOMAIN**: *default ""*
- **REDASH_GOOGLE_CLIENT_ID**: *default ""*
- **REDASH_GOOGLE_CLIENT_SECRET**: *default ""*
- **REDASH_SAML_METADATA_URL**: *default ""*
- **REDASH_SAML_CALLBACK_SERVER_NAME**: *default ""*
- **REDASH_STATIC_ASSETS_PATH**: *default "../rd_ui/app/"*
- **REDASH_JOB_EXPIRY_TIME**: *default 3600 * 6*
- **REDASH_COOKIE_SECRET**: *default "c292a0a3aa32397cdb050e233733900f"*
- **REDASH_LOG_LEVEL**: *default "INFO"*
- **REDASH_ANALYTICS**: *default ""*
- **REDASH_MAIL_SERVER**: *default "localhost"*
- **REDASH_MAIL_PORT**: *default 25*
- **REDASH_MAIL_USE_TLS**: *default "false"*
- **REDASH_MAIL_USE_SSL**: *default "false"*
- **REDASH_MAIL_USERNAME**: *default None*
- **REDASH_MAIL_PASSWORD**: *default None*
- **REDASH_MAIL_DEFAULT_SENDER**: *default None*
- **REDASH_MAIL_MAX_EMAILS**: *default None*
- **REDASH_MAIL_ASCII_ATTACHMENTS**: *default "false"*
- **REDASH_HOST**: *default ""*
- **REDASH_HIPCHAT_API_TOKEN**: *default None*
- **REDASH_HIPCHAT_API_URL**: *default None*
- **REDASH_HIPCHAT_ROOM_ID**: *default None*
- **REDASH_WEBHOOK_ENDPOINT**: *default None*
- **REDASH_WEBHOOK_USERNAME**: *default None*
- **REDASH_CORS_ACCESS_CONTROL_ALLOW_ORIGIN**: *default ""*
- **REDASH_CORS_ACCESS_CONTROL_ALLOW_CREDENTIALS**: *default "false"*
- **REDASH_CORS_ACCESS_CONTROL_REQUEST_METHOD**: *default GET, POST, PUT""*
- **REDASH_CORS_ACCESS_CONTROL_ALLOW_HEADERS**: *default "Content-Type"*
- **REDASH_ENABLED_QUERY_RUNNERS**: *default ",".join(default_query_runners)*
- **REDASH_ADDITIONAL_QUERY_RUNNERS**: *default ""*
- **REDASH_SENTRY_DSN**: *default ""*
- **REDASH_ALLOW_SCRIPTS_IN_USER_INPUT**: disable sanitization of text input, allowing full HTML, *default "true"*
- **REDASH_DATE_FORMAT**: *default "DD/MM/YY"*
- **REDASH_FEATURE_ALLOW_ALL_TO_EDIT**: *default "true"*
- **REDASH_FEATURE_TABLES_PERMISSIONS**: *default "false"*
- **REDASH_VERSION_CEHCK**: *default "true"*
- **REDASH_BIGQUERY_HTTP_TIMEOUT**: *default "600"*
- **REDASH_SCHEMA_RUN_TABLE_SIZE_CALCULATIONS**: *default "false"*

180
docs/setup.rst Normal file
View File

@@ -0,0 +1,180 @@
Setting up re:dash instance
###########################
The `provisioning
script <https://raw.githubusercontent.com/getredash/redash/master/setup/ubuntu/bootstrap.sh>`__
works on Ubuntu 12.04, Ubuntu 14.04 and Debian Wheezy. This script
installs all needed dependencies and creates basic setup.
To ease the process, there are also images for AWS and Google Compute
Cloud. These images created with the same provision script using Packer.
Create an instance
==================
AWS
---
Launch the instance with from the pre-baked AMI (for small deployments
t2.micro should be enough):
- us-east-1: `ami-a7ddfbcd <https://console.aws.amazon.com/ec2/home?region=us-east-1#LaunchInstanceWizard:ami=ami-a7ddfbcd>`__
- us-west-1: `ami-269feb46 <https://console.aws.amazon.com/ec2/home?region=us-west-1#LaunchInstanceWizard:ami=ami-269feb46>`__
- us-west-2: `ami-435fba23 <https://console.aws.amazon.com/ec2/home?region=us-west-2#LaunchInstanceWizard:ami=ami-435fba23>`__
- eu-west-1: `ami-b4c277c7 <https://console.aws.amazon.com/ec2/home?region=eu-west-1#LaunchInstanceWizard:ami=ami-b4c277c7>`__
- eu-central-1: `ami-07ced76b <https://console.aws.amazon.com/ec2/home?region=eu-central-1#LaunchInstanceWizard:ami=ami-07ced76b>`__
- sa-east-1: `ami-6e2eaf02 <https://console.aws.amazon.com/ec2/home?region=sa-east-1#LaunchInstanceWizard:ami=ami-6e2eaf02>`__
- ap-northeast-1: `ami-aa5a64c4 <https://console.aws.amazon.com/ec2/home?region=ap-northeast-1#LaunchInstanceWizard:ami=ami-aa5a64c4>`__
- ap-southeast-1: `ami-1c45897f <https://console.aws.amazon.com/ec2/home?region=ap-southeast-1#LaunchInstanceWizard:ami=ami-1c45897f>`__
- ap-southeast-2: `ami-42b79221 <https://console.aws.amazon.com/ec2/home?region=ap-southeast-2#LaunchInstanceWizard:ami=ami-42b79221>`__
(the above AMIs are of version: 0.9.1)
When launching the instance make sure to use a security group, that **only** allows incoming traffic on: port 22 (SSH), 80 (HTTP) and 443 (HTTPS).
Now proceed to `"Setup" <#setup>`__.
Google Compute Engine
---------------------
First, you need to add the images to your account:
.. code:: bash
$ gcloud compute images create "redash-091-b1377" --source-uri gs://redash-images/redash.0.9.1.b1377.tar.gz
Next you need to launch an instance using this image (n1-standard-1
instance type is recommended). If you plan using re:dash with BigQuery,
you can use a dedicated image which comes with BigQuery preconfigured
(using instance permissions):
.. code:: bash
$ gcloud compute images create "redash-091-b1377-bq" --source-uri gs://redash-images/redash.0.9.1.b1377-bq.tar.gz
Note that you need to launch this instance with BigQuery access:
.. code:: bash
$ gcloud compute instances create <your_instance_name> --image redash-091-b1377-bq --scopes storage-ro,bigquery
(the same can be done from the web interface, just make sure to enable
BigQuery access)
Now proceed to `"Setup" <#setup>`__.
Other
-----
Download the provision script and run it on your machine. Note that:
1. You need to run the script as root.
2. It was tested only on Ubuntu 12.04, Ubuntu 14.04 and Debian Wheezy.
3. It's designed to run on a "clean" machine. If you're running this script on a machine that is used for other purposes, you might want to tweak it to your needs (like removing the ``apt-get dist-upgrade`` call at the beginning of it).
Setup
=====
Once you created the instance with either the image or the script, you
should have a running re:dash instance with everything you need to get
started. You can now login to it with the user "admin" (password:
"admin"). But to make it useful, there are a few more steps that you
need to manually do to complete the setup:
First ssh to your instance and change directory to ``/opt/redash``. If
you're using the GCE image, switch to root (``sudo su``).
Users & Google Authentication setup
-----------------------------------
Most of the settings you need to edit are in the ``/opt/redash/.env``
file.
1. Update the cookie secret (important! otherwise anyone can sign new
cookies and impersonate users): change "veryverysecret" in the line:
``export REDASH_COOKIE_SECRET=veryverysecret`` to something else (you
can run the command ``pwgen 32 -1`` to generate a random string).
2. By default we create an admin user with the password "admin". You
can change this password opening the: ``/users/me#password`` page after
logging in as admin.
3. If you want to use Google OAuth to authenticate users, you need to
create a Google Developers project (see :doc:`instructions </misc/google_developers_project>`)
and then add the needed configuration in the ``.env`` file:
.. code::
export REDASH_GOOGLE_CLIENT_ID=""
export REDASH_GOOGLE_CLIENT_SECRET=""
4. Configure the domain(s) you want to allow to use with Google Apps, by running the command:
.. code::
cd /opt/redash/current
sudo -u redash bin/run ./manage.py org set_google_apps_domains {{domains}}
If you're passing multiple domains, separate them with commas.
5. Restart the web server to apply the configuration changes:
``sudo supervisorctl restart redash_server``.
6. Once you have Google OAuth enabled, you can login using your Google
Apps account. If you want to grant admin permissions to some users,
you can do this by adding them to the admin group (from ``/groups`` page).
7. If you don't use Google OAuth or just need username/password logins,
you can create additional users by opening the ``/users/new`` page.
Datasources
-----------
To make re:dash truly useful, you need to setup your data sources in it. Browse to ``/data_sources`` on your instance,
to create new data source connection.
See :doc:`documentation </datasources>` for the different options.
Your instance comes ready with dependencies needed to setup supported sources.
Mail Configuration
------------------
For the system to be able to send emails (for example when alerts trigger), you need to set the mail server to use and the
host name of your re:dash server. If you're using one of our images, you can do this by editing the `.env` file:
.. code::
# Note that not all values are required, as they have default values.
export REDASH_MAIL_SERVER="" # default: localhost
export REDASH_MAIL_PORT="" # default: 25
export REDASH_MAIL_USE_TLS="" # default: False
export REDASH_MAIL_USE_SSL="" # default: False
export REDASH_MAIL_USERNAME="" # default: None
export REDASH_MAIL_PASSWORD="" # default: None
export REDASH_MAIL_DEFAULT_SENDER="" # Email address to send from
export REDASH_HOST="" # base address of your re:dash instance, for example: "https://demo.redash.io"
- Note that not all values are required, as there are default values.
- It's recommended to use some mail service, like `Amazon SES <https://aws.amazon.com/ses/>`__, `Mailgun <http://www.mailgun.com/>`__
or `Mandrill <http://mandrillapp.com>`__ to send emails to ensure deliverability.
To test email configuration, you can run `bin/run ./manage.py send_test_mail` (from `/opt/redash/current`).
How to upgrade?
---------------
It's recommended to upgrade once in a while your re:dash instance to
benefit from bug fixes and new features. See :doc:`here </upgrade>` for full upgrade
instructions (including Fabric script).
Notes
=====
- If this is a production setup, you should enforce HTTPS and make sure
you set the cookie secret (see :doc:`instructions </misc/ssl>`).

36
docs/upgrade.rst Normal file
View File

@@ -0,0 +1,36 @@
How to Upgrade
##############
It's recommended to upgrade your re:dash instance once there are new
releases, to benefit from new features and bug fixes. The upgrade
process is relatively simple, and assuming you used one of the base
images we provide, you can just use the
`Fabric <http://www.fabfile.org/>`__ script provided here:
https://gist.github.com/arikfr/440d1403b4aeb76ebaf8.
How to run the Fabric script
============================
1. Install Fabric: ``pip install fabric requests`` (needed only once)
2. Download the ``fabfile.py`` from the gist.
3. Run the script:
``fab -H{your re:dash host} -u{the ssh user for this host} -i{path to key file for passwordless login} deploy_latest_release``
``-i`` is optional and it is only needed in case you're using private-key based authentication (and didn't add the key file to your authentication agent or set its path in your SSH config).
What the Fabric script does
===========================
Even if you didn't use the image, it's very likely you can reuse most of
this script with small modifications. What this script does is:
1. Find the URL of the latest release tarball (from `GitHub releases
page <github.com/getredash/redash/releases>`__).
2. Download it.
3. Create new directory for this version (for example:
``/opt/redash/redash.0.5.0.b685``).
4. Unpack that (``tar -C {dir} -xvf {tarball path}``).
5. Link ``/opt/redash/.env`` file into this directory.
6. Apply any new migrations.
7. Link ``/opt/redash/current`` to new version.
8. Restart web server and celery workers.

11
docs/usage.rst Normal file
View File

@@ -0,0 +1,11 @@
Usage
=====
.. toctree::
:maxdepth: 2
:glob:
usage/maintenance.rst
usage/*

View File

@@ -0,0 +1,72 @@
ElasticSearch: Querying
#######################
ElasticSearch currently supports only simple Lucene style queries (like
Kibana but without the aggregation).
Full blown JSON based ElasticSearch queries (including aggregations)
will be added later.
Simple query example:
=====================
- Query the index named "twitter"
- Filter by "user:kimchy"
- Return the fields: "@timestamp", "tweet" and "user"
- Return up to 15 results
- Sort by @timestamp ascending
.. code:: json
{
"index" : "twitter",
"query" : "user:kimchy",
"fields" : ["@timestamp", "tweet", "user"],
"size" : 15,
"sort" : "@timestamp:asc"
}
Simple query on a logstash ElasticSearch instance:
==================================================
- Query the index named "logstash-2015.04.\*" (in this case its all of
April 2015)
- Filter by type:events AND eventName:UserUpgrade AND channel:selfserve
- Return fields: "@timestamp", "userId", "channel", "utm\_source",
"utm\_medium", "utm\_campaign", "utm\_content"
- Return up to 250 results
- Sort by @timestamp ascending
.. code:: json
{
"index" : "logstash-2015.04.*",
"query" : "type:events AND eventName:UserUpgrade AND channel:selfserve",
"fields" : ["@timestamp", "userId", "channel", "utm_source", "utm_medium", "utm_campaign", "utm_content"],
"size" : 250,
"sort" : "@timestamp:asc"
}
Simple query on a ElasticSearch instance:
==================================================
- Query the index named "twitter"
- Filter by user equal "kimchy"
- Return the fields: "@timestamp", "tweet" and "user"
- Return up to 15 results
- Sort by @timestamp ascending
.. code:: json
{
"index" : "twitter",
"query" : {
"match": {
"user" : "kimchy"
}
},
"fields" : ["@timestamp", "tweet", "user"],
"size" : 15,
"sort" : "@timestamp:asc"
}

View File

@@ -0,0 +1,72 @@
Ongoing Maintanence and Basic Operations
########################################
Configuration and logs
======================
The supervisor config can be found in
``/opt/redash/supervisord/supervisord.conf``.
There you can see the names of its programs (``redash_celery``,
``redash_server``) and the location of their logs.
Restart
=======
Restarting the Web Server
-------------------------
``sudo supervisorctl stop redash_server``
Restarting Celery Workers
-------------------------
``sudo supervisorctl restart redash_celery``
Restarting Celery Workers & the Queries Queue
---------------------------------------------
In case you are handling a problem, and you need to stop the currently
running queries and reset the queue, follow the steps below.
1. Stop celery: ``sudo supervisorctl stop redash_celery`` (celery might
take some time to stop, if it's in the middle of running a query)
2. Flush redis: ``redis-cli flushall``.
3. Start celery: ``sudo supervisorctl start redash_celery``
Changing the Number of Workers
==============================
By default, Celery will start a worker per CPU core. Because most of
re:dash's tasks are IO bound, the real limit for number of workers you
can use depends on the amount of memory your machine has. It's
recommended to increase number of workers, to support more concurrent
queries.
1. Open the supervisord configuration file:
``/opt/redash/supervisord/supervisord.conf``
2. Edit the ``[program:redash_celery]`` section and add to the *command*
value, the param "-c" with the number of concurrent workers you need.
3. Restart supervisord to apply new configuration:
``sudo /etc/init.d/redash_supervisord restart``.
DB
==
Backup re:dash's DB:
--------------------
Uncompressed backup: ``sudo -u redash pg_dump > backup_filename.sql``
Compressed backup: ``sudo -u redash pg_dump redash | gzip > backup_filename.gz``
Version
=======
See current version:
``bin/run ./manage.py version``

View File

@@ -0,0 +1,74 @@
MongoDB: Querying
#################
Simple query example:
=====================
.. code:: json
{
"collection" : "my_collection",
"query" : {
"date" : {
"$gt" : "ISODate(\"2015-01-15 11:41\")",
},
"type" : 1
},
"fields" : {
"_id" : 1,
"name" : 2
},
"sort" : [
{
"name" : "date",
"direction" : -1
}
]
}
Live example on the demo instance:
http://demo.redash.io/queries/394/source.
Aggregation
===========
Uses a syntax similar to the one used in PyMongo, however to support the
correct order of sorting, it uses a regular list for the "$sort"
operation that converts into a SON (sorted dictionary) object before
execution.
Aggregation query example:
.. code:: json
{
"collection" : "things",
"aggregate" : [
{
"$unwind" : "$tags"
},
{
"$group" : {
"_id" : "$tags",
"count" : { "$sum" : 1 }
}
},
{
"$sort" : [
{
"name" : "count",
"direction" : -1
},
{
"name" : "_id",
"direction" : -1
}
]
}
]
}
Live examples on the demo instance:
1. http://demo.redash.io/queries/393/source
2. http://demo.redash.io/queries/387/source

View File

@@ -4,19 +4,19 @@ CLI to manage redash.
"""
import json
from flask.ext.script import Manager
from flask_script import Manager
from redash import settings, models, __version__
from redash.wsgi import app
from redash.import_export import import_manager
from redash.cli import users, database, data_sources
from redash.cli import users, database, data_sources, organization
from redash.monitor import get_status
manager = Manager(app)
manager.add_command("database", database.manager)
manager.add_command("users", users.manager)
manager.add_command("import", import_manager)
manager.add_command("ds", data_sources.manager)
manager.add_command("org", organization.manager)
@manager.command
@@ -43,12 +43,15 @@ def make_shell_context():
@manager.command
def check_settings():
"""Show the settings as re:dash sees them (useful for debugging)."""
from types import ModuleType
for name, item in settings.all_settings().iteritems():
print "{} = {}".format(name, item)
for name in dir(settings):
item = getattr(settings, name)
if not callable(item) and not name.startswith("__") and not isinstance(item, ModuleType):
print "{} = {}".format(name, item)
@manager.command
def send_test_mail():
from redash import mail
from flask_mail import Message
mail.send(Message(subject="Test Message from re:dash", recipients=[settings.MAIL_DEFAULT_SENDER], body="Test message."))
if __name__ == '__main__':

View File

@@ -18,4 +18,3 @@ if __name__ == '__main__':
db.database.execute_sql("ALTER TABLE {} ALTER COLUMN {} TYPE timestamp with time zone;".format(*column))
db.close_db(None)

View File

@@ -1,13 +1,31 @@
import json
import jsonschema
from jsonschema import ValidationError
from redash import query_runner
from redash.models import DataSource
def validate_configuration(query_runner_type, configuration_json):
query_runner_class = query_runner.query_runners.get(query_runner_type, None)
if query_runner_class is None:
return False
try:
if isinstance(configuration_json, basestring):
configuration = json.loads(configuration_json)
else:
configuration = configuration_json
jsonschema.validate(configuration, query_runner_class.configuration_schema())
except (ValidationError, ValueError):
return False
return True
def update(data_source):
print "[%s] Old options: %s" % (data_source.name, data_source.options)
if query_runner.validate_configuration(data_source.type, data_source.options):
if validate_configuration(data_source.type, data_source.options):
print "[%s] configuration already valid. skipping." % data_source.name
return
@@ -65,9 +83,9 @@ def update(data_source):
print "[%s] No need to convert type of: %s" % (data_source.name, data_source.type)
print "[%s] New options: %s" % (data_source.name, data_source.options)
data_source.save()
data_source.save(only=data_source.dirty_fields)
if __name__ == '__main__':
for data_source in DataSource.all():
update(data_source)
for data_source in DataSource.select(DataSource.id, DataSource.name, DataSource.type, DataSource.options):
update(data_source)

View File

@@ -23,4 +23,3 @@ if __name__ == '__main__':
db.database.execute_sql("UPDATE widgets SET updated_at = created_at;")
db.close_db(None)

View File

@@ -15,5 +15,3 @@ if __name__ == '__main__':
db.database.execute_sql("UPDATE queries SET last_modified_by_id = user_id;")
db.close_db(None)

View File

@@ -19,5 +19,3 @@ if __name__ == '__main__':
)
db.close_db(None)

View File

@@ -14,14 +14,11 @@ if __name__ == '__main__':
migrator.add_column('users', 'api_key', models.User.api_key),
)
for user in models.User.select():
user.save()
for user in models.User.select(models.User.id, models.User.api_key):
user.save(only=user.dirty_fields)
migrate(
migrator.add_not_null('users', 'api_key')
)
db.close_db(None)

View File

@@ -0,0 +1,14 @@
from playhouse.migrate import PostgresqlMigrator, migrate
from redash.models import db
if __name__ == '__main__':
db.connect_db()
migrator = PostgresqlMigrator(db.database)
with db.database.transaction():
migrate(
migrator.drop_not_null('queries', 'data_source_id'),
)
db.close_db(None)

View File

@@ -0,0 +1,8 @@
from redash.models import db, Alert, AlertSubscription
if __name__ == '__main__':
with db.database.transaction():
Alert.create_table()
AlertSubscription.create_table()
db.close_db(None)

View File

@@ -0,0 +1,44 @@
from base64 import b64encode
import json
from redash.models import DataSource
def convert_p12_to_pem(p12file):
from OpenSSL import crypto
with open(p12file, 'rb') as f:
p12 = crypto.load_pkcs12(f.read(), "notasecret")
return crypto.dump_privatekey(crypto.FILETYPE_PEM, p12.get_privatekey())
if __name__ == '__main__':
for ds in DataSource.select(DataSource.id, DataSource.type, DataSource.options):
if ds.type == 'bigquery':
options = json.loads(ds.options)
if 'jsonKeyFile' in options:
continue
new_options = {
'projectId': options['projectId'],
'jsonKeyFile': b64encode(json.dumps({
'client_email': options['serviceAccount'],
'private_key': convert_p12_to_pem(options['privateKey'])
}))
}
ds.options = json.dumps(new_options)
ds.save(only=ds.dirty_fields)
elif ds.type == 'google_spreadsheets':
options = json.loads(ds.options)
if 'jsonKeyFile' in options:
continue
with open(options['credentialsFilePath']) as f:
new_options = {
'jsonKeyFile': b64encode(f.read())
}
ds.options = json.dumps(new_options)
ds.save(only=ds.dirty_fields)

View File

@@ -0,0 +1,7 @@
from redash import models
if __name__ == '__main__':
default_group = models.Group.select(models.Group.id, models.Group.permissions).where(models.Group.name=='default').first()
default_group.permissions.append('list_users')
default_group.save(only=[models.Group.permissions])

View File

@@ -0,0 +1,23 @@
import json
from redash import models
if __name__ == '__main__':
for vis in models.Visualization.select():
if vis.type == 'COUNTER':
options = json.loads(vis.options)
print "Before: ", options
if 'rowNumber' in options and options['rowNumber'] is not None:
options['rowNumber'] += 1
else:
options['rowNumber'] = 1
if 'counterColName' not in options:
options['counterColName'] = 'counter'
if 'targetColName' not in options:
options['targetColName'] = 'target'
options['targetRowNumber'] = options['rowNumber']
print "After: ", options
vis.options = json.dumps(options)
vis.save()

View File

@@ -0,0 +1,21 @@
import peewee
from playhouse.migrate import PostgresqlMigrator, migrate
from redash.models import db
from redash import models
if __name__ == '__main__':
db.connect_db()
migrator = PostgresqlMigrator(db.database)
cursor = db.database.execute_sql("SELECT column_name FROM information_schema.columns WHERE table_name='alerts' and column_name='rearm';")
if cursor.rowcount > 0:
print "Column exists. Skipping."
exit()
with db.database.transaction():
migrate(
migrator.add_column('alerts', 'rearm', models.Alert.rearm),
)
db.close_db(None)

View File

@@ -0,0 +1,10 @@
__author__ = 'lior'
from redash.models import DataSource
if __name__ == '__main__':
for ds in DataSource.select(DataSource.id, DataSource.type):
if ds.type == 'elasticsearch':
ds.type = 'kibana'
ds.save(only=ds.dirty_fields)

View File

@@ -0,0 +1,6 @@
from redash import models
if __name__ == '__main__':
default_group = models.Group.select(models.Group.id, models.Group.permissions).where(models.Group.name=='default').first()
default_group.permissions.append('schedule_query')
default_group.save(only=[models.Group.permissions])

View File

@@ -0,0 +1,9 @@
from redash.models import db, Alert, AlertSubscription
if __name__ == '__main__':
with db.database.transaction():
# There was an AWS/GCE image created without this table, to make sure this exists we run this migration.
if not AlertSubscription.table_exists():
AlertSubscription.create_table()
db.close_db(None)

View File

@@ -0,0 +1,14 @@
from playhouse.migrate import PostgresqlMigrator, migrate
from redash.models import db
if __name__ == '__main__':
db.connect_db()
migrator = PostgresqlMigrator(db.database)
with db.database.transaction():
migrate(
migrator.drop_column('groups', 'tables')
)
db.close_db(None)

View File

@@ -0,0 +1,34 @@
from redash.models import db, Organization, Group
from redash import settings
from playhouse.migrate import PostgresqlMigrator, migrate
if __name__ == '__main__':
migrator = PostgresqlMigrator(db.database)
with db.database.transaction():
Organization.create_table()
default_org = Organization.create(name="Default", slug='default', settings={
Organization.SETTING_GOOGLE_APPS_DOMAINS: list(settings.GOOGLE_APPS_DOMAIN)
})
column = Group.org
column.default = default_org
migrate(
migrator.add_column('groups', 'org_id', column),
migrator.add_column('events', 'org_id', column),
migrator.add_column('data_sources', 'org_id', column),
migrator.add_column('users', 'org_id', column),
migrator.add_column('dashboards', 'org_id', column),
migrator.add_column('queries', 'org_id', column),
migrator.add_column('query_results', 'org_id', column),
)
# Change the uniqueness constraint on user email to be (org, email):
migrate(
migrator.drop_index('users', 'users_email'),
migrator.add_index('users', ('org_id', 'email'), unique=True)
)
db.close_db(None)

View File

@@ -0,0 +1,44 @@
from collections import defaultdict
from redash.models import db, DataSourceGroup, DataSource, Group, Organization, User
from playhouse.migrate import PostgresqlMigrator, migrate
import peewee
if __name__ == '__main__':
migrator = PostgresqlMigrator(db.database)
with db.database.transaction():
# Add type to groups
migrate(
migrator.add_column('groups', 'type', Group.type)
)
for name in ['default', 'admin']:
group = Group.get(Group.name==name)
group.type = Group.BUILTIN_GROUP
group.save()
# Create association table between data sources and groups
DataSourceGroup.create_table()
# add default to existing data source:
default_org = Organization.get_by_id(1)
default_group = Group.get(Group.name=="default")
for ds in DataSource.all(default_org):
DataSourceGroup.create(data_source=ds, group=default_group)
# change the groups list on a user object to be an ids list
migrate(
migrator.rename_column('users', 'groups', 'old_groups'),
)
migrate(migrator.add_column('users', 'groups', User.groups))
group_map = dict(map(lambda g: (g.name, g.id), Group.select()))
user_map = defaultdict(list)
for user in User.select(User, peewee.SQL('old_groups')):
group_ids = [group_map[group] for group in user.old_groups]
user.update_instance(groups=group_ids)
migrate(migrator.drop_column('users', 'old_groups'))
db.close_db(None)

View File

@@ -0,0 +1,6 @@
from redash import models
if __name__ == '__main__':
admin_group = models.Group.get(models.Group.name=='admin')
admin_group.permissions.append('super_admin')
admin_group.save()

View File

@@ -0,0 +1,19 @@
from redash.models import db
import peewee
from playhouse.migrate import PostgresqlMigrator, migrate
if __name__ == '__main__':
migrator = PostgresqlMigrator(db.database)
with db.database.transaction():
# Change the uniqueness constraint on data source name to be (org, name):
# In some cases it's a constraint:
db.database.execute_sql('ALTER TABLE data_sources DROP CONSTRAINT IF EXISTS unique_name')
# In others only an index:
db.database.execute_sql('DROP INDEX IF EXISTS data_sources_name')
migrate(
migrator.add_index('data_sources', ('org_id', 'name'), unique=True)
)
db.close_db(None)

View File

@@ -1,13 +0,0 @@
from playhouse.migrate import Migrator
from redash import db
from redash import models
if __name__ == '__main__':
db.connect_db()
migrator = Migrator(db.database)
with db.database.transaction():
migrator.add_column(models.Dashboard, models.Dashboard.created_at, 'created_at')
migrator.add_column(models.Widget, models.Widget.created_at, 'created_at')
db.close_db(None)

View File

@@ -1,12 +0,0 @@
from playhouse.migrate import Migrator
from redash import models
from redash.models import db
if __name__ == '__main__':
db.connect_db()
migrator = Migrator(db.database)
with db.database.transaction():
migrator.add_column(models.Dashboard, models.Dashboard.dashboard_filters_enabled, 'dashboard_filters_enabled')
db.close_db(None)

View File

@@ -1,12 +0,0 @@
from playhouse.migrate import Migrator
from redash import db
from redash import models
if __name__ == '__main__':
db.connect_db()
migrator = Migrator(db.database)
with db.database.transaction():
migrator.add_column(models.User, models.User.password_hash, 'password_hash')
db.close_db(None)

View File

@@ -1,13 +0,0 @@
from playhouse.migrate import Migrator
from redash import db
from redash import models
if __name__ == '__main__':
db.connect_db()
migrator = Migrator(db.database)
with db.database.transaction():
migrator.add_column(models.User, models.User.permissions, 'permissions')
models.User.update(permissions=['admin'] + models.User.DEFAULT_PERMISSIONS).where(models.User.is_admin == True).execute()
db.close_db(None)

View File

@@ -1,13 +0,0 @@
from playhouse.migrate import Migrator
from redash.models import db
from redash import models
if __name__ == '__main__':
db.connect_db()
migrator = Migrator(db.database)
with db.database.transaction():
migrator.add_column(models.DataSource, models.DataSource.queue_name, 'queue_name')
migrator.add_column(models.DataSource, models.DataSource.scheduled_queue_name, 'scheduled_queue_name')
db.close_db(None)

View File

@@ -1,13 +0,0 @@
from playhouse.migrate import Migrator
from redash.models import db
from redash import models
if __name__ == '__main__':
db.connect_db()
migrator = Migrator(db.database)
with db.database.transaction():
migrator.add_column(models.Widget, models.Widget.text, 'text')
migrator.set_nullable(models.Widget, models.Widget.visualization, True)
db.close_db(None)

View File

@@ -1,13 +0,0 @@
import peewee
from redash import db
from redash import models
if __name__ == '__main__':
db.connect_db()
previous_default_permissions = models.User.DEFAULT_PERMISSIONS[:]
previous_default_permissions.remove('view_query')
models.User.update(permissions=peewee.fn.array_append(models.User.permissions, 'view_query')).where(peewee.SQL("'view_source' = any(permissions)")).execute()
db.close_db(None)

View File

@@ -1,12 +0,0 @@
from playhouse.migrate import Migrator
from redash import db
from redash import models
if __name__ == '__main__':
db.connect_db()
migrator = Migrator(db.database)
with db.database.transaction():
migrator.set_nullable(models.Query, models.Query.description, True)
db.close_db(None)

View File

@@ -1,13 +0,0 @@
from playhouse.migrate import Migrator
from redash import db
from redash import models
if __name__ == '__main__':
db.connect_db()
migrator = Migrator(db.database)
with db.database.transaction():
migrator.set_nullable(models.Widget, models.Widget.query_id, True)
migrator.set_nullable(models.Widget, models.Widget.type, True)
db.close_db(None)

View File

@@ -1,11 +0,0 @@
from redash import db
from redash import models
if __name__ == '__main__':
db.connect_db()
if not models.ActivityLog.table_exists():
print "Creating activity_log table..."
models.ActivityLog.create_table()
db.close_db(None)

View File

@@ -1,48 +0,0 @@
import logging
import peewee
from playhouse.migrate import Migrator
from redash import db
from redash import models
from redash import settings
if __name__ == '__main__':
db.connect_db()
if not models.DataSource.table_exists():
print "Creating data_sources table..."
models.DataSource.create_table()
default_data_source = models.DataSource.create(name="Default",
type=settings.CONNECTION_ADAPTER,
options=settings.CONNECTION_STRING)
else:
default_data_source = models.DataSource.select().first()
migrator = Migrator(db.database)
models.Query.data_source.null = True
models.QueryResult.data_source.null = True
try:
with db.database.transaction():
migrator.add_column(models.Query, models.Query.data_source, "data_source_id")
except peewee.ProgrammingError:
print "Failed to create data_source_id column -- assuming it already exists"
try:
with db.database.transaction():
migrator.add_column(models.QueryResult, models.QueryResult.data_source, "data_source_id")
except peewee.ProgrammingError:
print "Failed to create data_source_id column -- assuming it already exists"
print "Updating data source to existing one..."
models.Query.update(data_source=default_data_source.id).execute()
models.QueryResult.update(data_source=default_data_source.id).execute()
with db.database.transaction():
print "Setting data source to non nullable..."
migrator.set_nullable(models.Query, models.Query.data_source, False)
with db.database.transaction():
print "Setting data source to non nullable..."
migrator.set_nullable(models.QueryResult, models.QueryResult.data_source, False)
db.close_db(None)

View File

@@ -1,12 +0,0 @@
from redash.models import db
from redash import models
if __name__ == '__main__':
db.connect_db()
if not models.Event.table_exists():
print "Creating events table..."
models.Event.create_table()
db.close_db(None)

View File

@@ -1,56 +0,0 @@
import json
import itertools
import peewee
from playhouse.migrate import Migrator
from redash import db, settings
from redash import models
if __name__ == '__main__':
db.connect_db()
if not models.User.table_exists():
print "Creating user table..."
models.User.create_table()
migrator = Migrator(db.database)
with db.database.transaction():
print "Creating user field on dashboard and queries..."
try:
migrator.rename_column(models.Query, '"user"', "user_email")
migrator.rename_column(models.Dashboard, '"user"', "user_email")
except peewee.ProgrammingError:
print "Failed to rename user column -- assuming it already exists"
with db.database.transaction():
models.Query.user.null = True
models.Dashboard.user.null = True
try:
migrator.add_column(models.Query, models.Query.user, "user_id")
migrator.add_column(models.Dashboard, models.Dashboard.user, "user_id")
except peewee.ProgrammingError:
print "Failed to create user_id column -- assuming it already exists"
print "Creating user for all queries and dashboards..."
for obj in itertools.chain(models.Query.select(), models.Dashboard.select()):
# Some old databases might have queries with empty string as user email:
email = obj.user_email or settings.ADMINS[0]
email = email.split(',')[0]
print ".. {} , {}, {}".format(type(obj), obj.id, email)
try:
user = models.User.get(models.User.email == email)
except models.User.DoesNotExist:
is_admin = email in settings.ADMINS
user = models.User.create(email=email, name=email, is_admin=is_admin)
obj.user = user
obj.save()
print "Set user_id to non null..."
with db.database.transaction():
migrator.set_nullable(models.Query, models.Query.user, False)
migrator.set_nullable(models.Dashboard, models.Dashboard.user, False)
migrator.set_nullable(models.Query, models.Query.user_email, True)
migrator.set_nullable(models.Dashboard, models.Dashboard.user_email, True)

View File

@@ -1,70 +0,0 @@
import json
from playhouse.migrate import Migrator
from redash import db
from redash import models
if __name__ == '__main__':
default_options = {"series": {"type": "column"}}
db.connect_db()
if not models.Visualization.table_exists():
print "Creating visualization table..."
models.Visualization.create_table()
with db.database.transaction():
migrator = Migrator(db.database)
print "Adding visualization_id to widgets:"
field = models.Widget.visualization
field.null = True
migrator.add_column(models.Widget, models.Widget.visualization, 'visualization_id')
print 'Creating TABLE visualizations for all queries...'
for query in models.Query.select():
vis = models.Visualization(query=query, name="Table",
description=query.description or "",
type="TABLE", options="{}")
vis.save()
print 'Creating COHORT visualizations for all queries named like %cohort%...'
for query in models.Query.select().where(models.Query.name ** "%cohort%"):
vis = models.Visualization(query=query, name="Cohort",
description=query.description or "",
type="COHORT", options="{}")
vis.save()
print 'Create visualization for all widgets (unless exists already):'
for widget in models.Widget.select():
print 'Processing widget id: %d:' % widget.id
vis_type = widget.type.upper()
if vis_type == 'GRID':
vis_type = 'TABLE'
query = models.Query.get_by_id(widget.query_id)
vis = query.visualizations.where(models.Visualization.type == vis_type).first()
if vis:
print '... visualization type (%s) found.' % vis_type
widget.visualization = vis
widget.save()
else:
vis_name = vis_type.title()
options = json.loads(widget.options)
vis_options = {"series": options} if options else default_options
vis_options = json.dumps(vis_options)
vis = models.Visualization(query=query, name=vis_name,
description=query.description or "",
type=vis_type, options=vis_options)
print '... Created visualization for type: %s' % vis_type
vis.save()
widget.visualization = vis
widget.save()
with db.database.transaction():
migrator = Migrator(db.database)
print "Setting visualization_id as not null..."
migrator.set_nullable(models.Widget, models.Widget.visualization, False)
db.close_db(None)

View File

@@ -1,29 +0,0 @@
import peewee
from playhouse.migrate import Migrator
from redash import models
from redash.models import db
if __name__ == '__main__':
db.connect_db()
migrator = Migrator(db.database)
if not models.Group.table_exists():
print "Creating groups table..."
models.Group.create_table()
with db.database.transaction():
models.Group.insert(name='admin', permissions=['admin'], tables=['*']).execute()
models.Group.insert(name='api', permissions=['view_query'], tables=['*']).execute()
models.Group.insert(name='default', permissions=models.Group.DEFAULT_PERMISSIONS, tables=['*']).execute()
migrator.add_column(models.User, models.User.groups, 'groups')
models.User.update(groups=['admin', 'default']).where(peewee.SQL("is_admin = true")).execute()
models.User.update(groups=['admin', 'default']).where(peewee.SQL("'admin' = any(permissions)")).execute()
models.User.update(groups=['default']).where(peewee.SQL("is_admin = false")).execute()
migrator.drop_column(models.User, 'permissions')
migrator.drop_column(models.User, 'is_admin')
db.close_db(None)

View File

@@ -11,14 +11,17 @@
"latedef": true,
"newcap": true,
"noarg": true,
"quotmark": "single",
"quotmark": false,
"regexp": true,
"undef": true,
"unused": true,
"strict": true,
"strict": false,
"trailing": true,
"smarttabs": true,
"globals": {
"angular": false
"angular": false,
"_": false,
"$": false,
"currentUser": false
}
}

View File

@@ -1,416 +0,0 @@
// Generated on 2014-07-30 using generator-angular 0.9.2
'use strict';
// # Globbing
// for performance reasons we're only matching one level down:
// 'test/spec/{,*/}*.js'
// use this if you want to recursively match all subfolders:
// 'test/spec/**/*.js'
module.exports = function (grunt) {
// Load grunt tasks automatically
require('load-grunt-tasks')(grunt);
// Time how long tasks take. Can help when optimizing build times
require('time-grunt')(grunt);
// Configurable paths for the application
var appConfig = {
app: require('./bower.json').appPath || 'app',
dist: 'dist'
};
// Define the configuration for all the tasks
grunt.initConfig({
// Project settings
yeoman: appConfig,
// Watches files for changes and runs tasks based on the changed files
watch: {
bower: {
files: ['bower.json'],
tasks: ['wiredep']
},
js: {
files: ['<%= yeoman.app %>/scripts/{,*/}*.js'],
tasks: ['newer:jshint:all'],
options: {
livereload: '<%= connect.options.livereload %>'
}
},
jsTest: {
files: ['test/spec/{,*/}*.js'],
tasks: ['newer:jshint:test', 'karma']
},
styles: {
files: ['<%= yeoman.app %>/styles/{,*/}*.css'],
tasks: ['newer:copy:styles', 'autoprefixer']
},
gruntfile: {
files: ['Gruntfile.js']
},
livereload: {
options: {
livereload: '<%= connect.options.livereload %>'
},
files: [
'<%= yeoman.app %>/{,*/}*.html',
'.tmp/styles/{,*/}*.css',
'<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}'
]
}
},
// The actual grunt server settings
connect: {
options: {
port: 9000,
// Change this to '0.0.0.0' to access the server from outside.
hostname: 'localhost',
livereload: 35729
},
livereload: {
options: {
open: true,
middleware: function (connect) {
return [
connect.static('.tmp'),
connect().use(
'/bower_components',
connect.static('./bower_components')
),
connect.static(appConfig.app)
];
}
}
},
test: {
options: {
port: 9001,
middleware: function (connect) {
return [
connect.static('.tmp'),
connect.static('test'),
connect().use(
'/bower_components',
connect.static('./bower_components')
),
connect.static(appConfig.app)
];
}
}
},
dist: {
options: {
open: true,
base: '<%= yeoman.dist %>'
}
}
},
// Make sure code styles are up to par and there are no obvious mistakes
jshint: {
options: {
jshintrc: '.jshintrc',
reporter: require('jshint-stylish')
},
all: {
src: [
'Gruntfile.js',
'<%= yeoman.app %>/scripts/{,*/}*.js'
]
},
test: {
options: {
jshintrc: 'test/.jshintrc'
},
src: ['test/spec/{,*/}*.js']
}
},
// Empties folders to start fresh
clean: {
dist: {
files: [{
dot: true,
src: [
'.tmp',
'<%= yeoman.dist %>/{,*/}*',
'!<%= yeoman.dist %>/.git*'
]
}]
},
server: '.tmp'
},
// Add vendor prefixed styles
autoprefixer: {
options: {
browsers: ['last 1 version']
},
dist: {
files: [{
expand: true,
cwd: '.tmp/styles/',
src: '{,*/}*.css',
dest: '.tmp/styles/'
}]
}
},
// Automatically inject Bower components into the app
wiredep: {
options: {
},
app: {
src: ['<%= yeoman.app %>/index.html'],
ignorePath: /\.\.\//
}
},
// Renames files for browser caching purposes
filerev: {
dist: {
src: [
'<%= yeoman.dist %>/scripts/{,*/}*.js',
'<%= yeoman.dist %>/styles/{,*/}*.css',
'<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}',
'<%= yeoman.dist %>/styles/fonts/*'
]
}
},
// Reads HTML for usemin blocks to enable smart builds that automatically
// concat, minify and revision files. Creates configurations in memory so
// additional tasks can operate on them
useminPrepare: {
html: ['<%= yeoman.app %>/index.html', '<%= yeoman.app %>/login.html'],
options: {
dest: '<%= yeoman.dist %>',
flow: {
html: {
steps: {
js: ['concat', 'uglifyjs'],
css: ['cssmin']
},
post: {}
}
}
}
},
// Performs rewrites based on filerev and the useminPrepare configuration
usemin: {
html: ['<%= yeoman.dist %>/{,*/}*.html'],
css: ['<%= yeoman.dist %>/styles/{,*/}*.css'],
options: {
assetsDirs: ['<%= yeoman.dist %>','<%= yeoman.dist %>/images']
}
},
// The following *-min tasks will produce minified files in the dist folder
// By default, your `index.html`'s <!-- Usemin block --> will take care of
// minification. These next options are pre-configured if you do not wish
// to use the Usemin blocks.
// cssmin: {
// dist: {
// files: {
// '<%= yeoman.dist %>/styles/main.css': [
// '.tmp/styles/{,*/}*.css'
// ]
// }
// }
// },
// uglify: {
// dist: {
// files: {
// '<%= yeoman.dist %>/scripts/scripts.js': [
// '<%= yeoman.dist %>/scripts/scripts.js'
// ]
// }
// }
// },
// concat: {
// dist: {}
// },
imagemin: {
dist: {
files: [{
expand: true,
cwd: '<%= yeoman.app %>/images',
src: '{,*/}*.{png,jpg,jpeg,gif}',
dest: '<%= yeoman.dist %>/images'
}]
}
},
svgmin: {
dist: {
files: [{
expand: true,
cwd: '<%= yeoman.app %>/images',
src: '{,*/}*.svg',
dest: '<%= yeoman.dist %>/images'
}]
}
},
htmlmin: {
dist: {
options: {
collapseWhitespace: true,
conservativeCollapse: true,
collapseBooleanAttributes: true,
removeCommentsFromCDATA: true,
removeOptionalTags: true
},
files: [{
expand: true,
cwd: '<%= yeoman.dist %>',
src: ['*.html', 'views/{,*/}*.html'],
dest: '<%= yeoman.dist %>'
}]
}
},
// ngmin tries to make the code safe for minification automatically by
// using the Angular long form for dependency injection. It doesn't work on
// things like resolve or inject so those have to be done manually.
ngmin: {
dist: {
files: [{
expand: true,
cwd: '.tmp/concat/scripts',
src: '*.js',
dest: '.tmp/concat/scripts'
}]
}
},
// Replace Google CDN references
cdnify: {
dist: {
html: ['<%= yeoman.dist %>/*.html']
}
},
// Copies remaining files to places other tasks can use
copy: {
dist: {
files: [{
expand: true,
dot: true,
cwd: '<%= yeoman.app %>',
dest: '<%= yeoman.dist %>',
src: [
'*.{ico,png,txt}',
'.htaccess',
'*.html',
'views/{,*/}*.html',
'images/{,*/}*.{webp}',
'fonts/*'
]
}, {
expand: true,
cwd: '.tmp/images',
dest: '<%= yeoman.dist %>/images',
src: ['generated/*']
}, {
expand: true,
cwd: '<%= yeoman.app %>/bower_components/bootstrap/dist',
src: 'fonts/*',
dest: '<%= yeoman.dist %>'
}, {
expand: true,
cwd: '<%= yeoman.app %>/bower_components/font-awesome',
src: 'fonts/*',
dest: '<%= yeoman.dist %>'
}]
},
styles: {
expand: true,
cwd: '<%= yeoman.app %>/styles',
dest: '.tmp/styles/',
src: '{,*/}*.css'
}
},
// Run some tasks in parallel to speed up the build process
concurrent: {
server: [
'copy:styles'
],
test: [
'copy:styles'
],
dist: [
'copy:styles',
'imagemin',
'svgmin'
]
},
// Test settings
karma: {
unit: {
configFile: 'test/karma.conf.js',
singleRun: true
}
}
});
grunt.registerTask('serve', 'Compile then start a connect web server', function (target) {
if (target === 'dist') {
return grunt.task.run(['build', 'connect:dist:keepalive']);
}
grunt.task.run([
'clean:server',
'wiredep',
'concurrent:server',
'autoprefixer',
'connect:livereload',
'watch'
]);
});
grunt.registerTask('server', 'DEPRECATED TASK. Use the "serve" task instead', function (target) {
grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
grunt.task.run(['serve:' + target]);
});
grunt.registerTask('test', [
'clean:server',
'concurrent:test',
'autoprefixer',
'connect:test',
'karma'
]);
grunt.registerTask('build', [
'clean:dist',
'wiredep',
'useminPrepare',
'concurrent:dist',
'autoprefixer',
'concat',
'ngmin',
'copy:dist',
'cdnify',
'cssmin',
'uglify',
'filerev',
'usemin',
'htmlmin'
]);
grunt.registerTask('default', [
'newer:jshint',
'test',
'build'
]);
};

82
rd_ui/app/embed.html Normal file
View File

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7" ng-app="redash" ng-controller='MainCtrl'> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8" ng-app="redash" ng-controller='MainCtrl'> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9" ng-app="redash" ng-controller='MainCtrl'> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" ng-app="redash" ng-controller='EmbedCtrl'> <!--<![endif]-->
<head>
<base href="{{base_href}}">
<title ng-bind="'{{name}} | ' + pageTitle"></title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- build:css /styles/embed.css -->
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.css">
<link rel="stylesheet" href="/bower_components/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="/bower_components/gridster/dist/jquery.gridster.css">
<link rel="stylesheet" href="/bower_components/pivottable/dist/pivot.css">
<link rel="stylesheet" href="/bower_components/cornelius/src/cornelius.css">
<link rel="stylesheet" href="/bower_components/angular-ui-select/dist/select.css">
<link rel="stylesheet" href="/bower_components/pace/themes/pace-theme-minimal.css">
<link rel="stylesheet" href="/bower_components/font-awesome/css/font-awesome.css">
<link rel="stylesheet" href="/bower_components/codemirror/addon/hint/show-hint.css">
<link rel="stylesheet" href="/bower_components/leaflet/dist/leaflet.css">
<link rel="stylesheet" href="/styles/redash.css">
<!-- endbuild -->
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
<style>
body { padding:0; }
.col-lg-12, .row, .container, .panel { margin:0; padding:0; }
.container::after, .row::after { display:none; }
</style>
</head>
<body>
<div growl></div>
<div ng-view></div>
{% include 'vendor_scripts.html' %}
<!-- build:js({.tmp,app}) /scripts/embed-scripts.js -->
<script src="/scripts/embed.js"></script>
<script src="/scripts/services/services.js"></script>
<script src="/scripts/services/resources.js"></script>
<script src="/scripts/services/notifications.js"></script>
<script src="/scripts/services/dashboards.js"></script>
<script src="/scripts/controllers/controllers.js"></script>
<script src="/scripts/controllers/dashboard.js"></script>
<script src="/scripts/controllers/admin_controllers.js"></script>
<script src="/scripts/controllers/data_sources.js"></script>
<script src="/scripts/controllers/query_view.js"></script>
<script src="/scripts/controllers/query_source.js"></script>
<script src="/scripts/controllers/users.js"></script>
<script src="/scripts/visualizations/base.js"></script>
<script src="/scripts/visualizations/chart.js"></script>
<script src="/scripts/visualizations/cohort.js"></script>
<script src="/scripts/visualizations/map.js"></script>
<script src="/scripts/visualizations/counter.js"></script>
<script src="/scripts/visualizations/boxplot.js"></script>
<script src="/scripts/visualizations/box.js"></script>
<script src="/scripts/visualizations/table.js"></script>
<script src="/scripts/visualizations/pivot.js"></script>
<script src="/scripts/visualizations/date_range_selector.js"></script>
<script src="/scripts/directives/directives.js"></script>
<script src="/scripts/directives/query_directives.js"></script>
<script src="/scripts/directives/data_source_directives.js"></script>
<script src="/scripts/directives/dashboard_directives.js"></script>
<script src="/scripts/filters.js"></script>
<script src="/scripts/controllers/alerts.js"></script>
<!-- endbuild -->
<script>
var clientConfig = {{ client_config|safe }};
var visualization = {{ visualization|safe }};
var query_result = {{ query_result|safe }};
{{ analytics|safe }}
</script>
</body>
</html>

View File

@@ -4,6 +4,7 @@
<!--[if IE 8]> <html class="no-js lt-ie9" ng-app="redash" ng-controller='MainCtrl'> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" ng-app="redash" ng-controller='MainCtrl'> <!--<![endif]-->
<head>
<base href="{{base_href}}">
<title ng-bind="'{{name}} | ' + pageTitle"></title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
@@ -14,7 +15,6 @@
<link rel="stylesheet" href="/bower_components/gridster/dist/jquery.gridster.css">
<link rel="stylesheet" href="/bower_components/pivottable/dist/pivot.css">
<link rel="stylesheet" href="/bower_components/cornelius/src/cornelius.css">
<link rel="stylesheet" href="/bower_components/select2/select2.css">
<link rel="stylesheet" href="/bower_components/angular-ui-select/dist/select.css">
<link rel="stylesheet" href="/bower_components/pace/themes/pace-theme-minimal.css">
<link rel="stylesheet" href="/bower_components/font-awesome/css/font-awesome.css">
@@ -40,27 +40,26 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/"><img src="/images/redash_icon_small.png"/></a>
<a class="navbar-brand" href="{{base_href}}"><img src="/images/redash_icon_small.png"/></a>
</div>
{% raw %}
<div class="collapse navbar-collapse navbar-ex1-collapse">
<ul class="nav navbar-nav">
<li class="active" ng-show="pageTitle"><a class="page-title" ng-bind="pageTitle"></a></li>
<li class="dropdown" ng-show="groupedDashboards.length > 0 || otherDashboards.length > 0 || currentUser.hasPermission('create_dashboard')" dropdown>
<a href="#" class="dropdown-toggle" dropdown-toggle><span class="glyphicon glyphicon-th-large"></span> <b class="caret"></b></a>
<a href="#" class="dropdown-toggle" dropdown-toggle><span class="fa fa-tachometer"></span> <b class="caret"></b></a>
<ul class="dropdown-menu" dropdown-menu>
<span ng-repeat="(name, group) in groupedDashboards">
<li class="dropdown-submenu">
<a href="#" ng-bind="name"></a>
<ul class="dropdown-menu">
<li ng-repeat="dashboard in group" role="presentation">
<a role="menu-item" ng-href="/dashboard/{{dashboard.slug}}" ng-bind="dashboard.name"></a>
<a role="menu-item" ng-href="dashboard/{{dashboard.slug}}" ng-bind="dashboard.name"></a>
</li>
</ul>
</li>
</span>
<li ng-repeat="dashboard in otherDashboards">
<a role="menu-item" ng-href="/dashboard/{{dashboard.slug}}" ng-bind="dashboard.name"></a>
<a role="menu-item" ng-href="dashboard/{{dashboard.slug}}" ng-bind="dashboard.name"></a>
</li>
<li class="divider" ng-show="currentUser.hasPermission('create_dashboard') && (groupedDashboards.length > 0 || otherDashboards.length > 0)"></li>
<li><a data-toggle="modal" href="#new_dashboard_dialog" ng-show="currentUser.hasPermission('create_dashboard')">New Dashboard</a></li>
@@ -69,10 +68,13 @@
<li class="dropdown" ng-show="currentUser.hasPermission('view_query')" dropdown>
<a href="#" class="dropdown-toggle" dropdown-toggle>Queries <b class="caret"></b></a>
<ul class="dropdown-menu" dropdown-menu>
<li ng-show="currentUser.hasPermission('create_query')"><a href="/queries/new">New Query</a></li>
<li><a href="/queries">Queries</a></li>
<li ng-show="currentUser.hasPermission('create_query')"><a href="queries/new">New Query</a></li>
<li><a href="queries">Queries</a></li>
</ul>
</li>
<li>
<a href="alerts">Alerts</a>
</li>
</ul>
<form class="navbar-form navbar-left" role="search" ng-submit="searchQueries()">
<div class="form-group">
@@ -81,12 +83,34 @@
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-search"></span></button>
</form>
<ul class="nav navbar-nav navbar-right">
<p class="navbar-text avatar" ng-show="currentUser.id" ng-cloak>
<img ng-src="{{currentUser.gravatar_url}}" class="img-circle" alt="{{currentUser.name}}"/>
<a target="_self" href="/logout" id="logout" title="Logout">
<span class="glyphicon glyphicon-log-out"></span>
</a>
</p>
<li ng-show="currentUser.hasPermission('admin')">
<a href="data_sources" title="Data Sources"><i class="fa fa-database"></i></a>
</li>
<li ng-show="currentUser.hasPermission('list_users')">
<a href="users" title="Users"><i class="fa fa-users"></i></a>
</li>
<li class="dropdown" dropdown>
<a href="#" class="dropdown-toggle" dropdown-toggle><span ng-bind="currentUser.name"></span> <span class="caret"></span></a>
<ul class="dropdown-menu" dropdown-menu>
<li style="width:300px">
<a ng-href="users/{{currentUser.id}}">
<div class="row">
<div class="col-sm-2">
<img ng-src="{{currentUser.gravatar_url}}" size="40px" class="img-circle"/>
</div>
<div class="col-sm-10">
<p><strong>{{currentUser.name}}</strong></p>
</div>
</div>
</a>
</li>
<li class="divider">
</li>
<li>
<a href="logout" target="_self">Log out</a>
</li>
</ul>
</li>
</ul>
</div>
{% endraw %}
@@ -96,51 +120,38 @@
<edit-dashboard-form dashboard="newDashboard" id="new_dashboard_dialog"></edit-dashboard-form>
<div ng-view></div>
<div ng-if="showPermissionError" class="ng-cloak container" ng-cloak>
<div class="row">
<div class="text-center">
<h1><span class="glyphicon glyphicon-lock"></span></h1>
<p class="text-muted">
You do not have permission to view the requested page.
</p>
</div>
</div>
</div>
<script src="/bower_components/jquery/jquery.js"></script>
{% raw %}
<div class="visible-print">
<hr>
Source: {{location}}
</div>
<div class="container-fluid footer hidden-print">
<hr/>
<div class="container">
<div class="row">
<a href="http://redash.io">re:dash</a> <span ng-bind="version"></span>
<small ng-if="newVersionAvailable" ng-cloak class="ng-cloak"><a href="http://version.redash.io/">(new re:dash version available)</a></small>
<div class="pull-right">
<a href="http://docs.redash.io/">Docs</a>
<a href="http://github.com/getredash/redash">Contribute</a>
</div>
</div>
</div>
</div>
{% endraw %}
<!-- build:js /scripts/plugins.js -->
<script src="/bower_components/angular/angular.js"></script>
<script src="/bower_components/jquery-ui/ui/jquery-ui.js"></script>
<script src="/bower_components/bootstrap/js/collapse.js"></script>
<script src="/bower_components/bootstrap/js/modal.js"></script>
<script src="/bower_components/angular-resource/angular-resource.js"></script>
<script src="/bower_components/angular-route/angular-route.js"></script>
<script src="/bower_components/underscore/underscore.js"></script>
<script src="/bower_components/moment/moment.js"></script>
<script src="/bower_components/angular-moment/angular-moment.js"></script>
<script src="/bower_components/codemirror/lib/codemirror.js"></script>
<script src="/bower_components/codemirror/addon/edit/matchbrackets.js"></script>
<script src="/bower_components/codemirror/addon/edit/closebrackets.js"></script>
<script src="/bower_components/codemirror/addon/hint/show-hint.js"></script>
<script src="/bower_components/codemirror/addon/hint/anyword-hint.js"></script>
<script src="/bower_components/codemirror/mode/sql/sql.js"></script>
<script src="/bower_components/codemirror/mode/python/python.js"></script>
<script src="/bower_components/codemirror/mode/javascript/javascript.js"></script>
<script src="/bower_components/highcharts/highcharts.js"></script>
<script src="/bower_components/highcharts/modules/exporting.js"></script>
<script src="/bower_components/gridster/dist/jquery.gridster.js"></script>
<script src="/bower_components/angular-growl/build/angular-growl.js"></script>
<script src="/bower_components/pivottable/dist/pivot.js"></script>
<script src="/bower_components/cornelius/src/cornelius.js"></script>
<script src="/bower_components/mousetrap/mousetrap.js"></script>
<script src="/bower_components/mousetrap/plugins/global-bind/mousetrap-global-bind.js"></script>
<script src="/bower_components/select2/select2.js"></script>
<script src="/bower_components/angular-ui-select2/src/select2.js"></script>
<script src="/bower_components/angular-ui-select/dist/select.js"></script>
<script src="/bower_components/underscore.string/lib/underscore.string.js"></script>
<script src="/bower_components/marked/lib/marked.js"></script>
<script src="/scripts/ng_highchart.js"></script>
<script src="/scripts/ng_smart_table.js"></script>
<script src="/bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.js"></script>
<script src="/bower_components/bucky/bucky.js"></script>
<script src="/bower_components/pace/pace.js"></script>
<script src="/bower_components/mustache/mustache.js"></script>
<script src="/bower_components/canvg/rgbcolor.js"></script>
<script src="/bower_components/canvg/StackBlur.js"></script>
<script src="/bower_components/canvg/canvg.js"></script>
<script src="/bower_components/leaflet/dist/leaflet.js"></script>
<!-- endbuild -->
{% include 'vendor_scripts.html' %}
<!-- build:js({.tmp,app}) /scripts/scripts.js -->
<script src="/scripts/app.js"></script>
@@ -151,34 +162,46 @@
<script src="/scripts/controllers/controllers.js"></script>
<script src="/scripts/controllers/dashboard.js"></script>
<script src="/scripts/controllers/admin_controllers.js"></script>
<script src="/scripts/controllers/data_sources.js"></script>
<script src="/scripts/controllers/query_view.js"></script>
<script src="/scripts/controllers/query_source.js"></script>
<script src="/scripts/controllers/users.js"></script>
<script src="/scripts/visualizations/base.js"></script>
<script src="/scripts/visualizations/chart.js"></script>
<script src="/scripts/visualizations/cohort.js"></script>
<script src="/scripts/visualizations/map.js"></script>
<script src="/scripts/visualizations/counter.js"></script>
<script src="/scripts/visualizations/boxplot.js"></script>
<script src="/scripts/visualizations/box.js"></script>
<script src="/scripts/visualizations/table.js"></script>
<script src="/scripts/visualizations/pivot.js"></script>
<script src="/scripts/visualizations/date_range_selector.js"></script>
<script src="/scripts/directives/directives.js"></script>
<script src="/scripts/directives/query_directives.js"></script>
<script src="/scripts/directives/data_source_directives.js"></script>
<script src="/scripts/directives/dashboard_directives.js"></script>
<script src="/scripts/filters.js"></script>
<script src="/scripts/controllers/alerts.js"></script>
<!-- endbuild -->
<script>
// TODO: move currentUser & features to be an Angular service
var featureFlags = {{ features|safe }};
var clientConfig = {{ client_config|safe }};
var basePath = "{{base_href}}";
var currentUser = {{ user|safe }};
var currentOrgSlug = "{{ org_slug }}";
currentUser.canEdit = function(object) {
var user_id = object.user_id || (object.user && object.user.id);
return user_id && (user_id == currentUser.id);
return this.hasPermission('admin') || (user_id && (user_id == currentUser.id));
};
currentUser.hasPermission = function(permission) {
return this.permissions.indexOf(permission) != -1;
}
};
currentUser.isAdmin = currentUser.hasPermission('admin');
{{ analytics|safe }}
</script>

View File

@@ -49,7 +49,7 @@
{% if show_google_openid %}
<div class="row">
<a href="/oauth/google?next={{next}}"><img src="/google_login.png" class="login-button"/></a>
<a href="{{ google_auth_url }}"><img src="/google_login.png" class="login-button"/></a>
</div>
<div class="login-or">
@@ -74,8 +74,8 @@
<form role="form" method="post" name="login">
<div class="form-group">
<label for="inputUsernameEmail">Username or email</label>
<input type="text" class="form-control" id="inputUsernameEmail" name="username" value="{{username}}">
<label for="inputEmail">Email</label>
<input type="text" class="form-control" id="inputEmail" name="email" value="{{email}}">
</div>
<div class="form-group">
<!--<a class="pull-right" href="#">Forgot password?</a>-->

View File

@@ -4,33 +4,29 @@ angular.module('redash', [
'redash.controllers',
'redash.filters',
'redash.services',
'redash.renderers',
'redash.visualization',
'highchart',
'ui.select2',
'plotly',
'plotly-chart',
'angular-growl',
'angularMoment',
'ui.bootstrap',
'ui.sortable',
'smartTable.table',
'ngResource',
'ngRoute',
'ui.select'
]).config(['$routeProvider', '$locationProvider', '$compileProvider', 'growlProvider',
function ($routeProvider, $locationProvider, $compileProvider, growlProvider) {
if (featureFlags.clientSideMetrics) {
Bucky.setOptions({
host: '/api/metrics'
});
Bucky.requests.monitor('ajax_requsts');
Bucky.requests.transforms.enable('dashboards', /dashboard\/[\w-]+/ig, '/dashboard');
}
'ui.select',
'naif.base64',
'ui.bootstrap.showErrors',
'ngSanitize'
]).config(['$routeProvider', '$locationProvider', '$compileProvider', 'growlProvider', 'uiSelectConfig',
function ($routeProvider, $locationProvider, $compileProvider, growlProvider, uiSelectConfig) {
function getQuery(Query, $route) {
var query = Query.get({'id': $route.current.params.queryId });
return query.$promise;
};
uiSelectConfig.theme = "bootstrap";
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|http|data):/);
$locationProvider.html5Mode(true);
growlProvider.globalTimeToLive(2000);
@@ -52,7 +48,8 @@ angular.module('redash', [
resolve: {
'query': ['Query', function newQuery(Query) {
return Query.newQuery();
}]
}],
'dataSources': ['DataSource', function (DataSource) { return DataSource.query().$promise }]
}
});
$routeProvider.when('/queries/search', {
@@ -80,18 +77,56 @@ angular.module('redash', [
templateUrl: '/views/admin_status.html',
controller: 'AdminStatusCtrl'
});
$routeProvider.when('/admin/workers', {
templateUrl: '/views/admin_workers.html',
controller: 'AdminWorkersCtrl'
$routeProvider.when('/alerts', {
templateUrl: '/views/alerts/list.html',
controller: 'AlertsCtrl'
});
$routeProvider.when('/alerts/:alertId', {
templateUrl: '/views/alerts/edit.html',
controller: 'AlertCtrl'
});
$routeProvider.when('/data_sources/:dataSourceId', {
templateUrl: '/views/data_sources/edit.html',
controller: 'DataSourceCtrl'
});
$routeProvider.when('/data_sources', {
templateUrl: '/views/data_sources/list.html',
controller: 'DataSourcesCtrl'
});
$routeProvider.when('/users/new', {
templateUrl: '/views/users/new.html',
controller: 'NewUserCtrl'
});
$routeProvider.when('/users/:userId', {
templateUrl: '/views/users/show.html',
reloadOnSearch: false,
controller: 'UserCtrl'
});
$routeProvider.when('/users', {
templateUrl: '/views/users/list.html',
controller: 'UsersCtrl'
});
$routeProvider.when('/groups/:groupId/data_sources', {
templateUrl: '/views/groups/show_data_sources.html',
controller: 'GroupDataSourcesCtrl'
});
$routeProvider.when('/groups/:groupId', {
templateUrl: '/views/groups/show.html',
controller: 'GroupCtrl'
});
$routeProvider.when('/groups', {
templateUrl: '/views/groups/list.html',
controller: 'GroupsCtrl'
})
$routeProvider.when('/', {
templateUrl: '/views/index.html',
controller: 'IndexCtrl'
});
$routeProvider.when('/personal', {
templateUrl: '/views/personal.html',
controller: 'PersonalIndexCtrl'
redirectTo: '/'
});
$routeProvider.otherwise({
redirectTo: '/'

View File

@@ -17,7 +17,7 @@
};
refresh();
}
};
angular.module('redash.admin_controllers', [])
.controller('AdminStatusCtrl', ['$scope', 'Events', '$http', '$timeout', AdminStatusCtrl])

View File

@@ -0,0 +1,176 @@
(function() {
var AlertsCtrl = function($scope, Events, Alert) {
Events.record(currentUser, "view", "page", "alerts");
$scope.$parent.pageTitle = "Alerts";
$scope.alerts = []
Alert.query(function(alerts) {
var stateClass = {
'ok': 'label label-success',
'triggered': 'label label-danger',
'unknown': 'label label-warning'
};
_.each(alerts, function(alert) {
alert.class = stateClass[alert.state];
})
$scope.alerts = alerts;
});
$scope.gridConfig = {
isPaginationEnabled: true,
itemsByPage: 50,
maxSize: 8,
};
$scope.gridColumns = [
{
"label": "Name",
"map": "name",
"cellTemplate": '<a href="alerts/{{dataRow.id}}">{{dataRow.name}}</a> (<a href="queries/{{dataRow.query.id}}">query</a>)'
},
{
'label': 'Created By',
'map': 'user.name'
},
{
'label': 'State',
'cellTemplate': '<span ng-class="dataRow.class">{{dataRow.state | uppercase}}</span> since <span am-time-ago="dataRow.updated_at"></span>'
},
{
'label': 'Created At',
'cellTemplate': '<span am-time-ago="dataRow.created_at"></span>'
}
];
};
var AlertCtrl = function($scope, $routeParams, $location, growl, Query, Events, Alert) {
$scope.$parent.pageTitle = "Alerts";
$scope.alertId = $routeParams.alertId;
if ($scope.alertId === "new") {
Events.record(currentUser, 'view', 'page', 'alerts/new');
} else {
Events.record(currentUser, 'view', 'alert', $scope.alertId);
}
$scope.onQuerySelected = function(item) {
$scope.selectedQuery = item;
item.getQueryResultPromise().then(function(result) {
$scope.queryResult = result;
$scope.alert.options.column = $scope.alert.options.column || result.getColumnNames()[0];
});
};
if ($scope.alertId === "new") {
$scope.alert = new Alert({options: {}});
} else {
$scope.alert = Alert.get({id: $scope.alertId}, function(alert) {
$scope.onQuerySelected(new Query($scope.alert.query));
});
}
$scope.ops = ['greater than', 'less than', 'equals'];
$scope.selectedQuery = null;
$scope.getDefaultName = function() {
if (!$scope.alert.query) {
return undefined;
}
return _.template("<%= query.name %>: <%= options.column %> <%= options.op %> <%= options.value %>", $scope.alert);
};
$scope.searchQueries = function (term) {
if (!term || term.length < 3) {
return;
}
Query.search({q: term}, function(results) {
$scope.queries = results;
});
};
$scope.saveChanges = function() {
if ($scope.alert.name === undefined || $scope.alert.name === '') {
$scope.alert.name = $scope.getDefaultName();
}
if ($scope.alert.rearm === '' || $scope.alert.rearm === 0) {
$scope.alert.rearm = null;
}
$scope.alert.$save(function(alert) {
growl.addSuccessMessage("Saved.");
if ($scope.alertId === "new") {
$location.path('/alerts/' + alert.id).replace();
}
}, function() {
growl.addErrorMessage("Failed saving alert.");
});
};
};
angular.module('redash.directives').directive('alertSubscribers', ['AlertSubscription', function (AlertSubscription) {
return {
restrict: 'E',
replace: true,
templateUrl: '/views/alerts/subscribers.html',
scope: {
'alertId': '='
},
controller: function ($scope) {
$scope.subscribers = AlertSubscription.query({alertId: $scope.alertId});
}
}
}]);
angular.module('redash.directives').directive('subscribeButton', ['AlertSubscription', 'growl', function (AlertSubscription, growl) {
return {
restrict: 'E',
replace: true,
template: '<button class="btn btn-default btn-xs" ng-click="toggleSubscription()"><i ng-class="class"></i></button>',
controller: function ($scope) {
var updateClass = function() {
if ($scope.subscription) {
$scope.class = "fa fa-eye-slash";
} else {
$scope.class = "fa fa-eye";
}
}
$scope.subscribers.$promise.then(function() {
$scope.subscription = _.find($scope.subscribers, function(subscription) {
return (subscription.user.email == currentUser.email);
});
updateClass();
});
$scope.toggleSubscription = function() {
if ($scope.subscription) {
$scope.subscription.$delete(function() {
$scope.subscribers = _.without($scope.subscribers, $scope.subscription);
$scope.subscription = undefined;
updateClass();
}, function() {
growl.addErrorMessage("Failed saving subscription.");
});
} else {
$scope.subscription = new AlertSubscription({alert_id: $scope.alertId});
$scope.subscription.$save(function() {
$scope.subscribers.push($scope.subscription);
updateClass();
}, function() {
growl.addErrorMessage("Unsubscription failed.");
});
}
}
}
}
}]);
angular.module('redash.controllers')
.controller('AlertsCtrl', ['$scope', 'Events', 'Alert', AlertsCtrl])
.controller('AlertCtrl', ['$scope', '$routeParams', '$location', 'growl', 'Query', 'Events', 'Alert', AlertCtrl])
})();

View File

@@ -3,7 +3,8 @@
if (!value) {
return "-";
}
return value.toDate().toLocaleString();
return value.format(clientConfig.dateTimeFormat);
};
var QuerySearchCtrl = function($scope, $location, $filter, Events, Query) {
@@ -23,7 +24,7 @@
},
{
'label': 'Created By',
'map': 'user_name'
'map': 'user.name'
},
{
'label': 'Created At',
@@ -45,7 +46,6 @@
Query.search({q: $scope.term }, function(results) {
$scope.queries = _.map(results, function(query) {
query.created_at = moment(query.created_at);
query.user_name = query.user.name;
return query;
});
});
@@ -93,7 +93,6 @@
$scope.allQueries = _.map(queries, function (query) {
query.created_at = moment(query.created_at);
query.retrieved_at = moment(query.retrieved_at);
query.user_name = query.user.name;
return query;
});
@@ -108,7 +107,7 @@
},
{
'label': 'Created By',
'map': 'user_name'
'map': 'user.name'
},
{
'label': 'Created At',
@@ -152,13 +151,21 @@
}
var MainCtrl = function ($scope, $location, Dashboard, notifications) {
if (featureFlags.clientSideMetrics) {
$scope.$on('$locationChangeSuccess', function(event, newLocation, oldLocation) {
// This will be called once per actual page load.
Bucky.sendPagePerformance();
});
}
$scope.$on("$routeChangeSuccess", function (event, current, previous, rejection) {
if ($scope.showPermissionError) {
$scope.showPermissionError = false;
}
});
$scope.$on("$routeChangeError", function (event, current, previous, rejection) {
if (rejection.status === 403) {
$scope.showPermissionError = true;
}
});
$scope.location = String(document.location);
$scope.version = clientConfig.version;
$scope.newVersionAvailable = clientConfig.newVersionAvailable && currentUser.hasPermission("admin");
$scope.dashboards = [];
$scope.reloadDashboards = function () {
@@ -193,41 +200,17 @@
});
};
var IndexCtrl = function ($scope, Events, Dashboard) {
Events.record(currentUser, "view", "page", "homepage");
$scope.$parent.pageTitle = "Home";
$scope.archiveDashboard = function (dashboard) {
if (confirm('Are you sure you want to delete "' + dashboard.name + '" dashboard?')) {
Events.record(currentUser, "archive", "dashboard", dashboard.id);
dashboard.$delete(function () {
$scope.$parent.reloadDashboards();
});
}
}
};
var PersonalIndexCtrl = function ($scope, Events, Dashboard, Query) {
var IndexCtrl = function ($scope, Events, Dashboard, Query) {
Events.record(currentUser, "view", "page", "personal_homepage");
$scope.$parent.pageTitle = "Home";
$scope.recentQueries = Query.recent();
$scope.recentDashboards = Dashboard.recent();
$scope.archiveDashboard = function (dashboard) {
if (confirm('Are you sure you want to delete "' + dashboard.name + '" dashboard?')) {
Events.record(currentUser, "archive", "dashboard", dashboard.id);
dashboard.$delete(function () {
$scope.$parent.reloadDashboards();
});
}
}
};
angular.module('redash.controllers', [])
.controller('QueriesCtrl', ['$scope', '$http', '$location', '$filter', 'Query', QueriesCtrl])
.controller('IndexCtrl', ['$scope', 'Events', 'Dashboard', IndexCtrl])
.controller('PersonalIndexCtrl', ['$scope', 'Events', 'Dashboard', 'Query', PersonalIndexCtrl])
.controller('IndexCtrl', ['$scope', 'Events', 'Dashboard', 'Query', IndexCtrl])
.controller('MainCtrl', ['$scope', '$location', 'Dashboard', 'notifications', MainCtrl])
.controller('QuerySearchCtrl', ['$scope', '$location', '$filter', 'Events', 'Query', QuerySearchCtrl]);
})();

View File

@@ -1,71 +1,72 @@
(function() {
var DashboardCtrl = function($scope, Events, Widget, $routeParams, $location, $http, $timeout, $q, Dashboard) {
$scope.refreshEnabled = false;
$scope.isFullscreen = false;
$scope.refreshRate = 60;
var loadDashboard = _.throttle(function() {
$scope.dashboard = Dashboard.get({ slug: $routeParams.dashboardSlug }, function (dashboard) {
Events.record(currentUser, "view", "dashboard", dashboard.id);
var renderDashboard = function (dashboard) {
$scope.$parent.pageTitle = dashboard.name;
$scope.$parent.pageTitle = dashboard.name;
var promises = [];
var promises = [];
_.each($scope.dashboard.widgets, function (row) {
return _.each(row, function (widget) {
if (widget.visualization) {
var queryResult = widget.getQuery().getQueryResult();
if (angular.isDefined(queryResult))
promises.push(queryResult.toPromise());
}
});
});
$scope.dashboard.widgets = _.map($scope.dashboard.widgets, function (row) {
return _.map(row, function (widget) {
var w = new Widget(widget);
$q.all(promises).then(function(queryResults) {
var filters = {};
_.each(queryResults, function(queryResult) {
var queryFilters = queryResult.getFilters();
_.each(queryFilters, function (queryFilter) {
var hasQueryStringValue = _.has($location.search(), queryFilter.name);
if (w.visualization) {
promises.push(w.getQuery().getQueryResult().toPromise());
if (!(hasQueryStringValue || dashboard.dashboard_filters_enabled)) {
// If dashboard filters not enabled, or no query string value given, skip filters linking.
return;
}
return w;
});
});
$q.all(promises).then(function(queryResults) {
var filters = {};
_.each(queryResults, function(queryResult) {
var queryFilters = queryResult.getFilters();
_.each(queryFilters, function (queryFilter) {
var hasQueryStringValue = _.has($location.search(), queryFilter.name);
if (!(hasQueryStringValue || dashboard.dashboard_filters_enabled)) {
// If dashboard filters not enabled, or no query string value given, skip filters linking.
return;
if (!_.has(filters, queryFilter.name)) {
var filter = _.extend({}, queryFilter);
filters[filter.name] = filter;
filters[filter.name].originFilters = [];
if (hasQueryStringValue) {
filter.current = $location.search()[filter.name];
}
if (!_.has(filters, queryFilter.name)) {
var filter = _.extend({}, queryFilter);
filters[filter.name] = filter;
filters[filter.name].originFilters = [];
if (hasQueryStringValue) {
filter.current = $location.search()[filter.name];
}
$scope.$watch(function () { return filter.current }, function (value) {
_.each(filter.originFilters, function (originFilter) {
originFilter.current = value;
});
$scope.$watch(function () { return filter.current }, function (value) {
_.each(filter.originFilters, function (originFilter) {
originFilter.current = value;
});
}
});
}
// TODO: merge values.
filters[queryFilter.name].originFilters.push(queryFilter);
});
// TODO: merge values.
filters[queryFilter.name].originFilters.push(queryFilter);
});
$scope.filters = _.values(filters);
});
}, function () {
// error...
// try again. we wrap loadDashboard with throttle so it doesn't happen too often.\
// we might want to consider exponential backoff and also move this as a general solution in $http/$resource for
// all AJAX calls.
loadDashboard();
$scope.filters = _.values(filters);
});
}
var loadDashboard = _.throttle(function () {
$scope.dashboard = Dashboard.get({slug: $routeParams.dashboardSlug}, function (dashboard) {
Events.record(currentUser, "view", "dashboard", dashboard.id);
renderDashboard(dashboard);
}, function () {
// error...
// try again. we wrap loadDashboard with throttle so it doesn't happen too often.\
// we might want to consider exponential backoff and also move this as a general solution in $http/$resource for
// all AJAX calls.
loadDashboard();
}
);
}, 1000);
loadDashboard();
@@ -94,6 +95,19 @@
}
};
$scope.archiveDashboard = function () {
if (confirm('Are you sure you want to archive the "' + $scope.dashboard.name + '" dashboard?')) {
Events.record(currentUser, "archive", "dashboard", $scope.dashboard.id);
$scope.dashboard.$delete(function () {
$scope.$parent.reloadDashboards();
});
}
}
$scope.toggleFullscreen = function() {
$scope.isFullscreen = !$scope.isFullscreen;
};
$scope.triggerRefresh = function() {
$scope.refreshEnabled = !$scope.refreshEnabled;
@@ -123,12 +137,16 @@
Events.record(currentUser, "delete", "widget", $scope.widget.id);
$scope.widget.$delete(function() {
$scope.widget.$delete(function(response) {
$scope.dashboard.widgets = _.map($scope.dashboard.widgets, function(row) {
return _.filter(row, function(widget) {
return widget.id != undefined;
})
});
$scope.dashboard.widgets = _.filter($scope.dashboard.widgets, function(row) { return row.length > 0 });
$scope.dashboard.layout = response.layout;
});
};
@@ -144,6 +162,8 @@
$scope.queryResult = $scope.query.getQueryResult(maxAge, parameters);
$scope.type = 'visualization';
} else if ($scope.widget.restricted) {
$scope.type = 'restricted';
} else {
$scope.type = 'textbox';
}

View File

@@ -0,0 +1,47 @@
(function () {
var DataSourcesCtrl = function ($scope, $location, growl, Events, DataSource) {
Events.record(currentUser, "view", "page", "admin/data_sources");
$scope.$parent.pageTitle = "Data Sources";
$scope.dataSources = DataSource.query();
$scope.openDataSource = function(datasource) {
$location.path('/data_sources/' + datasource.id);
};
$scope.deleteDataSource = function(event, datasource) {
event.stopPropagation();
Events.record(currentUser, "delete", "datasource", datasource.id);
datasource.$delete(function(resource) {
growl.addSuccessMessage("Data source deleted successfully.");
this.$parent.dataSources = _.without(this.dataSources, resource);
}.bind(this), function(httpResponse) {
console.log("Failed to delete data source: ", httpResponse.status, httpResponse.statusText, httpResponse.data);
growl.addErrorMessage("Failed to delete data source.");
});
}
};
var DataSourceCtrl = function ($scope, $routeParams, $http, $location, Events, DataSource) {
Events.record(currentUser, "view", "page", "admin/data_source");
$scope.$parent.pageTitle = "Data Sources";
$scope.dataSourceId = $routeParams.dataSourceId;
if ($scope.dataSourceId == "new") {
$scope.dataSource = new DataSource({options: {}});
} else {
$scope.dataSource = DataSource.get({id: $routeParams.dataSourceId});
}
$scope.$watch('dataSource.id', function(id) {
if (id != $scope.dataSourceId && id !== undefined) {
$location.path('/data_sources/' + id).replace();
}
});
};
angular.module('redash.controllers')
.controller('DataSourcesCtrl', ['$scope', '$location', 'growl', 'Events', 'DataSource', DataSourcesCtrl])
.controller('DataSourceCtrl', ['$scope', '$routeParams', '$http', '$location', 'Events', 'DataSource', DataSourceCtrl])
})();

View File

@@ -17,8 +17,9 @@
saveQuery = $scope.saveQuery;
$scope.sourceMode = true;
$scope.canEdit = true;
$scope.canEdit = currentUser.canEdit($scope.query);// TODO: bring this back? || clientConfig.allowAllToEditQueries;
$scope.isDirty = false;
$scope.base_url = $location.protocol()+"://"+$location.host()+":"+$location.port();
$scope.newVisualization = undefined;
@@ -67,9 +68,9 @@
$scope.duplicateQuery = function() {
Events.record(currentUser, 'fork', 'query', $scope.query.id);
$scope.query.name = 'Copy of (#'+$scope.query.id+') '+$scope.query.name;
$scope.query.id = null;
$scope.query.schedule = null;
$scope.saveQuery({
successMessage: 'Query forked',
errorMessage: 'Query could not be forked'

View File

@@ -4,6 +4,8 @@
function QueryViewCtrl($scope, Events, $route, $location, notifications, growl, $modal, Query, DataSource) {
var DEFAULT_TAB = 'table';
$scope.base_url = $location.protocol()+"://"+$location.host()+":"+$location.port();
var getQueryResult = function(maxAge) {
// Collect params, and getQueryResult with params; getQueryResult merges it into the query
var parameters = Query.collectParamsFromQueryString($location, $scope.query);
@@ -19,14 +21,60 @@
$scope.queryResult = $scope.query.getQueryResult(maxAge, parameters);
}
var getDataSourceId = function() {
// Try to get the query's data source id
var dataSourceId = $scope.query.data_source_id;
// If there is no source yet, then parse what we have in localStorage
// e.g. `null` -> `NaN`, malformed data -> `NaN`, "1" -> 1
if (dataSourceId === undefined) {
dataSourceId = parseInt(localStorage.lastSelectedDataSourceId, 10);
}
// If we had an invalid value in localStorage (e.g. nothing, deleted source), then use the first data source
var isValidDataSourceId = !isNaN(dataSourceId) && _.some($scope.dataSources, function(ds) {
return ds.id == dataSourceId;
});
if (!isValidDataSourceId) {
dataSourceId = $scope.dataSources[0].id;
}
// Return our data source id
return dataSourceId;
}
var updateDataSources = function(dataSources) {
// Filter out data sources the user can't query (or used by current query):
$scope.dataSources = _.filter(dataSources, function(dataSource) {
return !dataSource.view_only || dataSource.id === $scope.query.data_source_id;
});
if ($scope.dataSources.length == 0) {
$scope.noDataSources = true;
return;
}
if ($scope.query.isNew()) {
$scope.query.data_source_id = getDataSourceId();
}
$scope.dataSource = _.find(dataSources, function(ds) { return ds.id == $scope.query.data_source_id; });
//$scope.canExecuteQuery = $scope.canExecuteQuery && _.some(dataSources, function(ds) { return !ds.view_only });
$scope.canCreateQuery = _.any(dataSources, function(ds) { return !ds.view_only });
updateSchema();
}
$scope.dataSource = {};
$scope.query = $route.current.locals.query;
var updateSchema = function() {
$scope.hasSchema = false;
$scope.editorSize = "col-md-12";
var dataSourceId = $scope.query.data_source_id || $scope.dataSources[0].id;
DataSource.getSchema({id: dataSourceId}, function(data) {
DataSource.getSchema({id: $scope.query.data_source_id}, function(data) {
if (data && data.length > 0) {
$scope.schema = data;
_.each(data, function(table) {
@@ -49,11 +97,18 @@
$scope.isQueryOwner = (currentUser.id === $scope.query.user.id) || currentUser.hasPermission('admin');
$scope.canViewSource = currentUser.hasPermission('view_source');
$scope.dataSources = DataSource.get(function(dataSources) {
updateSchema();
$scope.query.data_source_id = $scope.query.data_source_id || dataSources[0].id;
$scope.dataSource = _.find(dataSources, function(ds) { return ds.id == $scope.query.data_source_id; });
});
$scope.canExecuteQuery = function() {
return currentUser.hasPermission('execute_query') && !$scope.dataSource.view_only;
}
$scope.canScheduleQuery = currentUser.hasPermission('schedule_query');
if ($route.current.locals.dataSources) {
$scope.dataSources = $route.current.locals.dataSources;
updateDataSources($route.current.locals.dataSources);
} else {
$scope.dataSources = DataSource.query(updateDataSources);
}
// in view mode, latest dataset is always visible
// source mode changes this behavior
@@ -101,6 +156,14 @@
};
$scope.executeQuery = function() {
if (!$scope.canExecuteQuery()) {
return;
}
if (!$scope.query.query) {
return;
}
getQueryResult(0);
$scope.lockButton(true);
$scope.cancelling = false;
@@ -140,6 +203,7 @@
$scope.updateDataSource = function() {
Events.record(currentUser, 'update_data_source', 'query', $scope.query.id);
localStorage.lastSelectedDataSourceId = $scope.query.data_source_id;
$scope.query.latest_query_data = null;
$scope.query.latest_query_data_id = null;
@@ -206,7 +270,7 @@
});
$scope.openScheduleForm = function() {
if (!$scope.isQueryOwner) {
if (!$scope.isQueryOwner || !$scope.canScheduleQuery) {
return;
};

View File

@@ -0,0 +1,349 @@
(function () {
var GroupsCtrl = function ($scope, $location, $modal, growl, Events, Group) {
Events.record(currentUser, "view", "page", "groups");
$scope.$parent.pageTitle = "Groups";
$scope.gridConfig = {
isPaginationEnabled: true,
itemsByPage: 20,
maxSize: 8,
};
$scope.gridColumns = [
{
"label": "Name",
"map": "name",
"cellTemplate": '<a href="groups/{{dataRow.id}}">{{dataRow.name}}</a>'
}
];
$scope.groups = [];
Group.query(function(groups) {
$scope.groups = groups;
});
$scope.newGroup = function() {
$modal.open({
templateUrl: '/views/groups/edit_group_form.html',
size: 'sm',
resolve: {
group: function() { return new Group({}); }
},
controller: ['$scope', '$modalInstance', 'group', function($scope, $modalInstance, group) {
$scope.group = group;
var newGroup = group.id === undefined;
if (newGroup) {
$scope.saveButtonText = "Create";
$scope.title = "Create a New Group";
} else {
$scope.saveButtonText = "Save";
$scope.title = "Edit Group";
}
$scope.ok = function() {
$scope.group.$save(function(group) {
if (newGroup) {
$location.path('/groups/' + group.id).replace();
$modalInstance.close();
} else {
$modalInstance.close();
}
});
}
$scope.cancel = function() {
$modalInstance.close();
}
}]
});
}
};
var usersNav = function($location) {
return {
restrict: 'E',
replace: true,
template:
'<ul class="nav nav-tabs">' +
'<li role="presentation" ng-class="{\'active\': usersPage }"><a href="users">Users</a></li>' +
'<li role="presentation" ng-class="{\'active\': groupsPage }" ng-if="showGroupsLink"><a href="groups">Groups</a></li>' +
'</ul>',
controller: ['$scope', function ($scope) {
$scope.usersPage = _.string.startsWith($location.path(), '/users');
$scope.groupsPage = _.string.startsWith($location.path(), '/groups');
$scope.showGroupsLink = currentUser.hasPermission('list_users');
}]
}
}
var groupName = function ($location, growl) {
return {
restrict: 'E',
scope: {
'group': '='
},
transclude: true,
template:
'<h2>'+
'<edit-in-place editable="canEdit()" done="saveName" ignore-blanks=\'true\' value="group.name"></edit-in-place>&nbsp;' +
'<button class="btn btn-xs btn-danger" ng-if="canEdit()" ng-click="deleteGroup()">Delete this group</button>' +
'</h2>',
replace: true,
controller: ['$scope', function ($scope) {
$scope.canEdit = function() {
return currentUser.isAdmin && $scope.group.type != 'builtin';
};
$scope.saveName = function() {
$scope.group.$save();
};
$scope.deleteGroup = function() {
if (confirm("Are you sure you want to delete this group?")) {
$scope.group.$delete(function() {
$location.path('/groups').replace();
growl.addSuccessMessage("Group deleted successfully.");
})
}
}
}]
}
};
var GroupDataSourcesCtrl = function($scope, $routeParams, $http, $location, growl, Events, Group, DataSource) {
Events.record(currentUser, "view", "group_data_sources", $scope.groupId);
$scope.group = Group.get({id: $routeParams.groupId});
$scope.dataSources = Group.dataSources({id: $routeParams.groupId});
$scope.newDataSource = {};
$scope.findDataSource = function(search) {
if ($scope.foundDataSources === undefined) {
DataSource.query(function(dataSources) {
var existingIds = _.map($scope.dataSources, function(m) { return m.id; });
$scope.foundDataSources = _.filter(dataSources, function(ds) { return !_.contains(existingIds, ds.id); });
});
}
};
$scope.addDataSource = function(dataSource) {
// Clear selection, to clear up the input control.
$scope.newDataSource.selected = undefined;
$http.post('api/groups/' + $routeParams.groupId + '/data_sources', {'data_source_id': dataSource.id}).success(function(user) {
dataSource.view_only = false;
$scope.dataSources.unshift(dataSource);
if ($scope.foundDataSources) {
$scope.foundDataSources = _.filter($scope.foundDataSources, function(ds) { return ds != dataSource; });
}
});
};
$scope.changePermission = function(dataSource, viewOnly) {
$http.post('api/groups/' + $routeParams.groupId + '/data_sources/' + dataSource.id, {view_only: viewOnly}).success(function() {
dataSource.view_only = viewOnly;
});
};
$scope.removeDataSource = function(dataSource) {
$http.delete('api/groups/' + $routeParams.groupId + '/data_sources/' + dataSource.id).success(function() {
$scope.dataSources = _.filter($scope.dataSources, function(ds) { return dataSource != ds; });
});
};
}
var GroupCtrl = function($scope, $routeParams, $http, $location, growl, Events, Group, User) {
Events.record(currentUser, "view", "group", $scope.groupId);
$scope.group = Group.get({id: $routeParams.groupId});
$scope.members = Group.members({id: $routeParams.groupId});
$scope.newMember = {};
$scope.findUser = function(search) {
if (search == "") {
return;
}
if ($scope.foundUsers === undefined) {
User.query(function(users) {
var existingIds = _.map($scope.members, function(m) { return m.id; });
_.each(users, function(user) { user.alreadyMember = _.contains(existingIds, user.id); });
$scope.foundUsers = users;
});
}
};
$scope.addMember = function(user) {
// Clear selection, to clear up the input control.
$scope.newMember.selected = undefined;
$http.post('api/groups/' + $routeParams.groupId + '/members', {'user_id': user.id}).success(function() {
$scope.members.unshift(user);
user.alreadyMember = true;
});
};
$scope.removeMember = function(member) {
$http.delete('api/groups/' + $routeParams.groupId + '/members/' + member.id).success(function() {
$scope.members = _.filter($scope.members, function(m) { return m != member });
if ($scope.foundUsers) {
_.each($scope.foundUsers, function(user) { if (user.id == member.id) { user.alreadyMember = false }; });
}
});
};
}
var UsersCtrl = function ($scope, $location, growl, Events, User) {
Events.record(currentUser, "view", "page", "users");
$scope.$parent.pageTitle = "Users";
$scope.gridConfig = {
isPaginationEnabled: true,
itemsByPage: 20,
maxSize: 8,
};
$scope.gridColumns = [
{
"label": "Name",
"map": "name",
"cellTemplate": '<img src="{{dataRow.gravatar_url}}" height="40px"/> <a href="users/{{dataRow.id}}">{{dataRow.name}}</a>'
},
{
'label': 'Joined',
'cellTemplate': '<span am-time-ago="dataRow.created_at"></span>'
}
];
$scope.users = [];
User.query(function(users) {
$scope.users = users;
});
};
var UserCtrl = function ($scope, $routeParams, $http, $location, growl, Events, User) {
$scope.$parent.pageTitle = "Users";
$scope.userId = $routeParams.userId;
if ($scope.userId === 'me') {
$scope.userId = currentUser.id;
}
Events.record(currentUser, "view", "user", $scope.userId);
$scope.canEdit = currentUser.hasPermission("admin") || currentUser.id === parseInt($scope.userId);
$scope.showSettings = false;
$scope.showPasswordSettings = false;
$scope.selectTab = function(tab) {
_.each($scope.tabs, function(v, k) {
$scope.tabs[k] = (k === tab);
});
};
$scope.setTab = function(tab) {
$location.hash(tab);
}
$scope.tabs = {
profile: false,
apiKey: false,
settings: false,
password: false
};
$scope.selectTab($location.hash() || 'profile');
$scope.user = User.get({id: $scope.userId}, function(user) {
if (user.auth_type == 'password') {
$scope.showSettings = $scope.canEdit;
$scope.showPasswordSettings = $scope.canEdit;
}
});
$scope.password = {
current: '',
new: '',
newRepeat: ''
};
$scope.savePassword = function(form) {
$scope.$broadcast('show-errors-check-validity');
if (!form.$valid) {
return;
}
var data = {
id: $scope.user.id,
password: $scope.password.new,
old_password: $scope.password.current
};
User.save(data, function() {
growl.addSuccessMessage("Password Saved.")
$scope.password = {
current: '',
new: '',
newRepeat: ''
};
}, function(error) {
var message = error.data.message || "Failed saving password.";
growl.addErrorMessage(message);
});
};
$scope.updateUser = function(form) {
$scope.$broadcast('show-errors-check-validity');
if (!form.$valid) {
return;
}
var data = {
id: $scope.user.id,
name: $scope.user.name,
email: $scope.user.email
};
User.save(data, function(user) {
growl.addSuccessMessage("Saved.")
$scope.user = user;
}, function(error) {
var message = error.data.message || "Failed saving.";
growl.addErrorMessage(message);
});
};
};
var NewUserCtrl = function ($scope, $location, growl, Events, User) {
Events.record(currentUser, "view", "page", "users/new");
$scope.user = new User({});
$scope.saveUser = function() {
$scope.$broadcast('show-errors-check-validity');
if (!$scope.userForm.$valid) {
return;
}
$scope.user.$save(function(user) {
growl.addSuccessMessage("Saved.")
$location.path('/users/' + user.id).replace();
}, function(error) {
var message = error.data.message || "Failed saving.";
growl.addErrorMessage(message);
});
}
};
angular.module('redash.controllers')
.controller('GroupsCtrl', ['$scope', '$location', '$modal', 'growl', 'Events', 'Group', GroupsCtrl])
.directive('groupName', ['$location', 'growl', groupName])
.directive('usersNav', ['$location', usersNav])
.controller('GroupCtrl', ['$scope', '$routeParams', '$http', '$location', 'growl', 'Events', 'Group', 'User', GroupCtrl])
.controller('GroupDataSourcesCtrl', ['$scope', '$routeParams', '$http', '$location', 'growl', 'Events', 'Group', 'DataSource', GroupDataSourcesCtrl])
.controller('UsersCtrl', ['$scope', '$location', 'growl', 'Events', 'User', UsersCtrl])
.controller('UserCtrl', ['$scope', '$routeParams', '$http', '$location', 'growl', 'Events', 'User', UserCtrl])
.controller('NewUserCtrl', ['$scope', '$location', 'growl', 'Events', 'User', NewUserCtrl])
})();

View File

@@ -31,7 +31,7 @@
'<div class="panel-heading">{name}' +
'</div></li>';
$scope.$watch('dashboard.widgets && dashboard.widgets.length', function(widgets_length) {
$scope.$watch('dashboard.layout', function() {
$timeout(function() {
gridster.remove_all_widgets();
@@ -57,7 +57,7 @@
});
}
});
});
}, true);
$scope.saveDashboard = function() {
$scope.saveInProgress = true;
@@ -81,18 +81,15 @@
$scope.dashboard.layout = layout;
layout = JSON.stringify(layout);
$http.post('/api/dashboards/' + $scope.dashboard.id, {
'name': $scope.dashboard.name,
'layout': layout
}).success(function(response) {
$scope.dashboard = new Dashboard(response);
Dashboard.save({slug: $scope.dashboard.id, name: $scope.dashboard.name, layout: layout}, function(dashboard) {
$scope.dashboard = dashboard;
$scope.saveInProgress = false;
$(element).modal('hide');
});
Events.record(currentUser, 'edit', 'dashboard', $scope.dashboard.id);
} else {
$http.post('/api/dashboards', {
$http.post('api/dashboards', {
'name': $scope.dashboard.name
}).success(function(response) {
$(element).modal('hide');
@@ -142,6 +139,11 @@
$scope.setType = function (type) {
$scope.type = type;
if (type == 'textbox') {
$scope.widgetSizes.push({name: 'Hidden', value: 0});
} else if ($scope.widgetSizes.length > 2) {
$scope.widgetSizes.pop();
}
};
var reset = function() {
@@ -186,7 +188,6 @@
$scope.saveWidget = function() {
$scope.saveInProgress = true;
var widget = new Widget({
'visualization_id': $scope.selectedVis && $scope.selectedVis.id,
'dashboard_id': $scope.dashboard.id,
@@ -219,4 +220,4 @@
}
}
])
})();
})();

View File

@@ -0,0 +1,83 @@
(function () {
'use strict';
var directives = angular.module('redash.directives');
// Angular strips data- from the directive, so data-source-form becomes sourceForm...
directives.directive('sourceForm', ['$http', 'growl', '$q', function ($http, growl, $q) {
return {
restrict: 'E',
replace: true,
templateUrl: '/views/data_sources/form.html',
scope: {
'dataSource': '='
},
link: function ($scope) {
var setType = function(types) {
if ($scope.dataSource.type === undefined) {
$scope.dataSource.type = types[0].type;
return types[0];
}
$scope.type = _.find(types, function (t) {
return t.type == $scope.dataSource.type;
});
};
$scope.files = {};
$scope.$watchCollection('files', function() {
_.each($scope.files, function(v, k) {
if (v) {
$scope.dataSource.options[k] = v.base64;
}
});
});
var typesPromise = $http.get('api/data_sources/types');
$q.all([typesPromise, $scope.dataSource.$promise]).then(function(responses) {
var types = responses[0].data;
setType(types);
$scope.dataSourceTypes = types;
_.each(types, function (type) {
_.each(type.configuration_schema.properties, function (prop, name) {
if (name == 'password' || name == 'passwd') {
prop.type = 'password';
}
if (_.string.endsWith(name, "File")) {
prop.type = 'file';
}
if (prop.type == 'boolean') {
prop.type = 'checkbox';
}
prop.required = _.contains(type.configuration_schema.required, name);
});
});
});
$scope.$watch('dataSource.type', function(current, prev) {
if (prev !== current) {
if (prev !== undefined) {
$scope.dataSource.options = {};
}
setType($scope.dataSourceTypes);
}
});
$scope.saveChanges = function() {
$scope.dataSource.$save(function() {
growl.addSuccessMessage("Saved.");
}, function() {
growl.addErrorMessage("Failed saving.");
});
}
}
}
}]);
})();

View File

@@ -40,7 +40,20 @@
}
}]);
directives.directive('rdTab', function () {
directives.directive('hashLink', ['$location', function($location) {
return {
restrict: 'A',
scope: {
'hash': '@'
},
link: function (scope, element) {
var basePath = $location.path().substring(1);
element[0].href = basePath + "#" + scope.hash;
}
};
}]);
directives.directive('rdTab', ['$location', function ($location) {
return {
restrict: 'E',
scope: {
@@ -48,9 +61,10 @@
'name': '@'
},
transclude: true,
template: '<li class="rd-tab" ng-class="{active: tabId==selectedTab}"><a href="#{{tabId}}">{{name}}<span ng-transclude></span></a></li>',
template: '<li class="rd-tab" ng-class="{active: tabId==selectedTab}"><a href="{{basePath}}#{{tabId}}">{{name}}<span ng-transclude></span></a></li>',
replace: true,
link: function (scope) {
scope.basePath = $location.path().substring(1);
scope.$watch(function () {
return scope.$parent.selectedTab
}, function (tab) {
@@ -58,7 +72,7 @@
});
}
}
});
}]);
directives.directive('rdTabs', ['$location', function ($location) {
return {
@@ -67,9 +81,10 @@
tabsCollection: '=',
selectedTab: '='
},
template: '<ul class="nav nav-tabs"><li ng-class="{active: tab==selectedTab}" ng-repeat="tab in tabsCollection"><a href="#{{tab.key}}">{{tab.name}}</a></li></ul>',
template: '<ul class="nav nav-tabs"><li ng-class="{active: tab==selectedTab}" ng-repeat="tab in tabsCollection"><a href="{{basePath}}#{{tab.key}}">{{tab.name}}</a></li></ul>',
replace: true,
link: function ($scope, element, attrs) {
$scope.basePath = $location.path().substring(1);
$scope.selectTab = function (tabKey) {
$scope.selectedTab = _.find($scope.tabsCollection, function (tab) {
return tab.key == tabKey;
@@ -247,4 +262,82 @@
};
}]
);
directives.directive('compareTo', function () {
return {
require: "ngModel",
scope: {
otherModelValue: "=compareTo"
},
link: function (scope, element, attributes, ngModel) {
var validate = function(value) {
ngModel.$setValidity("compareTo", value === scope.otherModelValue);
};
scope.$watch("otherModelValue", function() {
validate(ngModel.$modelValue);
});
ngModel.$parsers.push(function(value) {
validate(value);
return value;
});
}
};
});
directives.directive('inputErrors', function () {
return {
restrict: "E",
templateUrl: "/views/directives/input_errors.html",
replace: true,
scope: {
errors: "="
}
};
});
directives.directive('onDestroy', function () {
/* This directive can be used to invoke a callback when an element is destroyed,
A useful example is the following:
<div ng-if="includeText" on-destroy="form.text = null;">
<input type="text" ng-model="form.text">
</div>
*/
return {
restrict: "A",
scope: {
onDestroy: "&",
},
link: function(scope, elem, attrs) {
scope.$on('$destroy', function() {
scope.onDestroy();
});
}
};
});
directives.directive('colorBox', function () {
return {
restrict: "E",
scope: {color: "="},
template: "<span style='width: 12px; height: 12px; background-color: {{color}}; display: inline-block; margin-right: 5px;'></span>"
};
});
directives.directive('overlay', function() {
return {
restrict: "E",
transclude: true,
template: "" +
'<div>' +
'<div class="overlay"></div>' +
'<div style="width: 100%; position:absolute; top:50px; z-index:2000">' +
'<div class="well well-lg" style="width: 70%; margin: auto;" ng-transclude>' +
'</div>' +
'</div>' +
'</div>'
}
})
})();

View File

@@ -0,0 +1,286 @@
(function () {
'use strict';
var ColorPalette = {
'Blue': '#4572A7',
'Red': '#AA4643',
'Green': '#89A54E',
'Purple': '#80699B',
'Cyan': '#3D96AE',
'Orange': '#DB843D',
'Light Blue': '#92A8CD',
'Lilac': '#A47D7C',
'Light Green': '#B5CA92',
'Brown': '#A52A2A',
'Black': '#000000',
'Gray': '#808080',
'Pink': '#FFC0CB',
'Dark Blue': '#00008b'
};
var ColorPaletteArray = _.values(ColorPalette);
var fillXValues = function(seriesList) {
var xValues = _.sortBy(_.union.apply(_, _.pluck(seriesList, 'x')), _.identity);
_.each(seriesList, function(series) {
series.x = _.sortBy(series.x, _.identity);
_.each(xValues, function(value, index) {
if (series.x[index] !== value) {
series.x.splice(index, 0, value);
series.y.splice(index, 0, null);
}
});
});
};
var normalAreaStacking = function(seriesList) {
fillXValues(seriesList);
_.each(seriesList, function(series) {
series.text = [];
series.hoverinfo = 'text+name';
});
for (var i = 0; i < seriesList.length; i++) {
for (var j = 0; j < seriesList[i].y.length; j++) {
var sum = i > 0 ? seriesList[i-1].y[j] : 0;
seriesList[i].text.push('Value: ' + seriesList[i].y[j] + '<br>Sum: ' + (sum + seriesList[i].y[j]));
seriesList[i].y[j] += sum;
}
}
};
var percentAreaStacking = function(seriesList) {
if (seriesList.length === 0) {
return;
}
fillXValues(seriesList);
_.each(seriesList, function(series) {
series.text = [];
series.hoverinfo = 'text+name';
});
for (var i = 0; i < seriesList[0].y.length; i++) {
var sum = 0;
for(var j = 0; j < seriesList.length; j++) {
sum += seriesList[j].y[i];
}
for(var j = 0; j < seriesList.length; j++) {
var value = seriesList[j].y[i] / sum * 100;
seriesList[j].text.push('Value: ' + seriesList[j].y[i] + '<br>Relative: ' + value.toFixed(2) + '%');
seriesList[j].y[i] = value;
if (j > 0) {
seriesList[j].y[i] += seriesList[j-1].y[i];
}
}
}
};
var percentBarStacking = function(seriesList) {
if (seriesList.length === 0) {
return;
}
fillXValues(seriesList);
_.each(seriesList, function(series) {
series.text = [];
series.hoverinfo = 'text+name';
});
for (var i = 0; i < seriesList[0].y.length; i++) {
var sum = 0;
for(var j = 0; j < seriesList.length; j++) {
sum += seriesList[j].y[i];
}
for(var j = 0; j < seriesList.length; j++) {
var value = seriesList[j].y[i] / sum * 100;
seriesList[j].text.push('Value: ' + seriesList[j].y[i] + '<br>Relative: ' + value.toFixed(2) + '%');
seriesList[j].y[i] = value;
}
}
}
var normalizeValue = function(value) {
if (moment.isMoment(value)) {
return value.format("YYYY-MM-DD HH:mm:ss");
}
return value;
}
angular.module('plotly-chart', [])
.constant('ColorPalette', ColorPalette)
.directive('plotlyChart', function () {
return {
restrict: 'E',
template: '<plotly data="data" layout="layout" options="plotlyOptions"></plotly>',
scope: {
options: "=",
series: "=",
minHeight: "="
},
link: function (scope) {
var getScaleType = function(scale) {
if (scale === 'datetime') {
return 'date';
}
if (scale === 'logarithmic') {
return 'log';
}
return scale;
};
var setType = function(series, type) {
if (type === 'column') {
series.type = 'bar';
} else if (type === 'line') {
series.mode = 'lines';
} else if (type === 'area') {
series.fill = scope.options.series.stacking === null ? 'tozeroy' : 'tonexty';
series.mode = 'lines';
} else if (type === 'scatter') {
series.type = 'scatter';
series.mode = 'markers';
}
};
var getColor = function(index) {
return ColorPaletteArray[index % ColorPaletteArray.length];
};
var bottomMargin = 50,
pixelsPerLegendRow = 21;
var redraw = function() {
scope.data.length = 0;
scope.layout.showlegend = _.has(scope.options, 'legend') ? scope.options.legend.enabled : true;
delete scope.layout.barmode;
delete scope.layout.xaxis;
delete scope.layout.yaxis;
delete scope.layout.yaxis2;
if (scope.options.globalSeriesType === 'pie') {
var hasX = _.contains(_.values(scope.options.columnMapping), 'x');
var rows = scope.series.length > 2 ? 2 : 1;
var cellsInRow = Math.ceil(scope.series.length / rows);
var cellWidth = 1 / cellsInRow;
var cellHeight = 1 / rows;
var xPadding = 0.02;
var yPadding = 0.05;
var largestXCount = 0;
_.each(scope.series, function(series, index) {
var xPosition = (index % cellsInRow) * cellWidth;
var yPosition = Math.floor(index / cellsInRow) * cellHeight;
var plotlySeries = {values: [], labels: [], type: 'pie', hole: .4,
marker: {colors: ColorPaletteArray},
text: series.name, textposition: 'inside', name: series.name,
domain: {x: [xPosition, xPosition + cellWidth - xPadding],
y: [yPosition, yPosition + cellHeight - yPadding]}};
_.each(series.data, function(row, index) {
plotlySeries.values.push(row.y);
plotlySeries.labels.push(hasX ? row.x : 'Slice ' + index);
});
scope.data.push(plotlySeries);
largestXCount = Math.max(largestXCount, plotlySeries.labels.length);
});
scope.layout.height = Math.max(scope.minHeight, pixelsPerLegendRow * largestXCount);
scope.layout.margin.b = scope.layout.height - (scope.minHeight - bottomMargin);
return;
}
scope.layout.height = Math.max(scope.minHeight, pixelsPerLegendRow * scope.series.length);
scope.layout.margin.b = scope.layout.height - (scope.minHeight - bottomMargin);
var hasY2 = false;
var sortX = scope.options.sortX === true || scope.options.sortX === undefined;
var useUnifiedXaxis = sortX && scope.options.xAxis.type === 'category';
var unifiedX = null;
if (useUnifiedXaxis) {
unifiedX = _.sortBy(_.union.apply(_, _.map(scope.series, function(s) { return _.pluck(s.data, 'x'); })), _.identity);
}
_.each(scope.series, function(series, index) {
var seriesOptions = scope.options.seriesOptions[series.name] || {};
var plotlySeries = {x: [],
y: [],
name: seriesOptions.name || series.name,
marker: {color: seriesOptions.color ? seriesOptions.color : getColor(index)}};
if (seriesOptions.yAxis === 1 && (scope.options.series.stacking === null || seriesOptions.type === 'line')) {
hasY2 = true;
plotlySeries.yaxis = 'y2';
}
setType(plotlySeries, seriesOptions.type);
var data = series.data;
if (sortX) {
data = _.sortBy(data, 'x');
}
if (useUnifiedXaxis && index === 0) {
var values = {};
_.each(data, function(row) {
values[row.x] = row.y;
});
_.each(unifiedX, function(x) {
plotlySeries.x.push(normalizeValue(x));
plotlySeries.y.push(normalizeValue(values[x] || null));
});
} else {
_.each(data, function(row) {
plotlySeries.x.push(normalizeValue(row.x));
plotlySeries.y.push(normalizeValue(row.y));
});
}
scope.data.push(plotlySeries);
});
var getTitle = function(axis) {
if (angular.isDefined(axis) && angular.isDefined(axis.title)) {
return axis.title.text;
}
return null;
};
scope.layout.xaxis = {title: getTitle(scope.options.xAxis),
type: getScaleType(scope.options.xAxis.type)};
if (angular.isDefined(scope.options.xAxis.labels)) {
scope.layout.xaxis.showticklabels = scope.options.xAxis.labels.enabled;
}
if (angular.isArray(scope.options.yAxis)) {
scope.layout.yaxis = {title: getTitle(scope.options.yAxis[0]),
type: getScaleType(scope.options.yAxis[0].type)};
}
if (hasY2 && angular.isDefined(scope.options.yAxis)) {
scope.layout.yaxis2 = {title: getTitle(scope.options.yAxis[1]),
type: getScaleType(scope.options.yAxis[1].type),
overlaying: 'y',
side: 'right'};
} else {
delete scope.layout.yaxis2;
}
if (scope.options.series.stacking === 'normal') {
scope.layout.barmode = 'stack';
if (scope.options.globalSeriesType === 'area') {
normalAreaStacking(scope.data);
}
} else if (scope.options.series.stacking === 'percent') {
scope.layout.barmode = 'stack';
if (scope.options.globalSeriesType === 'area') {
percentAreaStacking(scope.data);
} else if (scope.options.globalSeriesType === 'column') {
percentBarStacking(scope.data);
}
}
};
scope.$watch('series', redraw);
scope.$watch('options', redraw, true);
scope.layout = {margin: {l: 50, r: 50, b: 50, t: 20, pad: 4}, hovermode: 'closest'};
scope.plotlyOptions = {showLink: false, displaylogo: false};
scope.data = [];
}
};
});
})();

View File

@@ -8,9 +8,9 @@
'query': '=',
'visualization': '=?'
},
template: '<small><span class="glyphicon glyphicon-link"></span></small> <a ng-href="{{link}}" class="query-link">{{query.name}}</a>',
template: '<a ng-href="{{link}}" class="query-link">{{query.name}}</a>',
link: function(scope, element) {
scope.link = '/queries/' + scope.query.id;
scope.link = 'queries/' + scope.query.id;
if (scope.visualization) {
if (scope.visualization.type === 'TABLE') {
// link to hard-coded table tab instead of the (hidden) visualization tab
@@ -29,19 +29,21 @@
restrict: 'E',
template: '<span ng-show="query.id && canViewSource">\
<a ng-show="!sourceMode"\
ng-href="/queries/{{query.id}}/source#{{selectedTab}}">Show Source\
ng-href="queries/{{query.id}}/source#{{selectedTab}}">Show Source\
</a>\
<a ng-show="sourceMode"\
ng-href="/queries/{{query.id}}#{{selectedTab}}">Hide Source\
ng-href="queries/{{query.id}}#{{selectedTab}}">Hide Source\
</a>\
</span>'
}
}
function queryResultCSVLink() {
function queryResultLink() {
return {
restrict: 'A',
link: function (scope, element) {
link: function (scope, element, attrs) {
var fileType = attrs.fileType ? attrs.fileType : "csv";
scope.$watch('queryResult && queryResult.getData()', function(data) {
if (!data) {
return;
@@ -50,8 +52,8 @@
if (scope.queryResult.getId() == null) {
element.attr('href', '');
} else {
element.attr('href', '/api/queries/' + scope.query.id + '/results/' + scope.queryResult.getId() + '.csv');
element.attr('download', scope.query.name.replace(" ", "_") + moment(scope.queryResult.getUpdatedAt()).format("_YYYY_MM_DD") + ".csv");
element.attr('href', 'api/queries/' + scope.query.id + '/results/' + scope.queryResult.getId() + '.' + fileType);
element.attr('download', scope.query.name.replace(" ", "_") + moment(scope.queryResult.getUpdatedAt()).format("_YYYY_MM_DD") + "." + fileType);
}
});
}
@@ -265,6 +267,10 @@
value: String(7 * 24 * 3600),
name: 'Once a week'
});
$scope.refreshOptions.push({
value: String(30 * 24 * 3600),
name: 'Every 30d'
});
$scope.$watch('refreshType', function() {
if ($scope.refreshType == 'periodic') {
@@ -282,9 +288,9 @@
angular.module('redash.directives')
.directive('queryLink', queryLink)
.directive('querySourceLink', querySourceLink)
.directive('queryResultLink', queryResultCSVLink)
.directive('queryResultLink', queryResultLink)
.directive('queryEditor', queryEditor)
.directive('queryRefreshSelect', queryRefreshSelect)
.directive('queryTimePicker', queryTimePicker)
.directive('queryFormatter', ['$http', queryFormatter]);
})();
})();

View File

@@ -0,0 +1,55 @@
angular.module('redash', [
'redash.directives',
'redash.admin_controllers',
'redash.controllers',
'redash.filters',
'redash.services',
'redash.visualization',
'plotly',
'plotly-chart',
'angular-growl',
'angularMoment',
'ui.bootstrap',
'ui.sortable',
'smartTable.table',
'ngResource',
'ngRoute',
'ui.select',
'naif.base64',
'ui.bootstrap.showErrors',
'ngSanitize'
]).config(['$routeProvider', '$locationProvider', '$compileProvider', 'growlProvider', 'uiSelectConfig',
function ($routeProvider, $locationProvider, $compileProvider, growlProvider, uiSelectConfig) {
function getQuery(Query, $route) {
var query = Query.get({'id': $route.current.params.queryId });
return query.$promise;
};
uiSelectConfig.theme = "bootstrap";
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|http|data):/);
$locationProvider.html5Mode(true);
growlProvider.globalTimeToLive(2000);
$routeProvider.when('/embed/query/:queryId/visualization/:visualizationId', {
templateUrl: '/views/visualization-embed.html',
controller: 'EmbedCtrl',
reloadOnSearch: false
});
$routeProvider.otherwise({
redirectTo: '/embed'
});
}
])
.controller('EmbedCtrl', ['$scope', function ($scope) {} ])
.controller('EmbeddedVisualizationCtrl', ['$scope', 'Query', 'QueryResult',
function ($scope, Query, QueryResult) {
$scope.embed = true;
$scope.visualization = visualization;
$scope.query = visualization.query;
query = new Query(visualization.query);
$scope.queryResult = new QueryResult({query_result:query_result});
} ])
;

View File

@@ -48,6 +48,9 @@ angular.module('redash.filters', []).
.filter('colWidth', function () {
return function (widgetWidth) {
if (widgetWidth == 0) {
return 0;
}
if (widgetWidth == 1) {
return 6;
}
@@ -66,6 +69,12 @@ angular.module('redash.filters', []).
}
})
.filter('dateTime', function() {
return function(value) {
return moment(value).format(clientConfig.dateTimeFormat);
}
})
.filter('linkify', function () {
return function (text) {
return text.replace(urlPattern, "$1<a href='$2' target='_blank'>$2</a>");
@@ -77,7 +86,13 @@ angular.module('redash.filters', []).
if (!text) {
return "";
}
return $sce.trustAsHtml(marked(text));
var html = marked(text);
if (clientConfig.allowScriptsInUserInput) {
html = $sce.trustAsHtml(html);
}
return html;
}
}])
@@ -88,4 +103,21 @@ angular.module('redash.filters', []).
}
return $sce.trustAsHtml(text);
}
}]);
}])
.filter('remove', function() {
return function(items, item) {
if (items == undefined)
return items;
if (item instanceof Array) {
var notEquals = function(other) { return item.indexOf(other) == -1; }
} else {
var notEquals = function(other) { return item != other; }
}
var filtered = [];
for (var i = 0; i < items.length; i++)
if (notEquals(items[i]))
filtered.push(items[i])
return filtered;
};
});

View File

@@ -1,414 +0,0 @@
(function () {
'use strict';
var ColorPalette = {
'Blue':'#4572A7',
'Red':'#AA4643',
'Green': '#89A54E',
'Purple': '#80699B',
'Cyan': '#3D96AE',
'Orange': '#DB843D',
'Light Blue': '#92A8CD',
'Lilac': '#A47D7C',
'Light Green': '#B5CA92',
};
Highcharts.setOptions({
colors: _.values(ColorPalette)
});
var defaultOptions = {
title: {
"text": null
},
xAxis: {
type: 'datetime'
},
yAxis: [
{
title: {
text: null
},
// showEmpty: true // by default
},
{
title: {
text: null
},
opposite: true,
showEmpty: false
}
],
tooltip: {
valueDecimals: 2,
formatter: function () {
if (!this.points) {
this.points = [this.point];
}
;
if (moment.isMoment(this.x)) {
var s = '<b>' + this.x.toDate().toLocaleString() + '</b>',
pointsCount = this.points.length;
$.each(this.points, function (i, point) {
s += '<br/><span style="color:' + point.series.color + '">' + point.series.name + '</span>: ' +
Highcharts.numberFormat(point.y);
if (pointsCount > 1 && point.percentage) {
s += " (" + Highcharts.numberFormat(point.percentage) + "%)";
}
});
} else {
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) + '%)';
}
}
});
}
return s;
},
shared: true
},
exporting: {
chartOptions: {
title: {
text: ''
}
},
buttons: {
contextButton: {
menuItems: [
{
text: 'Toggle % Stacking',
onclick: function () {
var newStacking = "normal";
if (this.series[0].options.stacking == "normal") {
newStacking = "percent";
}
_.each(this.series, function (series) {
series.update({stacking: newStacking}, true);
});
}
},
{
text: 'Select All',
onclick: function () {
_.each(this.series, function (s) {
s.setVisible(true, false);
});
this.redraw();
}
},
{
text: 'Unselect All',
onclick: function () {
_.each(this.series, function (s) {
s.setVisible(false, false);
});
this.redraw();
}
},
{
text: 'Show Total',
onclick: function () {
var hasTotalsAlready = _.some(this.series, function (s) {
var res = (s.name == 'Total');
//if 'Total' already exists - just make it visible
if (res) s.setVisible(true, false);
return res;
})
var data = {};
_.each(this.series, function (s) {
if (s.name != 'Total') s.setVisible(false, false);
if (!hasTotalsAlready) {
_.each(s.data, function (p) {
data[p.x] = data[p.x] || {'x': p.x, 'y': 0};
data[p.x].y = data[p.x].y + p.y;
});
}
});
if (!hasTotalsAlready) {
this.addSeries({
data: _.values(data),
type: 'line',
name: 'Total'
}, false)
}
this.redraw();
}
},
{
text: 'Save Image',
onclick: function () {
var canvas = document.createElement('canvas');
window.canvg(canvas, this.getSVG());
var href = canvas.toDataURL('image/png');
var a = document.createElement('a');
a.href = href;
var filenameSuffix = new Date().toISOString().replace(/:/g,'_').replace('Z', '');
if (this.title) {
filenameSuffix = this.title.text;
}
a.download = 'redash_charts_'+filenameSuffix+'.png';
document.body.appendChild(a);
a.click();
a.remove();
}
}
]
}
}
},
credits: {
enabled: false
},
plotOptions: {
area: {
marker: {
enabled: false,
symbol: 'circle',
radius: 2,
states: {
hover: {
enabled: true
}
}
}
},
column: {
stacking: "normal",
pointPadding: 0,
borderWidth: 1,
groupPadding: 0,
shadow: false
},
line: {
marker: {
radius: 1
},
lineWidth: 2,
states: {
hover: {
lineWidth: 2,
marker: {
radius: 3
}
}
}
},
pie: {
allowPointSelect: true,
cursor: 'pointer',
dataLabels: {
enabled: true,
color: '#000000',
connectorColor: '#000000',
format: '<b>{point.name}</b>: {point.y} ({point.percentage:.1f} %)'
}
},
scatter: {
marker: {
radius: 5,
states: {
hover: {
enabled: true,
lineColor: 'rgb(100,100,100)'
}
}
},
tooltip: {
headerFormat: '<b>{series.name}</b><br>',
pointFormat: '{point.x}, {point.y}'
}
}
},
series: []
};
angular.module('highchart', [])
.constant('ColorPalette', ColorPalette)
.directive('chart', ['$timeout', function ($timeout) {
return {
restrict: 'E',
template: '<div></div>',
scope: {
options: "=options",
series: "=series"
},
transclude: true,
replace: true,
link: function (scope, element, attrs) {
var chartsDefaults = {
chart: {
renderTo: element[0],
type: attrs.type || null,
height: attrs.height || null,
width: attrs.width || null
}
};
var chartOptions = $.extend(true, {}, defaultOptions, chartsDefaults);
// $timeout makes sure that this function invoked after the DOM ready. When draw/init
// invoked after the DOM is ready, we see first an empty HighCharts objects and later
// they get filled up. Which gives the feeling that the charts loading faster (otherwise
// we stare at an empty screen until the HighCharts object is ready).
$timeout(function () {
// Update when options change
scope.$watch('options', function (newOptions) {
initChart(newOptions);
}, true);
//Update when charts data changes
scope.$watchCollection('series', function (series) {
if (!series || series.length == 0) {
scope.chart.showLoading();
} else {
drawChart();
}
;
});
});
function initChart(options) {
if (scope.chart) {
scope.chart.destroy();
}
;
$.extend(true, chartOptions, options);
scope.chart = new Highcharts.Chart(chartOptions);
drawChart();
}
function drawChart() {
while (scope.chart.series.length > 0) {
scope.chart.series[0].remove(false);
};
// We check either for true or undefined for backward compatibility.
var series = scope.series;
// If this is a chart that has just one row for multiple columns, sort
// by the Y values. For example:
//
// A | B | C
// 20 | 30 | 15
//
// Will be sorted:
// C | A | B
// 15 | 20 | 30
var sortable = _.every(series, function(s) { return s.data.length == 1 });
if (sortable) {
series = _.sortBy(series, function (s) {
return s.data[0].y
});
}
if (!('xAxis' in chartOptions && 'type' in chartOptions['xAxis'])) {
if (series.length > 0 && _.some(series[0].data, function (p) {
return (angular.isString(p.x) || angular.isDefined(p.name));
})) {
chartOptions['xAxis'] = chartOptions['xAxis'] || {};
chartOptions['xAxis']['type'] = 'category';
} else {
chartOptions['xAxis'] = chartOptions['xAxis'] || {};
chartOptions['xAxis']['type'] = 'datetime';
}
}
if (chartOptions['xAxis']['type'] == 'category' || chartOptions['series']['type']=='pie') {
if (!angular.isDefined(series[0].data[0].name)) {
// We need to make sure that for each category, each series has a value.
var categories = _.union.apply(this, _.map(series, function (s) {
return _.pluck(s.data, 'x')
}));
_.each(series, function (s) {
// TODO: move this logic to Query#getChartData
var yValues = _.groupBy(s.data, 'x');
var newData = _.map(categories, function (category) {
return {
name: category,
y: (yValues[category] && yValues[category][0].y) || 0
}
});
s.data = newData;
});
}
}
if (chartOptions['sortX'] === true || chartOptions['sortX'] === undefined) {
var seriesCopy = [];
_.each(series, function (s) {
// make a copy of series data, so we don't override original.
var fieldName = 'x';
if (s.data.length > 0 && _.has(s.data[0], 'name')) {
fieldName = 'name';
};
var sorted = _.extend({}, s, {data: _.sortBy(s.data, fieldName)});
seriesCopy.push(sorted);
});
series = seriesCopy;
}
scope.chart.counters.color = 0;
_.each(series, function (s) {
// here we override the series with the visualization config
s = _.extend(s, chartOptions['series']);
if (s.type == 'area') {
_.each(s.data, function (p) {
// This is an insane hack: somewhere deep in HighChart's code,
// when you stack areas, it tries to convert the string representation
// of point's x into a number. With the default implementation of toString
// it fails....
if (moment.isMoment(p.x)) {
p.x.toString = function () {
return String(this.toDate().getTime());
};
}
});
}
;
scope.chart.addSeries(s, false);
});
scope.chart.redraw();
scope.chart.hideLoading();
}
}
};
}]);
})();

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,32 @@
(function () {
var Dashboard = function($resource) {
var resource = $resource('/api/dashboards/:slug', {slug: '@slug'}, {
var Dashboard = function($resource, $http, Widget) {
var transformSingle = function(dashboard) {
dashboard.widgets = _.map(dashboard.widgets, function (row) {
return _.map(row, function (widget) {
return new Widget(widget);
});
});
};
var transform = $http.defaults.transformResponse.concat(function(data, headers) {
if (_.isArray(data)) {
_.each(data, transformSingle);
} else {
transformSingle(data);
}
return data;
});
var resource = $resource('api/dashboards/:slug', {slug: '@slug'}, {
'get': {method: 'GET', transformResponse: transform},
'save': {method: 'POST', transformResponse: transform},
'query': {method: 'GET', isArray: true, transformResponse: transform},
recent: {
method: 'get',
isArray: true,
url: "/api/dashboards/recent"
url: "api/dashboards/recent",
transformResponse: transform
}});
resource.prototype.canEdit = function() {
@@ -14,5 +36,5 @@
}
angular.module('redash.services')
.factory('Dashboard', ['$resource', Dashboard])
.factory('Dashboard', ['$resource', '$http', 'Widget', Dashboard])
})();

View File

@@ -1,7 +1,31 @@
(function () {
function QueryResultError(errorMessage) {
this.errorMessage = errorMessage;
}
QueryResultError.prototype.getError = function() {
return this.errorMessage;
};
QueryResultError.prototype.getStatus = function() {
return 'failed';
};
QueryResultError.prototype.getData = function() {
return null;
};
QueryResultError.prototype.getLog = function() {
return null;
};
QueryResultError.prototype.getChartData = function() {
return null;
};
var QueryResult = function ($resource, $timeout, $q) {
var QueryResultResource = $resource('/api/query_results/:id', {id: '@id'}, {'post': {'method': 'POST'}});
var Job = $resource('/api/jobs/:id', {id: '@id'});
var QueryResultResource = $resource('api/query_results/:id', {id: '@id'}, {'post': {'method': 'POST'}});
var Job = $resource('api/jobs/:id', {id: '@id'});
var updateFunction = function (props) {
angular.extend(this, props);
@@ -19,10 +43,10 @@
if (angular.isNumber(v)) {
columnTypes[k] = 'float';
} else if (_.isString(v) && v.match(/^\d{4}-\d{2}-\d{2}T/)) {
row[k] = moment(v);
row[k] = moment.utc(v);
columnTypes[k] = 'datetime';
} else if (_.isString(v) && v.match(/^\d{4}-\d{2}-\d{2}/)) {
row[k] = moment(v);
} else if (_.isString(v) && v.match(/^\d{4}-\d{2}-\d{2}$/)) {
row[k] = moment.utc(v);
columnTypes[k] = 'date';
} else if (typeof(v) == 'object' && v !== null) {
row[k] = JSON.stringify(v);
@@ -44,7 +68,7 @@
} else {
this.status = undefined;
}
}
};
function QueryResult(props) {
this.deferred = $q.defer();
@@ -162,7 +186,22 @@
}
return this.filteredData;
}
};
/**
* Helper function to add a point into a series
*/
QueryResult.prototype._addPointToSeries = function (point, seriesCollection, seriesName) {
if (seriesCollection[seriesName] == undefined) {
seriesCollection[seriesName] = {
name: seriesName,
type: 'column',
data: []
};
}
seriesCollection[seriesName]['data'].push(point);
};
QueryResult.prototype.getChartData = function (mapping) {
var series = {};
@@ -174,8 +213,8 @@
var yValues = {};
_.each(row, function (value, definition) {
var name = definition.split("::")[0];
var type = definition.split("::")[1];
var name = definition.split("::")[0] || definition.split("__")[0];
var type = definition.split("::")[1] || definition.split("__")[1];
if (mapping) {
type = mapping[definition];
}
@@ -200,31 +239,20 @@
seriesName = String(value);
}
if (type == 'multi-filter') {
if (type == 'multiFilter' || type == 'multi-filter') {
seriesName = String(value);
}
});
var addPointToSeries = function (seriesName, point) {
if (series[seriesName] == undefined) {
series[seriesName] = {
name: seriesName,
type: 'column',
data: []
}
}
series[seriesName]['data'].push(point);
}
if (seriesName === undefined) {
_.each(yValues, function (yValue, seriesName) {
addPointToSeries(seriesName, {'x': xValue, 'y': yValue});
});
} else {
addPointToSeries(seriesName, point);
this._addPointToSeries({'x': xValue, 'y': yValue}, series, seriesName);
}.bind(this));
}
});
else {
this._addPointToSeries(point, series, seriesName);
}
}.bind(this));
return _.values(series);
};
@@ -248,7 +276,16 @@
}
QueryResult.prototype.getColumnNameWithoutType = function (column) {
var parts = column.split('::');
var typeSplit;
if (column.indexOf("::") != -1) {
typeSplit = "::";
} else if (column.indexOf("__" != -1)) {
typeSplit = "__";
} else {
return column;
}
var parts = column.split(typeSplit);
if (parts[0] == "" && parts.length == 2) {
return parts[1];
}
@@ -289,16 +326,18 @@
QueryResult.prototype.prepareFilters = function () {
var filters = [];
var filterTypes = ['filter', 'multi-filter'];
_.each(this.getColumnNames(), function (col) {
var type = col.split('::')[1]
var filterTypes = ['filter', 'multi-filter', 'multiFilter'];
_.each(this.getColumns(), function (col) {
var name = col.name;
var type = name.split('::')[1] || name.split('__')[1];
if (_.contains(filterTypes, type)) {
// filter found
var filter = {
name: col,
friendlyName: this.getColumnFriendlyName(col),
name: name,
friendlyName: this.getColumnFriendlyName(name),
column: col,
values: [],
multiple: (type=='multi-filter')
multiple: (type=='multiFilter') || (type=='multi-filter')
}
filters.push(filter);
}
@@ -364,6 +403,10 @@
if ('job' in response) {
refreshStatus(queryResult, query);
}
}, function(error) {
if (error.status === 403) {
queryResult.update(error.data);
}
});
return queryResult;
@@ -373,17 +416,17 @@
};
var Query = function ($resource, QueryResult, DataSource) {
var Query = $resource('/api/queries/:id', {id: '@id'},
var Query = $resource('api/queries/:id', {id: '@id'},
{
search: {
method: 'get',
isArray: true,
url: "/api/queries/search"
url: "api/queries/search"
},
recent: {
method: 'get',
isArray: true,
url: "/api/queries/recent"
url: "api/queries/recent"
}});
Query.newQuery = function () {
@@ -414,20 +457,23 @@
return '/queries/' + this.id + '/source';
};
Query.prototype.isNew = function() {
return this.id === undefined;
};
Query.prototype.hasDailySchedule = function() {
return (this.schedule && this.schedule.match(/\d\d:\d\d/) !== null);
}
};
Query.prototype.scheduleInLocalTime = function() {
var parts = this.schedule.split(':');
return moment.utc().hour(parts[0]).minute(parts[1]).local().format('HH:mm');
}
};
Query.prototype.getQueryResult = function (maxAge, parameters) {
// if (ttl == undefined) {
// ttl = this.ttl;
// }
if (!this.query) {
return;
}
var queryText = this.query;
var queryParameters = this.getParameters();
@@ -462,6 +508,8 @@
}
} else if (this.data_source_id) {
this.queryResult = QueryResult.get(this.data_source_id, queryText, maxAge, this.id);
} else {
return new QueryResultError("Please select data source to run this query.");
}
return this.queryResult;
@@ -498,17 +546,82 @@
var DataSource = function ($resource) {
var actions = {
'get': {'method': 'GET', 'cache': true, 'isArray': true},
'getSchema': {'method': 'GET', 'cache': true, 'isArray': true, 'url': '/api/data_sources/:id/schema'}
'get': {'method': 'GET', 'cache': false, 'isArray': false},
'query': {'method': 'GET', 'cache': false, 'isArray': true},
'getSchema': {'method': 'GET', 'cache': true, 'isArray': true, 'url': 'api/data_sources/:id/schema'}
};
var DataSourceResource = $resource('/api/data_sources/:id', {id: '@id'}, actions);
var DataSourceResource = $resource('api/data_sources/:id', {id: '@id'}, actions);
return DataSourceResource;
}
};
var User = function ($resource, $http) {
var transformSingle = function(user) {
if (user.groups !== undefined) {
user.admin = user.groups.indexOf("admin") != -1;
}
};
var transform = $http.defaults.transformResponse.concat(function(data, headers) {
if (_.isArray(data)) {
_.each(data, transformSingle);
} else {
transformSingle(data);
}
return data;
});
var actions = {
'get': {method: 'GET', transformResponse: transform},
'save': {method: 'POST', transformResponse: transform},
'query': {method: 'GET', isArray: true, transformResponse: transform},
'delete': {method: 'DELETE', transformResponse: transform}
};
var UserResource = $resource('api/users/:id', {id: '@id'}, actions);
return UserResource;
};
var Group = function ($resource) {
var actions = {
'get': {'method': 'GET', 'cache': false, 'isArray': false},
'query': {'method': 'GET', 'cache': false, 'isArray': true},
'members': {'method': 'GET', 'cache': true, 'isArray': true, 'url': 'api/groups/:id/members'},
'dataSources': {'method': 'GET', 'cache': true, 'isArray': true, 'url': 'api/groups/:id/data_sources'}
};
var resource = $resource('api/groups/:id', {id: '@id'}, actions);
return resource;
};
var AlertSubscription = function ($resource) {
var resource = $resource('api/alerts/:alertId/subscriptions/:userId', {alertId: '@alert_id', userId: '@user.id'});
return resource;
};
var Alert = function ($resource, $http) {
var actions = {
save: {
method: 'POST',
transformRequest: [function(data) {
var newData = _.extend({}, data);
if (newData.query_id === undefined) {
newData.query_id = newData.query.id;
delete newData.query;
}
return newData;
}].concat($http.defaults.transformRequest)
}
};
var resource = $resource('api/alerts/:id', {id: '@id'}, actions);
return resource;
};
var Widget = function ($resource, Query) {
var WidgetResource = $resource('/api/widgets/:id', {id: '@id'});
var WidgetResource = $resource('api/widgets/:id', {id: '@id'});
WidgetResource.prototype.getQuery = function () {
if (!this.query && this.visualization) {
@@ -532,5 +645,9 @@
.factory('QueryResult', ['$resource', '$timeout', '$q', QueryResult])
.factory('Query', ['$resource', 'QueryResult', 'DataSource', Query])
.factory('DataSource', ['$resource', DataSource])
.factory('Widget', ['$resource', 'Query', Widget]);
.factory('Alert', ['$resource', '$http', Alert])
.factory('AlertSubscription', ['$resource', AlertSubscription])
.factory('Widget', ['$resource', 'Query', Widget])
.factory('User', ['$resource', '$http', User])
.factory('Group', ['$resource', Group]);
})();

View File

@@ -26,7 +26,7 @@
var events = this.events;
this.events = [];
$http.post('/api/events', events);
$http.post('api/events', events);
}, 1000);

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@
defaultOptions: {},
skipTypes: false,
editorTemplate: null
}
};
this.registerVisualization = function (config) {
var visualization = _.extend({}, defaultConfig, config);
@@ -21,11 +21,10 @@
if (!config.skipTypes) {
this.visualizationTypes[config.name] = config.type;
}
;
};
this.getSwitchTemplate = function (property) {
var pattern = /(<[a-zA-Z0-9-]*?)( |>)/
var pattern = /(<[a-zA-Z0-9-]*?)( |>)/;
var mergedTemplates = _.reduce(this.visualizations, function (templates, visualization) {
if (visualization[property]) {
@@ -41,10 +40,10 @@
mergedTemplates = '<div ng-switch on="visualization.type">' + mergedTemplates + "</div>";
return mergedTemplates;
}
};
this.$get = ['$resource', function ($resource) {
var Visualization = $resource('/api/visualizations/:id', {id: '@id'});
var Visualization = $resource('api/visualizations/:id', {id: '@id'});
Visualization.visualizations = this.visualizations;
Visualization.visualizationTypes = this.visualizationTypes;
Visualization.renderVisualizationsTemplate = this.getSwitchTemplate('renderTemplate');
@@ -64,12 +63,12 @@
template: '<small>{{name}}</small>',
replace: false,
link: function (scope) {
if (Visualization.visualizations[scope.visualization.type].name != scope.visualization.name) {
if (Visualization.visualizations[scope.visualization.type].name !== scope.visualization.name) {
scope.name = scope.visualization.name;
}
}
}
}
};
};
var VisualizationRenderer = function ($location, Visualization) {
return {
@@ -84,17 +83,13 @@
template: '<filters></filters>\n' + Visualization.renderVisualizationsTemplate,
replace: false,
link: function (scope) {
scope.select2Options = {
width: '50%'
};
scope.$watch('queryResult && queryResult.getFilters()', function (filters) {
if (filters) {
scope.filters = filters;
}
});
}
}
};
};
var VisualizationOptionsEditor = function (Visualization) {
@@ -102,15 +97,36 @@
restrict: 'E',
template: Visualization.editorTemplate,
replace: false
}
};
};
var Filters = function () {
return {
restrict: 'E',
templateUrl: '/views/visualizations/filters.html'
}
}
};
};
var FilterValueFilter = function() {
return function(value, filter) {
if (_.isArray(value)) {
value = value[0];
}
// TODO: deduplicate code with table.js:
if (filter.column.type === 'date') {
if (value && moment.isMoment(value)) {
return value.format(clientConfig.dateFormat);
}
} else if (filter.column.type === 'datetime') {
if (value && moment.isMoment(value)) {
return value.format(clientConfig.dateTimeFormat);
}
}
return value;
};
};
var EditVisualizationForm = function (Events, Visualization, growl) {
return {
@@ -124,7 +140,7 @@
openEditor: '@',
onNewSuccess: '=?'
},
link: function (scope, element, attrs) {
link: function (scope) {
scope.editRawOptions = currentUser.hasPermission('edit_raw_chart');
scope.visTypes = Visualization.visualizationTypes;
@@ -135,7 +151,7 @@
'description': '',
'options': Visualization.defaultVisualization.defaultOptions
};
}
};
if (!scope.visualization) {
var unwatch = scope.$watch('query.id', function (queryId) {
@@ -149,14 +165,13 @@
scope.$watch('visualization.type', function (type, oldType) {
// if not edited by user, set name to match type
if (type && oldType != type && scope.visualization && !scope.visForm.name.$dirty) {
if (type && oldType !== type && scope.visualization && !scope.visForm.name.$dirty) {
scope.visualization.name = _.string.titleize(scope.visualization.type);
}
if (type && oldType != type && scope.visualization) {
if (type && oldType !== type && scope.visualization) {
scope.visualization.options = Visualization.visualizations[scope.visualization.type].defaultOptions;
}
});
scope.submit = function () {
@@ -187,15 +202,44 @@
});
};
}
}
};
};
var EmbedCode = function () {
return {
restrict: 'E',
scope: {
visualization: '=',
query: '='
},
template:
'<div class="col-lg-8 embed-code">' +
'<i class="fa fa-code" ng-click="showCode = showCode==true ? false : true;"></i>' +
'<div ng-show="showCode">' +
'<span class="text-muted">Embed code for this visualization: <small>(height should be adjusted)</small></span>' +
'<code>&lt;iframe src="{{ embedUrl }}"<br/>' +
'&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;' +
'width="720" height="391"&gt;&lt;/iframe&gt;</code>' +
'</div>' +
'</div>',
replace: true,
link: function (scope) {
scope.$watch('visualization', function(visualization) {
if (visualization) {
scope.embedUrl = basePath + 'embed/query/' + scope.query.id + '/visualization/' + scope.visualization.id + '?api_key=' + scope.query.api_key;
}
});
}
};
};
angular.module('redash.visualization', [])
.provider('Visualization', VisualizationProvider)
.directive('visualizationRenderer', ['$location', 'Visualization', VisualizationRenderer])
.directive('visualizationOptionsEditor', ['Visualization', VisualizationOptionsEditor])
.directive('visualizationName', ['Visualization', VisualizationName])
.directive('embedCode', EmbedCode)
.directive('filters', Filters)
.directive('editVisulatizationForm', ['Events', 'Visualization', 'growl', EditVisualizationForm])
.filter('filterValue', FilterValueFilter)
.directive('editVisulatizationForm', ['Events', 'Visualization', 'growl', EditVisualizationForm]);
})();

View File

@@ -0,0 +1,307 @@
(function() {
// Inspired by http://informationandvisualization.de/blog/box-plot
d3.box = function() {
var width = 1,
height = 1,
duration = 0,
domain = null,
value = Number,
whiskers = boxWhiskers,
quartiles = boxQuartiles,
tickFormat = null;
// For each small multiple…
function box(g) {
g.each(function(d, i) {
d = d.map(value).sort(d3.ascending);
var g = d3.select(this),
n = d.length,
min = d[0],
max = d[n - 1];
// Compute quartiles. Must return exactly 3 elements.
var quartileData = d.quartiles = quartiles(d);
// Compute whiskers. Must return exactly 2 elements, or null.
var whiskerIndices = whiskers && whiskers.call(this, d, i),
whiskerData = whiskerIndices && whiskerIndices.map(function(i) { return d[i]; });
// Compute outliers. If no whiskers are specified, all data are "outliers".
// We compute the outliers as indices, so that we can join across transitions!
var outlierIndices = whiskerIndices
? d3.range(0, whiskerIndices[0]).concat(d3.range(whiskerIndices[1] + 1, n))
: d3.range(n);
// Compute the new x-scale.
var x1 = d3.scale.linear()
.domain(domain && domain.call(this, d, i) || [min, max])
.range([height, 0]);
// Retrieve the old x-scale, if this is an update.
var x0 = this.__chart__ || d3.scale.linear()
.domain([0, Infinity])
.range(x1.range());
// Stash the new scale.
this.__chart__ = x1;
// Note: the box, median, and box tick elements are fixed in number,
// so we only have to handle enter and update. In contrast, the outliers
// and other elements are variable, so we need to exit them! Variable
// elements also fade in and out.
// Update center line: the vertical line spanning the whiskers.
var center = g.selectAll("line.center")
.data(whiskerData ? [whiskerData] : []);
center.enter().insert("line", "rect")
.attr("class", "center")
.attr("x1", width / 2)
.attr("y1", function(d) { return x0(d[0]); })
.attr("x2", width / 2)
.attr("y2", function(d) { return x0(d[1]); })
.style("opacity", 1e-6)
.transition()
.duration(duration)
.style("opacity", 1)
.attr("y1", function(d) { return x1(d[0]); })
.attr("y2", function(d) { return x1(d[1]); });
center.transition()
.duration(duration)
.style("opacity", 1)
.attr("y1", function(d) { return x1(d[0]); })
.attr("y2", function(d) { return x1(d[1]); });
center.exit().transition()
.duration(duration)
.style("opacity", 1e-6)
.attr("y1", function(d) { return x1(d[0]); })
.attr("y2", function(d) { return x1(d[1]); })
.remove();
// Update innerquartile box.
var box = g.selectAll("rect.box")
.data([quartileData]);
box.enter().append("rect")
.attr("class", "box")
.attr("x", 0)
.attr("y", function(d) { return x0(d[2]); })
.attr("width", width)
.attr("height", function(d) { return x0(d[0]) - x0(d[2]); })
.transition()
.duration(duration)
.attr("y", function(d) { return x1(d[2]); })
.attr("height", function(d) { return x1(d[0]) - x1(d[2]); });
box.transition()
.duration(duration)
.attr("y", function(d) { return x1(d[2]); })
.attr("height", function(d) { return x1(d[0]) - x1(d[2]); });
box.exit().remove()
// Update median line.
var medianLine = g.selectAll("line.median")
.data([quartileData[1]]);
medianLine.enter().append("line")
.attr("class", "median")
.attr("x1", 0)
.attr("y1", x0)
.attr("x2", width)
.attr("y2", x0)
.transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1);
medianLine.transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1);
medianLine.exit().remove()
// Update whiskers.
var whisker = g.selectAll("line.whisker")
.data(whiskerData || []);
whisker.enter().insert("line", "circle, text")
.attr("class", "whisker")
.attr("x1", 0)
.attr("y1", x0)
.attr("x2", width)
.attr("y2", x0)
.style("opacity", 1e-6)
.transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1)
.style("opacity", 1);
whisker.transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1)
.style("opacity", 1);
whisker.exit().transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1)
.style("opacity", 1e-6)
.remove();
// Update outliers.
var outlier = g.selectAll("circle.outlier")
.data(outlierIndices, Number);
outlier.enter().insert("circle", "text")
.attr("class", "outlier")
.attr("r", 5)
.attr("cx", width / 2)
.attr("cy", function(i) { return x0(d[i]); })
.style("opacity", 1e-6)
.transition()
.duration(duration)
.attr("cy", function(i) { return x1(d[i]); })
.style("opacity", 1);
outlier.transition()
.duration(duration)
.attr("cy", function(i) { return x1(d[i]); })
.style("opacity", 1);
outlier.exit().transition()
.duration(duration)
.attr("cy", function(i) { return x1(d[i]); })
.style("opacity", 1e-6)
.remove();
// Compute the tick format.
var format = tickFormat || x1.tickFormat(8);
// Update box ticks.
var boxTick = g.selectAll("text.box")
.data(quartileData);
boxTick.enter().append("text")
.attr("class", "box")
.attr("dy", ".3em")
.attr("dx", function(d, i) { return i & 1 ? 6 : -6 })
.attr("x", function(d, i) { return i & 1 ? width : 0 })
.attr("y", x0)
.attr("text-anchor", function(d, i) { return i & 1 ? "start" : "end"; })
.text(format)
.transition()
.duration(duration)
.attr("y", x1);
boxTick.transition()
.duration(duration)
.text(format)
.attr("y", x1);
boxTick.exit().remove()
// Update whisker ticks. These are handled separately from the box
// ticks because they may or may not exist, and we want don't want
// to join box ticks pre-transition with whisker ticks post-.
var whiskerTick = g.selectAll("text.whisker")
.data(whiskerData || []);
whiskerTick.enter().append("text")
.attr("class", "whisker")
.attr("dy", ".3em")
.attr("dx", 6)
.attr("x", width)
.attr("y", x0)
.text(format)
.style("opacity", 1e-6)
.transition()
.duration(duration)
.attr("y", x1)
.style("opacity", 1);
whiskerTick.transition()
.duration(duration)
.text(format)
.attr("y", x1)
.style("opacity", 1);
whiskerTick.exit().transition()
.duration(duration)
.attr("y", x1)
.style("opacity", 1e-6)
.remove();
});
d3.timer.flush();
}
box.width = function(x) {
if (!arguments.length) return width;
width = x;
return box;
};
box.height = function(x) {
if (!arguments.length) return height;
height = x;
return box;
};
box.tickFormat = function(x) {
if (!arguments.length) return tickFormat;
tickFormat = x;
return box;
};
box.duration = function(x) {
if (!arguments.length) return duration;
duration = x;
return box;
};
box.domain = function(x) {
if (!arguments.length) return domain;
domain = x == null ? x : d3.functor(x);
return box;
};
box.value = function(x) {
if (!arguments.length) return value;
value = x;
return box;
};
box.whiskers = function(x) {
if (!arguments.length) return whiskers;
whiskers = x;
return box;
};
box.quartiles = function(x) {
if (!arguments.length) return quartiles;
quartiles = x;
return box;
};
return box;
};
function boxWhiskers(d) {
return [0, d.length - 1];
}
function boxQuartiles(d) {
return [
d3.quantile(d, .25),
d3.quantile(d, .5),
d3.quantile(d, .75)
];
}
})();

View File

@@ -0,0 +1,173 @@
(function() {
var module = angular.module('redash.visualization');
module.config(['VisualizationProvider', function(VisualizationProvider) {
var renderTemplate =
'<boxplot-renderer ' +
'options="visualization.options" query-result="queryResult">' +
'</boxplot-renderer>';
var editTemplate = '<boxplot-editor></boxplot-editor>';
VisualizationProvider.registerVisualization({
type: 'BOXPLOT',
name: 'Boxplot',
renderTemplate: renderTemplate,
editorTemplate: editTemplate
});
}
]);
module.directive('boxplotRenderer', function() {
return {
restrict: 'E',
templateUrl: '/views/visualizations/boxplot.html',
link: function($scope, elm, attrs) {
function iqr(k) {
return function(d, i) {
var q1 = d.quartiles[0],
q3 = d.quartiles[2],
iqr = (q3 - q1) * k,
i = -1,
j = d.length;
while (d[++i] < q1 - iqr);
while (d[--j] > q3 + iqr);
return [i, j];
};
};
$scope.$watch('[queryResult && queryResult.getData(), visualization.options]', function () {
var data = $scope.queryResult.getData();
var parentWidth = d3.select(elm[0].parentNode).node().getBoundingClientRect().width;
var margin = {top: 10, right: 50, bottom: 40, left: 50, inner: 25},
width = parentWidth - margin.right - margin.left
height = 500 - margin.top - margin.bottom;
var min = Infinity,
max = -Infinity;
var mydata = [];
var value = 0;
var d = [];
var xAxisLabel = $scope.visualization.options.xAxisLabel;
var yAxisLabel = $scope.visualization.options.yAxisLabel;
var columns = $scope.queryResult.columnNames;
var xscale = d3.scale.ordinal()
.domain(columns)
.rangeBands([0, parentWidth-margin.left-margin.right]);
if (columns.length > 1){
boxWidth = Math.min(xscale(columns[1]),120.0);
} else {
boxWidth=120.0;
};
margin.inner = boxWidth/3.0;
_.each(columns, function(column, i){
d = mydata[i] = [];
_.each(data, function (row) {
value = row[column];
d.push(value);
if (value > max) max = Math.ceil(value);
if (value < min) min = Math.floor(value);
});
});
var yscale = d3.scale.linear()
.domain([min*0.99,max*1.01])
.range([height, 0]);
var chart = d3.box()
.whiskers(iqr(1.5))
.width(boxWidth-2*margin.inner)
.height(height)
.domain([min*0.99,max*1.01]);
var xAxis = d3.svg.axis()
.scale(xscale)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(yscale)
.orient("left");
var xLines = d3.svg.axis()
.scale(xscale)
.tickSize(height)
.orient("bottom");
var yLines = d3.svg.axis()
.scale(yscale)
.tickSize(width)
.orient("right");
var barOffset = function(i){
return xscale(columns[i]) + (xscale(columns[1]) - margin.inner)/2.0;
};
d3.select(elm[0]).selectAll("svg").remove();
var plot = d3.select(elm[0])
.append("svg")
.attr("width",parentWidth)
.attr("height",height + margin.bottom + margin.top)
.append("g")
.attr("width",parentWidth-margin.left-margin.right)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
d3.select("svg").append("text")
.attr("class", "box")
.attr("x", parentWidth/2.0)
.attr("text-anchor", "middle")
.attr("y", height+margin.bottom)
.text(xAxisLabel)
d3.select("svg").append("text")
.attr("class", "box")
.attr("transform","translate(10,"+(height+margin.top+margin.bottom)/2.0+")rotate(-90)")
.attr("text-anchor", "middle")
.text(yAxisLabel)
plot.append("rect")
.attr("class", "grid-background")
.attr("width", width)
.attr("height", height);
plot.append("g")
.attr("class","grid")
.call(yLines)
plot.append("g")
.attr("class","grid")
.call(xLines)
plot.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
plot.append("g")
.attr("class", "y axis")
.call(yAxis);
plot.selectAll(".box").data(mydata)
.enter().append("g")
.attr("class", "box")
.attr("width", boxWidth)
.attr("height", height)
.attr("transform", function(d,i) { return "translate(" + barOffset(i) + "," + 0 + ")"; } )
.call(chart);
}, true);
}
}
});
module.directive('boxplotEditor', function() {
return {
restrict: 'E',
templateUrl: '/views/visualizations/boxplot_editor.html'
};
});
})();

View File

@@ -3,12 +3,17 @@
chartVisualization.config(['VisualizationProvider', function (VisualizationProvider) {
var renderTemplate = '<chart-renderer options="visualization.options" query-result="queryResult"></chart-renderer>';
var editTemplate = '<chart-editor></chart-editor>';
var editTemplate = '<chart-editor options="visualization.options" query-result="queryResult"></chart-editor>';
var defaultOptions = {
'series': {
// 'type': 'column',
'stacking': null
}
globalSeriesType: 'column',
sortX: true,
legend: {enabled: true},
yAxis: [{type: 'linear'}, {type: 'linear', opposite: true}],
xAxis: {type: 'datetime', labels: {enabled: true}},
series: {stacking: null},
seriesOptions: {},
columnMapping: {}
};
VisualizationProvider.registerVisualization({
@@ -27,52 +32,30 @@
queryResult: '=',
options: '=?'
},
template: "<chart options='chartOptions' series='chartSeries' class='graph'></chart>",
templateUrl: '/views/visualizations/chart.html',
replace: false,
controller: ['$scope', function ($scope) {
$scope.chartSeries = [];
$scope.chartOptions = {};
var reloadData = function(data) {
if (!data || ($scope.queryResult && $scope.queryResult.getData()) == null) {
$scope.chartSeries.splice(0, $scope.chartSeries.length);
} else {
$scope.chartSeries.splice(0, $scope.chartSeries.length);
_.each($scope.queryResult.getChartData($scope.options.columnMapping), function (s) {
var additional = {'stacking': 'normal'};
if ('globalSeriesType' in $scope.options) {
additional['type'] = $scope.options.globalSeriesType;
}
if ($scope.options.seriesOptions && $scope.options.seriesOptions[s.name]) {
additional = $scope.options.seriesOptions[s.name];
if (!additional.name || additional.name == "") {
additional.name = s.name;
}
}
$scope.chartSeries.push(_.extend(s, additional));
});
};
var reloadChart = function() {
reloadData();
$scope.plotlyOptions = $scope.options;
};
$scope.$watch('options', function (chartOptions) {
if (chartOptions) {
$scope.chartOptions = chartOptions;
var reloadData = function() {
if (angular.isDefined($scope.queryResult)) {
$scope.chartSeries = _.sortBy($scope.queryResult.getChartData($scope.options.columnMapping),
function(series) {
if ($scope.options.seriesOptions[series.name]) {
return $scope.options.seriesOptions[series.name].zIndex;
}
return 0;
});
}
});
};
$scope.$watch('options.seriesOptions', function () {
reloadData(true);
}, true);
$scope.$watchCollection('options.columnMapping', function (chartOptions) {
reloadData(true);
});
$scope.$watch('queryResult && queryResult.getData()', function (data) {
reloadData(data);
});
$scope.$watch('options', reloadChart, true);
$scope.$watch('queryResult && queryResult.getData()', reloadData);
}]
};
});
@@ -81,178 +64,144 @@
return {
restrict: 'E',
templateUrl: '/views/visualizations/chart_editor.html',
scope: {
queryResult: '=',
options: '=?'
},
link: function (scope, element, attrs) {
scope.palette = ColorPalette;
scope.seriesTypes = {
'Line': 'line',
'Column': 'column',
'Area': 'area',
'Scatter': 'scatter',
'Pie': 'pie'
};
scope.globalSeriesType = scope.visualization.options.globalSeriesType || 'column';
scope.colors = _.extend({'Automatic': null}, ColorPalette);
scope.stackingOptions = {
"None": "none",
"Normal": "normal",
"Percent": "percent"
'Disabled': null,
'Enabled': 'normal',
'Percent': 'percent'
};
scope.xAxisOptions = {
"Date/Time": "datetime",
"Linear": "linear",
"Category": "category"
scope.chartTypes = {
'line': {name: 'Line', icon: 'line-chart'},
'column': {name: 'Bar', icon: 'bar-chart'},
'area': {name: 'Area', icon: 'area-chart'},
'pie': {name: 'Pie', icon: 'pie-chart'},
'scatter': {name: 'Scatter', icon: 'circle-o'}
};
scope.xAxisType = "datetime";
scope.stacking = "none";
scope.columnTypes = {
"X": "x",
"Y": "y",
"Series": "series",
"Unused": "unused"
scope.chartTypeChanged = function() {
_.each(scope.options.seriesOptions, function(options) {
options.type = scope.options.globalSeriesType;
});
};
scope.series = [];
scope.xAxisScales = ['datetime', 'linear', 'logarithmic', 'category'];
scope.yAxisScales = ['linear', 'logarithmic'];
scope.columnTypeSelection = {};
var chartOptionsUnwatch = null,
columnsWatch = null;
scope.$watch('globalSeriesType', function(type, old) {
scope.visualization.options.globalSeriesType = type;
if (type && old && type !== old && scope.visualization.options.seriesOptions) {
_.each(scope.visualization.options.seriesOptions, function(sOptions) {
sOptions.type = type;
var refreshColumns = function() {
scope.columns = scope.queryResult.getColumns();
scope.columnNames = _.pluck(scope.columns, 'name');
if (scope.columnNames.length > 0)
_.each(_.difference(_.keys(scope.options.columnMapping), scope.columnNames), function(column) {
delete scope.options.columnMapping[column];
});
};
refreshColumns();
var refreshColumnsAndForm = function() {
refreshColumns();
if (!scope.queryResult.getData() || scope.queryResult.getData().length == 0 || scope.columns.length == 0)
return;
scope.form.yAxisColumns = _.intersection(scope.form.yAxisColumns, scope.columnNames);
if (!_.contains(scope.columnNames, scope.form.xAxisColumn))
scope.form.xAxisColumn = undefined;
if (!_.contains(scope.columnNames, scope.form.groupby))
scope.form.groupby = undefined;
}
var refreshSeries = function() {
var seriesNames = _.pluck(scope.queryResult.getChartData(scope.options.columnMapping), 'name');
var existing = _.keys(scope.options.seriesOptions);
_.each(_.difference(seriesNames, existing), function(name) {
scope.options.seriesOptions[name] = {
'type': scope.options.globalSeriesType,
'yAxis': 0,
};
scope.form.seriesList.push(name);
});
_.each(_.difference(existing, seriesNames), function(name) {
scope.form.seriesList = _.without(scope.form.seriesList, name)
delete scope.options.seriesOptions[name];
});
};
scope.$watch('options.columnMapping', function() {
if (scope.queryResult.status === "done") {
refreshSeries();
}
}, true);
scope.$watch(function() {return [scope.queryResult.getId(), scope.queryResult.status];}, function(changed) {
if (!changed[0] || changed[1] !== "done") {
return;
}
refreshColumnsAndForm();
refreshSeries();
}, true);
scope.form = {
yAxisColumns: [],
seriesList: _.sortBy(_.keys(scope.options.seriesOptions), function(name) {
return scope.options.seriesOptions[name].zIndex;
})
};
scope.$watchCollection('form.seriesList', function(value, old) {
_.each(value, function(name, index) {
scope.options.seriesOptions[name].zIndex = index;
scope.options.seriesOptions[name].index = 0; // is this needed?
});
});
var setColumnRole = function(role, column) {
scope.options.columnMapping[column] = role;
}
var unsetColumn = function(column) {
setColumnRole('unused', column);
}
scope.$watchCollection('form.yAxisColumns', function(value, old) {
_.each(old, unsetColumn);
_.each(value, _.partial(setColumnRole, 'y'));
});
scope.$watch('form.xAxisColumn', function(value, old) {
if (old !== undefined)
unsetColumn(old);
if (value !== undefined)
setColumnRole('x', value);
});
scope.$watch('form.groupby', function(value, old) {
if (old !== undefined)
unsetColumn(old)
if (value !== undefined) {
setColumnRole('series', value);
}
});
scope.$watch('visualization.type', function (visualizationType) {
if (visualizationType == 'CHART') {
if (scope.visualization.options.series.stacking === null) {
scope.stacking = "none";
} else if (scope.visualization.options.series.stacking === undefined) {
scope.stacking = "normal";
} else {
scope.stacking = scope.visualization.options.series.stacking;
}
if (!_.has(scope.options, 'legend')) {
scope.options.legend = {enabled: true};
}
if (scope.visualization.options.sortX === undefined) {
scope.visualization.options.sortX = true;
}
var refreshSeries = function() {
scope.series = _.map(scope.queryResult.getChartData(scope.visualization.options.columnMapping), function (s) { return s.name; });
// TODO: remove uneeded ones?
if (scope.visualization.options.seriesOptions == undefined) {
scope.visualization.options.seriesOptions = {
type: scope.globalSeriesType
};
};
_.each(scope.series, function(s, i) {
if (scope.visualization.options.seriesOptions[s] == undefined) {
scope.visualization.options.seriesOptions[s] = {'type': scope.visualization.options.globalSeriesType, 'yAxis': 0};
}
scope.visualization.options.seriesOptions[s].zIndex = scope.visualization.options.seriesOptions[s].zIndex === undefined ? i : scope.visualization.options.seriesOptions[s].zIndex;
scope.visualization.options.seriesOptions[s].index = scope.visualization.options.seriesOptions[s].index === undefined ? i : scope.visualization.options.seriesOptions[s].index;
});
scope.zIndexes = _.range(scope.series.length);
scope.yAxes = [[0, 'left'], [1, 'right']];
};
var initColumnMapping = function() {
scope.columns = scope.queryResult.getColumns();
if (scope.visualization.options.columnMapping == undefined) {
scope.visualization.options.columnMapping = {};
}
scope.columnTypeSelection = scope.visualization.options.columnMapping;
_.each(scope.columns, function(column) {
var definition = column.name.split("::"),
definedColumns = _.keys(scope.visualization.options.columnMapping);
if (_.indexOf(definedColumns, column.name) != -1) {
// Skip already defined columns.
return;
};
if (definition.length == 1) {
scope.columnTypeSelection[column.name] = scope.visualization.options.columnMapping[column.name] = 'unused';
} else if (definition == 'multi-filter') {
scope.columnTypeSelection[column.name] = scope.visualization.options.columnMapping[column.name] = 'series';
} else if (_.indexOf(_.values(scope.columnTypes), definition[1]) != -1) {
scope.columnTypeSelection[column.name] = scope.visualization.options.columnMapping[column.name] = definition[1];
} else {
scope.columnTypeSelection[column.name] = scope.visualization.options.columnMapping[column.name] = 'unused';
}
});
};
columnsWatch = scope.$watch('queryResult.getId()', function(id) {
if (!id) {
return;
}
initColumnMapping();
refreshSeries();
});
scope.$watchCollection('columnTypeSelection', function(selections) {
_.each(scope.columnTypeSelection, function(type, name) {
scope.visualization.options.columnMapping[name] = type;
});
refreshSeries();
});
chartOptionsUnwatch = scope.$watch("stacking", function (stacking) {
if (stacking == "none") {
scope.visualization.options.series.stacking = null;
} else {
scope.visualization.options.series.stacking = stacking;
}
});
scope.visualization.options.xAxis = scope.visualization.options.xAxis || {};
scope.visualization.options.xAxis.labels = scope.visualization.options.xAxis.labels || {};
if (scope.visualization.options.xAxis.labels.enabled === undefined) {
scope.visualization.options.xAxis.labels.enabled = true;
}
scope.xAxisType = (scope.visualization.options.xAxis && scope.visualization.options.xAxis.type) || scope.xAxisType;
xAxisUnwatch = scope.$watch("xAxisType", function (xAxisType) {
scope.visualization.options.xAxis = scope.visualization.options.xAxis || {};
scope.visualization.options.xAxis.type = xAxisType;
});
} else {
if (chartOptionsUnwatch) {
chartOptionsUnwatch();
chartOptionsUnwatch = null;
}
if (columnsWatch) {
columnWatch();
columnWatch = null;
}
if (xAxisUnwatch) {
xAxisUnwatch();
xAxisUnwatch = null;
}
}
});
if (scope.columnNames)
_.each(scope.options.columnMapping, function(value, key) {
if (scope.columnNames.length > 0 && !_.contains(scope.columnNames, key))
return;
if (value == 'x')
scope.form.xAxisColumn = key;
else if (value == 'y')
scope.form.yAxisColumns.push(key);
else if (value == 'series')
scope.form.groupby = key;
});
}
}
});

View File

@@ -1,64 +1,87 @@
(function () {
var cohortVisualization = angular.module('redash.visualization');
var cohortVisualization = angular.module('redash.visualization');
cohortVisualization.config(['VisualizationProvider', function(VisualizationProvider) {
VisualizationProvider.registerVisualization({
type: 'COHORT',
name: 'Cohort',
renderTemplate: '<cohort-renderer options="visualization.options" query-result="queryResult"></cohort-renderer>'
});
}]);
cohortVisualization.config(['VisualizationProvider', function (VisualizationProvider) {
cohortVisualization.directive('cohortRenderer', function() {
return {
restrict: 'E',
scope: {
queryResult: '='
},
template: "",
replace: false,
link: function($scope, element, attrs) {
$scope.$watch('queryResult && queryResult.getData()', function (data) {
if (!data) {
return;
}
var editTemplate = '<cohort-editor></cohort-editor>';
var defaultOptions = {
'timeInterval': 'daily'
};
if ($scope.queryResult.getData() == null) {
} else {
var sortedData = _.sortBy($scope.queryResult.getData(), "date");
var grouped = _.groupBy(sortedData, "date");
var maxColumns = _.reduce(grouped, function(memo, data){
return (data.length > memo)? data.length : memo;
}, 0);
var data = _.map(grouped, function(values, date) {
var row = [values[0].total];
_.each(values, function(value) { row.push(value.value); });
_.each(_.range(values.length, maxColumns), function() { row.push(null); });
return row;
});
var initialDate = moment(sortedData[0].date).toDate(),
container = angular.element(element)[0];
Cornelius.draw({
initialDate: initialDate,
container: container,
cohort: data,
title: null,
timeInterval: 'daily',
labels: {
time: 'Activation Day',
people: 'Users'
},
formatHeaderLabel: function (i) {
return "Day " + (i - 1);
}
});
}
});
}
}
VisualizationProvider.registerVisualization({
type: 'COHORT',
name: 'Cohort',
renderTemplate: '<cohort-renderer options="visualization.options" query-result="queryResult"></cohort-renderer>',
editorTemplate: editTemplate,
defaultOptions: defaultOptions
});
}]);
}());
cohortVisualization.directive('cohortRenderer', function () {
return {
restrict: 'E',
scope: {
queryResult: '=',
options: '='
},
template: "",
replace: false,
link: function ($scope, element, attrs) {
$scope.options.timeInterval = $scope.options.timeInterval || 'daily';
var updateCohort = function () {
if ($scope.queryResult.getData() === null) {
return;
}
var sortedData = _.sortBy($scope.queryResult.getData(), function (r) {
return r['date'] + r['day_number'];
});
var grouped = _.groupBy(sortedData, "date");
var maxColumns = _.reduce(grouped, function (memo, data) {
return (data.length > memo) ? data.length : memo;
}, 0);
var data = _.map(grouped, function (values, date) {
var row = [values[0].total];
_.each(values, function (value) {
row.push(value.value);
});
_.each(_.range(values.length, maxColumns), function () {
row.push(null);
});
return row;
});
var initialDate = moment(sortedData[0].date).toDate(),
container = angular.element(element)[0];
Cornelius.draw({
initialDate: initialDate,
container: container,
cohort: data,
title: null,
timeInterval: $scope.options.timeInterval,
labels: {
time: 'Time',
people: 'Users',
weekOf: 'Week of'
}
});
}
$scope.$watch('queryResult && queryResult.getData()', updateCohort);
$scope.$watch('options.timeInterval', updateCohort);
}
}
});
cohortVisualization.directive('cohortEditor', function () {
return {
restrict: 'E',
templateUrl: '/views/visualizations/cohort_editor.html'
}
});
}());

Some files were not shown because too many files have changed in this diff Show More