Compare commits

..

321 Commits

Author SHA1 Message Date
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
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
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
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
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
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
John Wu
40ee0d8a6e Add date-range-selector to chart 2015-09-24 15:06:35 -07:00
214 changed files with 6884 additions and 3038 deletions

View File

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

2
.gitignore vendored
View File

@@ -21,4 +21,4 @@ venv
dump.rdb
# Docker related
docker-compose.yaml
docker-compose.yml

View File

@@ -3,11 +3,13 @@ MAINTAINER Di Wu <diwu@yelp.com>
# Ubuntu packages
RUN apt-get update && \
apt-get install -y python-pip python-dev curl build-essential pwgen libffi-dev sudo git-core wget && \
apt-get install -y python-pip python-dev curl build-essential pwgen libffi-dev sudo git-core wget \
# Postgres client
apt-get -y install libpq-dev postgresql-client && \
libpq-dev \
# Additional packages required for data sources:
apt-get install -y libssl-dev libmysqlclient-dev
libssl-dev libmysqlclient-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Users creation
RUN useradd --system --comment " " --create-home redash
@@ -16,13 +18,8 @@ RUN useradd --system --comment " " --create-home redash
RUN pip install -U setuptools && \
pip install supervisor==3.1.2
# Download latest source and extract into /opt/redash/current
# COPY setup/latest_release_url.py /tmp/latest_release_url.py
# RUN wget $(python /tmp/latest_release_url.py) -O redash.tar.gz && \
# mkdir -p /opt/redash/current && \
# tar -C /opt/redash/current -xvf redash.tar.gz && \
# rm redash.tar.gz
COPY . /opt/redash/current
RUN chown -R redash /opt/redash/current
# Setting working directory
WORKDIR /opt/redash/current
@@ -31,10 +28,18 @@ WORKDIR /opt/redash/current
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/files/supervisord_docker.conf /opt/redash/supervisord/supervisord.conf
cp /opt/redash/current/setup/docker/supervisord/supervisord.conf /opt/redash/supervisord/supervisord.conf
# Fix permissions
RUN chown -R redash /opt/redash

View File

@@ -6,10 +6,9 @@ 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

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.
@@ -22,31 +26,27 @@ Presto, Google Spreadsheets, Cloudera Impala, Hive and custom scripts.
## Demo
![Screenshots](https://raw.github.com/EverythingMe/redash/screenshots/screenshots.gif)
![Screenshots](https://raw.github.com/getredash/redash/screenshots/screenshots.gif)
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 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 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'):

View File

@@ -1,16 +1,14 @@
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 requirements_dev.txt
- pip install -r requirements.txt
cache_directories:
@@ -18,14 +16,18 @@ dependencies:
- 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 deps
- make pack
- make upload
- echo "rd_ui/app" >> .dockerignore
- 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

@@ -5,7 +5,13 @@ redash:
links:
- redis
- postgres
env_file: .env
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:

View File

@@ -10,8 +10,8 @@ If one of the listed data source types isn't available when trying to create a n
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
---------------------
PostgreSQL / Redshift / Greenplum
---------------------------------
- **Options**:
@@ -20,7 +20,7 @@ PostgreSQL / Redshift
- Password
- Host
- Port
- **Additional requirements**:
- None
@@ -201,3 +201,33 @@ Vertica
- **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

View File

@@ -34,7 +34,7 @@ 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/EverythingMe/redash/blob/master/redash/models.py#L235>`__).
(`code <https://github.com/getredash/redash/blob/master/redash/models.py#L235>`__).
Client
------
@@ -69,7 +69,7 @@ Ideas on how to implement query parameters
Client side only implementation
-------------------------------
(This was actually implemented in. See pull request `#363 <https://github.com/EverythingMe/redash/pull/363>`__ for details.)
(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

View File

@@ -13,7 +13,7 @@ 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/EverythingMe/redash.git``.
``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.
@@ -30,20 +30,7 @@ To get started with this box:
::
PYTHONPATH=. bin/run python migrations/0001_allow_delete_query.py
PYTHONPATH=. bin/run python migrations/0002_fix_timestamp_fields.py
PYTHONPATH=. bin/run python migrations/0003_update_data_source_config.py
PYTHONPATH=. bin/run python migrations/0004_allow_null_in_event_user.py
PYTHONPATH=. bin/run python migrations/0005_add_updated_at.py
PYTHONPATH=. bin/run python migrations/0006_queries_last_edit_by.py
PYTHONPATH=. bin/run python migrations/0007_add_schedule_to_queries.py
PYTHONPATH=. bin/run python migrations/0008_make_ds_name_unique.py
PYTHONPATH=. bin/run python migrations/0009_add_api_key_to_user.py
PYTHONPATH=. bin/run python migrations/0010_create_alerts.py
PYTHONPATH=. bin/run python migrations/0010_allow_deleting_datasources.py
PYTHONPATH=. bin/run python migrations/0011_migrate_bigquery_to_json.py
PYTHONPATH=. bin/run python migrations/0012_add_list_users_permission.py
PYTHONPATH=. bin/run python migrations/0013_update_counter_options.py
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``.

View File

@@ -1,4 +1,4 @@
.. image:: http://redash.io/static/img/redash_logo.png
.. image:: http://redash.io/static/old_img/redash_logo.png
:width: 200px
Open Source Data Collaboration and Visualization Platform
@@ -21,7 +21,7 @@ Features
Demo
####
.. figure:: https://raw.github.com/EverythingMe/redash/screenshots/screenshots.gif
.. 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).
@@ -37,11 +37,11 @@ Getting Started
Getting Help
############
* Source: https://github.com/everythingme/redash
* Issues: https://github.com/everythingme/redash/issues
* 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/EverythingMe/redash
* Contact Arik, the maintainer directly: arik@everything.me.
* Gitter (chat): https://gitter.im/getredash/redash
* Contact Arik, the maintainer directly: arik@redash.io.
TOC
###

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::
dbdrop 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.

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

View File

@@ -2,7 +2,7 @@ Setting up re:dash instance
###########################
The `provisioning
script <https://github.com/EverythingMe/redash/blob/master/setup/bootstrap.sh>`__
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.
@@ -18,15 +18,17 @@ AWS
Launch the instance with from the pre-baked AMI (for small deployments
t2.micro should be enough):
- us-east-1: `ami-e32b5986 <https://console.aws.amazon.com/ec2/home?region=us-east-1#LaunchInstanceWizard:ami=ami-e32b5986>`__
- us-west-1: `ami-abcf0bef <https://console.aws.amazon.com/ec2/home?region=us-west-1#LaunchInstanceWizard:ami=ami-abcf0bef>`__
- us-west-2: `ami-bd89978d <https://console.aws.amazon.com/ec2/home?region=us-west-2#LaunchInstanceWizard:ami=ami-bd89978d>`__
- eu-west-1: `ami-15eac562 <https://console.aws.amazon.com/ec2/home?region=eu-west-1#LaunchInstanceWizard:ami=ami-15eac562>`__
- eu-central-1: `ami-520f0c4f <https://console.aws.amazon.com/ec2/home?region=eu-central-1#LaunchInstanceWizard:ami=ami-520f0c4f>`__
- sa-east-1: `ami-e79501fa <https://console.aws.amazon.com/ec2/home?region=sa-east-1#LaunchInstanceWizard:ami=ami-e79501fa>`__
- ap-northeast-1: `ami-fae373fa <https://console.aws.amazon.com/ec2/home?region=ap-northeast-1#LaunchInstanceWizard:ami=ami-fae373fa>`__
- ap-southeast-2: `ami-578fc16d <https://console.aws.amazon.com/ec2/home?region=ap-southeast-2#LaunchInstanceWizard:ami=ami-578fc16d>`__
- ap-southeast-1: `ami-20081c72 <https://console.aws.amazon.com/ec2/home?region=ap-southeast-1#LaunchInstanceWizard:ami=ami-20081c72>`__
- us-east-1: `ami-752c7f10 <https://console.aws.amazon.com/ec2/home?region=us-east-1#LaunchInstanceWizard:ami=ami-752c7f10>`__
- us-west-1: `ami-b36babf7 <https://console.aws.amazon.com/ec2/home?region=us-west-1#LaunchInstanceWizard:ami=ami-b36babf7>`__
- us-west-2: `ami-a0a04393 <https://console.aws.amazon.com/ec2/home?region=us-west-2#LaunchInstanceWizard:ami=ami-a0a04393>`__
- eu-west-1: `ami-198cb16e <https://console.aws.amazon.com/ec2/home?region=eu-west-1#LaunchInstanceWizard:ami=ami-198cb16e>`__
- eu-central-1: `ami-a81418b5 <https://console.aws.amazon.com/ec2/home?region=eu-central-1#LaunchInstanceWizard:ami=ami-a81418b5>`__
- sa-east-1: `ami-2b52c336 <https://console.aws.amazon.com/ec2/home?region=sa-east-1#LaunchInstanceWizard:ami=ami-2b52c336>`__
- ap-northeast-1: `ami-4898fb48 <https://console.aws.amazon.com/ec2/home?region=ap-northeast-1#LaunchInstanceWizard:ami=ami-4898fb48>`__
- ap-southeast-2: `ami-7559134f <https://console.aws.amazon.com/ec2/home?region=ap-southeast-2#LaunchInstanceWizard:ami=ami-7559134f>`__
- ap-southeast-1: `ami-a0786bf2 <https://console.aws.amazon.com/ec2/home?region=ap-southeast-1#LaunchInstanceWizard:ami=ami-a0786bf2>`__
When launching the instance make sure to use a security grop, that only allows incoming traffic on: port 22 (SSH), 80 (HTTP) and 443 (HTTPS).
Now proceed to `"Setup" <#setup>`__.
@@ -37,7 +39,7 @@ First, you need to add the images to your account:
.. code:: bash
$ gcloud compute images create "redash-080-b1058" --source-uri gs://redash-images/redash.0.8.0.b1058.tar.gz
$ gcloud compute images create "redash-081-b1110" --source-uri gs://redash-images/redash.0.8.1.b1110.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,
@@ -46,13 +48,13 @@ you can use a dedicated image which comes with BigQuery preconfigured
.. code:: bash
$ gcloud compute images create "redash-080-b1058-bq" --source-uri gs://redash-images/redash.0.8.0.b1058-bq.tar.gz
$ gcloud compute images create "redash-081-b1110-bq" --source-uri gs://redash-images/redash.0.8.1.b1110-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-080-b1058-bq --scopes storage-ro,bigquery
$ gcloud compute instances create <your_instance_name> --image redash-081-b1110-bq --scopes storage-ro,bigquery
(the same can be done from the web interface, just make sure to enable
BigQuery access)
@@ -90,10 +92,11 @@ 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 use ``pwgen 32 -1`` to generate random string).
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 at: ``/users/me#password``.
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>`)
@@ -103,22 +106,29 @@ file.
export REDASH_GOOGLE_CLIENT_ID=""
export REDASH_GOOGLE_CLIENT_SECRET=""
export REDASH_GOOGLE_APPS_DOMAIN=""
4. Configure the domain(s) you want to allow to use with Google Apps, by running the command:
``REDASH_GOOGLE_CLIENT_ID`` and ``REDASH_GOOGLE_CLIENT_SECRET`` are the values you get after registering with Google. ``READASH_GOOGLE_APPS_DOMAIN`` is used in case you want to limit access to single Google apps domain (*if you leave it empty anyone with a Google account can access your instance*).
.. code::
4. Restart the web server to apply the configuration changes:
cd /opt/redash/current
sudo -u redash bin/run ./manage.py 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``.
5. Once you have Google OAuth enabled, you can login using your Google
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 editing the user profile and enabling admin
permission for it.
6. If you don't use Google OAuth or just need username/password logins,
you can create additional users at: ``/users/new``.
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
-----------
@@ -129,6 +139,32 @@ 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?
---------------

View File

@@ -25,7 +25,7 @@ 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/everythingme/redash/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``).

View File

@@ -46,3 +46,27 @@ Simple query on a logstash ElasticSearch instance:
"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

@@ -8,15 +8,15 @@ from flask.ext.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

View File

@@ -69,5 +69,5 @@ def update(data_source):
if __name__ == '__main__':
for data_source in DataSource.all():
for data_source in DataSource.select():
update(data_source)

View File

@@ -12,7 +12,7 @@ def convert_p12_to_pem(p12file):
if __name__ == '__main__':
for ds in DataSource.all():
for ds in DataSource.select():
if ds.type == 'bigquery':
options = json.loads(ds.options)

View File

@@ -0,0 +1,14 @@
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)
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():
if ds.type == 'elasticsearch':
ds.type = 'kibana'
ds.save()

View File

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

View File

@@ -0,0 +1,10 @@
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,18 @@
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,35 @@
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,45 @@
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

@@ -186,7 +186,7 @@ module.exports = function (grunt) {
// 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'],
html: ['<%= yeoman.app %>/index.html', '<%= yeoman.app %>/login.html', '<%= yeoman.app %>/embed.html'],
options: {
dest: '<%= yeoman.dist %>',
flow: {

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

@@ -0,0 +1,127 @@
<!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>
<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>
<script src="/bower_components/jquery/jquery.js"></script>
<!-- build:js /scripts/embed-plugins.js -->
<script src="/bower_components/angular/angular.js"></script>
<script src="/bower_components/angular-sanitize/angular-sanitize.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/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/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="/bower_components/angular-base64-upload/dist/angular-base64-upload.js"></script>
<script src="/bower_components/plotly/plotly.js"></script>
<script src="/bower_components/angular-plotly/src/angular-plotly.js"></script>
<script src="/scripts/directives/plotly.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>
<script src="/bower_components/angular-bootstrap-show-errors/src/showErrors.js"></script>
<script src="/bower_components/d3/d3.min.js"></script>
<script src="/bower_components/angular-ui-sortable/sortable.js"></script>
<!-- endbuild -->
<!-- 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">
@@ -39,7 +40,7 @@
<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">
@@ -52,13 +53,13 @@
<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>
@@ -67,12 +68,12 @@
<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>
<a href="alerts">Alerts</a>
</li>
</ul>
<form class="navbar-form navbar-left" role="search" ng-submit="searchQueries()">
@@ -83,19 +84,19 @@
</form>
<ul class="nav navbar-nav navbar-right">
<li ng-show="currentUser.hasPermission('admin')">
<a href="/data_sources" title="Data Sources"><i class="fa fa-database"></i></a>
<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>
<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}}">
<a ng-href="users/{{currentUser.id}}">
<div class="row">
<div class="col-sm-2">
<img src="{{currentUser.gravatar_url}}" size="40px" class="img-circle"/>
<img ng-src="{{currentUser.gravatar_url}}" size="40px" class="img-circle"/>
</div>
<div class="col-sm-10">
<p><strong>{{currentUser.name}}</strong></p>
@@ -106,7 +107,7 @@
<li class="divider">
</li>
<li>
<a href="/logout" target="_self">Log out</a>
<a href="logout" target="_self">Log out</a>
</li>
</ul>
</li>
@@ -119,6 +120,32 @@
<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>
{% raw %}
<div class="container-fluid footer">
<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 %}
<script src="/bower_components/jquery/jquery.js"></script>
@@ -141,12 +168,9 @@
<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/pivottable/dist/export_renderers.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>
@@ -154,18 +178,20 @@
<script src="/bower_components/underscore.string/lib/underscore.string.js"></script>
<script src="/bower_components/marked/lib/marked.js"></script>
<script src="/bower_components/angular-base64-upload/dist/angular-base64-upload.js"></script>
<script src="/scripts/ng_highchart.js"></script>
<script src="/bower_components/plotly/plotly.js"></script>
<script src="/bower_components/angular-plotly/src/angular-plotly.js"></script>
<script src="/scripts/directives/plotly.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/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>
<script src="/bower_components/angular-bootstrap-show-errors/src/showErrors.js"></script>
<script src="/bower_components/d3/d3.min.js"></script>
<script src="/bower_components/angular-ui-sortable/sortable.js"></script>
<!-- endbuild -->
<!-- build:js({.tmp,app}) /scripts/scripts.js -->
@@ -190,6 +216,7 @@
<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>
@@ -200,8 +227,9 @@
<script>
// TODO: move currentUser & features to be an Angular service
var featureFlags = {{ features|safe }};
var clientConfig = {{ client_config|safe }};
var currentUser = {{ user|safe }};
var currentOrgSlug = "{{ org_slug }}";
currentUser.canEdit = function(object) {
var user_id = object.user_id || (object.user && object.user.id);
@@ -212,6 +240,9 @@
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">

View File

@@ -6,10 +6,12 @@ angular.module('redash', [
'redash.services',
'redash.renderers',
'redash.visualization',
'highchart',
'plotly',
'plotly-chart',
'angular-growl',
'angularMoment',
'ui.bootstrap',
'ui.sortable',
'smartTable.table',
'ngResource',
'ngRoute',
@@ -19,15 +21,6 @@ angular.module('redash', [
'ngSanitize'
]).config(['$routeProvider', '$locationProvider', '$compileProvider', 'growlProvider', 'uiSelectConfig',
function ($routeProvider, $locationProvider, $compileProvider, growlProvider, uiSelectConfig) {
if (featureFlags.clientSideMetrics) {
Bucky.setOptions({
host: '/api/metrics'
});
Bucky.requests.monitor('ajax_requsts');
Bucky.requests.transforms.enable('dashboards', /dashboard\/[\w-]+/ig, '/dashboard');
}
function getQuery(Query, $route) {
var query = Query.get({'id': $route.current.params.queryId });
return query.$promise;
@@ -56,7 +49,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', {
@@ -116,15 +110,24 @@ angular.module('redash', [
templateUrl: '/views/users/list.html',
controller: 'UsersCtrl'
});
$routeProvider.when('/', {
templateUrl: '/views/personal.html',
controller: 'PersonalIndexCtrl'
$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

@@ -29,7 +29,7 @@
{
"label": "Name",
"map": "name",
"cellTemplate": '<a href="/alerts/{{dataRow.id}}">{{dataRow.name}}</a> (<a href="/queries/{{dataRow.query.id}}">query</a>)'
"cellTemplate": '<a href="alerts/{{dataRow.id}}">{{dataRow.name}}</a> (<a href="queries/{{dataRow.query.id}}">query</a>)'
},
{
'label': 'Created By',
@@ -96,7 +96,9 @@
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") {
@@ -171,4 +173,4 @@
.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) {
@@ -150,13 +151,20 @@
}
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.version = clientConfig.version;
$scope.newVersionAvailable = clientConfig.newVersionAvailable && currentUser.hasPermission("admin");
$scope.dashboards = [];
$scope.reloadDashboards = function () {
@@ -191,12 +199,7 @@
});
};
var IndexCtrl = function ($scope, Events, Dashboard) {
Events.record(currentUser, "view", "page", "homepage");
$scope.$parent.pageTitle = "Home";
};
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";
@@ -206,8 +209,7 @@
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

@@ -3,69 +3,69 @@
$scope.refreshEnabled = 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();
@@ -132,12 +132,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;
});
};
@@ -153,6 +157,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

@@ -13,7 +13,7 @@
event.stopPropagation();
Events.record(currentUser, "delete", "datasource", datasource.id);
datasource.$delete(function(resource) {
growl.addSuccessMessage("Data source deleted succesfully.");
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);

View File

@@ -17,8 +17,9 @@
saveQuery = $scope.saveQuery;
$scope.sourceMode = true;
$scope.canEdit = currentUser.canEdit($scope.query) || featureFlags.allowAllToEditQueries;
$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);
@@ -33,6 +35,7 @@
var isValidDataSourceId = !isNaN(dataSourceId) && _.some($scope.dataSources, function(ds) {
return ds.id == dataSourceId;
});
if (!isValidDataSourceId) {
dataSourceId = $scope.dataSources[0].id;
}
@@ -41,13 +44,37 @@
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";
DataSource.getSchema({id: getDataSourceId()}, function(data) {
DataSource.getSchema({id: $scope.query.data_source_id}, function(data) {
if (data && data.length > 0) {
$scope.schema = data;
_.each(data, function(table) {
@@ -69,16 +96,19 @@
$scope.isQueryOwner = (currentUser.id === $scope.query.user.id) || currentUser.hasPermission('admin');
$scope.canViewSource = currentUser.hasPermission('view_source');
$scope.canExecuteQuery = currentUser.hasPermission('execute_query');
$scope.dataSources = DataSource.query(function(dataSources) {
updateSchema();
$scope.canExecuteQuery = function() {
return currentUser.hasPermission('execute_query') && !$scope.dataSource.view_only;
}
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.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
@@ -126,7 +156,7 @@
};
$scope.executeQuery = function() {
if (!$scope.canExecuteQuery) {
if (!$scope.canExecuteQuery()) {
return;
}
@@ -240,7 +270,7 @@
});
$scope.openScheduleForm = function() {
if (!$scope.isQueryOwner) {
if (!$scope.isQueryOwner || !$scope.canScheduleQuery) {
return;
};

View File

@@ -1,24 +1,214 @@
(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: 50,
itemsByPage: 20,
maxSize: 8,
};
$scope.gridColumns = [
{
"label": "",
"map": "gravatar_url",
"cellTemplate": '<img src="{{dataRow.gravatar_url}}" height="40px"/>'
},
{
"label": "Name",
"map": "name",
"cellTemplate": '<a href="/users/{{dataRow.id}}">{{dataRow.name}}</a>'
"cellTemplate": '<img src="{{dataRow.gravatar_url}}" height="40px"/> <a href="users/{{dataRow.id}}">{{dataRow.name}}</a>'
},
{
'label': 'Joined',
@@ -154,6 +344,11 @@
};
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');
@@ -223,4 +220,4 @@
}
}
])
})();
})();

View File

@@ -34,7 +34,7 @@
});
});
$http.get('/api/data_sources/types').success(function (types) {
$http.get('api/data_sources/types').success(function (types) {
setType(types);
$scope.dataSourceTypes = types;
@@ -49,6 +49,10 @@
prop.type = 'file';
}
if (prop.type == 'boolean') {
prop.type = 'checkbox';
}
prop.required = _.contains(type.configuration_schema.required, name);
});
});

View File

@@ -40,7 +40,7 @@
}
}]);
directives.directive('rdTab', function () {
directives.directive('rdTab', ['$location', function ($location) {
return {
restrict: 'E',
scope: {
@@ -48,9 +48,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 +59,7 @@
});
}
}
});
}]);
directives.directive('rdTabs', ['$location', function ($location) {
return {
@@ -67,9 +68,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;
@@ -281,4 +283,48 @@
}
};
});
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,251 @@
(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 = _.uniq(_.flatten(_.pluck(seriesList, 'x')));
xValues.sort();
_.each(seriesList, function(series) {
series.x.sort();
_.each(xValues, function(value, index) {
if (series.x[index] != value) {
series.x.splice(index, 0, value);
series.y.splice(index, 0, 0);
}
});
});
}
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.ssssss");
}
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, element, attrs) {
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;
_.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 (scope.options.sortX) {
data = _.sortBy(data, 'x');
}
_.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

@@ -10,7 +10,7 @@
},
template: '<small><span class="glyphicon glyphicon-link"></span></small> <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,10 +29,10 @@
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>'
}
@@ -50,8 +50,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() + '.csv');
element.attr('download', scope.query.name.replace(" ", "_") + moment(scope.queryResult.getUpdatedAt()).format("_YYYY_MM_DD") + ".csv");
}
});
}
@@ -265,6 +265,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') {
@@ -287,4 +291,4 @@
.directive('queryRefreshSelect', queryRefreshSelect)
.directive('queryTimePicker', queryTimePicker)
.directive('queryFormatter', ['$http', queryFormatter]);
})();
})();

View File

@@ -0,0 +1,56 @@
angular.module('redash', [
'redash.directives',
'redash.admin_controllers',
'redash.controllers',
'redash.filters',
'redash.services',
'redash.renderers',
'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

@@ -82,7 +82,7 @@ angular.module('redash.filters', []).
}
var html = marked(text);
if (featureFlags.allowScriptsInUserInput) {
if (clientConfig.allowScriptsInUserInput) {
html = $sce.trustAsHtml(html);
}
@@ -97,4 +97,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,423 +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',
'Brown':'#A52A2A',
'Yellow':'#ffff00',
'Black':'#000000',
'Gray':'#808080',
'Pink':'#FFC0CB',
'Dark Blue':'#00008b'
};
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: _.sortBy(_.values(data), 'x'),
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);
chartOptions.plotOptions.series = {
turboThreshold: featureFlags.highChartsTurboThreshold
}
// $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();
}
}
};
}]);
})();

View File

@@ -718,7 +718,7 @@ angular.module("partials/smartTable.html", []).run(["$templateCache", function (
" </tbody>\n" +
" <tfoot ng-show=\"isPaginationEnabled\">\n" +
" <tr class=\"smart-table-footer-row\">\n" +
" <td colspan=\"{{columns.length}}\">\n" +
" <td class=\"text-center\" colspan=\"{{columns.length}}\">\n" +
" <div pagination-smart-table=\"\" num-pages=\"numberOfPages\" max-size=\"maxSize\" current-page=\"currentPage\"></div>\n" +
" </td>\n" +
" </tr>\n" +

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

@@ -24,8 +24,8 @@
};
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);
@@ -43,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);
@@ -186,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 = {};
@@ -199,7 +214,7 @@
_.each(row, function (value, definition) {
var name = definition.split("::")[0] || definition.split("__")[0];
var type = definition.split("::")[1] || definition.split("__")[0];
var type = definition.split("::")[1] || definition.split("__")[1];
if (mapping) {
type = mapping[definition];
}
@@ -229,26 +244,15 @@
}
});
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);
};
@@ -397,6 +401,10 @@
if ('job' in response) {
refreshStatus(queryResult, query);
}
}, function(error) {
if (error.status === 403) {
queryResult.update(error.data);
}
});
return queryResult;
@@ -406,17 +414,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 () {
@@ -538,10 +546,10 @@
var actions = {
'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'}
'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;
};
@@ -569,13 +577,24 @@
'delete': {method: 'DELETE', transformResponse: transform}
};
var UserResource = $resource('/api/users/:id', {id: '@id'}, actions);
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'});
var resource = $resource('api/alerts/:alertId/subscriptions/:userId', {alertId: '@alert_id', userId: '@user.id'});
return resource;
};
@@ -594,13 +613,13 @@
}].concat($http.defaults.transformRequest)
}
};
var resource = $resource('/api/alerts/:id', {id: '@id'}, actions);
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) {
@@ -627,5 +646,6 @@
.factory('Alert', ['$resource', '$http', Alert])
.factory('AlertSubscription', ['$resource', AlertSubscription])
.factory('Widget', ['$resource', 'Query', Widget])
.factory('User', ['$resource', '$http', User]);
.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);

View File

@@ -44,7 +44,7 @@
}
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');

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,29 @@
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);
var reloadChart = function() {
reloadData();
$scope.plotlyOptions = $scope.options;
}
_.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));
});
};
};
$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,197 +63,139 @@
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",
"Logarithmic": "logarithmic",
"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.yAxisOptions = {
"Linear": "linear",
"Logarithmic": "logarithmic"
};
scope.chartTypeChanged = function() {
_.each(scope.options.seriesOptions, function(options) {
options.type = scope.options.globalSeriesType;
});
}
scope.xAxisType = "datetime";
scope.yAxisType = "linear";
scope.stacking = "none";
scope.xAxisScales = ['datetime', 'linear', 'logarithmic', 'category'];
scope.yAxisScales = ['linear', 'logarithmic'];
scope.columnTypes = {
"X": "x",
"Y": "y",
"Series": "series",
"Unused": "unused"
};
scope.series = [];
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', refreshSeries, true);
scope.$watch(function() {return [scope.queryResult.getId(), scope.queryResult.status]}, function(changed) {
if (!changed[0]) {
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;
});
scope.yAxisType = (scope.visualization.options.yAxis && scope.visualization.options.yAxis.type) || scope.yAxisType;
yAxisUnwatch = scope.$watch("yAxisType", function (yAxisType) {
scope.visualization.options.yAxis = scope.visualization.options.yAxis || {};
scope.visualization.options.yAxis.type = yAxisType;
});
} else {
if (chartOptionsUnwatch) {
chartOptionsUnwatch();
chartOptionsUnwatch = null;
}
if (columnsWatch) {
columnWatch();
columnWatch = null;
}
if (xAxisUnwatch) {
xAxisUnwatch();
xAxisUnwatch = null;
}
if (yAxisUnwatch) {
yAxisUnwatch();
yAxisUnwatch = 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

@@ -31,31 +31,33 @@
restrict: 'E',
templateUrl: '/views/visualizations/counter.html',
link: function($scope, elm, attrs) {
$scope.$watch('[queryResult && queryResult.getData(), visualization.options]',
function() {
var queryData = $scope.queryResult.getData();
if (queryData) {
var rowNumber = $scope.visualization.options.rowNumber - 1;
var targetRowNumber = $scope.visualization.options.targetRowNumber - 1;
var counterColName = $scope.visualization.options.counterColName;
var targetColName = $scope.visualization.options.targetColName;
var refreshData = function() {
var queryData = $scope.queryResult.getData();
if (queryData) {
var rowNumber = $scope.visualization.options.rowNumber - 1;
var targetRowNumber = $scope.visualization.options.targetRowNumber - 1;
var counterColName = $scope.visualization.options.counterColName;
var targetColName = $scope.visualization.options.targetColName;
if (counterColName) {
$scope.counterValue = queryData[rowNumber][counterColName];
}
if (targetColName) {
$scope.targetValue = queryData[targetRowNumber][targetColName];
if ($scope.targetValue) {
$scope.delta = $scope.counterValue - $scope.targetValue;
$scope.trendPositive = $scope.delta >= 0;
}
} else {
$scope.targetValue = null;
}
if (counterColName) {
$scope.counterValue = queryData[rowNumber][counterColName];
}
}, true);
if (targetColName) {
$scope.targetValue = queryData[targetRowNumber][targetColName];
if ($scope.targetValue) {
$scope.delta = $scope.counterValue - $scope.targetValue;
$scope.trendPositive = $scope.delta >= 0;
}
} else {
$scope.targetValue = null;
}
}
};
$scope.$watch("visualization.options", refreshData, true);
$scope.$watch("queryResult && queryResult.getData()", refreshData);
}
}
});

View File

@@ -0,0 +1,43 @@
(function (window) {
var module = angular.module('redash.visualization');
module.directive('dateRangeSelector', [function () {
return {
restrict: 'E',
scope: {
dateRange: "="
},
templateUrl: '/views/visualizations/date_range_selector.html',
replace: true,
controller: ['$scope', function ($scope) {
$scope.dateRangeHuman = {
min: null,
max: null
};
$scope.$watch('dateRange', function (dateRange, oldDateRange, scope) {
scope.dateRangeHuman.min = dateRange.min.format('YYYY-MM-DD');
scope.dateRangeHuman.max = dateRange.max.format('YYYY-MM-DD');
});
$scope.$watch('dateRangeHuman', function (dateRangeHuman, oldDateRangeHuman, scope) {
var newDateRangeMin = moment.utc(dateRangeHuman.min);
var newDateRangeMax = moment.utc(dateRangeHuman.max);
if (!newDateRangeMin ||
!newDateRangeMax ||
!newDateRangeMin.isValid() ||
!newDateRangeMax.isValid() ||
newDateRangeMin.isAfter(newDateRangeMax)) {
// Prevent invalid date input
// No need to show up a notification to user here, it will be too noisy.
// Instead, simply preventing changes to the scope silently.
scope.dateRangeHuman = oldDateRangeHuman;
return;
}
scope.dateRange.min = newDateRangeMin;
scope.dateRange.max = newDateRangeMax;
}, true);
}]
}
}]);
})(window);

View File

@@ -19,10 +19,8 @@ renderers.directive('pivotTableRenderer', function () {
// We need to give the pivot table its own copy of the data, because its change
// it which interferes with other visualizations.
var data = $.extend(true, [], $scope.queryResult.getData());
var renderers = $.extend($.pivotUtilities.renderers,
$.pivotUtilities.export_renderers)
$(element).pivotUI(data, {
renderers: renderers
renderers: $.pivotUtilities.renderers
}, true);
}
});

View File

@@ -79,14 +79,14 @@
} else if (columnType === 'date') {
columnDefinition.formatFunction = function (value) {
if (value && moment.isMoment(value)) {
return value.toDate().toLocaleDateString();
return value.format(clientConfig.dateFormat);
}
return value;
};
} else if (columnType === 'datetime') {
columnDefinition.formatFunction = function (value) {
if (value && moment.isMoment(value)) {
return value.toDate().toLocaleString();
return value.format(clientConfig.dateTimeFormat);
}
return value;
};

View File

@@ -26,10 +26,6 @@ a.navbar-brand img {
height: 40px;
}
.graph {
height: 300px;
}
.avatar {
margin-top: 5px;
margin-bottom: 5px;
@@ -432,16 +428,87 @@ div.table-name {
padding: 30px;
}
/*
bootstrap's hidden-xs class adds display:block when not hidden
use this class when you need to keep the original display value
*/
@media (max-width: 767px) {
.rd-hidden-xs {
display: none !important;
}
}
.log-container {
margin-bottom: 50px;
}
/* Footer */
.footer {
color: #818d9f;
padding-bottom: 30px;
}
.footer a {
color: #818d9f;
margin-left: 20px;
}
.col-table .missing-value {
color: #b94a48;
}
.col-table .super-small-input {
padding-left: 3px;
height: 24px;
}
.col-table .ui-select-toggle, .col-table .ui-select-search {
padding: 2px;
padding-left: 5px;
height: 24px;
}
.clearable button {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
/* Immediately apply ng-cloak, instead of waiting for angular.js to load: */
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
display: none !important;
}
/* Smart Table */
.smart-table {
margin-bottom: 0px;
}
.smart-table .pagination {
margin-bottom: 5px;
margin-top: 10px;
}
/* Embed Code */
.embed-code code {
display: block;
white-space: normal;
width: 100%;
}
.embed-code i {
cursor: pointer;
}
.voffset { margin-top: 2px; }
.voffset1 { margin-top: 5px; }
.voffset2 { margin-top: 10px; }
.voffset3 { margin-top: 15px; }
.voffset4 { margin-top: 30px; }
.voffset5 { margin-top: 40px; }
.voffset6 { margin-top: 60px; }
.voffset7 { margin-top: 80px; }
.voffset8 { margin-top: 100px; }
.voffset9 { margin-top: 150px; }
.overlay {
background-color: #808080;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
padding: 0;
z-index: 1000;
opacity: 0.7;
}

View File

@@ -1,6 +1,6 @@
<div class="container">
<div class="container">
<ol class="breadcrumb">
<li><a href="/alerts">Alerts</a></li>
<li><a href="alerts">Alerts</a></li>
<li class="active">{{alert.name || getDefaultName() || "New"}}</li>
</ol>
<div class="row">
@@ -44,6 +44,12 @@
<input type="number" class="form-control" ng-model="alert.options.value" placeholder="reference value" required/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2">Rearm seconds</label>
<div class="col-md-4">
<input type="number" class="form-control" ng-model="alert.rearm" />
</div>
</div>
</div>
<div class="form-group">
@@ -55,4 +61,4 @@
<alert-subscribers alert-id="alert.id"></alert-subscribers>
</div>
</div>
</div>
</div>

View File

@@ -5,7 +5,7 @@
<div class="row">
<div class="col-md-12">
<p>
<a href="/alerts/new" class="btn btn-default"><i class="fa fa-plus"></i> New Alert</a>
<a href="alerts/new" class="btn btn-default"><i class="fa fa-plus"></i> New Alert</a>
</p>
<smart-table rows="alerts" columns="gridColumns"

View File

@@ -46,7 +46,7 @@
tooltip-placement="bottom">Updated: <span am-time-ago="queryResult.getUpdatedAt()"></span></span>
<span class="pull-right">
<a class="btn btn-default btn-xs" ng-href="/queries/{{query.id}}#{{widget.visualization.id}}" ng-show="currentUser.hasPermission('view_query')"><span class="glyphicon glyphicon-link"></span></a>
<a class="btn btn-default btn-xs" ng-href="queries/{{query.id}}#{{widget.visualization.id}}" ng-show="currentUser.hasPermission('view_query')"><span class="glyphicon glyphicon-link"></span></a>
<button type="button" class="btn btn-default btn-xs" ng-show="dashboard.canEdit()" ng-click="deleteWidget()" title="Remove Widget"><span class="glyphicon glyphicon-trash"></span></button>
</span>
@@ -58,6 +58,21 @@
</div>
</div>
<div class="panel panel-default rd-widget-textbox" ng-if="type=='restricted'">
<div class="panel-body">
<div class="row">
<div class="col-lg-12">
<div class="text-center">
<h1><span class="glyphicon glyphicon-lock"></span></h1>
<p class="text-muted">
This widget requires access to a data source you don't have access to.
</p>
</div>
</div>
</div>
</div>
</div>
<div class="panel panel-default rd-widget-textbox" ng-hide="widget.width == 0" ng-if="type=='textbox'" ng-mouseenter="showControls = true" ng-mouseleave="showControls = false">
<div class="panel-body">
<div class="row">

View File

@@ -1,6 +1,6 @@
<div class="container">
<ol class="breadcrumb">
<li><a href="/data_sources">Data Sources</a></li>
<li><a href="data_sources">Data Sources</a></li>
<li class="active">{{dataSource.name || "New"}}</li>
</ol>
<div class="row">

View File

@@ -10,7 +10,7 @@
<div class="form-group" ng-class='{"has-error": !inner.input.$valid}' ng-form="inner" ng-repeat="(name, input) in type.configuration_schema.properties">
<label>{{input.title || name | capitalize}}</label>
<input name="input" type="{{input.type}}" class="form-control" ng-model="dataSource.options[name]" ng-required="input.required"
ng-if="input.type !== 'file'" accesskey="tab">
ng-if="input.type !== 'file'" accesskey="tab" placeholder="{{input.default}}">
<input name="input" type="file" class="form-control" ng-model="files[name]" ng-required="input.required && !dataSource.options[name]"
base-sixty-four-input

View File

@@ -9,7 +9,7 @@
<i class="fa fa-database"></i> {{dataSource.name}}
<button class="btn btn-xs btn-danger pull-right" ng-click="deleteDataSource($event, dataSource)">Delete</button>
</div>
<a ng-href="/data_sources/new" class="list-group-item">
<a ng-href="data_sources/new" class="list-group-item">
<i class="fa fa-plus"></i> Add Data Source
</a>
</div>

View File

@@ -0,0 +1,12 @@
<div class="modal-header">
<h3 class="modal-title">{{title}}</h3>
</div>
<div class="modal-body">
<form class="form">
<input type="text" ng-model="group.name" placeholder="Group Name" class="form-control"/>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-default" ng-click="cancel()">Cancel</button>
<button class="btn btn-primary" ng-click="ok()">{{saveButtonText}}</button>
</div>

View File

@@ -0,0 +1,15 @@
<div class="container">
<users-nav></users-nav>
<div class="row voffset1">
<div class="col-md-12">
<p ng-if="currentUser.hasPermission('admin')">
<a href="#" ng-click="newGroup()" class="btn btn-default"><i class="fa fa-plus"></i> New Group</a>
</p>
<smart-table rows="groups" columns="gridColumns"
config="gridConfig"
class="table table-condensed table-hover"></smart-table>
</div>
</div>
</div>

View File

@@ -0,0 +1,42 @@
<div class="container">
<users-nav></users-nav>
<group-name group="group"></group-name>
<div class="row">
<div class="col-lg-4">
<ul class="nav nav-pills">
<li role="presentation" class="active"><a href="groups/{{group.id}}">Members</a></li>
<li role="presentation" ng-if="currentUser.isAdmin"><a href="groups/{{group.id}}/data_sources">Data Sources</a></li>
</ul>
</div>
<div class="col-lg-8" ng-if="currentUser.isAdmin">
<ui-select ng-model="newMember.selected" on-select="addMember($item)">
<ui-select-match placeholder="Add New Member"></ui-select-match>
<ui-select-choices repeat="user in foundUsers | filter:$select.search"
refresh="findUser($select.search)"
refresh-delay="0"
ui-disable-choice="user.alreadyMember">
<div>
<img src="{{user.gravatar_url}}" height="24px">&nbsp;{{user.name}}
<small ng-if="user.alreadyMember">(already member in this group)</small>
</div>
</ui-select-choices>
</ui-select>
</div>
</div>
<div class="row voffset1">
<div class="col-lg-12">
<table class="table table-condensed table-hover">
<tbody>
<tr ng-repeat="member in members">
<td width="50px"><img src="{{member.gravatar_url}}" height="40px"/></td>
<td>{{member.name}} <button class="pull-right btn btn-sm btn-danger" ng-click="removeMember(member)" ng-if="currentUser.isAdmin">Remove</button></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -0,0 +1,57 @@
<div class="container">
<users-nav></users-nav>
<group-name group="group"></group-name>
<div class="row">
<div class="col-lg-4">
<ul class="nav nav-pills">
<li role="presentation"><a href="groups/{{group.id}}">Members</a></li>
<li role="presentation" class="active"><a href="groups/{{group.id}}/data_sources">Data Sources</a></li>
</ul>
</div>
<div class="col-lg-8">
<ui-select ng-model="newDataSource.selected" on-select="addDataSource($item)">
<ui-select-match placeholder="Add Data Source"></ui-select-match>
<ui-select-choices repeat="dataSource in foundDataSources | filter:$select.search"
refresh="findDataSource($select.search)"
refresh-delay="0">
<div>
{{dataSource.name}}
</div>
</ui-select-choices>
</ui-select>
</div>
</div>
<div class="row voffset1">
<div class="col-lg-12">
<table class="table table-condensed table-hover">
<thead>
</thead>
<tbody>
<tr ng-repeat="dataSource in dataSources">
<td> {{dataSource.name}}</td>
<td width="180px">
<div class="btn-group" dropdown>
<button type="button" class="btn btn-sm btn-default dropdown-toggle" dropdown-toggle ng-if="dataSource.view_only">
View Only <span class="caret"></span>
</button>
<button type="button" class="btn btn-sm btn-success dropdown-toggle" dropdown-toggle ng-if="!dataSource.view_only">
Full Access <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="#" ng-click="changePermission(dataSource, false)"><small ng-if="!dataSource.view_only"><span class="glyphicon glyphicon-ok"/></small> Full Access<br/></a></li>
<li><a href="#" ng-click="changePermission(dataSource, true)"><small ng-if="dataSource.view_only"><span class="glyphicon glyphicon-ok"/></small> View Only</a></li>
</ul>
</div>
<button class="pull-right btn btn-sm btn-danger" ng-click="removeDataSource(dataSource)">Remove</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -1,20 +1,34 @@
<div class="container">
<h2>Dashboards</h2>
<div class="list-group" ng-repeat="(name, dashboards) in allDashboards">
<div class="list-group-item active">
{{name}}
<button ng-show="currentUser.hasPermission('create_dashboard')" type="button" class="btn btn-sm btn-link" data-toggle="modal" href="#new_dashboard_dialog" tooltip="New Dashboard"><span class="glyphicon glyphicon-plus-sign"></span></button>
<div class="row">
<p>
<a href="queries/new" class="btn btn-default">New Query</a>
<button ng-show="currentUser.hasPermission('create_dashboard')" type="button" class="btn btn-default" data-toggle="modal" href="#new_dashboard_dialog">New Dashboard</button>
<a href="alerts/new" class="btn btn-default">New Alert</a>
</p>
</div>
<div class="row">
<div class="list-group col-md-6">
<div class="list-group-item active">
Recent Dashboards
</div>
<a ng-href="dashboard/{{dashboard.slug}}" class="list-group-item" ng-repeat="dashboard in recentDashboards">
{{dashboard.name}}
</a>
</div>
<div class="list-group-item" ng-repeat="dashboard in dashboards" >
<button type="button" class="close delete-button" aria-hidden="true" ng-show="dashboard.canEdit()" ng-click="archiveDashboard(dashboard)" tooltip="Delete Dashboard">&times;</button>
<a ng-href="/dashboard/{{dashboard.slug}}">{{dashboard.name}}</a>
<div class="list-group col-md-6">
<div class="list-group-item active">
Recent Queries
</div>
<a ng-href="queries/{{query.id}}" class="list-group-item" ng-repeat="query in recentQueries">{{query.name}}</a>
</div>
</div>
<div ng-show="currentUser.hasPermission('admin')">
<div ng-show="currentUser.hasPermission('super_admin')" class="row">
<div class="list-group">
<div class="list-group-item active">Admin</div>
<a href="/admin/status" class="list-group-item">Status</a>
<a href="admin/status" class="list-group-item">Status</a>
</div>
</div>
</div>

View File

@@ -1,34 +0,0 @@
<div class="container">
<div class="row">
<p>
<a href="/queries/new" class="btn btn-default">New Query</a>
<button ng-show="currentUser.hasPermission('create_dashboard')" type="button" class="btn btn-default" data-toggle="modal" href="#new_dashboard_dialog">New Dashboard</button>
<a href="/alerts/new" class="btn btn-default">New Alert</a>
</p>
</div>
<div class="row">
<div class="list-group col-md-6">
<div class="list-group-item active">
Recent Dashboards
</div>
<a ng-href="/dashboard/{{dashboard.slug}}" class="list-group-item" ng-repeat="dashboard in recentDashboards">
{{dashboard.name}}
</a>
</div>
<div class="list-group col-md-6">
<div class="list-group-item active">
Recent Queries
</div>
<a ng-href="/queries/{{query.id}}" class="list-group-item" ng-repeat="query in recentQueries">{{query.name}}</a>
</div>
</div>
<div ng-show="currentUser.hasPermission('admin')" class="row">
<div class="list-group">
<div class="list-group-item active">Admin</div>
<a href="/admin/status" class="list-group-item">Status</a>
</div>
</div>
</div>

View File

@@ -1 +1 @@
<a ng-href="/queries/{{dataRow.id}}">{{dataRow.name}}</a>
<a ng-href="queries/{{dataRow.id}}">{{dataRow.name}}</a>

View File

@@ -1,5 +1,17 @@
<div class="container" style="position:relative">
<overlay ng-if="canCreateQuery === false">
You don't have permission to create new queries on any of the data sources available to you.
You can either <a href="queries">browse existing queries</a>, or ask for additional permissions from your re:dash admin.
</overlay>
<div class="container">
<overlay ng-if="noDataSources && currentUser.isAdmin">
Looks like no data sources were created yet (or none of them available to the group(s) you're member of). Please create one first, and then start querying.<br/>
<a href="data_sources/new" class="btn btn-default">Create Data Source</a> <a href="groups" class="btn btn-default">Manage Group Permissions</a>
</overlay>
<overlay ng-if="noDataSources && !currentUser.isAdmin">
Looks like no data sources were created yet (or none of them available to the group(s) you're member of). Please ask your re:dash admin to create one first.
</overlay>
<p class="alert alert-warning" ng-if="query.is_archived">This query is archived and can't be used in dashboards, and won't appear in search results.</p>
<alert-unsaved-changes ng-if="canEdit" is-dirty="isDirty"></alert-unsaved-changes>
@@ -26,7 +38,7 @@
</div>
<div class="col-lg-2">
<div class="rd-hidden-xs pull-right">
<div class="pull-right">
<query-source-link></query-source-link>
</div>
</div>
@@ -63,12 +75,12 @@
<div ng-class="editorSize">
<div>
<p>
<button type="button" class="btn btn-primary btn-xs" ng-disabled="queryExecuting" ng-click="executeQuery()" ng-if="canExecuteQuery">
<button type="button" class="btn btn-primary btn-xs" ng-disabled="queryExecuting || !canExecuteQuery()" ng-click="executeQuery()">
<span class="glyphicon glyphicon-play"></span> Execute
</button>
<query-formatter></query-formatter>
<span class="pull-right">
<button class="btn btn-xs btn-default rd-hidden-xs" ng-click="duplicateQuery()">
<button class="btn btn-xs btn-default" ng-click="duplicateQuery()">
<span class="glyphicon glyphicon-share-alt"></span> Fork
</button>
@@ -90,7 +102,7 @@
<div class="schema-browser">
<div ng-repeat="table in schema | filter:schemaFilter track by table.name">
<div class="table-name" ng-click="table.collapsed = !table.collapsed">
<i class="fa fa-table"></i> <strong><span title="{{table.name}}">{{table.name}}</span></strong>
<i class="fa fa-table"></i> <strong><span title="{{table.name}}">{{table.name}}</span><span ng-if="table.size !== undefined"> ({{table.size}})</span></strong>
</div>
<div collapse="table.collapsed && !schemaFilter">
<div ng-repeat="column in table.columns track by column" style="padding-left:16px;">{{column}}</div>
@@ -103,7 +115,7 @@
</div>
<hr ng-if="sourceMode">
<div class="row">
<div class="col-lg-3 rd-hidden-xs">
<div class="col-lg-3">
<p>
<span class="glyphicon glyphicon-user"></span>
<span class="text-muted">Created By </span>
@@ -148,7 +160,7 @@
<p>
<a class="btn btn-primary btn-sm" ng-disabled="queryExecuting || !queryResult.getData()" query-result-link target="_self">
<span class="glyphicon glyphicon-cloud-download"></span>
<span class="rd-hidden-xs">Download Dataset</span>
<span>Download Dataset</span>
</a>
<a class="btn btn-warning btn-sm" ng-disabled="queryExecuting" data-toggle="modal" data-target="#archive-confirmation-modal"
@@ -213,7 +225,7 @@
<span class="remove" ng-click="deleteVisualization($event, vis)" ng-show="canEdit"> &times;</span>
</rd-tab>
<rd-tab tab-id="add" name="&plus; New Visualization" removeable="true" ng-show="canEdit"></rd-tab>
<li ng-if="!sourceMode && canExecuteQuery" class="rd-tab-btn"><button class="btn btn-sm btn-default" ng-click="executeQuery()" ng-disabled="queryExecuting" title="Refresh Dataset"><span class="glyphicon glyphicon-refresh"></span></button></li>
<li ng-if="!sourceMode" class="rd-tab-btn"><button class="btn btn-sm btn-default" ng-click="executeQuery()" ng-disabled="queryExecuting || !canExecuteQuery()" title="Refresh Dataset"><span class="glyphicon glyphicon-refresh"></span></button></li>
</ul>
</div>
</div>
@@ -222,6 +234,18 @@
<div ng-show="selectedTab == 'table'" >
<filters></filters>
<grid-renderer query-result="queryResult" items-per-page="50"></grid-renderer>
<div class="row" ng-if="vis.type=='TABLE'" ng-repeat="vis in query.visualizations">
<div class="col-lg-8 embed-code">
<i class="fa fa-code" ng-click="show_code = show_code==true ? false : true;"></i>
<div ng-show="show_code">
<span class="text-muted">Embed code for this table: <small>(height should be adjusted)</small></span>
<code>&lt;iframe src="{{ base_url }}/embed/query/{{query.id}}/visualization/{{ vis.id }}?api_key={{query.api_key}}"<br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
width="720" height="1650"&gt;&lt;/iframe&gt;</code>
</div>
</div>
</div>
</div>
<pivot-table-renderer ng-show="selectedTab == 'pivot'" query-result="queryResult"></pivot-table-renderer>
@@ -229,6 +253,18 @@
<div ng-show="selectedTab == vis.id" ng-repeat="vis in query.visualizations">
<visualization-renderer visualization="vis" query-result="queryResult"></visualization-renderer>
<div class="row">
<div class="col-lg-8 embed-code">
<i class="fa fa-code" ng-click="show_code = show_code==true ? false : true;"></i>
<div ng-show="show_code">
<span class="text-muted">Embed code for this chart: <small>(height should be adjusted)</small></span>
<code>&lt;iframe src="{{ base_url }}/embed/query/{{query.id}}/visualization/{{ vis.id }}?api_key={{query.api_key}}"<br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
width="720" height="391"&gt;&lt;/iframe&gt;</code>
</div>
</div>
</div>
<edit-visulatization-form visualization="vis" query="query" query-result="queryResult" ng-show="canEdit"></edit-visulatization-form>
</div>

View File

@@ -1,11 +1,10 @@
<div class="container">
<ol class="breadcrumb">
<li class="active">Users</li>
</ol>
<div class="row">
<users-nav></users-nav>
<div class="row voffset1">
<div class="col-md-12">
<p ng-if="currentUser.hasPermission('admin')">
<a href="/users/new" class="btn btn-default"><i class="fa fa-plus"></i> New User</a>
<a href="users/new" class="btn btn-default"><i class="fa fa-plus"></i> New User</a>
</p>
<smart-table rows="users" columns="gridColumns"

View File

@@ -1,8 +1,5 @@
<div class="container">
<ol class="breadcrumb">
<li><a href="/users">Users</a></li>
<li class="active">New</li>
</ol>
<users-nav></users-nav>
<form class="form" name="userForm" ng-submit="saveUser()" novalidate>
<div class="form-group required" show-errors>
@@ -26,7 +23,7 @@
<span class="help-block error" ng-if="userForm.passwordRepeat.$error.compareTo">Passwords don't match.</span>
</div>
<div class="form-group">
<button class="btn btn-primary">Save</button>
<button class="btn btn-primary">Create</button>
</div>
</form>
</div>

View File

@@ -1,9 +1,7 @@
<div class="container">
<ol class="breadcrumb">
<li ng-if="currentUser.hasPermission('list_users')"><a href="/users">Users</a></li>
<li ng-if="!currentUser.hasPermission('list_users')">Users</li>
<li class="active">{{user.name}}</li>
</ol>
<users-nav></users-nav>
<h2>{{user.name}}</h2>
<tabset>
<tab heading="Profile" active="tabs['profile']" select="setTab('profile')">

View File

@@ -0,0 +1,34 @@
<div class="container" id="widget">
<div class="row">
<div class="col-lg-12" ng-controller='EmbeddedVisualizationCtrl'>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
<p>
<visualization-name visualization="visualization"/>
</p>
<div class="text-muted" ng-bind-html="query.description | markdown"></div>
</h3>
</div>
<visualization-renderer visualization="visualization" query-result="queryResult">
</visualization-renderer>
<div class="panel-footer">
<span class="label label-default">Updated: <span am-time-ago="queryResult.getUpdatedAt()"></span></span>
<span class="pull-right">
<a class="btn btn-default btn-xs" ng-href="queries/{{query.id}}#{{widget.visualization.id}}" target="_blank"><span class="glyphicon glyphicon-link"></span></a>
</span>
<span class="pull-right">
<a class="btn btn-default btn-xs" ng-disabled="!queryResult.getData()" query-result-link target="_self">
<span class="glyphicon glyphicon-cloud-download"></span>
</a>
</span>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,3 @@
<div style="max-height: 300px;">
<plotly-chart options='plotlyOptions' series='chartSeries' min-height="300"></plotly-chart>
</div>

View File

@@ -1,169 +1,246 @@
<div class="form-horizontal">
<div class="panel panel-default">
<form class="form-horizontal" name="chartEditor">
<div class="row">
<div class="col-md-6">
<div class="form-group row">
<label class="control-label col-sm-5">Chart Type</label>
<div class="col-sm-7" ng-if="chartTypes"><!--the if is a weird workaround-->
<ui-select ng-model="options.globalSeriesType" on-select="chartTypeChanged()">
<ui-select-match placeholder="Choose chart type..."><i class="fa fa-{{$select.selected.value.icon}}"></i> {{$select.selected.value.name}}</ui-select-match>
<ui-select-choices repeat="info.chartType as (chartType, info) in chartTypes">
<div><i class="fa fa-{{info.value.icon}}"></i><span> </span><span ng-bind-html="info.value.name | highlight: $select.search"></span></div>
</ui-select-choices>
</ui-select>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group row">
<label class="control-label col-sm-5">Stacking</label>
<div class="col-sm-7" ng-if="stackingOptions"><!--the if is a weird workaround-->
<ui-select ng-model="options.series.stacking" ng-disabled="['line', 'area', 'column'].indexOf(options.globalSeriesType) == -1">
<ui-select-match placeholder="Choose Stacking...">{{$select.selected.key | capitalize}}</ui-select-match>
<ui-select-choices repeat="value.value as (key, value) in stackingOptions">
<div ng-bind-html="value.key | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group row" ng-class="{'has-error': chartEditor.xAxisColumn.$invalid}">
<label class="control-label col-sm-5">X Column</label>
<div class="col-sm-7">
<ui-select name="xAxisColumn" required ng-model="form.xAxisColumn">
<ui-select-match placeholder="Choose column...">{{$select.selected}}</ui-select-match>
<ui-select-choices repeat="column in columnNames | remove:form.yAxisColumns | remove:form.groupby">
<span ng-bind-html="column | highlight: $select.search"></span><span> </span><small class="text-muted" ng-bind="columns[column].type"></small>
</ui-select-choices>
</ui-select>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group row">
<label class="control-label col-sm-5">Group by</label>
<div class="col-sm-7">
<ui-select name="groupby" ng-model="form.groupby" class="clearable">
<ui-select-match allow-clear="true" placeholder="Choose column...">{{$select.selected}}</ui-select-match>
<ui-select-choices repeat="column in columnNames | remove:form.yAxisColumns | remove:form.xAxisColumn">
<span ng-bind-html="column | highlight: $select.search"></span><span> </span><small class="text-muted" ng-bind="columns[column].type"></small>
</ui-select-choices>
</ui-select>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<!-- not using regular validation (chartEditor.yAxisColumns.$invalid) due to a bug in ui-select with multiple choices-->
<div class="form-group row" ng-class="{'has-error': !form.yAxisColumns || form.yAxisColumns.length == 0}">
<label class="control-label col-sm-5">Y Columns</label>
<div class="col-sm-7">
<ui-select multiple name="yAxisColumns" required ng-model="form.yAxisColumns">
<ui-select-match placeholder="Choose columns...">{{$item}}</ui-select-match>
<ui-select-choices repeat="column in columnNames | remove:form.groupby | remove:form.xAxisColumn">
<span ng-bind-html="column | highlight: $select.search"></span><span> </span><small class="text-muted" ng-bind="columns[column].type"></small>
</ui-select-choices>
</ui-select>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group row">
<label class="control-label col-sm-5">Legend</label>
<div class="col-sm-7">
<input type="checkbox" ng-model="options.legend.enabled">
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">X Axis</h3>
</div>
<div class="panel-body">
<div class="form-group">
<label class="control-label col-sm-2">Stacking</label>
<div class="row">
<div class="col-sm-10">
<select required ng-model="stacking"
ng-options="value as key for (key, value) in stackingOptions"
class="form-control"></select>
<div class="col-md-6">
<div class="form-group row">
<label class="control-label col-sm-3">Scale</label>
<div class="col-sm-9">
<ui-select ng-model="options.xAxis.type">
<ui-select-match placeholder="Choose Scale...">{{$select.selected | capitalize}}</ui-select-match>
<ui-select-choices repeat="scaleType in xAxisScales">
<div ng-bind-html="scaleType | capitalize | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">X Axis Type</label>
<div class="col-sm-10">
<select required ng-model="xAxisType" ng-options="value as key for (key, value) in xAxisOptions"
class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">Y Axis Type</label>
<div class="col-sm-10">
<select required ng-model="yAxisType" ng-options="value as key for (key, value) in yAxisOptions"
class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">Series Type</label>
<div class="col-sm-10">
<select required ng-options="value as key for (key, value) in seriesTypes"
ng-model="globalSeriesType" class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">y Axis min</label>
<div class="col-sm-10">
<input name="yAxisMin" type="number" class="form-control"
ng-model="visualization.options.yAxis.min"
placeholder="Auto">
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">y Axis max</label>
<div class="col-sm-10">
<input name="yAxisMax" type="number" class="form-control"
ng-model="visualization.options.yAxis.max"
placeholder="Auto">
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">y Axis Name</label>
<div class="col-sm-10">
<input name="yAxisName" type="text" class="form-control"
ng-model="visualization.options.yAxis.title.text"
placeholder="">
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">Sort X Values</label>
<div class="col-sm-10">
<input name="sortX" type="checkbox" class="form-control"
ng-model="visualization.options.sortX">
<div class="col-md-6">
<div class="form-group row">
<label class="control-label col-sm-8">Sort Values</label>
<div class="col-sm-4">
<input type="checkbox" ng-model="options.sortX">
</div>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">Show X Axis Labels</label>
<div class="col-sm-10">
<input name="sortX" type="checkbox" class="form-control"
ng-model="visualization.options.xAxis.labels.enabled">
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group row">
<label class="control-label col-sm-3">Name</label>
<div class="col-sm-9">
<input ng-model="options.xAxis.title.text" type="text" class="form-control"></input>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group row">
<label class="control-label col-sm-8">Show Labels</label>
<div class="col-sm-4">
<input type="checkbox" ng-model="options.xAxis.labels.enabled">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div ng-repeat="yAxis in options.yAxis" class="col-md-3">
<div class="panel panel-default">
<div class="row">
<div class="col-lg-6">
<div class="list-group">
<div class="list-group-item active">
Columns Mapping
</div>
<div class="list-group-item">
<div class="form-group" ng-repeat="column in columns">
<label class="control-label col-sm-4">{{column.name}}</label>
<div class="col-sm-8">
<select ng-options="value as key for (key, value) in columnTypes" class="form-control"
ng-model="columnTypeSelection[column.name]"></select>
</div>
</div>
</div>
</div>
<div class="panel-heading">
<h3 class="panel-title">{{$index == 0 ? 'Left' : 'Right'}} Y Axis</h3>
</div>
<div class="col-lg-6" ng-if="series.length > 0">
<div class="list-group" ng-repeat="seriesName in series">
<div class="list-group-item active">
{{seriesName}}
</div>
<div class="list-group-item">
<div class="form-group">
<label class="control-label col-sm-3">Type</label>
<div class="col-sm-9">
<select required ng-model="visualization.options.seriesOptions[seriesName].type"
ng-options="value as key for (key, value) in seriesTypes"
class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3">zIndex</label>
<div class="col-sm-9">
<select required ng-model="visualization.options.seriesOptions[seriesName].zIndex"
ng-options="o as o for o in zIndexes"
class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3">Index</label>
<div class="col-sm-9">
<select required ng-model="visualization.options.seriesOptions[seriesName].index"
ng-options="o as o for o in zIndexes"
class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3">y Axis</label>
<div class="col-sm-9">
<select required ng-model="visualization.options.seriesOptions[seriesName].yAxis"
ng-options="o[0] as o[1] for o in yAxes"
class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3">Name</label>
<div class="col-sm-9">
<input name="seriesName" type="text" class="form-control"
ng-model="visualization.options.seriesOptions[seriesName].name"
placeholder="{{seriesName}}">
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3">Color</label>
<div class="col-sm-9">
<select class="form-control" ng-model="visualization.options.seriesOptions[seriesName].color" ng-options="val as key for (key,val) in palette"></select>
</div>
</div>
</div>
<div class="panel-body">
<div class="form-group row">
<label class="control-label col-sm-3">Scale</label>
<div class="col-sm-9">
<ui-select ng-model="yAxis.type">
<ui-select-match placeholder="Choose Scale...">{{$select.selected | capitalize}}</ui-select-match>
<ui-select-choices repeat="scaleType in yAxisScales">
<div ng-bind-html="scaleType | capitalize | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
</div>
</div>
<div class="form-group row">
<label class="control-label col-sm-3">Name</label>
<div class="col-sm-9">
<input ng-model="yAxis.title.text" type="text" class="form-control"></input>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title">Series Options</h3></div>
<div>
<table class="table table-condensed col-table">
<thead>
<th>zIndex</th>
<th>Column</th>
<th>Left Y Axis</th>
<th>Right Y Axis</th>
<th>Label</th>
<th>Color</th>
<th>Type</th>
</thead>
<tbody ui-sortable ng-model="form.seriesList">
<tr ng-repeat="name in form.seriesList">
<td style="cursor: move;"><i class="fa fa-arrows-v"></i> <span ng-bind="options.seriesOptions[name].zIndex + 1"></span></td>
<td>{{name}}</td>
<td>
<input type="radio" ng-value="0" ng-model="options.seriesOptions[name].yAxis">
</td>
<td>
<input type="radio" ng-value="1" ng-model="options.seriesOptions[name].yAxis">
</td>
<td style="padding: 3px; width: 140px;">
<input placeholder="{{name}}" class="form-control input-sm super-small-input" type="text" ng-model="options.seriesOptions[name].name">
</td>
<td style="padding: 3px; width: 35px;">
<ui-select ng-model="options.seriesOptions[name].color">
<ui-select-match><color-box color="$select.selected.value"></color-box></ui-select-match>
<ui-select-choices repeat="color.value as (key, color) in colors">
<color-box color="color.value"></color-box><span ng-bind-html="color.key | capitalize | highlight: $select.search"></span>
</ui-select-choices>
</ui-select>
</td>
<td style="padding: 3px; width: 105px;">
<ui-select ng-model="options.seriesOptions[name].type">
<ui-select-match placeholder="Chart Type"><i class="fa fa-{{$select.selected.value.icon}}"></i> {{$select.selected.value.name}}</ui-select-match>
<ui-select-choices repeat="info.chartType as (chartType, info) in chartTypes">
<div><i class="fa fa-{{info.value.icon}}"></i><span> </span><span ng-bind-html="info.value.name | highlight: $select.search"></span></div>
</ui-select-choices>
</ui-select>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</form>

View File

@@ -0,0 +1,8 @@
<div>
<span>
From <input type="date" ng-model="dateRangeHuman.min">
</span>
<span>
To <input type="date" ng-model="dateRangeHuman.max">
</span>
</div>

View File

@@ -1,25 +1,42 @@
<div>
<span ng-click="openEditor=!openEditor" class="details-toggle" ng-class="{open: openEditor}">Edit</span>
<form ng-show="openEditor" role="form" name="visForm" ng-submit="submit()">
<div class="form-group">
<label class="control-label">Name</label>
<input name="name" type="text" class="form-control" ng-model="visualization.name" placeholder="{{visualization.type | capitalize}}">
<form ng-show="openEditor" role="form" name="visForm" ng-submit="submit()" class="form-horizontal">
<div class="row">
<div class="col-md-6">
<div class="form-group row">
<label class="control-label col-sm-5">Visualization Type</label>
<div class="col-sm-7">
<select required ng-model="visualization.type" ng-options="value as key for (key, value) in visTypes" class="form-control" ng-change="typeChanged()"></select>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group row">
<label class="control-label col-sm-5">Name</label>
<div class="col-sm-7">
<input name="name" type="text" class="form-control" ng-model="visualization.name" placeholder="{{visualization.type | capitalize}}">
</div>
</div>
</div>
</div>
<div class="form-group">
<label class="control-label">Visualization Type</label>
<select required ng-model="visualization.type" ng-options="value as key for (key, value) in visTypes" class="form-control" ng-change="typeChanged()"></select>
<div class="row">
<div class="col-md-12">
<visualization-options-editor></visualization-options-editor>
</div>
</div>
<visualization-options-editor></visualization-options-editor>
<div class="form-group" ng-if="editRawOptions">
<label class="control-label">Advanced</label>
<textarea json-text ng-model="visualization.options" class="form-control" rows="10"></textarea>
</div>
<div class="form-group">
<div class="form-group text-center">
<button type="submit" class="btn btn-primary">Save</button>
</div>

View File

@@ -2,16 +2,16 @@
<div class="filter" ng-repeat="filter in filters">
<ui-select ng-model="filter.current" ng-if="!filter.multiple">
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{filter.friendlyName}}: {{$select.selected}}</ui-select-match>
<ui-select-choices repeat="value in filter.values | filter: $select.search track by $index">
<ui-select-choices repeat="value in filter.values | filter: $select.search">
{{value}}
</ui-select-choices>
</ui-select>
<ui-select ng-model="filter.current" multiple ng-if="filter.multiple">
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{filter.friendlyName}}: {{$item}}</ui-select-match>
<ui-select-choices repeat="value in filter.values | filter: $select.search track by $index">
<ui-select-choices repeat="value in filter.values | filter: $select.search">
{{value}}
</ui-select-choices>
</ui-select>
</div>
</div>
</div>

View File

@@ -10,19 +10,17 @@
"jquery": "1.9.1",
"bootstrap": "3.0.0",
"es5-shim": "2.0.8",
"angular-moment": "0.2.0",
"moment": "2.1.0",
"angular-moment": "0.10.3",
"moment": "~2.8.0",
"codemirror": "4.8.0",
"highcharts": "3.0.10",
"underscore": "1.5.1",
"pivottable": "1.6.3",
"pivottable": "~1.1.1",
"cornelius": "https://github.com/restorando/cornelius.git",
"gridster": "0.2.0",
"mousetrap": "~1.4.6",
"jquery-ui": "~1.10.4",
"underscore.string": "~2.3.3",
"marked": "~0.3.2",
"bucky": "~0.2.6",
"pace": "~0.5.1",
"font-awesome": "~4.2.0",
"mustache": "~1.0.0",
@@ -30,10 +28,13 @@
"angular-ui-bootstrap-bower": "~0.12.1",
"leaflet": "~0.7.3",
"angular-base64-upload": "~0.1.11",
"angular-ui-select": "0.8.2",
"angular-ui-select": "~0.13.2",
"angular-bootstrap-show-errors": "~2.3.0",
"angular-sanitize": "1.2.18",
"d3": "3.5.6"
"d3": "3.5.6",
"angular-ui-sortable": "~0.13.4",
"angular-plotly": "~0.1.2",
"plotly": "~0.0.2"
},
"devDependencies": {
"angular-mocks": "1.2.18",

View File

@@ -29,13 +29,16 @@
"grunt-karma": "~0.8.3",
"karma-phantomjs-launcher": "~0.1.4",
"karma": "~0.12.19",
"karma-ng-html2js-preprocessor": "~0.1.0"
"karma-ng-html2js-preprocessor": "~0.1.0",
"bower": "~1.7.1",
"grunt-cli": "~0.1.13"
},
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"test": "grunt test",
"build": "grunt build",
"bower": "bower"
}
}

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