Compare commits

..

627 Commits

Author SHA1 Message Date
Arik Fraimovich
ca39892c64 Update release date of 0.12.0. 2016-11-20 10:57:40 +02:00
Arik Fraimovich
2d7a497073 Merge pull request #1398 from getredash/012patches
Fix: missing images after minification
2016-11-16 08:58:47 +02:00
Arik Fraimovich
788c16ce37 Fix #1389: missing images after minification 2016-11-16 08:53:16 +02:00
Arik Fraimovich
2de4aa2a0c Merge pull request #1395 from getredash/012patches
Change: switch to requests in URL query runner
2016-11-15 18:23:06 +02:00
Arik Fraimovich
10cbb7fd52 Change: switch to requests in URL query runner 2016-11-15 18:19:44 +02:00
Arik Fraimovich
0cbbe7095d Merge pull request #1394 from getredash/012patches
Fix: conflict handling in dashboard widget editing was wrong
2016-11-15 16:05:30 +02:00
Arik Fraimovich
c88dafa4ad Fix: update version after deleting a widget 2016-11-15 15:57:34 +02:00
Arik Fraimovich
48a79fe996 Fix #1383: dashboard co-editors shuould be able to edit widgets 2016-11-15 15:53:06 +02:00
Arik Fraimovich
b70a24f6b4 Fix 1388: Wrong detection of conflict after adding a widget to the dashboard 2016-11-15 15:52:36 +02:00
Arik Fraimovich
d1b82694a6 Fix author name on #1113. 2016-11-15 15:51:46 +02:00
Arik Fraimovich
db1a941459 Merge pull request #1392 from getredash/012patches
Update documentation links to point at the new location.
2016-11-15 14:04:56 +02:00
Arik Fraimovich
e22706692a Remove old documentation. 👋 2016-11-15 14:01:15 +02:00
Arik Fraimovich
c06aeae84f Update references to documentation to point at the new one 2016-11-15 13:56:55 +02:00
Arik Fraimovich
e0617d9ad7 Merge pull request #1243 from kassyuz/patch-1
Create proxy_pass.rst
2016-11-15 13:17:54 +02:00
Arik Fraimovich
29a2fb1931 Merge pull request #1382 from getredash/changelog
Add: a changelog
2016-11-10 10:06:16 +02:00
Arik Fraimovich
e21799a754 Add a changelog 📜 💥 2016-11-10 10:05:09 +02:00
Arik Fraimovich
d6febb0cb4 Merge pull request #1375 from moonami/issue-1247
Fix: Download Dataset does not work when not logged in
2016-11-07 10:38:38 +02:00
Arik Fraimovich
61fe16e18e Merge pull request #1374 from washort/cors-star
Add: allow '*' in REDASH_CORS_ACCESS_CONTROL_ALLOW_ORIGIN
2016-11-05 17:46:55 +02:00
Joshua Dechant
2184f53277 JS vars should be camelCase 2016-11-04 23:46:54 -04:00
Joshua Dechant
733f245e36 This fixes #1247 2016-11-04 12:40:53 -04:00
Allen Short
6c4294b64d Allow '*' in REDASH_CORS_ACCESS_CONTROL_ALLOW_ORIGIN 2016-11-02 16:31:58 -05:00
Arik Fraimovich
2f090435a5 Merge pull request #1113 from whummer/feat/share-access-permissions
Add: share modify/access permissions for queries and dashboard
2016-10-28 19:11:03 +03:00
Arik Fraimovich
d9bad96e8e Remove MQL tests 2016-10-28 18:16:54 +03:00
Arik Fraimovich
b7a5d95bb8 Add JIRA connection test 2016-10-28 18:13:20 +03:00
Arik Fraimovich
ab85e43e58 Fix graphite test connection method 2016-10-28 17:50:26 +03:00
Arik Fraimovich
96553ad942 Merge pull request #1369 from someones/es_test_fix
Fix: missing format call in Elasticsearch test method
2016-10-28 09:14:10 +03:00
Adam Griffiths
c1847fbc12 Fix missing format call 2016-10-28 15:44:59 +11:00
Arik Fraimovich
dc345aa363 Fix: bring back correct timeout value 2016-10-27 19:34:00 +03:00
Arik Fraimovich
002f794f2a Merge pull request #1368 from getredash/rand
Change: added ability to disable auto update in admin views
2016-10-27 19:32:16 +03:00
Arik Fraimovich
d2c64c6da2 Ability to disable auto update in admin views 2016-10-27 19:28:43 +03:00
Arik Fraimovich
e2595e7540 Show queues data sources in a pop over 2016-10-27 19:21:42 +03:00
Arik Fraimovich
aa5d14ed02 Merge pull request #1366 from deecay/master
Change: improve error message for exception in the Python query runner
2016-10-27 09:35:00 +03:00
deecay
4ba7aa1fc0 Improve error message for exception. 2016-10-27 11:16:04 +09:00
Arik Fraimovich
fd9dc4b4e8 Update controller/view name 2016-10-26 22:17:17 +03:00
Arik Fraimovich
c57c765688 Merge pull request #1365 from washort/tz-fix
Fix: compare retrieval times in UTC timezone
2016-10-26 21:47:27 +03:00
Arik Fraimovich
52b87efb73 Add feature flag for the permissions control feature 2016-10-26 21:40:51 +03:00
Allen Short
36d01a2029 Compare retrieval times in UTC timezone 2016-10-26 10:18:33 -05:00
Arik Fraimovich
2592959550 Use version for partial query updates 2016-10-26 16:28:39 +03:00
Arik Fraimovich
6c5dd09a78 Add change tracking and fix tests 2016-10-26 16:09:55 +03:00
Arik Fraimovich
9cb9bdb515 Record events for permission changes 2016-10-26 11:47:57 +03:00
Arik Fraimovich
df17759ab4 Fix tests 2016-10-26 11:47:40 +03:00
Arik Fraimovich
028393b229 Return dashboard permission status with dashboard object 2016-10-26 11:22:51 +03:00
Arik Fraimovich
8245a667ef Return permission state with query object 2016-10-26 11:17:14 +03:00
Arik Fraimovich
6218421266 Tests for the permissions API (and rewrite) 2016-10-26 10:52:24 +03:00
Arik Fraimovich
f34471ec10 Return 409 when dashboard can't be updated due to conflict 2016-10-25 11:58:43 +03:00
Arik Fraimovich
40cc592591 Make sure error are logged in tests 2016-10-25 11:58:25 +03:00
Arik Fraimovich
bb96702ae6 Update dashboard testS 2016-10-25 10:45:00 +03:00
Arik Fraimovich
8b091129ed Use new helper for dashboards API 2016-10-24 21:33:49 +03:00
Arik Fraimovich
edea6f3a05 WIP:
- Move version/change tracking logic to mixins (the change mixin is still WIP).
- Tests for queries update API.
2016-10-24 16:58:30 +03:00
Arik Fraimovich
c51477ac93 Add tests outline 2016-10-24 13:56:39 +03:00
Arik Fraimovich
9f3bbfee13 Renames 2016-10-24 13:56:31 +03:00
Arik Fraimovich
7ba5a2062a Remove unused method 2016-10-24 13:37:32 +03:00
Arik Fraimovich
00a77f8d3a snake_case to camelCase 2016-10-24 12:57:54 +03:00
Arik Fraimovich
b9ab9135d0 Apply review to models code 2016-10-24 12:57:30 +03:00
Arik Fraimovich
19e5a0af86 Naming and indentation fixes 2016-10-24 12:40:17 +03:00
Arik Fraimovich
b748eb14f4 "Simplify" migration code 2016-10-24 12:35:52 +03:00
Waldemar Hummer
60a79cbe08 address code review comments 2016-10-24 12:33:29 +03:00
Waldemar Hummer
c0c4f453f2 fix handling of latest_version in query view 2016-10-24 12:33:29 +03:00
Waldemar Hummer
e0672f4c4d add optimistic locking for dashboard editing 2016-10-24 12:33:29 +03:00
Waldemar Hummer
6b540e03fc check for shared permissions in dashboard UI 2016-10-24 12:33:29 +03:00
Tatyana Tychshenko
95dca53b1e Finalised UI for sharing permissions 2016-10-24 12:33:29 +03:00
Waldemar Hummer
91a46ea1bb add optimistic locking for concurrent changes to queries by different users 2016-10-24 12:33:29 +03:00
Waldemar Hummer
903ba0c1e0 add backend API and tests for managing access permissions. 2016-10-24 12:33:29 +03:00
Tatyana Tychshenko
2a688200be UI for sharing edit permissions 2016-10-24 12:33:29 +03:00
Arik Fraimovich
37dff5f0a4 Merge pull request #1360 from shinji19/hotfix/mysql_connection_test
Fix: connection test was broken for MySQL
2016-10-24 12:09:07 +03:00
ichihara
6397b8ca1f fix variable name 2016-10-24 18:01:46 +09:00
Arik Fraimovich
360028c01f Merge pull request #1359 from laughingman7743/fix_schema_loading_query
Fix: schema loading query for Hive was wrong for non default schema
2016-10-23 16:12:42 +03:00
laughingman7743
6a42daffe2 Fix schema loading query for Hive query runner 2016-10-22 10:06:18 +09:00
Arik Fraimovich
7ee41d41b5 Merge pull request #1358 from getredash/add_user_id_to_run_query
Fix: make sure all calls to run_query updated with new parameter
2016-10-21 21:54:03 +03:00
Arik Fraimovich
c138d0592a Fix: make sure all calls to run_query updated with new parameter 2016-10-21 19:15:50 +03:00
Arik Fraimovich
3db0eea921 Merge pull request #1341 from zoetrope/specify-nameid-format-in-saml
Add: support for specifying SAML nameid-format
2016-10-21 08:17:11 +03:00
zoetrope
9ce211bf09 add environment variable name in doc 2016-10-21 13:58:27 +09:00
zoetrope
5610ce1721 specify nameid-format in SAML authentication 2016-10-21 13:58:12 +09:00
zoetrope
67528eeb73 Merge remote-tracking branch 'upstream/master' 2016-10-21 13:09:43 +09:00
Arik Fraimovich
880627c69c Merge pull request #1355 from getredash/add_user_id_to_run_query
Change: pass the user object to the run_query method
2016-10-21 00:06:05 +03:00
Arik Fraimovich
ae2cd5363f Change: pass the user object to the run_query method
This to allow different use cases like checking permissions
based on who is currently running the query.
2016-10-20 23:42:25 +03:00
Arik Fraimovich
b0ecd0e9a0 Merge pull request #1343 from zoetrope/use-local-metadata-in-saml
Add: support for local SAML metadata file
2016-10-20 15:39:34 +03:00
Arik Fraimovich
23c605b149 Merge pull request #1342 from zoetrope/add-entity-id-in-saml
SAML: specify entity id
2016-10-20 15:38:39 +03:00
Arik Fraimovich
464b8368bf Merge pull request #1301 from washort/connection-test
Add: "test connection" button for data sources
2016-10-20 10:21:31 +03:00
Arik Fraimovich
cb0ea7b63e Show spinner while testing connection 2016-10-20 10:11:16 +03:00
Arik Fraimovich
ef07388d2a Allow testing connection only after saving 2016-10-20 10:11:16 +03:00
Arik Fraimovich
8464d8c64a Fix the way we were calling the test API -
Previously it was overriding the content of $scope.dataSource and
resulting in a broken form.

Also updated the messages.
2016-10-20 10:11:16 +03:00
Arik Fraimovich
37a02bfe37 Make the DataSourceTest api more consistent with other APIs. 2016-10-20 10:11:16 +03:00
Arik Fraimovich
41f7791c87 Make the DataSourceList#get call more robust 2016-10-20 10:10:42 +03:00
Arik Fraimovich
9041ccabd3 Change: update Raven version (5.27.0 had a bug) 2016-10-20 10:10:42 +03:00
Arik Fraimovich
d4a1a5b239 Update CSS files in embed.html 2016-10-20 10:10:42 +03:00
Arik Fraimovich
cf7ed8fae7 Change: catch all exceptions from query runners 2016-10-20 10:10:42 +03:00
Arik Fraimovich
f63d43c3cf *Don't* anotate MSSQL queries.. 2016-10-20 10:10:42 +03:00
Arik Fraimovich
aec38614c0 Change: make visualization embeds more printer friendly 2016-10-20 10:10:42 +03:00
Arik Fraimovich
459a25bedd Revert settings screen menu to simple mode to allow extending it 2016-10-20 10:10:42 +03:00
Arik Fraimovich
14e024bca8 Add: allow changing alert email subject.
Fixes #1144.
2016-10-20 10:10:42 +03:00
Arik Fraimovich
fc8985f689 Change: allow specifying recipient address when sending email test message.
Closes #1224.
2016-10-20 10:10:42 +03:00
Arik Fraimovich
cecc1a9462 Fix: 0 is falsy, so need to check differently if there is value defined. 2016-10-20 10:10:42 +03:00
Arik Fraimovich
fe6497dfe7 Add: control over y axis min/max values 2016-10-20 10:10:42 +03:00
Arik Fraimovich
79df2b8d22 Add option to change the map tile layer 2016-10-20 10:10:42 +03:00
Arik Fraimovich
ef6a543850 Use group color for the marker-cluster 2016-10-20 10:10:42 +03:00
Arik Fraimovich
c17a6956dc Change: upgrade Python packages
- pytz
- funcy
- raven (Sentry client)
- xlsxwriter
2016-10-20 10:10:42 +03:00
Arik Fraimovich
6775f01684 Celery: Upgrade Celery to more recent version.
This might help with worker stopping to take new tasks after some time.

Closes #1258.
2016-10-20 10:10:42 +03:00
Arik Fraimovich
3a4754303d Change: upgrade Requests to latest version.
Closes #643.
Related #1258.
2016-10-20 10:10:42 +03:00
Arik Fraimovich
f9824675f1 Fix: Redis memory leak.
We were appending new QueryTaskTracker ids to query_task_trackers sorted set
but we were never removing them...

Also removed the migration to create index on org_id as it created by default
by Peewee for foreign keys.
2016-10-20 10:10:42 +03:00
Arik Fraimovich
42ae78a017 Add: support for snapshot generation service 2016-10-20 10:10:42 +03:00
Arik Fraimovich
f56cbf051c Fix: queries API was doing N+1 queries in most cases 2016-10-20 10:10:42 +03:00
Arik Fraimovich
ef80fb1d1a Change: add more logging and information for refresh schemas task 2016-10-20 10:10:42 +03:00
Arik Fraimovich
b92f22c36e Add: collect runtime metrics for Celery tasks 2016-10-20 10:10:42 +03:00
Arik Fraimovich
fde0ba1503 Add: feature toggle to control if to report query results count 2016-10-20 10:10:42 +03:00
Arik Fraimovich
c8b62755d0 Change: remove deprecated settings 2016-10-20 10:10:42 +03:00
Arik Fraimovich
7de2d6c101 Only show BigQuery GCE option when on a GCE instance 2016-10-20 10:10:42 +03:00
Arik Fraimovich
8cc4e2bee7 Add: support for loading BigQuery schema 2016-10-20 10:10:42 +03:00
Arik Fraimovich
7b0f5a195e Add: support MongoDB SSL connections 2016-10-20 10:10:42 +03:00
Arik Fraimovich
0d8ee9ced7 Change: more flexible column width calculation 2016-10-20 10:10:42 +03:00
Arik Fraimovich
838c211198 Add: additional configuration for Celery jobs 2016-10-20 10:10:42 +03:00
Arik Fraimovich
9c3baed230 Fix: BoxPlot visualization wasn't rendering on a dashboard 2016-10-20 10:10:42 +03:00
Arik Fraimovich
435b49fa9c Change: don't annotate MSSQL queries.
(I got tired of chasing weird unicode issues)
2016-10-20 10:10:42 +03:00
Arik Fraimovich
707df82b40 Add: support for date/time with seconds parameters 2016-10-20 10:10:42 +03:00
Arik Fraimovich
8116c6140f Fix: properly render checkboxes in dynamic forms 2016-10-20 10:10:42 +03:00
Arik Fraimovich
34543e67f7 Fix: support for Unicode columns name in Google Spreadsheets 2016-10-20 10:10:42 +03:00
Arik Fraimovich
afe5cae2a9 Add: API to force refresh data source schema 2016-10-20 10:10:42 +03:00
Arik Fraimovich
94a0bddb3d Typo fix in dashboard page. 2016-10-20 10:10:42 +03:00
Kazuhito Hokamura
9786063dbb Add a comment 2016-10-20 10:10:42 +03:00
Kazuhito Hokamura
025e9d2710 Limit the editing functionality to only the text 2016-10-20 10:10:42 +03:00
Arik Fraimovich
a9562d361f Change: paginate query page & add explicit urls.
- Paginate the queries API result.
- Split the API to /api/queries (all queries) and /api/queries/my which returns
  a user's queries (or drafts).
- In the interface have explicit URLs for all queries (/queries), my queries (/queries/my)
  and drafts (/queries/drafts).
2016-10-20 10:10:42 +03:00
Kazuhito Hokamura
97ad716d5a Make it possible to edit a TextBox 2016-10-20 10:10:42 +03:00
Arik Fraimovich
95367abc91 Add JQL to default enabled query runners. 2016-10-20 10:10:42 +03:00
Tsuyoshi Tatsukawa
f7af1fa82a add Content-Type to JSON response of QueryResultResource 2016-10-20 10:10:42 +03:00
Arik Fraimovich
5321948e46 Add documentation on JIRA datasource 2016-10-20 10:10:42 +03:00
Arik Fraimovich
df437999ca Add JIRA query runner 2016-10-20 10:10:42 +03:00
Arik Fraimovich
f4b87e76a3 Change: update Slack configuration titles. 2016-10-20 10:10:42 +03:00
Arik Fraimovich
f0d0d60dc1 Change: don't annotate BigQuery queries
It's not really useful anyway and breaks support for `#StandardSQL` directive.
2016-10-20 10:10:42 +03:00
Arik Fraimovich
26bd08bb2b Add markers cluster support & cleanup/refactor code. 2016-10-20 10:10:42 +03:00
Hirotaka Suzuki
e5146c3755 Add REDASH_FEATURE_DISABLE_REFRESH_QUERIES setting 2016-10-20 10:10:42 +03:00
IllusiveMilkman
f12d47752c Update bootstrap.sh
Check for Ubuntu 16.04 before upgrading pip.
2016-10-20 10:10:42 +03:00
Arik Fraimovich
7683402741 Move around some functions and add support for window resize event. 2016-10-20 10:10:42 +03:00
Arik Fraimovich
ba354ce65a Remove unnecessary code. 2016-10-20 10:10:42 +03:00
Arik Fraimovich
f892a3c70a Fix: use key_as_string when available. 2016-10-20 10:10:42 +03:00
IllusiveMilkman
cc1dae8eed Update bootstrap.sh
Testing script on clean install of Ubuntu Desktop 16.04.1.

pip fails if not upgraded beforehand, whether "sudo" or "sudo -s" or "sudo -sH" is run.

After this modification it works perfectly from a clean install.  Below are my steps if anyone should want to replicate or validate:
1. Clean install of Ubuntu 16.04.1 Desktop (on VM)
2. sudo apt-get -y install git
3. mkdir ~/git
4. cd ~/git
5. git clone https://github.com/getredash/redash
6. cd ~/git/redash/setup/ubuntu
7. chmod +x bootstrap.sh
8. nano bootstrap.sh
9. added "pip install --upgrade pip" to the script above the "pip install -U..." line.
10. Saved script.
11. sudo -H ./bootstrap.sh

Note that the "-H" is necessary in order to run the script successfully, otherwise you will run into pip ownership issues.
2016-10-20 10:10:42 +03:00
Arik Fraimovich
0436c3b5b7 Use smaller Plot.ly build 2016-10-20 10:10:42 +03:00
Arik Fraimovich
e810b36496 Sepcify specific version of Plotly 2016-10-20 10:10:42 +03:00
Arik Fraimovich
50ece739d9 Remove non existing cache directory. 2016-10-20 09:16:08 +03:00
Arik Fraimovich
2d2df5c9e0 Try newer version of NodeJS for builds. 2016-10-20 08:56:19 +03:00
Allen Short
d54e9125d9 Connection-test UI 2016-10-19 12:03:28 -05:00
zoetrope
78bc42e65c add environment variable name in doc 2016-10-13 10:58:00 +09:00
zoetrope
186537d849 add environment variable name in doc 2016-10-13 10:57:13 +09:00
zoetrope
a729601dff specify entityid in AuthnRequest in SAML 2016-10-13 10:19:41 +09:00
zoetrope
07af792943 use local metadata in saml 2016-10-13 10:11:37 +09:00
Arik Fraimovich
c14d119fe7 Change: update Raven version (5.27.0 had a bug) 2016-10-10 18:08:15 +03:00
Arik Fraimovich
1c4225beff Update CSS files in embed.html 2016-10-09 22:57:34 +03:00
Arik Fraimovich
53b710ee7b Change: catch all exceptions from query runners 2016-10-09 22:52:28 +03:00
Arik Fraimovich
04398ff909 *Don't* anotate MSSQL queries.. 2016-10-09 13:50:00 +03:00
Arik Fraimovich
ce77f452c7 Change: make visualization embeds more printer friendly 2016-10-09 13:23:37 +03:00
Arik Fraimovich
7a855d1e0a Revert settings screen menu to simple mode to allow extending it 2016-10-09 13:15:14 +03:00
Arik Fraimovich
0235d37005 Merge pull request #1335 from getredash/add-yaxis-scales
Add: allow changing alert email subject.
2016-10-09 10:02:19 +03:00
Arik Fraimovich
5df4e7eb78 Add: allow changing alert email subject.
Fixes #1144.
2016-10-09 09:44:34 +03:00
Arik Fraimovich
015b1dc8fd Merge pull request #1334 from getredash/add-yaxis-scales
Change: allow specifying recipient address when sending email test message
2016-10-09 09:19:08 +03:00
Arik Fraimovich
8e9e288a1d Change: allow specifying recipient address when sending email test message.
Closes #1224.
2016-10-09 09:15:47 +03:00
Arik Fraimovich
2135dfd2e5 Merge pull request #1333 from getredash/add-yaxis-scales
Add: control over y axis min/max values
2016-10-09 09:14:58 +03:00
Arik Fraimovich
08676a3d0b Fix: 0 is falsy, so need to check differently if there is value defined. 2016-10-09 09:06:33 +03:00
Arik Fraimovich
1ac3119648 Add: control over y axis min/max values 2016-10-08 23:29:30 +03:00
Arik Fraimovich
39aaa2fd94 Merge pull request #1292 from getredash/add-cluster-marker
Change: improvements to map visualization
2016-10-08 15:37:51 +03:00
Arik Fraimovich
85fe74f3db Add option to change the map tile layer 2016-10-08 15:34:20 +03:00
Arik Fraimovich
7cbf350b73 Use group color for the marker-cluster 2016-10-08 11:59:19 +03:00
Arik Fraimovich
b22191b789 Merge pull request #1332 from getredash/small
Change: upgrade Python packages
2016-10-08 11:33:16 +03:00
Arik Fraimovich
23ba98bc94 Merge pull request #1331 from getredash/small
Celery: Upgrade Celery to more recent version.
2016-10-08 11:26:23 +03:00
Arik Fraimovich
66f8922d5b Change: upgrade Python packages
- pytz
- funcy
- raven (Sentry client)
- xlsxwriter
2016-10-08 11:25:54 +03:00
Arik Fraimovich
3283116518 Merge pull request #1330 from getredash/small
Change: upgrade Requests to latest version.
2016-10-08 11:10:09 +03:00
Arik Fraimovich
2565af604e Celery: Upgrade Celery to more recent version.
This might help with worker stopping to take new tasks after some time.

Closes #1258.
2016-10-08 11:08:28 +03:00
Arik Fraimovich
0d944794e4 Merge pull request #1329 from getredash/small
Fix: Redis memory leak.
2016-10-08 11:03:56 +03:00
Arik Fraimovich
7cc22c71a1 Change: upgrade Requests to latest version.
Closes #643.
Related #1258.
2016-10-08 11:03:43 +03:00
Arik Fraimovich
4d47583a94 Fix: Redis memory leak.
We were appending new QueryTaskTracker ids to query_task_trackers sorted set
but we were never removing them...

Also removed the migration to create index on org_id as it created by default
by Peewee for foreign keys.
2016-10-08 10:59:52 +03:00
Arik Fraimovich
49e788a1aa Merge pull request #1328 from getredash/small
Add: support for snapshot generation service
2016-10-07 17:34:10 +03:00
Arik Fraimovich
7145aa2086 Merge pull request #1325 from getredash/small
Fix: queries API was doing N+1 queries in most cases
2016-10-07 00:08:07 +03:00
Arik Fraimovich
d1a3ed312a Add: support for snapshot generation service 2016-10-07 00:07:54 +03:00
Arik Fraimovich
2db4b67505 Fix: queries API was doing N+1 queries in most cases 2016-10-07 00:00:41 +03:00
Arik Fraimovich
39091e006a Merge pull request #1324 from getredash/small
Change: add more logging and information for refresh schemas task
2016-10-06 22:20:20 +03:00
Arik Fraimovich
229ca6cb52 Merge pull request #1323 from getredash/small
Add: collect runtime metrics for Celery tasks
2016-10-06 15:54:58 +03:00
Arik Fraimovich
2ac64a7d08 Change: add more logging and information for refresh schemas task 2016-10-06 15:54:46 +03:00
Arik Fraimovich
00acaa214b Add: collect runtime metrics for Celery tasks 2016-10-06 15:30:43 +03:00
Arik Fraimovich
462faea52d Merge pull request #1316 from getredash/small
Change: remove deprecated settings
2016-10-05 17:22:02 +03:00
Arik Fraimovich
d6dd95db31 Add: feature toggle to control if to report query results count 2016-10-05 17:19:25 +03:00
Arik Fraimovich
a8fa68a563 Change: remove deprecated settings 2016-10-05 17:15:46 +03:00
Arik Fraimovich
931a1f3379 Merge pull request #1315 from getredash/small
Add: support for loading BigQuery schema
2016-10-05 17:15:13 +03:00
Arik Fraimovich
0952cf8178 Only show BigQuery GCE option when on a GCE instance 2016-10-05 17:07:01 +03:00
Arik Fraimovich
0eab12880f Add: support for loading BigQuery schema 2016-10-05 16:56:59 +03:00
Arik Fraimovich
39b4f9af22 Merge pull request #1314 from getredash/small
Add: support MongoDB SSL connections
2016-10-05 16:56:55 +03:00
Arik Fraimovich
1049d46a20 Add: support MongoDB SSL connections 2016-10-05 16:54:22 +03:00
Arik Fraimovich
73e1837469 Merge pull request #1313 from getredash/small
Change: more flexible column width calculation
2016-10-05 16:53:52 +03:00
Arik Fraimovich
ca1ca9b451 Merge pull request #1312 from getredash/small
Add: additional configuration for Celery jobs
2016-10-05 16:49:32 +03:00
Arik Fraimovich
fb30a8217c Change: more flexible column width calculation 2016-10-05 16:43:45 +03:00
Arik Fraimovich
30451bc0d9 Add: additional configuration for Celery jobs 2016-10-05 16:41:41 +03:00
Arik Fraimovich
cd2e9276fb Merge pull request #1311 from getredash/small
Fix: BoxPlot visualization wasn't rendering on a dashboard
2016-10-05 16:35:10 +03:00
Arik Fraimovich
fc00e61d49 Merge pull request #1310 from getredash/small
Add: support for date/time with seconds parameters
2016-10-05 16:34:32 +03:00
Arik Fraimovich
6a973f31b3 Fix: BoxPlot visualization wasn't rendering on a dashboard 2016-10-05 16:32:53 +03:00
Arik Fraimovich
a562ce748d Merge pull request #1309 from getredash/small
Fix: properly render checkboxes in dynamic forms
2016-10-05 16:32:20 +03:00
Arik Fraimovich
4462afc670 Change: don't annotate MSSQL queries.
(I got tired of chasing weird unicode issues)
2016-10-05 16:31:23 +03:00
Arik Fraimovich
ad5e4f46d6 Add: support for date/time with seconds parameters 2016-10-05 16:27:39 +03:00
Arik Fraimovich
d48192cb0f Merge pull request #1308 from getredash/small
Fix: support for Unicode columns name in Google Spreadsheets
2016-10-05 16:26:05 +03:00
Arik Fraimovich
1e85caa6c1 Fix: properly render checkboxes in dynamic forms 2016-10-05 16:25:34 +03:00
Arik Fraimovich
649e0bc53f Fix: support for Unicode columns name in Google Spreadsheets 2016-10-05 16:23:07 +03:00
Arik Fraimovich
d72a19894a Merge pull request #1307 from getredash/small
Add: API to force refresh data source schema
2016-10-05 16:20:45 +03:00
Arik Fraimovich
11a2b55c08 Add: API to force refresh data source schema 2016-10-05 16:17:48 +03:00
Arik Fraimovich
9cd9958827 Typo fix in dashboard page. 2016-10-05 16:14:19 +03:00
Arik Fraimovich
beb89ec657 Merge pull request #1305 from hokaccha/edit-text-box
Add: UI to edit dashboard text box widget
2016-10-05 16:12:47 +03:00
Kazuhito Hokamura
eb47d88b33 Add a comment 2016-10-05 22:08:23 +09:00
Kazuhito Hokamura
0530b5fe1e Limit the editing functionality to only the text 2016-10-05 22:03:39 +09:00
Arik Fraimovich
e8eb840d32 Merge pull request #1279 from IllusiveMilkman/patch-2
Change: update bootstrap.sh to support Ubuntu 16.04
2016-10-05 14:47:35 +03:00
Arik Fraimovich
8cf0252b07 Merge pull request #1262 from getredash/plotly_upgrade
Change: upgrade Plot.ly version and switch to smaller build
2016-10-05 14:45:26 +03:00
Arik Fraimovich
0b79fb833e Merge pull request #1306 from getredash/pagination
Change: paginate queries page & add explicit urls.
2016-10-05 14:44:01 +03:00
Arik Fraimovich
5096e4ed79 Change: paginate query page & add explicit urls.
- Paginate the queries API result.
- Split the API to /api/queries (all queries) and /api/queries/my which returns
  a user's queries (or drafts).
- In the interface have explicit URLs for all queries (/queries), my queries (/queries/my)
  and drafts (/queries/drafts).
2016-10-05 14:14:26 +03:00
Kazuhito Hokamura
83ffd915c8 Make it possible to edit a TextBox 2016-10-04 22:31:28 +09:00
Arik Fraimovich
e8582ec100 Add JQL to default enabled query runners. 2016-09-28 09:43:47 +03:00
Allen Short
6829192854 Connection-test queries 2016-09-27 18:57:47 -05:00
Arik Fraimovich
41f99f54cf Merge pull request #1299 from tatsukawa/json-contenttype
Change: send Content-Type header (application/json) in query results responses
2016-09-27 10:50:44 +03:00
Tsuyoshi Tatsukawa
3b6017495e add Content-Type to JSON response of QueryResultResource 2016-09-27 15:14:32 +09:00
Arik Fraimovich
808fdd4507 Merge pull request #1298 from getredash/add_jql
Add: JIRA (JQL) query runner
2016-09-26 17:08:39 +03:00
Arik Fraimovich
aefd2fde0a Add documentation on JIRA datasource 2016-09-26 17:03:12 +03:00
Arik Fraimovich
af56f59255 Add JIRA query runner 2016-09-26 16:48:53 +03:00
Arik Fraimovich
dfb1a204e2 Merge pull request #1297 from getredash/arikfr-patch-1
Change: update Slack configuration titles.
2016-09-26 13:34:19 +03:00
Arik Fraimovich
b711e5c4a2 Change: update Slack configuration titles. 2016-09-26 11:04:48 +03:00
Arik Fraimovich
8c1056cc4f Merge pull request #1294 from getredash/arikfr-patch-1
Change: don't annotate BigQuery queries
2016-09-26 10:15:04 +03:00
Arik Fraimovich
9d6b3f14a5 Change: don't annotate BigQuery queries
It's not really useful anyway and breaks support for `#StandardSQL` directive.
2016-09-23 18:13:42 +03:00
Arik Fraimovich
03217dd7ea Add markers cluster support & cleanup/refactor code. 2016-09-22 23:08:32 +03:00
Arik Fraimovich
ff9e844204 Merge pull request #1280 from hirotaka-s/feature/disable_refresh_queries_setting
Add: configuration flag to disable scheduled queries
2016-09-20 12:41:24 +03:00
Hirotaka Suzuki
01eb099c3d Add REDASH_FEATURE_DISABLE_REFRESH_QUERIES setting 2016-09-20 17:56:18 +09:00
IllusiveMilkman
2b25f2e80a Update bootstrap.sh
Check for Ubuntu 16.04 before upgrading pip.
2016-09-19 20:35:35 +02:00
Arik Fraimovich
6d686f03a3 Move around some functions and add support for window resize event. 2016-09-18 15:34:08 +03:00
Arik Fraimovich
de222429a1 Remove unnecessary code. 2016-09-18 15:23:29 +03:00
Arik Fraimovich
a3cf92ecf6 Merge pull request #1289 from getredash/es_parser
Change: use key_as_string when available (ElasticSearch query runner)
2016-09-18 08:49:05 +03:00
Arik Fraimovich
37b40164ab Fix: use key_as_string when available. 2016-09-17 16:50:54 +03:00
Arik Fraimovich
e155191c93 Merge pull request #1285 from falling-down/oracle-metadata
Change: do not display Oracle tablespace name in schema browser
2016-09-14 22:24:11 +03:00
Matthew Carter
b20b263ed1 Do not display Oracle tablespace name in schema browser
Does not make sense to list tablespace name to end user. 
Also include all tables visible to user for querying not just owned by user.
Do not list known system tables in schema browser.
2016-09-12 12:47:00 -04:00
Matthew Carter
8cfbf8b8bb Merge pull request #1 from getredash/master
update master from upstream
2016-09-12 11:57:50 -04:00
Arik Fraimovich
e42f93fcce Merge pull request #1283 from getredash/fix/gs_dup_columns
Fix: schema browser was unstable after opening a table
2016-09-11 15:29:35 +03:00
Arik Fraimovich
b9d1e43a8e Fix: schema browser was unstable after opening a table (closes #1261) 2016-09-11 15:26:48 +03:00
Arik Fraimovich
6cbc39cbe2 Merge pull request #1282 from getredash/fix/gs_dup_columns
Change: deduplicate Google Spreadsheet columns
2016-09-11 10:10:51 +03:00
Arik Fraimovich
09a848f524 Change: deduplicate Google Spreadsheet columns 2016-09-11 09:41:15 +03:00
Arik Fraimovich
21a9b4b03e Fix: in multi-org setup login after logout is broken 2016-09-08 15:26:56 +03:00
IllusiveMilkman
d9623faf8c Update bootstrap.sh
Testing script on clean install of Ubuntu Desktop 16.04.1.

pip fails if not upgraded beforehand, whether "sudo" or "sudo -s" or "sudo -sH" is run.

After this modification it works perfectly from a clean install.  Below are my steps if anyone should want to replicate or validate:
1. Clean install of Ubuntu 16.04.1 Desktop (on VM)
2. sudo apt-get -y install git
3. mkdir ~/git
4. cd ~/git
5. git clone https://github.com/getredash/redash
6. cd ~/git/redash/setup/ubuntu
7. chmod +x bootstrap.sh
8. nano bootstrap.sh
9. added "pip install --upgrade pip" to the script above the "pip install -U..." line.
10. Saved script.
11. sudo -H ./bootstrap.sh

Note that the "-H" is necessary in order to run the script successfully, otherwise you will run into pip ownership issues.
2016-09-08 10:28:11 +02:00
Arik Fraimovich
ef4699aca7 Merge pull request #1269 from kurtgooden/big_query_drive
Add: Google Drive federated tables support in BigQuery query runner
2016-09-07 17:55:44 +03:00
Kurt Gooden
43075f741d Reverted GCE change 2016-09-07 06:16:06 -04:00
Kurt Gooden
ddd91e37db Merge remote-tracking branch 'getredash/master' into big_query_drive 2016-09-07 06:13:48 -04:00
Arik Fraimovich
4caf2e309d Merge pull request #1277 from getredash/arikfr-patch-1
Set specific version of cryptography lib
2016-09-07 08:48:04 +03:00
Arik Fraimovich
0eb5a7d203 Set specific version of cryptography
This to avoid incompatibility with `oauth2client`.
2016-09-07 08:45:40 +03:00
Arik Fraimovich
170bd65237 Merge pull request #1272 from ariarijp/fix-treasuredata-query-runner
Fix: TreasureData get_schema method was returning array instead of string as column name
2016-09-06 08:28:00 +03:00
ariarijp
2739f04f1e Fix get_schema method 2016-09-03 22:02:16 +09:00
Kurt Gooden
4a8a67f6f4 Add Google Drive scope to Big Query
To access federated tables based off Google Drive files, an additional
scope is needed during OAuth.
2016-08-31 23:05:44 -04:00
Arik Fraimovich
4710c4193e Merge pull request #1265 from getredash/fix/dashboard_refresh
Fix: refresh modal not working for unsaved query
2016-08-30 18:32:55 +03:00
Arik Fraimovich
2e5ec26be9 Mention how to restart the services after mail configuration update. 2016-08-30 18:11:05 +03:00
Arik Fraimovich
cfbb466f92 Fix #1263: refresh modal not working for unsaved query 2016-08-30 18:04:58 +03:00
Arik Fraimovich
bc3a5ab04c Merge pull request #1264 from getredash/fix/dashboard_refresh
Fix: dashboard refresh not working
2016-08-30 17:57:56 +03:00
Arik Fraimovich
db4aec22f6 Fix #1184: dashboard refresh not working. 2016-08-30 17:55:10 +03:00
Arik Fraimovich
d22f0d44b6 Merge pull request #1216 from atsaki/noniteractive-upgrade
Change: bootstrap.sh - use non interactive dist-upgrade
2016-08-29 19:13:49 +03:00
Arik Fraimovich
03837c0659 Merge pull request #1242 from ereli/patch-1
Docs: add warning re. quotes on column names and BigQuery
2016-08-29 19:12:19 +03:00
Arik Fraimovich
2eeb94765d Merge pull request #1236 from yershalom/master
Add: query runner for Cassandra and ScyllaDB
2016-08-29 13:26:22 +03:00
Arik Fraimovich
9fef335315 Use smaller Plot.ly build 2016-08-28 23:29:58 +03:00
Arik Fraimovich
17726dbcb9 Sepcify specific version of Plotly 2016-08-28 23:29:45 +03:00
Arik Fraimovich
10b398e8e6 Merge pull request #1249 from mystelynx/override-slack-params
Add: override slack webhook parameters
2016-08-28 22:34:01 +03:00
Arik Fraimovich
2b5e34099f Merge pull request #1252 from rohanpd/get-presto-schema
Add: Schema loading support for Presto query runner (using information_schema)
2016-08-27 15:07:07 +03:00
Arik Fraimovich
e05a63db9a Update recommended instance size. 2016-08-27 14:58:33 +03:00
Arik Fraimovich
9a980759d3 Merge pull request #1255 from vishesh92/add-influxdb-documentation
Docs: add documentation for InfluxDB
2016-08-26 22:27:39 +03:00
vishesh92
8ce02d3003 Add documentation for influxdb 2016-08-25 12:39:35 +05:30
Arik Fraimovich
6202d0963d Merge pull request #1240 from easytaxibr/bugfix/public_dashboard_not_found
Fix: when shared dashboard token not found, return 404
2016-08-25 00:08:44 +03:00
syerushalmy
d41b84eb2e Fixed syntax error at _get_tables 2016-08-24 10:19:20 +03:00
Wesley Batista
e7d6ac07c9 Catch the error and respond with 404 2016-08-23 17:51:40 -03:00
Arik Fraimovich
ba30577601 Rename 0025_add_notification_destination.py to 0025_add_query_snippets_table.py 2016-08-23 23:06:36 +03:00
syerushalmy
7cce9d5d6e Added Auth importer for cassandra 2016-08-23 20:04:17 +03:00
syerushalmy
b308e0275c Removed cursor.close() from hive_ds because its not needed 2016-08-23 16:55:55 +03:00
Rohan Dhupelia
0319acc7ca add the ability to load the schema for the presto query runner 2016-08-23 21:32:12 +10:00
Arik Fraimovich
93aac14c87 Merge pull request #1251 from getredash/feature/snippets
Fix: autocomplete went crazy when database has no autocomplete.
2016-08-23 08:51:08 +03:00
Arik Fraimovich
ca6ee5e04f Fix: autocomplete went crazy when database has no autocomplete. 2016-08-23 01:43:29 +03:00
Arik Fraimovich
2aaf5dd2f0 Merge pull request #1250 from getredash/feature/snippets
Add: query snippets feature
2016-08-23 01:35:46 +03:00
Arik Fraimovich
10f5ecdb00 Add: query snippets feature 2016-08-22 23:58:00 +03:00
mystelynx
6ba76debf0 override webhook params 2016-08-22 11:12:46 +09:00
Arik Fraimovich
b8eca28e20 Merge pull request #1246 from getredash/large-schema
Fix: support large schemas in schema browser
2016-08-19 23:41:10 +03:00
Arik Fraimovich
490928d474 Fix: support large schemas in schema browser 2016-08-19 23:38:38 +03:00
Arik Fraimovich
19530f4132 Merge pull request #1245 from getredash/feature/ace
Change: switch from CodeMirror to Ace editor
2016-08-19 23:24:45 +03:00
Arik Fraimovich
2e1dce5961 Switch from CodeMirror to Ace editor 2016-08-19 23:15:34 +03:00
Cassio A. Moreto
d5b374c540 Create proxy_pass.rst
Add docpage of proxy pass configuration.
2016-08-19 15:56:34 -03:00
Ereli
94ce4b7b6e adding warning not to use quotes on column names
Bigquery  doesn't work with "Action__filter" as it doesn't support quoted field names.
2016-08-19 13:58:48 +01:00
Wesley Batista
dfb7cc1934 Catch the error and respond with 404 2016-08-18 12:27:18 -03:00
syerushalmy
37271c746c Switched to fetch_columns instead of messy code 2016-08-17 10:59:09 +03:00
syerushalmy
986dc686bb Removed unnessecery exception throw 2016-08-17 10:42:20 +03:00
syerushalmy
bd5039ad95 Fixed little syntax error 2016-08-16 16:04:21 +03:00
syerushalmy
37873196ec Fixed some syntax 2016-08-16 15:59:51 +03:00
syerushalmy
87d77d4d27 Added cassandra-driver to requirements_all_ds.txt file 2016-08-16 14:12:35 +03:00
syerushalmy
eee2e7c833 Added new DS for Cassandra and ScyllaDB 2016-08-16 14:10:50 +03:00
Arik Fraimovich
dfb92dbb4e Merge pull request #1234 from getredash/arikfr-patch-1
Change: MongoDB query runner set DB name as mandatory
2016-08-11 15:40:15 +03:00
Arik Fraimovich
5baf72a01e Change: MongoDB query runner set DB name as mandatory 2016-08-11 15:36:46 +03:00
Arik Fraimovich
b78100355c Merge pull request #1223 from toyama0919/master
Fix: Alert: when hipchat Alert.name is multibyte character, occur error.
2016-08-10 17:04:00 +03:00
Arik Fraimovich
b750843865 Merge pull request #1230 from kataring/presto-annotate_query-true
Change: annotate Presto queries with metadata
2016-08-10 17:03:31 +03:00
Arik Fraimovich
a69ee0cfe9 Merge pull request #1225 from hokaccha/fix-document
Fix: RST formatting of the Vagrant documentation
2016-08-10 16:43:47 +03:00
Arik Fraimovich
0b928e6a9b Merge pull request #1227 from hokaccha/fix-provisioning-script
Fix: Bower install fails in vagrant
2016-08-10 16:40:51 +03:00
Arik Fraimovich
a411af2512 Merge pull request #1232 from hokaccha/fix-confirm-dialog
Fix: don't show warning when query string (parameters value) changes
2016-08-10 15:53:44 +03:00
Kazuhito Hokamura
7843d2ee84 Fix: don't show warning when query string (parameters value) changes 2016-08-10 21:43:05 +09:00
Noriaki Katayama
1d693ad220 annotate_query to True 2016-08-09 19:12:49 +09:00
toyama0919
88d61e8faa apply reviews arikfr. string to unicode. 2016-08-09 19:01:13 +09:00
Kazuhito Hokamura
058b6bc37c Fix failing bower install 2016-08-06 22:21:00 +09:00
Arik Fraimovich
8d8af7386c Merge pull request #1226 from getredash/feature/sankey
Add: Sankey visualization
2016-08-05 23:00:42 +03:00
Arik Fraimovich
91ca74b46c Add: Sankey visualization 2016-08-05 22:32:20 +03:00
Kazuhito Hokamura
1e186d10a8 Fix RST formatting 2016-08-06 01:36:06 +09:00
Arik Fraimovich
14dea68e25 Update screenshots. 2016-08-04 08:30:39 +03:00
Arik Fraimovich
12896ed039 Update screenshots. 2016-08-04 08:30:04 +03:00
Arik Fraimovich
549fe8a465 Add ap-northeast-2 AMI (closes #1135) 2016-08-03 17:27:47 +03:00
Arik Fraimovich
eafe0dbe34 Update AWS images 2016-08-03 17:25:10 +03:00
Arik Fraimovich
7598048317 Update bootstrap.sh:
- Update version to 0.11.1.
- Move all apt commands to the beginning of the script and add the missing ones (lib-sasl).
- Fix the Redash Metadata data source creation command.
2016-08-03 16:52:45 +03:00
Arik Fraimovich
17fa957a91 Update Packer configuration to remove SSH keys to comply with AWS marketplace 2016-08-03 16:52:06 +03:00
toyama0919
ae3af64c09 Fix: Alert: when hipchat Alert.name is multibyte character, occur error. 2016-08-03 09:56:54 +09:00
Arik Fraimovich
a02eddabb5 Merge pull request #1222 from getredash/feature/sunburst_visualization
Add: additional results format for sunburst visualization
2016-07-31 23:10:18 +03:00
Arik Fraimovich
ca7d8699c8 Add additional results format for sunburst 2016-07-31 23:05:00 +03:00
Arik Fraimovich
3dbb5a6bfc Merge pull request #1221 from getredash/feature/sunburst_visualization
Fix: sunburst didn't handle all cases of path lengths
2016-07-31 15:48:01 +03:00
Arik Fraimovich
50419f3d8c Fix: sunburst didn't handle all cases of path lengths 2016-07-31 15:46:00 +03:00
Arik Fraimovich
0e70188cb4 Merge pull request #1213 from getredash/feature/sunburst_visualization
Add: new sunburst sequence visualization
2016-07-31 10:55:53 +03:00
Arik Fraimovich
77ce9b1d58 Add help paragraph on data shape 2016-07-31 10:53:00 +03:00
Arik Fraimovich
20206048af Redraw sunburst on window resize or options change 2016-07-31 10:48:52 +03:00
Arik Fraimovich
a7cc1eee5f Merge pull request #1218 from getredash/arikfr-patch-1
Fix: updated result not being saved when changing query text.
2016-07-28 15:30:16 +03:00
Arik Fraimovich
295ca92e44 Simplify the code. 2016-07-28 15:30:05 +03:00
Arik Fraimovich
1995fe4258 Fix: updated result not being saved when changing query text. 2016-07-28 15:26:00 +03:00
Arik Fraimovich
5b20fe21aa Merge pull request #1217 from atsaki/install-libffi-dev
Change: install libffi-dev for Cryptography (Ubuntu setup script)
2016-07-28 11:32:55 +03:00
Atsushi Sasaki
738cd1d69d non interactive dist-upgrade 2016-07-28 15:24:13 +09:00
Atsushi Sasaki
57651f177b install libffi-dev for cryptography 2016-07-28 15:23:12 +09:00
Arik Fraimovich
061783313a Merge pull request #1215 from getredash/fix/alert_email
Fix: email alerts not working
2016-07-27 16:54:02 +03:00
Arik Fraimovich
218937b175 Skip email sending if there are no recipients 2016-07-27 16:48:17 +03:00
Arik Fraimovich
c43357cc77 Fix #1212: email alerts not being sent 2016-07-27 16:47:16 +03:00
Arik Fraimovich
42e7a41fcc Typo fix. 2016-07-27 16:21:15 +03:00
Arik Fraimovich
52cbb42aaf Implement the visualization rendering logic 2016-07-26 18:06:54 +03:00
Arik Fraimovich
767fc3644a Base skeleton for the new visualization 2016-07-25 11:27:39 +03:00
Arik Fraimovich
9a6d2d7c62 Merge pull request #1204 from falling-down/vertica-metadata
Add: show views in schema browser for Vertica data sources
2016-07-25 10:43:16 +03:00
Arik Fraimovich
a9fac34560 Merge pull request #1206 from kitsuyui/update-pymssql-version
Change: update pymssql version to 2.1.3
2016-07-25 10:42:25 +03:00
kitsuyui
6a9467451a Update pymssql version. (is needed for tds_version keyword argument)
- https://github.com/pymssql/pymssql/releases/tag/v2.1.3
2016-07-25 12:15:31 +09:00
Matthew Carter
56ffec1be7 view metadata missing from Vertica 2016-07-24 15:31:11 -04:00
Arik Fraimovich
5d43cbe67f Merge pull request #1198 from mystelynx/add/bigquery-config-sql-type
Change: add support for Standard SQL in BigQuery query runner
2016-07-24 11:45:09 +03:00
mystelynx
e0e5dd3dd8 fix key name and title 2016-07-21 21:56:57 +09:00
mystelynx
2dac682e8e specify to use standard sql or not 2016-07-21 21:44:16 +09:00
mystelynx
f524dda88b add bigquery configuration to use legacy/standard sql 2016-07-21 21:13:48 +09:00
Arik Fraimovich
84d0c2294c Merge pull request #1193 from ken880guchi/modify-ken880guchi-patch-1
Change: modify the argument order of moment.add function call
2016-07-20 15:49:09 +03:00
Arik Fraimovich
e0485dec56 Merge pull request #1195 from AntoineAugusti/patch-2
Docs: fix typo in maintenance page title
2016-07-20 11:17:07 +03:00
Antoine Augusti
fb523725f6 Typo 2016-07-20 10:12:51 +02:00
Kenya Yamaguchi
1dd736d9b5 Modify the argument order of moment.add function 2016-07-20 01:19:21 +09:00
Arik Fraimovich
600afa5c82 Merge pull request #1138 from smartcanvas/feature/bigquery_udf
Add: ability to register user defined function (UDF) resources for BigQuery DataSource/Query
2016-07-15 19:17:45 +03:00
Arik Fraimovich
78f65b145a Merge pull request #1187 from getredash/arikfr-patch-3
Fix: read only users receive the permission error modal in query view
2016-07-14 22:05:57 +03:00
Arik Fraimovich
ea28e71170 Fix: read only users receive the permission error modal in query view
Closes #1153.
2016-07-14 22:03:34 +03:00
Arik Fraimovich
9193fed393 Mention Extended JSON support in MongoDB docs. 2016-07-12 09:35:44 +03:00
Arik Fraimovich
57ee9fd18b Fix formatting. 2016-07-12 09:31:52 +03:00
Arik Fraimovich
3f1d48b1f2 Update restart documentation. 2016-07-12 09:27:23 +03:00
Arik Fraimovich
7844b908de Merge pull request #1104 from kitsuyui/fix/unicode-error-with-sql-server
Fix #1101: MSSQL Query runner: query execution fails if user name has unicode characters
2016-07-11 18:17:30 +03:00
Arik Fraimovich
28ffff8930 Update Query Result Format page. 2016-07-11 16:19:55 +03:00
Arik Fraimovich
21283e2e83 Merge pull request #1181 from getredash/fix/gs
Fix: Excel file generation fails when row missing column value.
2016-07-11 11:54:25 +03:00
Arik Fraimovich
d4bfbc2c57 Fix: Excel file generation fails when row missing column value. 2016-07-11 11:51:55 +03:00
Arik Fraimovich
49d8a99bc4 Merge pull request #1176 from someones/org_groups_list
Add: 'list' command for org and groups CLI
2016-07-11 10:55:55 +03:00
Arik Fraimovich
dae2907ca3 Merge pull request #1177 from someones/users_list_fix
Fix: users list CLI command was broken
2016-07-11 10:54:50 +03:00
Arik Fraimovich
dd45fe04ee Merge pull request #1179 from kitsuyui/fix/not-shown-left-pane
Fix: schema not loading when table name has unicode characters
2016-07-11 10:54:11 +03:00
kitsuyui
74021c2d5a Fix: #1178 Left Pane is not shown when database contains table that name has unicode characters (MSSQL)
- Update table_name to be unicode (not str).
2016-07-11 16:46:17 +09:00
Adam Griffiths
87d7d9cb5d Fix a copy paste error that broke list command 2016-07-11 17:05:09 +10:00
Adam Griffiths
697e377bec Add 'manage.py groups list' command
'groups list' simply prints out groups.
--org option is provided, limits printing to groups of
the specified organization.
--org accepts organization slugs.
2016-07-11 16:57:07 +10:00
Adam Griffiths
99906c1d0d Add 'manage.py org list' command
'org list' simply prints out the organizations.
2016-07-11 16:52:17 +10:00
Arik Fraimovich
b1937aaab2 Merge pull request #1173 from smartcanvas/fix/cursor-pointer-for-table-headers
Add: add cursor:pointer to SmartTable headers to indicate it's clickable
2016-07-06 15:58:12 +03:00
fabito
cd449183bf add cursor:pointer to smart table headers 2016-07-06 09:10:01 -03:00
Arik Fraimovich
dd0d29467e Merge pull request #1159 from shyamgopal/patch-1
Docs: update mention the option of querying Google Spreadsheet with Google BigQuery
2016-07-06 09:20:02 +03:00
Arik Fraimovich
ff49d25963 Merge pull request #1170 from easytaxibr/fix/docker_create_database
Fix: change parameters for 'manage.py ds new' CLI
2016-07-06 09:17:52 +03:00
ShyamK
5a1f4d9144 Update datasources.rst 2016-07-06 09:59:11 +05:30
kitsuyui
679e44c874 Add charset to configuration. 2016-07-06 11:40:39 +09:00
kitsuyui
eaf127da71 Merge remote-tracking branch 'upstream/master' into fix/unicode-error-with-sql-server 2016-07-06 11:35:20 +09:00
Wesley Batista
628122053b Fix: change parameters for 'manage.py ds new' 2016-07-05 20:36:38 -03:00
Arik Fraimovich
8a5a71421d Merge pull request #1168 from getredash/fix/gs
Fix: improve Google Spreadsheets parsing
2016-07-05 21:50:45 +03:00
Arik Fraimovich
906365f011 Merge pull request #967 from lloydw/master
Add: extend ElasticSearch query_runner to support aggregations
2016-07-05 21:40:33 +03:00
Arik Fraimovich
bab1029c9d Merge pull request #1151 from masaedw/azuredb
Change: ability to set pymssql TDS version (to support Azure Database) #backward-incompatible
2016-07-05 21:30:06 +03:00
Arik Fraimovich
d263688da4 Merge pull request #1147 from someones/celery-worker-count
Control celery worker count with environment variables, defaults to 2
2016-07-05 13:46:51 +03:00
Adam Griffiths
7d10edd32c Replace -c2 in Procfile.heroku with env variable
Replace -c2 in Procfile.heroku with
-c${REDASH_HEROKU_CELERY_WORKER_COUNT:-2}. This reads from the
environment variable REDASH_HEROKU_CELERY_WORKER_COUNT and
uses the value of that if set, otherwise defaults to 2.
Add REDASH_HEROKU_CELERY_WORKER_COUNT to the settings page.
2016-07-05 11:16:45 +10:00
Arik Fraimovich
a34357d222 Move dateutil import higher 2016-07-04 22:34:13 +03:00
Arik Fraimovich
95fa6849b3 More robust parsing of worksheets with handling:
- handle the case of empty worksheet.
- handle the case of worksheet with no data.
2016-07-04 22:25:10 +03:00
Arik Fraimovich
4496a004e8 Meaningful message for not found spreadsheet 2016-07-04 22:21:05 +03:00
Arik Fraimovich
6905340c2d Google Spreadsheets: Add support for unicode 2016-07-04 18:31:42 +03:00
Arik Fraimovich
3ec113e8d0 Bump version. 2016-07-03 15:22:15 +03:00
Arik Fraimovich
bba801f9d5 Merge pull request #1167 from ariarijp/fix-bootstrap-script-for-ubuntu
Fix the version of setuptools on bootstrap script for Ubuntu
2016-07-03 14:17:03 +03:00
Arik Fraimovich
b41041014f Merge pull request #1166 from getredash/refersh_14_days
Feature: add "every 14 days" refresh option
2016-07-03 14:14:34 +03:00
Arik Fraimovich
31edf9cf80 Add every 14 days refresh option 2016-07-03 14:12:09 +03:00
Takuya Arita
522e07ac95 Fix the version of setuptools on bootstrap script for Ubuntu 2016-07-03 20:10:01 +09:00
Arik Fraimovich
837073144f Dockerfile: pin setuptools version to 23.1.0 until they resolve bug introduced in 24.0 2016-07-03 13:39:38 +03:00
Arik Fraimovich
9895e28a3f Cleanup/fix script tags. 2016-07-03 13:31:59 +03:00
Arik Fraimovich
ae9e295d2f Fix #1165: don't use hard coded org/user. 2016-07-03 13:26:54 +03:00
Arik Fraimovich
7681d3ee84 Merge pull request #1164 from darabos/patch-1
Docs: update permission documentation
2016-07-01 08:30:15 +03:00
Daniel Darabos
458f5eb032 Update permission documentation
https://github.com/getredash/redash/pull/957 has been merged.
2016-06-30 13:22:55 +02:00
Arik Fraimovich
ce81d69f91 Merge pull request #1160 from 5t111111/fix-indentation-in-docker-compose-example
Fix indentation in docker-compose-example.yml
2016-06-28 16:12:07 +03:00
Hirofumi Wakasugi
0456caf798 Fix indentation in docker-compose-example.yml 2016-06-28 22:05:23 +09:00
ShyamK
bb2574ef0b Update datasources.rst for a new way using Google Spreadsheet. 2016-06-27 15:18:06 +05:30
Arik Fraimovich
bcd3670282 Merge pull request #1156 from jcox92/patch-1
Docs: add SSL parameters to nginx configuration
2016-06-26 08:56:19 +03:00
Josh Cox
f7e556969a Add SSL parameters to nginx configuration
These parameters prevent some common exploits and vulnerabilities. My primary reference was here: https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
2016-06-23 15:24:17 -04:00
Arik Fraimovich
dd759fe4b0 Merge pull request #1155 from getredash/random
Fix: make all configuration values of Oracle required
2016-06-23 21:24:09 +03:00
Arik Fraimovich
988b301f65 Fix: make all configuration values of Oracle required 2016-06-23 21:21:04 +03:00
Arik Fraimovich
923b3b18e4 Merge pull request #1152 from getredash/random
Fix: add missing paddings in titles
2016-06-22 16:08:45 +03:00
Arik Fraimovich
95c47138ab Fix: add missing paddings in titles 2016-06-22 14:12:27 +03:00
Masayuki Muto
cfbffe0cce Specify tds_version 2016-06-22 16:34:34 +09:00
Masayuki Muto
991fe618b7 Update pymssql version to connect Azure Database 2016-06-22 16:19:38 +09:00
Masayuki Muto
0538fe401b Fix typo 2016-06-22 16:18:42 +09:00
Arik Fraimovich
31e7375a30 Merge pull request #1150 from getredash/random
UI Fixes for v0.11
2016-06-21 14:07:50 +03:00
Arik Fraimovich
8808e38de9 Fix: shared dashboard fail to render queries. 2016-06-21 11:56:02 +03:00
Arik Fraimovich
0314313285 Merge pull request #1149 from toru-takahashi/patch-2
Presto QueryRunner supports tinyint and smallint
2016-06-21 11:50:32 +03:00
Toru Takahashi
9c0d1da7f9 Presto QueryRunner supports tinyint and smallint
Presto0.148 started to support tinyint and smallint.
https://prestodb.io/docs/current/release/release-0.148.html
2016-06-21 17:36:23 +09:00
Arik Fraimovich
904ea9f90a Fix: after creating new alert, it uses wrong id to load subscribers 2016-06-20 19:36:27 +03:00
Arik Fraimovich
6bb09d8446 Merge pull request #1146 from someones/doc-add-settings
Docs: add a link to settings documentation.
2016-06-20 09:31:50 +03:00
Arik Fraimovich
d5e5b2438b Merge pull request #1136 from someones/org-aware-cli
Feature: add --org option to all relevant CLI commands.
2016-06-20 09:27:38 +03:00
Arik Fraimovich
dbd48e15bc Merge pull request #1143 from ariarijp/fix/email-input
Fix: use the email input type where needed
2016-06-20 08:56:43 +03:00
Takuya Arita
21fdd6b69d Revert input type 2016-06-20 14:05:51 +09:00
Adam Griffiths
a666adeaa7 Add a link to settings.
The settings page doesn't seem to have any links to it.
The only way to find it is through searching for it.
So we'll add one to the setup.rst page.
2016-06-20 11:05:28 +10:00
Adam Griffiths
15361cc81c Improve the help text text
Add "(leave blank for 'default')." or
"(leave blank for all organizations)" to organization options.
Remove the text "This commands assume single organization operation."
from the `users` and `groups` managers help text.
2016-06-20 09:56:05 +10:00
Adam Griffiths
704a167c74 Fix organization parameters.
Move organization to the end of the parameter
list to prevent breaking any existing code.
Provide a default for all organization parameters
to allow people to call the CLI functions without
breaking existing code.
Add missing organization parameter to `list` in
`users.py`.
2016-06-20 09:49:37 +10:00
ariarijp
df2c8d83b0 Use the email input type 2016-06-19 20:48:01 +09:00
Arik Fraimovich
7445972c10 Merge pull request #1142 from getredash/random
Fix: dates in filters might be duplicated
2016-06-19 08:54:02 +03:00
Arik Fraimovich
1933995a28 Fix: dates in filters might be duplicated 2016-06-19 08:51:45 +03:00
Arik Fraimovich
8df822eee2 Merge pull request #1141 from getredash/random
Make create data source button more prominent
2016-06-17 18:10:28 +03:00
Arik Fraimovich
227fe9b44a Make create data source button more prominent 2016-06-17 18:07:48 +03:00
Arik Fraimovich
5d0ed02caa Merge pull request #1140 from getredash/random
Fix: Hive should use the enabled variable
2016-06-17 18:06:54 +03:00
Arik Fraimovich
392627d6d6 Merge pull request #1139 from getredash/random
Fix: Impala data source referencing wrong variable
2016-06-17 17:53:30 +03:00
Arik Fraimovich
72d02e9e9d Fix: Hive should use the enabled variable 2016-06-17 17:50:26 +03:00
Arik Fraimovich
902ce24f6f Fix: Impala data source referencing wrong variable 2016-06-17 17:49:34 +03:00
Arik Fraimovich
c5bfbbaef7 Merge pull request #1133 from vishesh92/fix-scroll
Fix: query scrolling issues
2016-06-17 17:43:04 +03:00
fabito
58a9bedb64 add documentation for UDF Source URIs 2016-06-17 09:50:03 -03:00
Arik Fraimovich
a03f5f88fb Limit Celery concurrency 2016-06-17 10:19:33 +03:00
fabito
ec50cf97a9 Add new optional property "UDF Source URIs" to BigQuery datasource so that users can reuse UDFs amongs queries 2016-06-17 02:02:48 -03:00
Adam Griffiths
a66e182f73 Add --org option to all relevant CLI commands.
--org is the organization slug, not the name.
Allows the management of users, datasources and groups
with respect to organisations.
All commands default to 'default' slug, or None where
relevant, which means the commands will still work
as they did before without any changes.
2016-06-17 12:03:09 +10:00
Arik Fraimovich
96dd811607 Merge pull request #1127 from someones/schema_secrets
Mark basic_auth_password as secret
2016-06-16 21:24:16 +03:00
Vishesh Jindal
409200188e Fix query scrolling #1024 2016-06-16 17:40:48 +05:30
Arik Fraimovich
ad65391914 Merge pull request #1130 from AntoineAugusti/patch-1
Improve Slack notification style
2016-06-15 17:22:31 +03:00
Antoine Augusti
203f6afa09 Improve Slack notification style 2016-06-15 15:56:04 +02:00
Arik Fraimovich
2b710420ab Docs: start worker process on Heroku 2016-06-15 16:17:11 +03:00
Arik Fraimovich
01116f41ed Don't auto reload/run in debug mode on Heroku 2016-06-15 16:17:11 +03:00
Arik Fraimovich
87e25f2107 Merge pull request #1129 from getredash/fix-viz-options
Feature: support for JSON query formatting (Mongo, ElasticSearch)
2016-06-15 15:21:41 +03:00
Arik Fraimovich
c495250a54 Feature: support for JSON query formatting (Mongo, ElasticSearch) 2016-06-15 15:06:50 +03:00
Arik Fraimovich
c01d266030 Merge pull request #1128 from getredash/fix-viz-options
Fix: visualization options not updating after changing type
2016-06-15 12:49:43 +03:00
Arik Fraimovich
8515ac25bc Fix: visualization options not updating after changing type 2016-06-15 12:42:18 +03:00
Adam Griffiths
23988a72aa Mark basic_auth_password as secret 2016-06-15 11:34:02 +10:00
Arik Fraimovich
6bc0e7a716 Merge pull request #1126 from getredash/fix_669
Fix #669: save fails when doing partial save of new query
2016-06-14 15:39:58 +03:00
Arik Fraimovich
2c2ff0d252 Fix #669: save fails when doing partial save of new query 2016-06-14 15:39:12 +03:00
Arik Fraimovich
69cefee0d4 Update Heroku instructions. 2016-06-14 14:36:41 +03:00
Arik Fraimovich
02c065751a Update Heroku pre_compile hook 2016-06-14 14:23:56 +03:00
Arik Fraimovich
aed65f4bad Deduplicate parameter names 2016-06-14 14:00:38 +03:00
Arik Fraimovich
6bb2716fe3 Merge pull request #1121 from getredash/fix_949
Show error when failing to communicate with server
2016-06-14 13:18:06 +03:00
Arik Fraimovich
efaeb08178 Merge pull request #1120 from getredash/feature/params_ui
Fix: default vale for parameters should be [] and not {}
2016-06-14 13:17:21 +03:00
Arik Fraimovich
e18a073128 Show error when failing to communicate with server
Closes #949.
2016-06-14 12:12:31 +03:00
Arik Fraimovich
f21276ec06 Merge pull request #1119 from getredash/feature/params_ui
Feature: add UI to delete alerts
2016-06-14 12:00:53 +03:00
Arik Fraimovich
b0c0582e41 Fix: default vale for parameters should be [] and not {} 2016-06-14 11:59:11 +03:00
Arik Fraimovich
9ad85091ed Add UI to delete alerts (closes #731) 2016-06-14 11:55:00 +03:00
Arik Fraimovich
2d2fb69b7b Add API to delete alerts (#731). 2016-06-14 11:21:05 +03:00
Arik Fraimovich
3ce27b9652 Merge pull request #1118 from getredash/feature/params_ui
Fix: remove alerts for archived queries
2016-06-14 11:15:50 +03:00
Arik Fraimovich
da4db94cf8 Close #930: remove alerts for archived queries 2016-06-14 11:09:35 +03:00
Arik Fraimovich
4cbc79a7aa Use default user for alert_subscription factory 2016-06-14 11:04:41 +03:00
Arik Fraimovich
4fabaaea8a Merge pull request #1117 from getredash/feature/params_ui
Fix #1052: filter not working for date/time values
2016-06-14 11:02:02 +03:00
Arik Fraimovich
a7af596da0 Fix #1052: filter not working for date/time values 2016-06-14 10:58:33 +03:00
Arik Fraimovich
df637e3f6b Merge pull request #1116 from getredash/feature/params_ui
Return meaningful error when there is no cached result.
2016-06-14 10:44:24 +03:00
Arik Fraimovich
68465b0c60 Return meaningful error when there is no cached result.
Previously it was crashing as it was trying to access an unreferenced
variables (query_result).
2016-06-14 10:41:01 +03:00
Arik Fraimovich
86565402fa Merge pull request #1069 from getredash/feature/params_ui
Feature: UI for query parameters
2016-06-14 10:15:36 +03:00
Arik Fraimovich
c2e3637dce Feature: UI for query parameters
This pull request implements UI for parameters and also allows to set the default value and type of a parameter.
(Closes #583)

Other changes in this pull request:

- Loading/error state for dashboard widgets.
- Refresh button on dashboard widgets (Closes #810).
- Maintain sync between query/dashboard URL and current parameters, and preserve them when navigating.
- Removed Pivot Table tab.
2016-06-14 10:09:16 +03:00
Arik Fraimovich
52558043ee Merge pull request #1091 from whummer/feature/cache_embeds
Add caching for queries used in embeds
2016-06-14 08:34:13 +03:00
Waldemar Hummer
a045d7ddf7 simplify code to get parameters 2016-06-14 09:36:32 +10:00
Arik Fraimovich
c107c94a27 Merge pull request #1077 from nabilblk/master
Fix: install needed dependencies to use Hive in Docker image
2016-06-13 15:22:53 +03:00
Arik Fraimovich
790128ce77 Merge pull request #1067 from anthony-coble/add_word_cloud
Feature: word cloud visualization
2016-06-13 13:40:02 +03:00
Arik Fraimovich
abc790ce41 Merge pull request #1115 from getredash/fix-1097
Fix: allow non integers in alert reference value
2016-06-13 13:13:27 +03:00
Arik Fraimovich
f2643521f7 Fix: allow non integers in alert reference value 2016-06-13 13:03:08 +03:00
nabil
0d897e6878 move hive depencencies into req_all_ds.txt 2016-06-09 22:29:46 +00:00
Arik Fraimovich
4ec473cf5e Merge pull request #1110 from getredash/fix-1097
Fix #1109: mixed group permissions resulting in wrong permission
2016-06-09 20:02:06 +03:00
Arik Fraimovich
0c7f0c25a8 Fix #1109: mixed group permissions resulting in wrong permission 2016-06-09 19:59:26 +03:00
Arik Fraimovich
8c21e9149d Merge pull request #1108 from getredash/fix-1097
Remove potnetially concurrency not safe code form enqueue_query
2016-06-09 17:08:53 +03:00
Arik Fraimovich
7159f0beb0 Remove potnetially concurrency not safe code form enqueue_query.
This might have been causing the behavior described in #1097.
2016-06-09 16:53:29 +03:00
Arik Fraimovich
095e7596b5 Merge pull request #1103 from AntoineAugusti/patch-1
Docs: add section about monitoring
2016-06-09 16:32:24 +03:00
Arik Fraimovich
31013836ea Fix path reference in embed.html. 2016-06-09 11:06:52 +03:00
kitsuyui
91e99c42cd Fix #1101: Query execution fails if user name has unicode characters
- Encode the query with connection's charset when its type is unicode(not str).
2016-06-09 12:08:58 +09:00
Arik Fraimovich
b67f412f58 Add test for enqueue_query 2016-06-08 20:00:59 +03:00
Antoine Augusti
c1bf9dc67d Add section about monitoring 2016-06-08 15:47:00 +02:00
Arik Fraimovich
65635ec703 Merge pull request #1102 from getredash/feature/hipchat_v2
Switch to HipChat V2 API
2016-06-08 16:17:10 +03:00
Arik Fraimovich
ceaa00e448 Fix HipChat base URL 2016-06-08 16:09:06 +03:00
Arik Fraimovich
679b0a3125 Switch to HipChat V2. 2016-06-08 16:04:28 +03:00
Arik Fraimovich
fe81dbd3a2 Fix paths in Gulp build pipeline 2016-06-08 10:00:05 +03:00
Arik Fraimovich
1409907ef1 WIP: gulp fix 2016-06-08 09:32:14 +03:00
Arik Fraimovich
cbbfc4e931 Update bower.json 2016-06-08 08:57:51 +03:00
Arik Fraimovich
1ca5262fa8 Merge pull request #1099 from someones/heroku-clean
Fix RST syntax for links
2016-06-08 08:14:29 +03:00
Adam Griffiths
429b76f5a7 Fix RST syntax for links 2016-06-08 15:01:03 +10:00
Arik Fraimovich
8b73a2b135 Merge pull request #1098 from getredash/flexible_notifications
Feature: UI for alert destinations & new destination types
2016-06-07 15:18:11 +03:00
Arik Fraimovich
eed5485080 Update Alerts/subscriptions UI for new look and feel. 2016-06-07 15:12:47 +03:00
Arik Fraimovich
daa6c1cd6f Merge pull request #1096 from AntoineAugusti/patch-2
Fix typo in env variable VERSION_CHECK
2016-06-07 11:01:00 +03:00
Antoine Augusti
68dc3b033c Fix typo in env variable VERSION_CHECK 2016-06-07 09:58:49 +02:00
Anthony Coble
2e88e7f396 fixup! Add a word cloud vis 2016-06-06 17:49:31 -04:00
Arik Fraimovich
cd06d276e4 Merge pull request #1095 from ordd/fix_new_query_permission
Fix: use create_query permission for new query button.
2016-06-06 17:25:28 +03:00
Or
437f589fde Fix: use create_query permission for new query button. 2016-06-06 12:27:08 +03:00
Arik Fraimovich
1fbeb5d2a5 Merge pull request #1092 from someones/heroku-clean
Add Heroku support
2016-06-06 09:24:27 +03:00
Arik Fraimovich
df1e72ca01 Take into account that node_modules moves to root 2016-06-06 09:24:13 +03:00
Arik Fraimovich
fcc656e04e Add support for REDIS_URL and DATABASE_URL in settings.
(preparation for Heroku support - #1092)
2016-06-06 09:22:33 +03:00
Adam Griffiths
a0b97c1fc9 Update Heroku support as per comments
Fix comment in bin/pre_compile.
Remove .gitattributes and .travis.yml in rd_ui/.
Remove bin/run from Procfile.heroku.
Update documentation:
-Add a note about upgrading from version to version.
-Remove commands for DATABASE_URL and REDIS_URL.
-Add importance to the cookie secret variable.
-Merge adding redis and postgres addons into 1 step.
2016-06-06 11:39:10 +10:00
Arik Fraimovich
4d6599e0ea WIP 2016-06-05 15:51:49 +03:00
Adam Griffiths
c75054b320 Add Heroku support
Move .bowerrc, bower.json, gulpfile.js, package.json
down to root level.
Update paths in .bowerrc, gulpfile.js, Makefile
Add a heroku-postbuild step to package.json which
installs devDependencies and runs the build.
Add step in bin/pre_compile which adds the
requirements_all_ds.txt to requirements.txt to ensure that
cffi is installed. Also removes pymssql as this is
not supported on Heroku.
Add content from rd_ui/.gitignore to .gitignore and
remove rd_ui/.gitignore.
Add section in setup.rst about Heroku deployments.
2016-06-03 12:21:09 +10:00
Waldemar Hummer
011ca74338 add caching for queries used in embeds 2016-06-03 09:14:59 +10:00
Arik Fraimovich
434615a1be Merge remote-tracking branch 'origin/master' into flexible_notifications 2016-06-02 10:21:52 +03:00
Arik Fraimovich
2bc0b276b5 Merge pull request #1061 from thoughtworks/fix/area_plots
Fix: area chart stacking doesn't work
2016-06-01 21:21:18 +03:00
Arik Fraimovich
e942486ed7 Merge pull request #1090 from getredash/contributing_guide
Contributing Guide
2016-06-01 15:56:36 +03:00
Arik Fraimovich
9eff7ef8c9 Update CONTRIBUTING.md 2016-06-01 15:55:50 +03:00
Arik Fraimovich
34b305d232 Update CONTRIBUTING.md 2016-06-01 15:52:52 +03:00
Arik Fraimovich
f0d97bc5d1 Update CONTRIBUTING.md 2016-06-01 15:30:04 +03:00
Arik Fraimovich
f64622db77 Merge pull request #1089 from James226/master
Add support for serialising UUID type within MSSQL #961
2016-06-01 15:14:59 +03:00
Arik Fraimovich
8030baa6a5 Create "Contributing Guide" 2016-06-01 14:55:11 +03:00
Raymond Machira
3d82b702b3 Merge branch 'upstream/master' into fix/area_plots
# Conflicts:
#	rd_ui/app/scripts/directives/plotly.js
2016-05-31 10:50:05 -05:00
Arik Fraimovich
ad8676df2e Merge pull request #1087 from getredash/fix/chart_editor
Maintain fixed size of chart area
2016-05-31 11:29:37 +03:00
Arik Fraimovich
ea031e9a98 Maintain fixed size of chart area 2016-05-31 11:27:01 +03:00
Arik Fraimovich
9cfebedec9 Merge pull request #1086 from getredash/fix/chart_editor
Give bottom margin setting more meaningful name & change to input type to number.
2016-05-31 10:36:24 +03:00
Arik Fraimovich
772d263827 Give bottom margin setting more meaningful name & change to input to number. 2016-05-31 10:27:28 +03:00
Arik Fraimovich
8c455c8a1c Update login page title 2016-05-31 09:46:29 +03:00
Arik Fraimovich
857caab20e Merge pull request #1085 from getredash/feature/pause-api
Feature: API to pause a data source
2016-05-31 09:08:03 +03:00
Arik Fraimovich
59f8af2c44 Switch to Redis for pause state storage 2016-05-30 22:44:09 +03:00
Arik Fraimovich
9538ee7c31 Feature: API to pause a data source 2016-05-30 18:30:05 +03:00
Arik Fraimovich
e8312185dc Merge pull request #1084 from getredash/fix_dup_alerts
Fix #1049: duplicate alerts when data source belongs to multiple groups
2016-05-30 14:42:05 +03:00
Arik Fraimovich
07d2b5ba42 Fix #1049: duplicate alerts 2016-05-30 14:39:58 +03:00
Arik Fraimovich
f8120284d5 WIP: updated look and feel 2016-05-30 14:39:01 +03:00
James Parker
5b654fd1c8 Add support for serialising UUID type within MSSQL #961 2016-05-27 09:22:38 +01:00
Arik Fraimovich
6edb0ca8ec Merge pull request #1080 from jeffwidman/patch-1
Fix typo
2016-05-27 08:42:21 +03:00
Lloyd Weehuizen
3f208c03fd Add missing type parameter to collect_value call 2016-05-27 10:23:16 +12:00
Jeff Widman
ef0de1414d Fix typo 2016-05-26 14:29:40 -07:00
Arik Fraimovich
214aa3b799 Merge pull request #1060 from thoughtworks/saml-authorization
Feature: support configuring user's groups with SAML
2016-05-26 23:07:30 +03:00
Arik Fraimovich
64d7538040 Merge pull request #1074 from toyama0919/master
Fix: ElasticSearch wasn't using correct type names
2016-05-26 22:57:32 +03:00
Kumar Vora
69177752bc addresses PR feedback! 2016-05-26 14:46:25 -05:00
Kumar Vora
d83c6c42dd Raymond/Kumar : Fixes issue where going into edit more would break stacking. - selects legend divs from current graph only (instead of selecting from the entire dom) 2016-05-26 14:17:19 -05:00
nabil
2043834ae9 Issue #1076 : addtional dependencies for Hive Datasource 2016-05-26 10:38:07 +01:00
toyama0919
d6f4af448c fix bug. There is a case set unsupport type for elasticsearch. 2016-05-25 17:50:13 +09:00
Raymond Machira
43b425f91c Merge branch 'get-redash-master' of github.com:thoughtworks/redash into fix/area_plots 2016-05-24 18:11:13 -05:00
Raymond Machira
17427cf47b Names the legend item click listener to avoid overriding other places the event may be used. Also removes the event listener when type of chart changes. 2016-05-24 17:54:50 -05:00
Kumar Vora
b5be5a8fa4 no need to check count of results 2016-05-24 16:38:41 -05:00
Arik Fraimovich
14fcf01751 Merge pull request #1072 from getredash/arikfr-patch-2
Remove counter from the tasks Done tab (as it always shows 50). #1047
2016-05-23 21:24:01 +03:00
Arik Fraimovich
09848d65a1 Remove counter from the tasks Done tab (as it always shows 50). #1047 2016-05-23 18:32:59 +03:00
Arik Fraimovich
0d897ea959 Update AMIs to 0.10.1 2016-05-22 13:35:07 +03:00
Arik Fraimovich
e88d4c3d27 Remove reference to Google Groups and add Discourse instead 2016-05-22 10:33:46 +03:00
Arik Fraimovich
82f0b4c386 Update version reference in bootstrap scripts. 2016-05-22 10:33:46 +03:00
Arik Fraimovich
3037c4f90d Merge pull request #1066 from edwardsharp/edwardsharp-fix-1057
Docs: command type-o fix.
2016-05-22 08:59:12 +03:00
Kumar Vora
8900d02c95 fixing test 2016-05-20 14:35:49 -05:00
Kumar Vora
c1c2db4a73 use user.org instead of passing org as a separate argument 2016-05-20 14:28:08 -05:00
Anthony Coble
574d8a18ae Add a word cloud vis 2016-05-19 16:19:08 -04:00
edward sharp
82872db111 command type-o in setup docs. fix #1057 2016-05-19 12:57:18 -07:00
Arik Fraimovich
3f90dd9247 Merge pull request #1064 from getredash/fix/keys_cleanup
Fix: old task trackers were not really removed
2016-05-19 10:08:58 +03:00
Arik Fraimovich
b2e2277d0b Fix: old task trackers were not really removed 2016-05-19 09:58:30 +03:00
Arik Fraimovich
e20a00566a Merge pull request #1007 from vorakumar/issue-1006
Issue#1006:  Make bottom margin editable for Chart visualization
2016-05-18 23:18:12 +03:00
Arik Fraimovich
e10ecd2dad Merge pull request #1058 from AntoineAugusti/patch-1
Bring back filters if dashboard filters are enabled
2016-05-18 22:50:31 +03:00
Arik Fraimovich
6e0dd2b9a3 Merge pull request #1063 from windward-ltd/master
Add support for date/time Y axis
2016-05-18 22:50:13 +03:00
tomerb
0bb3fb9c40 added datetime to the yaxis scale options 2016-05-18 18:41:08 +03:00
Arik Fraimovich
1a1160eb76 Merge pull request #1062 from getredash/fix_dql
DynamoDB: Better exception handling
2016-05-18 14:14:00 +03:00
Arik Fraimovich
d4ae97aab2 Move pyparsing import to the try/except block 2016-05-18 14:08:42 +03:00
Arik Fraimovich
8bc42c8ad9 Remove duplicate reference to ParseException 2016-05-18 13:45:26 +03:00
Arik Fraimovich
6c5865bd3b Better exception handling 2016-05-18 13:44:38 +03:00
Arik Fraimovich
701035fabd Merge pull request #1059 from getredash/fix_dql
Fix: DynamoDB having issues when setting host
2016-05-18 13:42:48 +03:00
Arik Fraimovich
31aee1b6b9 Better exception handling 2016-05-18 13:30:20 +03:00
Arik Fraimovich
367ea859e4 If host param is empty, change it to None 2016-05-18 13:30:10 +03:00
Raymond Machira
d79d3da955 Merge branch 'get-redash-master' into fix/area_plots 2016-05-17 15:55:21 -05:00
Raymond Machira
6c822d1e4b Force area charts to recalculate on click of legend items.
This fixes issue #948 where stacked area plots do not update once series are enabled/disabled using the legend.
It also fixes the behaviour of percentage stack area plots, so as to have similar behaviour to percentage stacked bar charts.
2016-05-17 15:49:25 -05:00
Ama Asare
ad85b9a62c Ama/Kumar: Configure authorization for SAML 2016-05-17 14:01:18 -05:00
Arik Fraimovich
b5a4a6b880 Merge pull request #979 from ninneko/860-managepy_support_gropu_operation
Feature: Add CLI to edit group permissions
2016-05-17 15:05:16 +03:00
Arik Fraimovich
1828de20b0 Fix: DynamoDB having issues when setting host 2016-05-17 14:36:15 +03:00
Antoine Augusti
48c85645c6 Bring back filters if dashboard filters are enabled 2016-05-17 09:57:08 +02:00
Arik Fraimovich
ed45dcb01d Merge pull request #1044 from thoughtworks/improve-vagrant-flow
Improve vagrant flow
2016-05-13 15:47:10 +03:00
Ama Asare
d4ff7482ad Ama: Install just pymongo and not everything in requirements_all_ds.txt
Because some dependencies were missing and we dont really need everything in the file...just pymongo
Also removed unneeded files, reverted circle.yml to as it is on redash, so it doesnt show in the PR diff
2016-05-09 17:15:07 -05:00
Ama Asare
90f0b3b49a Ama: Consolidate vagrant_provision script, move files to more intuitive locations, include command from #1021.
Also reset circle.yml to be as in the main redash repo so our changes are not included in the changeset
Check earlier PR https://github.com/getredash/redash/pull/1027 to follow conversation.
2016-05-09 17:13:33 -05:00
Ama Asare
f8efb2d7ea Ama/Chris #7121 Added script to run or test redash vagrant server in one command. 2016-05-09 17:13:18 -05:00
Kumar Vora
d2ba0cb6cf Let plotly calculate the height based on provided margin values 2016-05-09 16:11:51 -05:00
Arik Fraimovich
cfb852e9c5 Add missing import. 2016-05-08 22:29:36 +03:00
Arik Fraimovich
d5c6e57c62 Optionally get org_slug from g. 2016-05-08 10:41:56 +03:00
Arik Fraimovich
2924d4fce6 Merge pull request #1040 from getredash/fix/visaulizations
Small fixes to visualizations view
2016-05-08 10:39:37 +03:00
Arik Fraimovich
e602b8cf2b Don't render visualizations in the background 2016-05-08 10:34:30 +03:00
Arik Fraimovich
0b806e2e7d Fix: link in pivot tab was broken 2016-05-08 10:34:14 +03:00
yohei.naruse
c3c302e11e modify indent size 2016-05-08 12:43:42 +09:00
Arik Fraimovich
aa837ed09b Update for new design 2016-05-07 22:20:06 +03:00
Arik Fraimovich
f07e7273c1 Fix: add destination to list of static routes 2016-05-07 22:19:53 +03:00
Arik Fraimovich
9b6f555d76 Update alert task to use destinations 2016-05-07 17:58:15 +03:00
Arik Fraimovich
e069374232 Merge w/ latest master 2016-05-07 17:49:49 +03:00
Arik Fraimovich
c496df3b87 Update ISSUE_TEMPLATE.md 2016-05-05 22:30:10 +03:00
Arik Fraimovich
2ee0065102 Merge pull request #1038 from getredash/docs-github
Add an ISSUE_TEMPLATE.md to direct people at the forum
2016-05-05 22:29:23 +03:00
Arik Fraimovich
c0ffea7083 Add an ISSUE_TEMPLATE.md to direct people at the forum 2016-05-05 22:28:04 +03:00
Arik Fraimovich
fec0d5fecc Merge pull request #1037 from dheerajrav/master
multifilter bug fix
2016-05-05 16:28:03 +03:00
dheerajrav
83a03a22b1 multifilter bug fix 2016-05-05 18:51:41 +05:30
Arik Fraimovich
8b5dc8ef68 Merge pull request #1036 from getredash/fixes_160504
Add optional block for more scripts in template
2016-05-05 11:26:22 +03:00
Arik Fraimovich
f3a274a5c0 Add optional block for more scripts in template 2016-05-05 11:26:02 +03:00
Arik Fraimovich
386d6efdaa Merge pull request #1035 from whummer/feat/test_params_on_embeds
Add test case for embeds with parameters
2016-05-05 09:56:56 +03:00
Waldemar Hummer
e415189017 add test case for embeds with parameters; minor fix in embeds.py 2016-05-05 11:15:49 +10:00
Arik Fraimovich
b066ce4b74 Merge pull request #1033 from getredash/fixes_160504
Fix: only ask for notification permissions if wasn't denied
2016-05-04 17:03:26 +03:00
Arik Fraimovich
056ae4f63e Fix: only ask for notification permissions if wasn't denied 2016-05-04 16:57:58 +03:00
Arik Fraimovich
6d495d2f2c Merge pull request #1014 from whummer/feat/params_on_embeds
Add server-side parameter handling for embeds
2016-05-04 16:56:35 +03:00
Arik Fraimovich
960c416fcb Merge pull request #1032 from getredash/fixes_160504
Fix: make sure we return dashboards only for current org only
2016-05-04 16:54:25 +03:00
Arik Fraimovich
f7322a413f Merge pull request #1021 from ariarijp/documentation-improvement
Vagrant docs: add purging the cache step
2016-05-04 16:49:58 +03:00
Arik Fraimovich
d9cc063be2 Fix: make sure we return dashboards only for current org 2016-05-04 16:32:49 +03:00
Arik Fraimovich
8fa6fdb0d5 Merge pull request #1030 from getredash/fixes_160504
Make sure data sources list ordered by id
2016-05-04 12:12:17 +03:00
Arik Fraimovich
7016477700 Restore support for forwarding events 2016-05-04 12:07:59 +03:00
Arik Fraimovich
0bb722df5d Make sure data sources ordered by id 2016-05-04 12:03:47 +03:00
Arik Fraimovich
b3844d3643 Merge pull request #1029 from getredash/fixes_160504
Hive: close connection only if it exists
2016-05-04 10:42:59 +03:00
Arik Fraimovich
e32bfe3db7 Hive: close connection only if it exists 2016-05-04 10:40:53 +03:00
Waldemar Hummer
4591eff557 add server-side parameter handling for embeds 2016-05-03 10:49:01 +10:00
Arik Fraimovich
7062873cd1 Add note re. GCE image version 2016-05-02 12:48:43 +03:00
Lloyd Weehuizen
203cf6e28b Style updates 2016-05-02 10:18:11 +12:00
Arik Fraimovich
9e23cc2bf2 update version references 2016-05-01 15:49:39 +03:00
ariarijp
c5d92b4e7e Add purging the cache step 2016-05-01 21:37:51 +09:00
Arik Fraimovich
41dfcd8cbf Bump version. 2016-05-01 11:18:32 +03:00
yohei.naruse
1fa701c136 apply reviews. 2016-04-28 11:43:01 +09:00
Kumar Vora
303e158eb1 Issue#1006: allowing user to change bottom margin for Chart visualizations 2016-04-22 13:16:00 -05:00
yohei.naruse
19aaa938d8 manage.py support group operations. 2016-04-10 22:08:36 +09:00
Lloyd Weehuizen
c850acb3b9 Extend ElasticSearch query_runner to support aggregations 2016-04-01 14:06:54 +13:00
Alex DeBrie
4bcb705a2a Hide user subscriptions if email is not enabled 2016-03-24 14:28:47 +00:00
Alex DeBrie
1c04f3cc29 Fix broken tests 2016-03-24 13:55:46 +00:00
Alex DeBrie
ee29f07802 Clean up after rebase 2016-03-24 02:46:59 +00:00
Alex DeBrie
df2067eec1 Make subscription display more clear 2016-03-23 20:57:19 +00:00
Alex DeBrie
601010e44e Remove email destination from AlertSubscription migration 2016-03-23 20:57:19 +00:00
Alex DeBrie
6c3b713b3d Add destination subscriptions to Alert page 2016-03-23 20:57:19 +00:00
Alex DeBrie
faf2f7dede Add user subscriptions back to Alert page 2016-03-23 20:57:19 +00:00
Alex DeBrie
bf880a834b Move notify to AlertSubscription; adjust email destination to take multiple addresses 2016-03-23 20:57:19 +00:00
Alex DeBrie
ce6ceac5c4 Add hipchat to default destinations 2016-03-23 20:57:19 +00:00
Alex DeBrie
70b4f9d447 Create dynamicForm directive 2016-03-23 20:57:18 +00:00
Alex DeBrie
3838b03417 Clean up tasks.py to remove unneeded code 2016-03-23 20:52:50 +00:00
Alex DeBrie
a11fa2717d Add migrations of existing alerts 2016-03-23 20:51:10 +00:00
Alex DeBrie
becf315e66 Add hipchat destination 2016-03-23 20:51:10 +00:00
Alex DeBrie
04eb37a7f2 Pass objects instead of IDs to notify method 2016-03-23 20:51:10 +00:00
Alex DeBrie
e91610f4b4 Remove hacky backwards compatibility for notifications 2016-03-23 20:51:10 +00:00
Alex DeBrie
63786c98df Remove unnecessary code 2016-03-23 20:51:10 +00:00
Alex DeBrie
54f3df6988 Move destination icons to BaseDestination 2016-03-23 20:49:20 +00:00
Alex DeBrie
bb3874e631 Pass subscription user to notify method 2016-03-23 20:49:20 +00:00
Alex DeBrie
eef18510d5 Fix email destination bugs; make email alerts backward compatible 2016-03-23 20:49:20 +00:00
Alex DeBrie
a3c0917d85 Fix AlertSubscription unsubscribe permissions and broken tests 2016-03-23 20:49:20 +00:00
Alex DeBrie
ed7f9ea5f0 Create UI for adding/removing alert subscriptions 2016-03-23 20:49:20 +00:00
Alex DeBrie
82b7146216 Enforce AlertSubscription uniqueness; update AlertSubscription delete handler 2016-03-23 20:49:20 +00:00
Alex DeBrie
3cfbb9855b Update notify logic in tasks and add destinations for Slack, email, and webhooks 2016-03-23 20:49:20 +00:00
Alex DeBrie
4938f8e013 Add ability to choose destination when creating alert 2016-03-23 20:47:50 +00:00
Alex DeBrie
a43761da39 Require destination_id in POSTs that create a subscription 2016-03-23 20:47:50 +00:00
Alex DeBrie
a3703b2058 Fix broken tests 2016-03-23 20:47:08 +00:00
Alex DeBrie
f2d5d52310 Remove destination groups; use ConfigurationContainer for options; Add user field on NotificationDestination 2016-03-23 20:44:14 +00:00
Alex DeBrie
eed2a41816 Add destination field to AlertSubscription 2016-03-23 20:39:22 +00:00
Alex DeBrie
16c0df4117 Group handlers for Destinations 2016-03-23 20:38:33 +00:00
Alex DeBrie
3844483776 Add destination elements to rd_ui 2016-03-23 20:35:50 +00:00
Alex DeBrie
53f8f1de3b Fix typo 2016-03-23 20:24:26 +00:00
Alex DeBrie
3ac7f02aea Add NotificationDestination model and handlers; Add BaseNotification class 2016-03-23 20:24:26 +00:00
229 changed files with 7979 additions and 3417 deletions

3
.bowerrc Normal file
View File

@@ -0,0 +1,3 @@
{
"directory": "rd_ui/app/bower_components"
}

24
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,24 @@
Welcome to Redash's GitHub repo! 👋🎉
Do you need help or have a question? Checkout the Support category in our discussion forum: https://discuss.redash.io/c/support.
Got an idea for a new feature? Check if it isn't on the roadmap already: http://bit.ly/redash-roadmap and start a new discussion in the features category: https://discuss.redash.io/c/feature-requests 🌟.
Found a bug? Please fill out the sections below... thank you 👍
### Issue Summary
A summary of the issue and the browser/OS environment in which it occurs.
### Steps to Reproduce
1. This is the first step
2. This is the second step, etc.
Any other info e.g. Why do you consider this to be a bug? What did you expect to happen instead?
### Technical details:
* Redash Version:
* Browser/OS:
* How did you install Redash:

6
.gitignore vendored
View File

@@ -22,3 +22,9 @@ dump.rdb
# Docker related
docker-compose.yml
node_modules
.tmp
.sass-cache
rd_ui/app/bower_components
npm-debug.log

196
CHANGELOG.md Normal file
View File

@@ -0,0 +1,196 @@
# Change Log
## v0.12.0 - 2016-11-20
### Added
61fe16e #1374: Add: allow '*' in REDASH_CORS_ACCESS_CONTROL_ALLOW_ORIGIN (Allen Short)
2f09043 #1113: Add: share modify/access permissions for queries and dashboard (whummer)
3db0eea #1341: Add: support for specifying SAML nameid-format (zoetrope)
b0ecd0e #1343: Add: support for local SAML metadata file (zoetrope)
0235d37 #1335: Add: allow changing alert email subject. (Arik Fraimovich)
2135dfd #1333: Add: control over y axis min/max values (Arik Fraimovich)
49e788a #1328: Add: support for snapshot generation service (Arik Fraimovich)
229ca6c #1323: Add: collect runtime metrics for Celery tasks (Arik Fraimovich)
931a1f3 #1315: Add: support for loading BigQuery schema (Arik Fraimovich)
39b4f9a #1314: Add: support MongoDB SSL connections (Arik Fraimovich)
ca1ca9b #1312: Add: additional configuration for Celery jobs (Arik Fraimovich)
fc00e61 #1310: Add: support for date/time with seconds parameters (Arik Fraimovich)
d72a198 #1307: Add: API to force refresh data source schema (Arik Fraimovich)
beb89ec #1305: Add: UI to edit dashboard text box widget (Kazuhito Hokamura)
808fdd4 #1298: Add: JIRA (JQL) query runner (Arik Fraimovich)
ff9e844 #1280: Add: configuration flag to disable scheduled queries (Hirotaka Suzuki)
ef4699a #1269: Add: Google Drive federated tables support in BigQuery query runner (Kurt Gooden)
2eeb947 #1236: Add: query runner for Cassandra and ScyllaDB (syerushalmy)
10b398e #1249: Add: override slack webhook parameters (mystelynx)
2b5e340 #1252: Add: Schema loading support for Presto query runner (using information_schema) (Rohan Dhupelia)
2aaf5dd #1250: Add: query snippets feature (Arik Fraimovich)
8d8af73 #1226: Add: Sankey visualization (Arik Fraimovich)
a02edda #1222: Add: additional results format for sunburst visualization (Arik Fraimovich)
0e70188 #1213: Add: new sunburst sequence visualization (Arik Fraimovich)
9a6d2d7 #1204: Add: show views in schema browser for Vertica data sources (Matthew Carter)
600afa5 #1138: Add: ability to register user defined function (UDF) resources for BigQuery DataSource/Query (fabito)
b410410 #1166: Add: "every 14 days" refresh option (Arik Fraimovich)
### Changed
002f794 #1368: Change: added ability to disable auto update in admin views (Arik Fraimovich)
aa5d14e #1366: Change: improve error message for exception in the Python query runner (deecay)
880627c #1355: Change: pass the user object to the run_query method (Arik Fraimovich)
23c605b #1342: SAML: specify entity id (zoetrope)
015b1dc #1334: Change: allow specifying recipient address when sending email test message (Arik Fraimovich)
39aaa2f #1292: Change: improvements to map visualization (Arik Fraimovich)
b22191b #1332: Change: upgrade Python packages (Arik Fraimovich)
23ba98b #1331: Celery: Upgrade Celery to more recent version. (Arik Fraimovich)
3283116 #1330: Change: upgrade Requests to latest version. (Arik Fraimovich)
39091e0 #1324: Change: add more logging and information for refresh schemas task (Arik Fraimovich)
462faea #1316: Change: remove deprecated settings (Arik Fraimovich)
73e1837 #1313: Change: more flexible column width calculation (Arik Fraimovich)
e8eb840 #1279: Change: update bootstrap.sh to support Ubuntu 16.04 (IllusiveMilkman)
8cf0252 #1262: Change: upgrade Plot.ly version and switch to smaller build (Arik Fraimovich)
0b79fb8 #1306: Change: paginate queries page & add explicit urls. (Arik Fraimovich)
41f99f5 #1299: Change: send Content-Type header (application/json) in query results responses (Tsuyoshi Tatsukawa)
dfb1a20 #1297: Change: update Slack configuration titles. (Arik Fraimovich)
8c1056c #1294: Change: don't annotate BigQuery queries (Arik Fraimovich)
a3cf92e #1289: Change: use key_as_string when available (ElasticSearch query runner) (Arik Fraimovich)
e155191 #1285: Change: do not display Oracle tablespace name in schema browser (Matthew Carter)
6cbc39c #1282: Change: deduplicate Google Spreadsheet columns (Arik Fraimovich)
4caf2e3 #1277: Set specific version of cryptography lib (Arik Fraimovich)
d22f0d4 #1216: Change: bootstrap.sh - use non interactive dist-upgrade (Atsushi Sasaki)
19530f4 #1245: Change: switch from CodeMirror to Ace editor (Arik Fraimovich)
dfb92db #1234: Change: MongoDB query runner set DB name as mandatory (Arik Fraimovich)
b750843 #1230: Change: annotate Presto queries with metadata (Noriaki Katayama)
5b20fe2 #1217: Change: install libffi-dev for Cryptography (Ubuntu setup script) (Atsushi Sasaki)
a9fac34 #1206: Change: update pymssql version to 2.1.3 (kitsuyui)
5d43cbe #1198: Change: add support for Standard SQL in BigQuery query runner (mystelynx)
84d0c22 #1193: Change: modify the argument order of moment.add function call (Kenya Yamaguchi)
### Fixed
d6febb0 #1375: Fix: Download Dataset does not work when not logged in (Joshua Dechant)
96553ad #1369: Fix: missing format call in Elasticsearch test method (Adam Griffiths)
c57c765 #1365: Fix: compare retrieval times in UTC timezone (Allen Short)
37dff5f #1360: Fix: connection test was broken for MySQL (ichihara)
360028c #1359: Fix: schema loading query for Hive was wrong for non default schema (laughingman7743)
7ee41d4 #1358: Fix: make sure all calls to run_query updated with new parameter (Arik Fraimovich)
0d94479 #1329: Fix: Redis memory leak. (Arik Fraimovich)
7145aa2 #1325: Fix: queries API was doing N+1 queries in most cases (Arik Fraimovich)
cd2e927 #1311: Fix: BoxPlot visualization wasn't rendering on a dashboard (Arik Fraimovich)
a562ce7 #1309: Fix: properly render checkboxes in dynamic forms (Arik Fraimovich)
d48192c #1308: Fix: support for Unicode columns name in Google Spreadsheets (Arik Fraimovich)
e42f93f #1283: Fix: schema browser was unstable after opening a table (Arik Fraimovich)
170bd65 #1272: Fix: TreasureData get_schema method was returning array instead of string as column name (ariarijp)
4710c41 #1265: Fix: refresh modal not working for unsaved query (Arik Fraimovich)
bc3a5ab #1264: Fix: dashboard refresh not working (Arik Fraimovich)
6202d09 #1240: Fix: when shared dashboard token not found, return 404 (Wesley Batista)
93aac14 #1251: Fix: autocomplete went crazy when database has no autocomplete. (Arik Fraimovich)
b8eca28 #1246: Fix: support large schemas in schema browser (Arik Fraimovich)
b781003 #1223: Fix: Alert: when hipchat Alert.name is multibyte character, occur error. (toyama0919)
0b928e6 #1227: Fix: Bower install fails in vagrant (Kazuhito Hokamura)
a411af2 #1232: Fix: don't show warning when query string (parameters value) changes (Kazuhito Hokamura)
3dbb5a6 #1221: Fix: sunburst didn't handle all cases of path lengths (Arik Fraimovich)
a7cc1ee #1218: Fix: updated result not being saved when changing query text. (Arik Fraimovich)
0617833 #1215: Fix: email alerts not working (Arik Fraimovich)
78f65b1 #1187: Fix: read only users receive the permission error modal in query view (Arik Fraimovich)
bba801f #1167: Fix the version of setuptools on bootstrap script for Ubuntu (Takuya Arita)
ce81d69 #1160: Fix indentation in docker-compose-example.yml (Hirofumi Wakasugi)
dd759fe #1155: Fix: make all configuration values of Oracle required (Arik Fraimovich)
### Docs
a69ee0c #1225: Fix: RST formatting of the Vagrant documentation (Kazuhito Hokamura)
03837c0 #1242: Docs: add warning re. quotes on column names and BigQuery (Ereli)
9a98075 #1255: Docs: add documentation for InfluxDB (vishesh92)
e0485de #1195: Docs: fix typo in maintenance page title (Antoine Augusti)
7681d3e #1164: Docs: update permission documentation (Daniel Darabos)
bcd3670 #1156: Docs: add SSL parameters to nginx configuration (Josh Cox)
## v0.11.1.b2095 - 2016-08-02
This is a hotfix release, which fixes an issue with email alerts in v0.11.0.
## v0.11.0.b2016 - 2016-07-03
The main features of this release are:
- Alert Destinations: ability to define multiple destinations for alert notifications (currently implemented: HipChat, Slack, Webhook and email).
- The long-awaited UI for query parameters (see example in #1069).
Also, this release includes numerous smaller features, improvements, and bug fixes.
A big thank you goes to all who contributed code and documentation in this release: @AntoineAugusti, @James226, @adamlwgriffiths, @alexdebrie, @anthony-coble, @ariarijp, @dheerajrav, @edwardsharp, @machira, @nabilblk, @ninneko, @ordd, @tomerben, @toru-takahashi, @vishesh92, @vorakumar and @whummer.
### Added
d5e5b24 #1136: Feature: add --org option to all relevant CLI commands. (@adamlwgriffiths)
87e25f2 #1129: Feature: support for JSON query formatting (Mongo, ElasticSearch) (@arikfr)
6bb2716 #1121: Show error when failing to communicate with server (@arikfr)
f21276e #1119: Feature: add UI to delete alerts (@arikfr)
8656540 #1069: Feature: UI for query parameters (@arikfr)
790128c #1067: Feature: word cloud visualization (@anthony-coble)
8b73a2b #1098: Feature: UI for alert destinations & new destination types (@alexdebrie)
1fbeb5d #1092: Add Heroku support (@adamlwgriffiths)
f64622d #1089: Add support for serialising UUID type within MSSQL #961 (@James226)
857caab #1085: Feature: API to pause a data source (@arikfr)
214aa3b #1060: Feature: support configuring user's groups with SAML (@vorakumar)
e20a005 #1007: Issue#1006: Make bottom margin editable for Chart visualization (@vorakumar)
6e0dd2b #1063: Add support for date/time Y axis (@tomerben)
b5a4a6b #979: Feature: Add CLI to edit group permissions (@ninneko)
6d495d2 #1014: Add server-side parameter handling for embeds (@whummer)
5255804 #1091: Add caching for queries used in embeds (@whummer)
### Changed
0314313 #1149: Presto QueryRunner supports tinyint and smallint (@toru-takahashi)
8fa6fdb #1030: Make sure data sources list ordered by id (@arikfr)
8df822e #1141: Make create data source button more prominent (@arikfr)
96dd811 #1127: Mark basic_auth_password as secret (@adamlwgriffiths)
ad65391 #1130: Improve Slack notification style (@AntoineAugusti)
df637e3 #1116: Return meaningful error when there is no cached result. (@arikfr)
65635ec #1102: Switch to HipChat V2 API (@arikfr)
14fcf01 #1072: Remove counter from the tasks Done tab (as it always shows 50). #1047 (@arikfr)
1a1160e #1062: DynamoDB: Better exception handling (@arikfr)
ed45dcb #1044: Improve vagrant flow (@staritza)
8b5dc8e #1036: Add optional block for more scripts in template (@arikfr)
### Fixed
dbd48e1 #1143: Fix: use the email input type where needed (@ariarijp)
7445972 #1142: Fix: dates in filters might be duplicated (@arikfr)
5d0ed02 #1140: Fix: Hive should use the enabled variable (@arikfr)
392627d #1139: Fix: Impala data source referencing wrong variable (@arikfr)
c5bfbba #1133: Fix: query scrolling issues (@vishesh92)
c01d266 #1128: Fix: visualization options not updating after changing type (@arikfr)
6bc0e7a #1126: Fix #669: save fails when doing partial save of new query (@arikfr)
3ce27b9 #1118: Fix: remove alerts for archived queries (@arikfr)
4fabaae #1117: Fix #1052: filter not working for date/time values (@arikfr)
c107c94 #1077: Fix: install needed dependencies to use Hive in Docker image (@nabilblk)
abc790c #1115: Fix: allow non integers in alert reference value (@arikfr)
4ec473c #1110: Fix #1109: mixed group permissions resulting in wrong permission (@arikfr)
1ca5262 #1099: Fix RST syntax for links (@adamlwgriffiths)
daa6c1c #1096: Fix typo in env variable VERSION_CHECK (@AntoineAugusti)
cd06d27 #1095: Fix: use create_query permission for new query button. (@ordd)
2bc0b27 #1061: Fix: area chart stacking doesn't work (@machira)
8c21e91 #1108: Remove potnetially concurrency not safe code form enqueue_query (@arikfr)
e831218 #1084: Fix #1049: duplicate alerts when data source belongs to multiple groups (@arikfr)
6edb0ca #1080: Fix typo (@jeffwidman)
64d7538 #1074: Fix: ElasticSearch wasn't using correct type names (@toyama0919)
3f90dd9 #1064: Fix: old task trackers were not really removed (@arikfr)
e10ecd2 #1058: Bring back filters if dashboard filters are enabled (@AntoineAugusti)
701035f #1059: Fix: DynamoDB having issues when setting host (@arikfr)
2924d4f #1040: Small fixes to visualizations view (@arikfr)
fec0d5f #1037: Fix: multi filter wasn't working with __ syntax (@dheerajrav)
b066ce4 #1033: Fix: only ask for notification permissions if wasn't denied (@arikfr)
960c416 #1032: Fix: make sure we return dashboards only for current org only (@arikfr)
b3844d3 #1029: Hive: close connection only if it exists (@arikfr)
### Docs
6bb09d8 #1146: Docs: add a link to settings documentation. (@adamlwgriffiths)
095e759 #1103: Docs: add section about monitoring (@AntoineAugusti)
e942486 #1090: Contributing Guide (@arikfr)
3037c4f #1066: Docs: command type-o fix. (@edwardsharp)
2ee0065 #1038: Add an ISSUE_TEMPLATE.md to direct people at the forum (@arikfr)
f7322a4 #1021: Vagrant docs: add purging the cache step (@ariarijp)
---
For older releases check the GitHub releases page:
https://github.com/getredash/redash/releases

77
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,77 @@
# Contributing Guide
Thank you for taking the time to contribute! :tada::+1:
The following is a set of guidelines for contributing to Redash. These are guidelines, not rules, please use your best judgement and feel free to propose changes to this document in a pull request.
## Quick Links:
- [Feature Roadmap](https://trello.com/b/b2LUHU7A/re-dash-roadmap)
- [Feature Requests](https://discuss.redash.io/c/feature-requests)
- [Gitter Chat](https://gitter.im/getredash/redash) or [Slack](https://slack.redash.io)
- [Documentation](https://redash.io/help/)
- [Blog](http://blog.redash.io/)
- [Twitter](https://twitter.com/getredash)
---
:star: If you already here and love the project, please make sure to press the Star button. :star:
---
## Table of Contents
[How can I contribute?](#how-can-i-contribute)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements / Feature Requests](#suggesting-enhancements--feature-requests)
- [Pull Requests](#pull-requests)
- [Documentation](#documentation)
- Design?
[Addtional Notes](#additional-notes)
- [Release Method](#release-method)
- [Code of Conduct](#code-of-conduct)
## How can I contribute?
### Reporting Bugs
When creating a new bug report, please make sure to:
- Search for existing issues first. If you find a previous report of your issue, please update the existing issue with additional information instead of creating a new one.
- If you are not sure if your issue is really a bug or just some configuration/setup problem, please start a discussion in [the support forum](https://discuss.redash.io/c/support) first. Unless you can provide clear steps to reproduce, it's probably better to start with a thread in the forum and later to open an issue.
- If you still decide to open an issue, please review the template and guidelines and include as much details as possible.
### Suggesting Enhancements / Feature Requests
If you would like to suggest an enchancement or ask for a new feature:
- Please check [the roadmap](https://trello.com/b/b2LUHU7A/re-dash-roadmap) for existing Trello card for what you want to suggest/ask. If there is, feel free to upvote it to signal interest or add your comments.
- If there is no existing card, open a thread in [the forum](https://discuss.redash.io/c/feature-requests) to start a discussion about what you want to suggest. Try to provide as much details and context as possible and include information about *the problem you want to solve* rather only *your proposed solution*.
### Pull Requests
- **Code contributions are welcomed**. For big changes or significant features, it's usually better to reach out first and discuss what you want to implement and how (we recommend reading: [Pull Request First](https://medium.com/practical-blend/pull-request-first-f6bb667a9b6#.ozlqxvj36)). This to make sure that what you want to implement is aligned with our goals for the project and that no one else is already working on it.
- Include screenshots and animated GIFs in your pull request whenever possible.
- Please add [documentation](#documentation) for new features or changes in functionality along with the code.
- Please follow existing code style. We use PEP8 for Python and sensible style for Javascript.
### Documentation
The project's documentation can be found at [https://redash.io/help/](https://redash.io/help/). The [documentation sources](https://github.com/getredash/website/tree/master/user-guide) are hosted on GitHub. To contribute edits / new pages, you can use GitHub's interface. Click the "Edit on GitHub" link on the documentation page to quickly open the edit interface.
## Additional Notes
### Release Method
We publish a stable release every ~2 months, although the goal is to get to a stable release every month. You can see the change log on [GitHub releases page](http://github.com/getredash/redash/releases).
Every build of the master branch updates the latest *RC release*. These releases are usually stable, but might contain regressions and therefore recommended for "advanced users" only.
When we release a new stable release, we also update the *latest* Docker image tag, the EC2 AMIs and GCE images.
## Code of Conduct
This project adheres to the Contributor Covenant [code of conduct](http://redash.io/community/code_of_conduct). By participating, you are expected to uphold this code. Please report unacceptable behavior to team@redash.io.

View File

@@ -6,7 +6,7 @@ RUN apt-get update && \
# Postgres client
libpq-dev \
# Additional packages required for data sources:
libssl-dev libmysqlclient-dev freetds-dev && \
libssl-dev libmysqlclient-dev freetds-dev libsasl2-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
@@ -14,7 +14,7 @@ RUN apt-get update && \
RUN useradd --system --comment " " --create-home redash
# Pip requirements for all data source types
RUN pip install -U setuptools && \
RUN pip install -U setuptools==23.1.0 && \
pip install supervisor==3.1.2
COPY . /opt/redash/current
@@ -32,7 +32,7 @@ RUN pip install -r requirements_all_ds.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 && \
rm -rf node_modules rd_ui/node_modules /home/redash/.npm /home/redash/.cache && \
apt-get purge -y nodejs && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

View File

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

2
Procfile.heroku Normal file
View File

@@ -0,0 +1,2 @@
web: ./manage.py runserver -d -r -p $PORT --host 0.0.0.0
worker: celery worker --app=redash.worker -c${REDASH_HEROKU_CELERY_WORKER_COUNT:-2} --beat -Q queries,celery,scheduled_queries

View File

@@ -1,56 +1,50 @@
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/old_img/redash_logo.png' width="200px"/>
<img title="Redash" 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/getredash/redash.png?circle-token=8a695aa5ec2cbfa89b48c275aea298318016f040'/>
</p>
[![Join the chat at https://gitter.im/getredash/redash](https://badges.gitter.im/getredash/redash.svg)](https://gitter.im/getredash/redash?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Documentation](https://img.shields.io/badge/docs-redash.io-brightgreen.svg)](http://docs.redash.io)
[![Documentation](https://img.shields.io/badge/docs-redash.io/help-brightgreen.svg)](https://redash.io/help/)
**_re:dash_** is our take on freeing the data within our company in a way that will better fit our culture and usage patterns.
**_Redash_** is our take on freeing the data within our company in a way that will better fit our culture and usage patterns.
Prior to **_re:dash_**, we tried to use traditional BI suites and discovered a set of bloated, technically challenged and slow tools/flows. What we were looking for was a more hacker'ish way to look at data, so we built one.
Prior to **_Redash_**, we tried to use traditional BI suites and discovered a set of bloated, technically challenged and slow tools/flows. What we were looking for was a more hacker'ish way to look at data, so we built one.
**_re:dash_** was built to allow fast and easy access to billions of records, that we process and collect using Amazon Redshift ("petabyte scale data warehouse" that "speaks" PostgreSQL).
Today **_re:dash_** has support for querying multiple databases, including: Redshift, Google BigQuery, PostgreSQL, MySQL, Graphite,
**_Redash_** was built to allow fast and easy access to billions of records, that we process and collect using Amazon Redshift ("petabyte scale data warehouse" that "speaks" PostgreSQL).
Today **_Redash_** has support for querying multiple databases, including: Redshift, Google BigQuery, PostgreSQL, MySQL, Graphite,
Presto, Google Spreadsheets, Cloudera Impala, Hive and custom scripts.
**_re:dash_** consists of two parts:
**_Redash_** consists of two parts:
1. **Query Editor**: think of [JS Fiddle](http://jsfiddle.net) for SQL queries. It's your way to share data in the organization in an open way, by sharing both the dataset and the query that generated it. This way everyone can peer review not only the resulting dataset but also the process that generated it. Also it's possible to fork it and generate new datasets and reach new insights.
2. **Dashboards/Visualizations**: once you have a dataset, you can create different visualizations out of it, and then combine several visualizations into a single dashboard. Currently it supports charts, pivot table and cohorts.
**_re:dash_** is a work in progress and has its rough edges and way to go to fulfill its full potential. The Query Editor part is quite solid, but the visualizations need more work to enrich them and to make them more user friendly.
## Demo
<img src="https://cloud.githubusercontent.com/assets/71468/12611424/1faf4d6a-c4f5-11e5-89b5-31efc1155d2c.gif" width="60%"/>
<img src="https://cloud.githubusercontent.com/assets/71468/17391289/8e83878e-5a1d-11e6-8938-af9054a33b19.gif" width="60%"/>
You can try out the demo instance: http://demo.redash.io/ (login with any Google account).
## Getting Started
* [Setting up re:dash instance](http://redash.io/deployment/setup.html) (includes links to ready made AWS/GCE images).
* [Documentation](http://docs.redash.io).
* [Setting up Redash instance](https://redash.io/help-onpremise/setup/setting-up-redash-instance.html) (includes links to ready made AWS/GCE images).
* [Documentation](https://redash.io/help/).
## Getting Help
* Issues: https://github.com/getredash/redash/issues
* Mailing List: https://groups.google.com/forum/#!forum/redash-users
* Discussion Forum: https://discuss.redash.io/
* Slack: http://slack.redash.io/
* Gitter (chat): https://gitter.im/getredash/redash
## Reporting Bugs and Contributing Code
* Want to report a bug or request a feature? Please open [an issue](https://github.com/getredash/redash/issues/new).
* Want to help us build **_re:dash_**? Fork the project, edit in a [dev environment](http://docs.redash.io/en/latest/dev/vagrant.html), and make a pull request. We need all the help we can get!
* Want to help us build **_Redash_**? Fork the project, edit in a [dev environment](https://redash.io/help-onpremise/setup/setting-up-development-environment-using-vagrant.html), and make a pull request. We need all the help we can get!
## License
See [LICENSE](https://github.com/getredash/redash/blob/master/LICENSE) file.
BSD-2-Clause.

4
Vagrantfile vendored
View File

@@ -8,4 +8,8 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "redash/dev"
config.vm.synced_folder "./", "/opt/redash/current"
config.vm.network "forwarded_port", guest: 5000, host: 9001
config.vm.provision "shell" do |s|
s.inline = "/opt/redash/current/setup/vagrant/provision.sh"
s.privileged = false
end
end

18
bin/pre_compile Normal file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Heroku pre_compile script
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
pushd $DIR/..
# heroku requires cffi to be in requirements.txt in order for libffi to be installed.
# https://github.com/heroku/heroku-buildpack-python/blob/master/bin/steps/cryptography
# to avoid making it a requirement for other build systems, we'll inject it now
# into the requirements.txt file
# Remove Heroku unsupported Python packages:
grep -v -E "^(pymssql|thrift|sasl|pyhive)" requirements_all_ds.txt >> requirements.txt
# make the heroku Procfile the active one
cp Procfile.heroku Procfile
popd

21
bin/vagrant_ctl.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/bash
set -e
help() {
echo "Usage: "
echo "`basename "$0"` {start, test}"
}
case "$1" in
start)
vagrant up
vagrant ssh -c "cd /opt/redash/current; bin/run honcho start -f Procfile.dev;"
;;
test)
vagrant up
vagrant ssh -c "cd /opt/redash/current; make test"
;;
*)
help
;;
esac

View File

@@ -1,6 +1,6 @@
{
"name": "rdUi",
"version": "0.1.0",
"name": "redash",
"version": "0.11.0",
"dependencies": {
"angular": "1.2.18",
"angular-resource": "1.2.18",
@@ -12,7 +12,6 @@
"es5-shim": "2.0.8",
"angular-moment": "0.10.3",
"moment": "~2.8.0",
"codemirror": "4.8.0",
"underscore": "1.5.1",
"pivottable": "2.0.2",
"cornelius": "https://github.com/restorando/cornelius.git",
@@ -35,7 +34,10 @@
"angular-ui-sortable": "~0.13.4",
"angular-resizable": "^1.2.0",
"material-design-iconic-font": "^2.2.0",
"plotly.js": "^1.9.0"
"plotly.js": "~1.16.0",
"angular-ui-ace": "bower",
"angular-vs-repeat": "^1.1.7",
"leaflet.markercluster": "^0.5.0"
},
"devDependencies": {
"angular-mocks": "1.2.18",

View File

@@ -3,7 +3,7 @@ machine:
- docker
node:
version:
0.12.4
6.1.0
python:
version:
2.7.3
@@ -14,14 +14,14 @@ dependencies:
- pip install pymongo==3.2.1
- if [ "$CIRCLE_BRANCH" = "master" ]; then make deps; fi
cache_directories:
- rd_ui/node_modules/
- node_modules/
- rd_ui/app/bower_components/
test:
override:
- nosetests --with-xunit --xunit-file=$CIRCLE_TEST_REPORTS/junit.xml --with-coverage --cover-package=redash tests/
deployment:
github_and_docker:
branch: [master, /hotfix_.*/]
branch: master
commands:
- make pack
- make upload

View File

@@ -16,7 +16,7 @@ redis:
postgres:
image: postgres:9.3
volumes:
- /opt/postgres-data:/var/lib/postgresql/data
- /opt/postgres-data:/var/lib/postgresql/data
redash-nginx:
image: redash/nginx:latest
ports:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,141 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,35 +0,0 @@
Permissions Model
#################
In version 0.9.0 we introduced a new permissions model based on groups. Each user by default joins the ``Default`` group, but
can be a member of any number of groups.
Group membership defines the actions you're allowed to take (although currently there is no UI to edit group action permissions),
but also what data sources you have access to (for this we have UI).
How does it work?
=================
* Each user belongs to one or more groups. By default each user joins the ``Default`` group. So the common
data sources, should be associated with this group.
* Each data source will be associated with one or more groups. Each connection to a group will define,
whether this group has full access to this data source (view existing queries and run new ones) or view only access,
which allows only viewing existing queries and results.
* Any dashboard can contain visualizations from any data source (as long as the creating user has access to them). When
a user who doesn't have access to some visualization (because he doesn't have access to the data source) opens a dashboard,
he will see that there is a visualization there but won't see the details.
.. figure:: https://cloud.githubusercontent.com/assets/71468/12002946/dc5032ca-ab16-11e5-90e7-aae9234a596b.png
Dashboard widget with a visualization the user doesn't have access to.
In current implementation all the users see a list of all the dashboards. Once `pull request #957 <https://github.com/getredash/redash/pull/957>`__
gets merged, we will filter out dashboards from the list that the user has no access to any of their widgets.
What if I want to limit the user to only some tables?
=====================================================
The idea is to leverage your database's security model, and hence create a user with access to the tables/columns you
want to give access to. Create a data source that is using this user and then associate it with a group of users who need
this level of access.

View File

@@ -1,44 +0,0 @@
Special Features
#################
Re:dash has a lot of very useful features and most of them can be found easily when using the UI. This page features the less well-known ones.
Queries
========
It is possible to have filters for query results and visualizations. Thanks to filters, you can restrain the result to a certain or multiple values. Filters are enabled by following a naming convention for columns.
If you want to focus only on a specific value, you will need to alias your column to ``<columnName>::filter``. Here is an example:
.. code:: sql
select action as "action::filter", count (0) as "actions count"
from events
group by action
You can see this query and the rendered UI `here <http://demo.redash.io/queries/143/source#table>`_.
If you are interested in multi filters (meaning that you can select multiple values), you will need to alias your column to ``<columnName>::multi-filter``. Here is an example:
.. code:: sql
select action as "action::multi-filter", count (0) as "actions count"
from events
group by action
You can see this query and the rendered UI `here <http://demo.redash.io/queries/144/source#table>`_.
Note that you can use ``__filter`` or ``__multiFilter`` if your database doesn't support ``::`` in column names (such as BigQuery).
Dashboards
==========
It is possible to group multiple dashboards in the dashboards menu. To do this, you need to follow a naming convention by using a column (``:``) to separate the dashboard group and the actual dashboard name. For example, if you name 2 dashboards ``Foo: Bar`` and ``Foo: Baz``, they will be grouped under the ``Foo`` namespace in the dropdown menu.
If you've got queries that have some filters and you want to apply filters at the dashboard level (that apply to all queries), you need to set a flag. Can you do it through the admin interface at ``/admin/dashboard`` or you can do it by manually setting the column ``dashboard_filters_enabled`` of the table ``dashboards`` to ``TRUE`` in the Re:dash database.
Exporting query results to CSV or JSON
======================================
Query results can be automatically exported to CSV or JSON by using your API key. Your API key can be found when viewing your profile, from the top right menu in the navigation bar.
The format of the URL is the following: ``https://<redash_domain>/api/queries/<query_id>/results.(csv|json)?api_key=<your_api_key>``. Here is a working example: `<http://demo.redash.io/api/queries/63/results.json?api_key=874fcd93ce4b6ef87a9aad41c712bcd5d17cdc8f>`_.
Using this URL you can easily import query results directly into Google Spreadsheets, using the ``importdata`` function. For example: ``=importdata("...")``.

View File

@@ -7,17 +7,28 @@ var lazypipe = require('lazypipe');
var rimraf = require('rimraf');
var wiredep = require('wiredep').stream;
var runSequence = require('run-sequence');
var map = require('lodash.map');
var yeoman = {
app: 'app',
dist: 'dist'
app: 'rd_ui/app',
dist: 'rd_ui/dist'
};
function applyAppPath(p) {
if (typeof p === 'string') {
return yeoman.app + p;
} else {
return map(p, function (path) {
return applyAppPath(path);
});
}
}
var paths = {
scripts: [yeoman.app + '/scripts/**/*.js'],
styles: [yeoman.app + '/styles/**/*.css'],
views: {
main: [yeoman.app + '/index.html', 'app/vendor_scripts.html', 'app/login.html', 'app/embed.html', 'app/public.html', 'app/app_layout.html', 'app/signed_out_layout.html'],
main: applyAppPath(['/index.html', '/vendor_scripts.html', '/login.html', '/embed.html', '/public.html', '/app_layout.html', '/signed_out_layout.html']),
files: [yeoman.app + '/views/**/*.html']
}
};
@@ -94,7 +105,7 @@ gulp.task('html', function () {
});
gulp.task('images', function () {
return gulp.src(yeoman.app + '/images/**/*')
return gulp.src(applyAppPath(['/images/**/*']))
.pipe($.cache($.imagemin({
optimizationLevel: 5,
progressive: true,
@@ -103,18 +114,28 @@ gulp.task('images', function () {
.pipe(gulp.dest(yeoman.dist + '/images'));
});
gulp.task('leaflet', function () {
return gulp.src(applyAppPath(['/bower_components/leaflet/dist/images/**/*']))
.pipe($.cache($.imagemin({
optimizationLevel: 5,
progressive: true,
interlaced: true
})))
.pipe(gulp.dest(yeoman.dist + '/styles/images'));
});
gulp.task('copy:extras', function () {
return gulp.src([yeoman.app + '/*/.*', 'app/google_login.png', 'favicon.ico', 'robots.txt'], { dot: true })
return gulp.src(applyAppPath(['/*/.*', '/google_login.png', '/favicon.ico', '/robots.txt']), { dot: true })
.pipe(gulp.dest(yeoman.dist));
});
gulp.task('copy:fonts', function () {
return gulp.src([yeoman.app + '/fonts/**/*', 'app/bower_components/font-awesome/fonts/*', 'app/bower_components/material-design-iconic-font/dist/fonts/*'])
return gulp.src(applyAppPath(['/fonts/**/*', '/bower_components/font-awesome/fonts/*', '/bower_components/material-design-iconic-font/dist/fonts/*']))
.pipe(gulp.dest(yeoman.dist + '/fonts'));
});
gulp.task('build', ['clean:dist'], function () {
runSequence(['images', 'copy:extras', 'copy:fonts', 'client:build']);
runSequence(['images', 'leaflet', 'copy:extras', 'copy:fonts', 'client:build']);
});
gulp.task('default', ['build']);

View File

@@ -8,12 +8,13 @@ from flask_script import Manager
from redash import settings, models, __version__
from redash.wsgi import app
from redash.cli import users, database, data_sources, organization
from redash.cli import users, groups, 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("groups", groups.manager)
manager.add_command("ds", data_sources.manager)
manager.add_command("org", organization.manager)
@@ -46,12 +47,16 @@ def check_settings():
for name, item in settings.all_settings().iteritems():
print "{} = {}".format(name, item)
@manager.command
def send_test_mail():
@manager.option('email', default=None, help="Email address to send test message to (default: the address you defined in MAIL_DEFAULT_SENDER)")
def send_test_mail(email=None):
from redash import mail
from flask_mail import Message
mail.send(Message(subject="Test Message from re:dash", recipients=[settings.MAIL_DEFAULT_SENDER], body="Test message."))
if email is None:
email = settings.MAIL_DEFAULT_SENDER
mail.send(Message(subject="Test Message from re:dash", recipients=[email], body="Test message."))
if __name__ == '__main__':

View File

@@ -1,7 +1,11 @@
import os
from redash.models import db, Organization, Group
from redash import settings
from playhouse.migrate import PostgresqlMigrator, migrate
# The following is deprecated and should be defined with the Organization object
GOOGLE_APPS_DOMAIN = settings.set_from_string(os.environ.get("REDASH_GOOGLE_APPS_DOMAIN", ""))
if __name__ == '__main__':
migrator = PostgresqlMigrator(db.database)
@@ -9,7 +13,7 @@ if __name__ == '__main__':
Organization.create_table()
default_org = Organization.create(name="Default", slug='default', settings={
Organization.SETTING_GOOGLE_APPS_DOMAINS: list(settings.GOOGLE_APPS_DOMAIN)
Organization.SETTING_GOOGLE_APPS_DOMAINS: list(GOOGLE_APPS_DOMAIN)
})
column = Group.org

View File

@@ -0,0 +1,89 @@
import os
import peewee
from redash.models import db, NotificationDestination, AlertSubscription, Alert, Organization, User
from redash.destinations import get_configuration_schema_for_destination_type
from redash.utils.configuration import ConfigurationContainer
from playhouse.migrate import PostgresqlMigrator, migrate
HIPCHAT_API_TOKEN = os.environ.get('REDASH_HIPCHAT_API_TOKEN', None)
HIPCHAT_API_URL = os.environ.get('REDASH_HIPCHAT_API_URL', None)
HIPCHAT_ROOM_ID = os.environ.get('REDASH_HIPCHAT_ROOM_ID', None)
WEBHOOK_ENDPOINT = os.environ.get('REDASH_WEBHOOK_ENDPOINT', None)
WEBHOOK_USERNAME = os.environ.get('REDASH_WEBHOOK_USERNAME', None)
WEBHOOK_PASSWORD = os.environ.get('REDASH_WEBHOOK_PASSWORD', None)
if __name__ == '__main__':
migrator = PostgresqlMigrator(db.database)
with db.database.transaction():
if not NotificationDestination.table_exists():
NotificationDestination.create_table()
# Update alert subscription fields
migrate(
migrator.add_column('alert_subscriptions', 'destination_id', AlertSubscription.destination)
)
try:
org = Organization.get_by_slug('default')
user = User.select().where(User.org==org, peewee.SQL("%s = ANY(groups)", org.admin_group.id)).get()
except Exception:
print "!!! Warning: failed finding default organization or admin user, won't migrate Webhook/HipChat alert subscriptions."
exit()
if WEBHOOK_ENDPOINT:
# Have all existing alerts send to webhook if already configured
schema = get_configuration_schema_for_destination_type('webhook')
conf = {'url': WEBHOOK_ENDPOINT}
if WEBHOOK_USERNAME:
conf['username'] = WEBHOOK_USERNAME
conf['password'] = WEBHOOK_PASSWORD
options = ConfigurationContainer(conf, schema)
webhook = NotificationDestination.create(
org=org,
user=user,
name="Webhook",
type="webhook",
options=options
)
for alert in Alert.select():
AlertSubscription.create(
user=user,
destination=webhook,
alert=alert
)
if HIPCHAT_API_TOKEN:
# Have all existing alerts send to HipChat if already configured
schema = get_configuration_schema_for_destination_type('hipchat')
conf = {}
if HIPCHAT_API_URL:
conf['url'] = '{url}/room/{room_id}/notification?auth_token={token}'.format(
url=HIPCHAT_API_URL, room_id=HIPCHAT_ROOM_ID, token=HIPCHAT_API_TOKEN)
else:
conf['url'] = 'https://hipchat.com/v2/room/{room_id}/notification?auth_token={token}'.format(
room_id=HIPCHAT_ROOM_ID, token=HIPCHAT_API_TOKEN)
options = ConfigurationContainer(conf, schema)
hipchat = NotificationDestination.create(
org=org,
user=user,
name="HipChat",
type="hipchat",
options=options
)
for alert in Alert.select():
AlertSubscription.create(
user=user,
destination=hipchat,
alert=alert
)
db.close_db(None)

View File

@@ -0,0 +1,10 @@
from redash.models import db, Query
from playhouse.migrate import PostgresqlMigrator, migrate
if __name__ == '__main__':
migrator = PostgresqlMigrator(db.database)
with db.database.transaction():
migrate(
migrator.add_column('queries', 'options', Query.options),
)

View File

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

View File

@@ -0,0 +1,22 @@
from redash.models import db, Change, AccessPermission, Query, Dashboard
from playhouse.migrate import PostgresqlMigrator, migrate
if __name__ == '__main__':
if not Change.table_exists():
Change.create_table()
if not AccessPermission.table_exists():
AccessPermission.create_table()
migrator = PostgresqlMigrator(db.database)
try:
migrate(
migrator.add_column('queries', 'version', Query.version),
migrator.add_column('dashboards', 'version', Dashboard.version)
)
except Exception as ex:
print "Error while adding version column to queries/dashboards. Maybe it already exists?"
print ex

View File

@@ -0,0 +1,4 @@
from redash import redis_connection
if __name__ == '__main__':
redis_connection.delete('query_task_trackers')

View File

@@ -26,7 +26,8 @@
"gulp-print": "^2.0.1",
"gulp-rev-all": "^0.8.22",
"bower": "~1.7.1",
"gulp-cli": "~1.2.0"
"gulp-cli": "~1.2.0",
"lodash.map": "^4.4.0"
},
"engines": {
"node": ">=0.10.0"
@@ -34,7 +35,8 @@
"scripts": {
"test": "echo 'No tests.'",
"build": "gulp build",
"bower": "bower"
"bower": "bower",
"heroku-postbuild": "npm install --dev && npm run bower install && npm run build && npm prune --production"
},
"dependencies": {
}

View File

@@ -1,3 +0,0 @@
{
"directory": "app/bower_components"
}

View File

@@ -1 +0,0 @@
* text=auto

4
rd_ui/.gitignore vendored
View File

@@ -1,4 +0,0 @@
node_modules
.tmp
.sass-cache
app/bower_components

View File

@@ -1,6 +0,0 @@
language: node_js
node_js:
- '0.10'
before_script:
- 'npm install -g bower grunt-cli'
- 'bower install'

View File

@@ -12,16 +12,16 @@
<!-- build:css /styles/main.css -->
<link rel="stylesheet" href="/styles/superflat_redash.css">
<link rel="stylesheet" href="/bower_components/material-design-iconic-font/dist/css/material-design-iconic-font.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="/bower_components/angular-resizable/src/angular-resizable.css">
<link rel="stylesheet" href="/bower_components/leaflet.markercluster/dist/MarkerCluster.css">
<link rel="stylesheet" href="/bower_components/leaflet.markercluster/dist/MarkerCluster.Default.css">
<link rel="stylesheet" href="/styles/redash.css">
<!-- endbuild -->
@@ -64,7 +64,9 @@
{% include 'vendor_scripts.html' %}
<!-- build:js({.tmp,app}) /scripts/scripts.js -->
<!-- build:js({.tmp,rd_ui/app}) /scripts/scripts.js -->
<script src="/scripts/vendor/cloud.js"></script>
<script src="/scripts/vendor/d3.sankey.js"></script>
<script src="/scripts/app.js"></script>
<script src="/scripts/services/services.js"></script>
<script src="/scripts/services/resources.js"></script>
@@ -74,9 +76,11 @@
<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/destinations.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/controllers/snippets.js"></script>
<script src="/scripts/visualizations/base.js"></script>
<script src="/scripts/visualizations/chart.js"></script>
<script src="/scripts/visualizations/cohort.js"></script>
@@ -86,10 +90,11 @@
<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/visualizations/wordcloud.js"></script>
<script src="/scripts/visualizations/sunburst_sequence.js"></script>
<script src="/scripts/visualizations/sankey.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>

View File

@@ -11,15 +11,15 @@
<!-- build:css /styles/embed.css -->
<link rel="stylesheet" href="/styles/superflat_redash.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/material-design-iconic-font/dist/css/material-design-iconic-font.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="/bower_components/leaflet.markercluster/dist/MarkerCluster.css">
<link rel="stylesheet" href="/bower_components/leaflet.markercluster/dist/MarkerCluster.Default.css">
<link rel="stylesheet" href="/styles/redash.css">
<!-- endbuild -->
@@ -37,7 +37,9 @@
{% include 'vendor_scripts.html' %}
<!-- build:js({.tmp,app}) /scripts/embed-scripts.js -->
<!-- build:js({.tmp,rd_ui/app}) /scripts/embed-scripts.js -->
<script src="/scripts/vendor/cloud.js"></script>
<script src="/scripts/vendor/d3.sankey.js"></script>
<script src="/scripts/embed.js"></script>
<script src="/scripts/services/services.js"></script>
<script src="/scripts/services/resources.js"></script>
@@ -59,10 +61,11 @@
<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/visualizations/wordcloud.js"></script>
<script src="/scripts/visualizations/sunburst_sequence.js"></script>
<script src="/scripts/visualizations/sankey.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>

BIN
rd_ui/app/favicon.ico Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,7 +1,5 @@
{% extends 'app_layout.html' %}
{% block content %}
<app-header></app-header>
<edit-dashboard-form dashboard="newDashboard" id="new_dashboard_dialog"></edit-dashboard-form>
{% endblock %}

View File

@@ -1,5 +1,5 @@
{% extends "signed_out_layout.html" %}
{% block title %}Login{% endblock %}
{% block title %}Login | Redash{% endblock %}
{% block content %}
{% with messages = get_flashed_messages() %}
{% if messages %}

View File

@@ -14,10 +14,12 @@ angular.module('redash', [
'ngResource',
'ngRoute',
'ui.select',
'ui.ace',
'naif.base64',
'ui.bootstrap.showErrors',
'angularResizable',
'ngSanitize'
'ngSanitize',
'vs-repeat'
]).config(['$routeProvider', '$locationProvider', '$compileProvider', 'growlProvider', 'uiSelectConfig', '$httpProvider',
function ($routeProvider, $locationProvider, $compileProvider, growlProvider, uiSelectConfig, $httpProvider) {
function getQuery(Query, $route) {
@@ -71,6 +73,16 @@ angular.module('redash', [
}]
}
});
$routeProvider.when('/queries/my', {
templateUrl: '/views/queries.html',
controller: 'QueriesCtrl',
reloadOnSearch: false
});
$routeProvider.when('/queries/drafts', {
templateUrl: '/views/queries.html',
controller: 'QueriesCtrl',
reloadOnSearch: false
});
$routeProvider.when('/queries/search', {
templateUrl: '/views/queries_search_results.html',
controller: 'QuerySearchCtrl',
@@ -115,6 +127,15 @@ angular.module('redash', [
controller: 'DataSourcesCtrl'
});
$routeProvider.when('/destinations/:destinationId', {
templateUrl: '/views/destinations/edit.html',
controller: 'DestinationCtrl'
});
$routeProvider.when('/destinations', {
templateUrl: '/views/destinations/list.html',
controller: 'DestinationsCtrl'
});
$routeProvider.when('/users/new', {
templateUrl: '/views/users/new.html',
controller: 'NewUserCtrl'
@@ -139,7 +160,15 @@ angular.module('redash', [
$routeProvider.when('/groups', {
templateUrl: '/views/groups/list.html',
controller: 'GroupsCtrl'
})
});
$routeProvider.when('/query_snippets/:snippetId', {
templateUrl: '/views/query_snippets/show.html',
controller: 'SnippetCtrl'
});
$routeProvider.when('/query_snippets', {
templateUrl: '/views/query_snippets/list.html',
controller: 'SnippetsCtrl'
});
$routeProvider.when('/', {
templateUrl: '/views/index.html',
controller: 'IndexCtrl'

View File

@@ -43,6 +43,7 @@
var AdminTasksCtrl = function ($scope, $location, Events, $http, $timeout, $filter) {
Events.record(currentUser, "view", "page", "admin/tasks");
$scope.$parent.pageTitle = "Running Queries";
$scope.autoUpdate = true;
$scope.gridConfig = {
isPaginationEnabled: true,
@@ -121,11 +122,13 @@
$scope.setTab($location.hash() || 'in_progress');
var refresh = function () {
$scope.refresh_time = moment().add('minutes', 1);
$http.get('/api/admin/queries/tasks').success(function (data) {
$scope.tasks = data;
$scope.showingTasks = $scope.tasks[$scope.selectedTab];
});
if ($scope.autoUpdate) {
$scope.refresh_time = moment().add(1, 'minutes');
$http.get('/api/admin/queries/tasks').success(function (data) {
$scope.tasks = data;
$scope.showingTasks = $scope.tasks[$scope.selectedTab];
});
}
var timer = $timeout(refresh, 5 * 1000);
@@ -142,6 +145,7 @@
var AdminOutdatedQueriesCtrl = function ($scope, Events, $http, $timeout, $filter) {
Events.record(currentUser, "view", "page", "admin/outdated_queries");
$scope.$parent.pageTitle = "Outdated Queries";
$scope.autoUpdate = true;
$scope.gridConfig = {
isPaginationEnabled: true,
@@ -190,11 +194,13 @@
];
var refresh = function () {
$scope.refresh_time = moment().add('minutes', 1);
$http.get('/api/admin/queries/outdated').success(function (data) {
$scope.queries = data.queries;
$scope.updatedAt = data.updated_at * 1000.0;
});
if ($scope.autoUpdate) {
$scope.refresh_time = moment().add(1, 'minutes');
$http.get('/api/admin/queries/outdated').success(function (data) {
$scope.queries = data.queries;
$scope.updatedAt = data.updated_at * 1000.0;
});
}
var timer = $timeout(refresh, 59 * 1000);

View File

@@ -46,7 +46,8 @@
];
};
var AlertCtrl = function($scope, $routeParams, $location, growl, Query, Events, Alert) {
var AlertCtrl = function($scope, $routeParams, $location, growl, Query, Events, Alert, Destination) {
$scope.selectedTab = 'users';
$scope.$parent.pageTitle = "Alerts";
$scope.alertId = $routeParams.alertId;
@@ -66,10 +67,12 @@
if ($scope.alertId === "new") {
$scope.alert = new Alert({options: {}});
$scope.canEdit = true;
} else {
$scope.alert = Alert.get({id: $scope.alertId}, function(alert) {
$scope.onQuerySelected(new Query($scope.alert.query));
});
$scope.canEdit = currentUser.canEdit($scope.alert);
}
$scope.ops = ['greater than', 'less than', 'equals'];
@@ -108,69 +111,118 @@
growl.addErrorMessage("Failed saving alert.");
});
};
$scope.delete = function() {
$scope.alert.$delete(function() {
$location.path('/alerts');
growl.addSuccessMessage("Alert deleted.");
}, function() {
growl.addErrorMessage("Failed deleting alert.");
});
}
};
angular.module('redash.directives').directive('alertSubscribers', ['AlertSubscription', function (AlertSubscription) {
angular.module('redash.directives').directive('alertSubscriptions', ['$q', '$sce', 'AlertSubscription', 'Destination', 'growl', function ($q, $sce, AlertSubscription, Destination, growl) {
return {
restrict: 'E',
replace: true,
templateUrl: '/views/alerts/subscribers.html',
templateUrl: '/views/alerts/alert_subscriptions.html',
scope: {
'alertId': '='
},
controller: function ($scope) {
$scope.subscribers = AlertSubscription.query({alertId: $scope.alertId});
}
}
}]);
$scope.newSubscription = {};
$scope.subscribers = [];
$scope.destinations = [];
$scope.currentUser = currentUser;
angular.module('redash.directives').directive('subscribeButton', ['AlertSubscription', 'growl', function (AlertSubscription, growl) {
return {
restrict: 'E',
replace: true,
template: '<button class="btn btn-default btn-xs" ng-click="toggleSubscription()"><i ng-class="class"></i></button>',
controller: function ($scope) {
var updateClass = function() {
if ($scope.subscription) {
$scope.class = "fa fa-eye-slash";
} else {
$scope.class = "fa fa-eye";
var destinations = Destination.query().$promise;
var subscribers = AlertSubscription.query({alertId: $scope.alertId}).$promise;
$q.all([destinations, subscribers]).then(function(responses) {
var destinations = responses[0];
var subscribers = responses[1];
var subscribedDestinations = _.compact(_.map(subscribers, function(s) { return s.destination && s.destination.id }));
var subscribedUsers = _.compact(_.map(subscribers, function(s) { if (!s.destination) { return s.user.id } }));
$scope.destinations = _.filter(destinations, function(d) { return !_.contains(subscribedDestinations, d.id); });
if (!_.contains(subscribedUsers, currentUser.id)) {
$scope.destinations.unshift({user: {name: currentUser.name}});
}
}
$scope.subscribers.$promise.then(function() {
$scope.subscription = _.find($scope.subscribers, function(subscription) {
return (subscription.user.email == currentUser.email);
});
updateClass();
$scope.newSubscription.destination = $scope.destinations[0];
$scope.subscribers = subscribers;
});
$scope.toggleSubscription = function() {
if ($scope.subscription) {
$scope.subscription.$delete(function() {
$scope.subscribers = _.without($scope.subscribers, $scope.subscription);
$scope.subscription = undefined;
updateClass();
}, function() {
growl.addErrorMessage("Failed saving subscription.");
});
} else {
$scope.subscription = new AlertSubscription({alert_id: $scope.alertId});
$scope.subscription.$save(function() {
$scope.subscribers.push($scope.subscription);
updateClass();
}, function() {
growl.addErrorMessage("Unsubscription failed.");
});
$scope.destinationsDisplay = function(destination) {
if (!destination) {
return '';
}
}
if (destination.destination) {
destination = destination.destination;
} else if (destination.user) {
destination = {
name: destination.user.name + ' (Email)',
icon: 'fa-envelope',
type: 'user'
};
}
return $sce.trustAsHtml('<i class="fa ' + destination.icon + '"></i>&nbsp;' + destination.name);
};
$scope.saveSubscriber = function() {
var sub = new AlertSubscription({alert_id: $scope.alertId});
if ($scope.newSubscription.destination.id) {
sub.destination_id = $scope.newSubscription.destination.id;
}
sub.$save(function () {
growl.addSuccessMessage("Subscribed.");
$scope.subscribers.push(sub);
$scope.destinations = _.without($scope.destinations, $scope.newSubscription.destination);
if ($scope.destinations.length > 0) {
$scope.newSubscription.destination = $scope.destinations[0];
} else {
$scope.newSubscription.destination = undefined;
}
console.log("dests: ", $scope.destinations);
}, function (response) {
growl.addErrorMessage("Failed saving subscription.");
});
};
$scope.unsubscribe = function(subscriber) {
var destination = subscriber.destination;
var user = subscriber.user;
subscriber.$delete(function () {
growl.addSuccessMessage("Unsubscribed");
$scope.subscribers = _.without($scope.subscribers, subscriber);
if (destination) {
$scope.destinations.push(destination);
} else if (user.id == currentUser.id) {
$scope.destinations.push({user: {name: currentUser.name}});
}
if ($scope.destinations.length == 1) {
$scope.newSubscription.destination = $scope.destinations[0];
}
}, function () {
growl.addErrorMessage("Failed unsubscribing.");
});
};
}
}
}]);
angular.module('redash.controllers')
.controller('AlertsCtrl', ['$scope', 'Events', 'Alert', AlertsCtrl])
.controller('AlertCtrl', ['$scope', '$routeParams', '$location', 'growl', 'Query', 'Events', 'Alert', AlertCtrl])
.controller('AlertCtrl', ['$scope', '$routeParams', '$location', 'growl', 'Query', 'Events', 'Alert', 'Destination', AlertCtrl])
})();

View File

@@ -63,91 +63,66 @@
};
var QueriesCtrl = function ($scope, $http, $location, $filter, Query) {
$scope.$parent.pageTitle = "All Queries";
$scope.gridConfig = {
isPaginationEnabled: true,
itemsByPage: 50,
maxSize: 8,
isGlobalSearchActivated: true};
var loader;
$scope.allQueries = [];
$scope.queries = [];
$scope.page = parseInt($location.search().page || 1);
$scope.total = undefined;
$scope.pageSize = 25;
var filterQueries = function () {
$scope.queries = _.filter($scope.allQueries, function (query) {
if (!$scope.selectedTab) {
return false;
}
if ($scope.selectedTab.key == 'my') {
return query.user.id == currentUser.id && query.name != 'New Query';
} else if ($scope.selectedTab.key == 'drafts') {
return query.user.id == currentUser.id && query.name == 'New Query';
}
return query.name != 'New Query';
});
function loadQueries(resource, defaultOptions) {
return function(options) {
options = _.extend({}, defaultOptions, options);
resource(options, function (queries) {
$scope.totalQueriesCount = queries.count;
$scope.queries = _.map(queries.results, function (query) {
query.created_at = moment(query.created_at);
query.retrieved_at = moment(query.retrieved_at);
return query;
});
});
}
}
Query.query(function (queries) {
$scope.allQueries = _.map(queries, function (query) {
query.created_at = moment(query.created_at);
query.retrieved_at = moment(query.retrieved_at);
return query;
});
switch($location.path()) {
case '/queries':
$scope.$parent.pageTitle = "Queries";
// page title
loader = loadQueries(Query.query);
break;
case '/queries/drafts':
$scope.$parent.pageTitle = "Drafts";
loader = loadQueries(Query.myQueries, {drafts: true});
break;
case '/queries/my':
$scope.$parent.pageTitle = "My Queries";
loader = loadQueries(Query.myQueries);
break;
}
filterQueries();
});
var loadAllQueries = loadQueries(Query.query);
var loadMyQueries = loadQueries(Query.myQueries);
$scope.gridColumns = [
{
"label": "Name",
"map": "name",
"cellTemplateUrl": "/views/queries_query_name_cell.html"
},
{
'label': 'Created By',
'map': 'user.name'
},
{
'label': 'Created At',
'map': 'created_at',
'formatFunction': dateFormatter
},
{
'label': 'Runtime',
'map': 'run_time',
'formatFunction': function (value) {
return $filter('durationHumanize')(value);
}
},
{
'label': 'Last Executed At',
'map': 'retrieved_at',
'formatFunction': dateFormatter
},
{
'label': 'Update Schedule',
'map': 'schedule',
'formatFunction': function (value) {
return $filter('scheduleHumanize')(value);
}
}
]
function load() {
var options = {page: $scope.page, page_size: $scope.pageSize};
loader(options);
}
$scope.selectPage = function(page) {
$location.search('page', page);
$scope.page = page;
load();
}
$scope.tabs = [
{"name": "My Queries", "key": "my"},
{"key": "all", "name": "All Queries"},
{"key": "drafts", "name": "Drafts"}
{"name": "My Queries", "path": "queries/my", loader: loadMyQueries},
{"path": "queries", "name": "All Queries", isActive: function(path) {
return path === '/queries';
}, "loader": loadAllQueries},
{"path": "queries/drafts", "name": "Drafts", loader: loadMyQueries},
];
$scope.$watch('selectedTab', function (tab) {
if (tab) {
$scope.$parent.pageTitle = tab.name;
}
filterQueries();
});
load();
}
var MainCtrl = function ($scope, $location, Dashboard) {
@@ -181,9 +156,76 @@
$scope.recentDashboards = Dashboard.recent();
};
// Controller for modal window share_permissions, works for both query and dashboards, needs apiAccess set in scope
var ManagePermissionsCtrl = function ($scope, $http, $modalInstance, User) {
$scope.grantees = [];
$scope.newGrantees = {};
// List users that are granted permissions
var loadGrantees = function() {
$http.get($scope.apiAccess).success(function(result) {
$scope.grantees = [];
for(var access_type in result) {
result[access_type].forEach(function(grantee) {
var item = grantee;
item['access_type'] = access_type;
$scope.grantees.push(item);
})
}
});
};
loadGrantees();
// Search for user
$scope.findUser = function(search) {
if (search == "") {
return;
}
if ($scope.foundUsers === undefined) {
User.query(function(users) {
var existingIds = _.map($scope.grantees, function(m) { return m.id; });
_.each(users, function(user) { user.alreadyGrantee = _.contains(existingIds, user.id); });
$scope.foundUsers = users;
});
}
};
// Add new user to grantees list
$scope.addGrantee = function(user) {
$scope.newGrantees.selected = undefined;
var body = {'access_type': 'modify', 'user_id': user.id};
$http.post($scope.apiAccess, body).success(function() {
user.alreadyGrantee = true;
loadGrantees();
});
};
// Remove user from grantees list
$scope.removeGrantee = function(user) {
var body = {'access_type': 'modify', 'user_id': user.id};
$http({ url: $scope.apiAccess, method: 'DELETE',
data: body, headers: {"Content-Type": "application/json"}
}).success(function() {
$scope.grantees = _.filter($scope.grantees, function(m) { return m != user });
if ($scope.foundUsers) {
_.each($scope.foundUsers, function(u) { if (u.id == user.id) { u.alreadyGrantee = false }; });
}
});
};
$scope.close = function() {
$modalInstance.close();
}
};
angular.module('redash.controllers', [])
.controller('QueriesCtrl', ['$scope', '$http', '$location', '$filter', 'Query', QueriesCtrl])
.controller('IndexCtrl', ['$scope', 'Events', 'Dashboard', 'Query', IndexCtrl])
.controller('MainCtrl', ['$scope', '$location', 'Dashboard', MainCtrl])
.controller('QuerySearchCtrl', ['$scope', '$location', '$filter', 'Events', 'Query', QuerySearchCtrl]);
.controller('QuerySearchCtrl', ['$scope', '$location', '$filter', 'Events', 'Query', QuerySearchCtrl])
.controller('ManagePermissionsCtrl', ['$scope', '$http', '$modalInstance', 'User', ManagePermissionsCtrl]);
})();

View File

@@ -13,6 +13,7 @@
$scope.refreshEnabled = false;
$scope.isFullscreen = false;
$scope.refreshRate = 60;
$scope.showPermissionsControl = clientConfig.showPermissionsControl;
var renderDashboard = function (dashboard) {
$scope.$parent.pageTitle = dashboard.name;
@@ -91,10 +92,10 @@
_.each($scope.dashboard.widgets, function(row) {
_.each(row, function(widget, i) {
var newWidget = newWidgets[widget.id];
var newWidget = newWidgets[widget.id][0];
if (newWidget.visualization) {
if (newWidget && newWidget[0].visualization.query.latest_query_data_id != widget.visualization.query.latest_query_data_id) {
row[i] = new Widget(newWidget[0]);
if (newWidget && newWidget.visualization.query.latest_query_data_id != widget.visualization.query.latest_query_data_id) {
row[i] = new Widget(newWidget);
}
}
});
@@ -114,7 +115,19 @@
$scope.$parent.reloadDashboards();
});
}
}
};
$scope.showManagePermissionsModal = function() {
// Create scope for share permissions dialog and pass api path to it
var scope = $scope.$new();
$scope.apiAccess = 'api/dashboards/' + $scope.dashboard.id + '/acl';
$modal.open({
scope: scope,
templateUrl: '/views/dialogs/manage_permissions.html',
controller: 'ManagePermissionsCtrl'
});
};
$scope.toggleFullscreen = function() {
$scope.isFullscreen = !$scope.isFullscreen;
@@ -146,7 +159,7 @@
}
}));
$scope.refreshRate = _.max([120, refreshRate * 2]) * 1000;
$scope.refreshRate = _.min([300, refreshRate]) * 1000;
autoRefresh();
}
@@ -188,7 +201,30 @@
}
};
var WidgetCtrl = function($scope, $location, Events, Query) {
var WidgetCtrl = function($scope, $location, Events, Query, $modal) {
$scope.editTextBox = function() {
$modal.open({
templateUrl: '/views/edit_text_box_form.html',
scope: $scope,
controller: ['$scope', '$modalInstance', 'growl', function($scope, $modalInstance, growl) {
$scope.close = function() {
$modalInstance.close();
};
$scope.saveWidget = function() {
$scope.saveInProgress = true;
$scope.widget.$save().then(function(response) {
$scope.close();
}).catch(function() {
growl.addErrorMessage("Widget can not be updated");
}).finally(function() {
$scope.saveInProgress = false;
});
};
}],
});
}
$scope.deleteWidget = function() {
if (!confirm('Are you sure you want to remove "' + $scope.widget.getName() + '" from the dashboard?')) {
return;
@@ -206,19 +242,26 @@
$scope.dashboard.widgets = _.filter($scope.dashboard.widgets, function(row) { return row.length > 0 });
$scope.dashboard.layout = response.layout;
$scope.dashboard.version = response.version;
});
};
Events.record(currentUser, "view", "widget", $scope.widget.id);
$scope.reload = function(force) {
var maxAge = $location.search()['maxAge'];
if (force) {
maxAge = 0;
}
$scope.queryResult = $scope.query.getQueryResult(maxAge);
};
if ($scope.widget.visualization) {
Events.record(currentUser, "view", "query", $scope.widget.visualization.query.id);
Events.record(currentUser, "view", "visualization", $scope.widget.visualization.id);
$scope.query = $scope.widget.getQuery();
var parameters = Query.collectParamsFromQueryString($location, $scope.query);
var maxAge = $location.search()['maxAge'];
$scope.queryResult = $scope.query.getQueryResult(maxAge, parameters);
$scope.reload(false);
$scope.type = 'visualization';
} else if ($scope.widget.restricted) {
@@ -231,6 +274,6 @@
angular.module('redash.controllers')
.controller('DashboardCtrl', ['$scope', 'Events', 'Widget', '$routeParams', '$location', '$http', '$timeout', '$q', '$modal', 'Dashboard', DashboardCtrl])
.controller('PublicDashboardCtrl', ['$scope', 'Events', 'Widget', '$routeParams', '$location', '$http', '$timeout', '$q', 'Dashboard', PublicDashboardCtrl])
.controller('WidgetCtrl', ['$scope', '$location', 'Events', 'Query', WidgetCtrl])
.controller('WidgetCtrl', ['$scope', '$location', 'Events', 'Query', '$modal', WidgetCtrl])
})();

View File

@@ -7,7 +7,7 @@
};
var DataSourceCtrl = function ($scope, $routeParams, $http, $location, Events, DataSource) {
var DataSourceCtrl = function ($scope, $routeParams, $http, $location, growl, Events, DataSource) {
Events.record(currentUser, "view", "page", "admin/data_source");
$scope.$parent.pageTitle = "Data Sources";
@@ -24,9 +24,43 @@
$location.path('/data_sources/' + id).replace();
}
});
function deleteDataSource() {
Events.record(currentUser, "delete", "datasource", $scope.dataSource.id);
$scope.dataSource.$delete(function (resource) {
growl.addSuccessMessage("Data source deleted successfully.");
$location.path('/data_sources/');
}.bind(this), function (httpResponse) {
console.log("Failed to delete data source: ", httpResponse.status, httpResponse.statusText, httpResponse.data);
growl.addErrorMessage("Failed to delete data source.");
});
}
function testConnection (callback) {
Events.record(currentUser, "test", "datasource", $scope.dataSource.id);
DataSource.test({id: $scope.dataSource.id}, function (httpResponse) {
if (httpResponse.ok) {
growl.addSuccessMessage('<i class="fa fa-check-circle"></i> <strong>Success.</strong>', {enableHtml: true, ttl: 3000});
} else {
growl.addErrorMessage('<i class="fa fa-exclamation-triangle"></i> <strong>Connection Test Failed:</strong><br/>' + httpResponse.message, {enableHtml: true, ttl: -1});
}
callback();
}, function (httpResponse) {
console.log("Failed to test data source: ", httpResponse.status, httpResponse.statusText, httpResponse);
growl.addErrorMessage('<i class="fa fa-exclamation-triangle"></i> <strong> Unknown error occurred while performing connection test. Please try again later.', {enableHtml: true, ttl: -1});
callback();
});
}
$scope.actions = [
{name: 'Delete', class: 'btn-danger', callback: deleteDataSource},
{name: 'Test Connection', class: 'btn-default', callback: testConnection, disableWhenDirty: true}
]
};
angular.module('redash.controllers')
.controller('DataSourcesCtrl', ['$scope', '$location', 'growl', 'Events', 'DataSource', DataSourcesCtrl])
.controller('DataSourceCtrl', ['$scope', '$routeParams', '$http', '$location', 'Events', 'DataSource', DataSourceCtrl])
.controller('DataSourceCtrl', ['$scope', '$routeParams', '$http', '$location', 'growl', 'Events', 'DataSource', DataSourceCtrl])
})();

View File

@@ -0,0 +1,44 @@
(function () {
var DestinationsCtrl = function ($scope, $location, growl, Events, Destination) {
Events.record(currentUser, "view", "page", "admin/destinations");
$scope.$parent.pageTitle = "Destinations";
$scope.destinations = Destination.query();
};
var DestinationCtrl = function ($scope, $routeParams, $http, $location, growl, Events, Destination) {
Events.record(currentUser, "view", "page", "admin/destination");
$scope.$parent.pageTitle = "Destinations";
$scope.destinationId = $routeParams.destinationId;
if ($scope.destinationId == "new") {
$scope.destination = new Destination({options: {}});
} else {
$scope.destination = Destination.get({id: $routeParams.destinationId});
}
$scope.$watch('destination.id', function(id) {
if (id != $scope.destinationId && id !== undefined) {
$location.path('/destinations/' + id).replace();
}
});
$scope.delete = function() {
Events.record(currentUser, "delete", "destination", $scope.destination.id);
$scope.destination.$delete(function(resource) {
growl.addSuccessMessage("Destination deleted successfully.");
$location.path('/destinations/');
}.bind(this), function(httpResponse) {
console.log("Failed to delete destination: ", httpResponse.status, httpResponse.statusText, httpResponse.data);
growl.addErrorMessage("Failed to delete destination.");
});
}
};
angular.module('redash.controllers')
.controller('DestinationsCtrl', ['$scope', '$location', 'growl', 'Events', 'Destination', DestinationsCtrl])
.controller('DestinationCtrl', ['$scope', '$routeParams', '$http', '$location', 'growl', 'Events', 'Destination', DestinationCtrl])
})();

View File

@@ -1,7 +1,7 @@
(function() {
'use strict';
function QuerySourceCtrl(Events, growl, $controller, $scope, $location, Query, Visualization, KeyboardShortcuts) {
function QuerySourceCtrl(Events, growl, $controller, $scope, $location, $http, Query, Visualization, KeyboardShortcuts) {
// extends QueryViewCtrl
$controller('QueryViewCtrl', {$scope: $scope});
// TODO:
@@ -17,7 +17,7 @@
saveQuery = $scope.saveQuery;
$scope.sourceMode = true;
$scope.canEdit = currentUser.canEdit($scope.query);// TODO: bring this back? || clientConfig.allowAllToEditQueries;
$scope.canEdit = currentUser.canEdit($scope.query) || $scope.query.can_edit;// TODO: bring this back? || clientConfig.allowAllToEditQueries;
$scope.isDirty = false;
$scope.base_url = $location.protocol()+"://"+$location.host()+":"+$location.port();
@@ -53,14 +53,25 @@
$scope.saveQuery = function(options, data) {
var savePromise = saveQuery(options, data);
if (!savePromise) {
return;
}
savePromise.then(function(savedQuery) {
queryText = savedQuery.query;
$scope.isDirty = $scope.query.query !== queryText;
// update to latest version number
$scope.query.version = savedQuery.version;
if (isNewQuery) {
// redirect to new created query (keep hash)
$location.path(savedQuery.getSourceLink());
}
}, function(error) {
if(error.status == 409) {
growl.addErrorMessage('It seems like the query has been modified by another user. ' +
'Please copy/backup your changes and reload this page.', {ttl: -1});
}
});
return savePromise;
@@ -110,7 +121,7 @@
}
angular.module('redash.controllers').controller('QuerySourceCtrl', [
'Events', 'growl', '$controller', '$scope', '$location', 'Query',
'Visualization', 'KeyboardShortcuts', QuerySourceCtrl
'Events', 'growl', '$controller', '$scope', '$location', '$http',
'Query', 'Visualization', 'KeyboardShortcuts', QuerySourceCtrl
]);
})();

View File

@@ -1,12 +1,10 @@
(function() {
'use strict';
function QueryViewCtrl($scope, Events, $route, $location, notifications, growl, $modal, Query, DataSource) {
function QueryViewCtrl($scope, Events, $route, $routeParams, $http, $location, notifications, growl, $modal, Query, DataSource, User) {
var DEFAULT_TAB = 'table';
var getQueryResult = function(maxAge) {
// Collect params, and getQueryResult with params; getQueryResult merges it into the query
var parameters = Query.collectParamsFromQueryString($location, $scope.query);
if (maxAge === undefined) {
maxAge = $location.search()['maxAge'];
}
@@ -16,7 +14,7 @@
}
$scope.showLog = false;
$scope.queryResult = $scope.query.getQueryResult(maxAge, parameters);
$scope.queryResult = $scope.query.getQueryResult(maxAge);
};
var getDataSourceId = function() {
@@ -68,6 +66,7 @@
$scope.dataSource = {};
$scope.query = $route.current.locals.query;
$scope.showPermissionsControl = clientConfig.showPermissionsControl;
var updateSchema = function() {
$scope.hasSchema = false;
@@ -82,6 +81,7 @@
$scope.editorSize = "col-md-9";
$scope.hasSchema = true;
} else {
$scope.schema = undefined;
$scope.hasSchema = false;
$scope.editorSize = "col-md-12";
}
@@ -125,9 +125,14 @@
$scope.saveQuery = function(options, data) {
if (data) {
// Don't save new query with partial data
if ($scope.query.isNew()) {
return;
}
data.id = $scope.query.id;
data.version = $scope.query.version;
} else {
data = _.clone($scope.query);
data = _.pick($scope.query, ["schedule", "query", "id", "description", "name", "data_source_id", "options", "latest_query_data_id", "version"]);
}
options = _.extend({}, {
@@ -135,13 +140,16 @@
errorMessage: 'Query could not be saved'
}, options);
delete data.latest_query_data;
delete data.queryResult;
return Query.save(data, function() {
return Query.save(data, function(updatedQuery) {
growl.addSuccessMessage(options.successMessage);
}, function(httpResponse) {
growl.addErrorMessage(options.errorMessage);
$scope.query.version = updatedQuery.version;
}, function(error) {
if(error.status == 409) {
growl.addErrorMessage('It seems like the query has been modified by another user. ' +
'Please copy/backup your changes and reload this page.', {ttl: -1});
} else {
growl.addErrorMessage(options.errorMessage);
}
}).$promise;
}
@@ -324,6 +332,9 @@
$modalInstance.close();
}
$scope.embedUrl = basePath + 'embed/query/' + query.id + '/visualization/' + visualization.id + '?api_key=' + query.api_key;
if (window.snapshotUrlBuilder) {
$scope.snapshotUrl = snapshotUrlBuilder(query, visualization);
}
}]
})
}
@@ -336,9 +347,19 @@
}
$scope.selectedTab = hash || DEFAULT_TAB;
});
};
$scope.showManagePermissionsModal = function() {
// Create scope for share permissions dialog and pass api path to it
var scope = $scope.$new();
$scope.apiAccess = 'api/queries/' + $routeParams.queryId + '/acl';
$modal.open({
scope: scope,
templateUrl: '/views/dialogs/manage_permissions.html',
controller: 'ManagePermissionsCtrl'
})
};
};
angular.module('redash.controllers')
.controller('QueryViewCtrl',
['$scope', 'Events', '$route', '$location', 'notifications', 'growl', '$modal', 'Query', 'DataSource', QueryViewCtrl]);
.controller('QueryViewCtrl', ['$scope', 'Events', '$route', '$routeParams', '$http', '$location', 'notifications', 'growl', '$modal', 'Query', 'DataSource', 'User', QueryViewCtrl]);
})();

View File

@@ -0,0 +1,93 @@
(function() {
var SnippetsCtrl = function ($scope, $location, growl, Events, QuerySnippet) {
Events.record(currentUser, "view", "page", "query_snippets");
$scope.$parent.pageTitle = "Query Snippets";
$scope.gridConfig = {
isPaginationEnabled: true,
itemsByPage: 20,
maxSize: 8,
};
$scope.gridColumns = [
{
"label": "Trigger",
"cellTemplate": '<a href="query_snippets/{{dataRow.id}}">{{dataRow.trigger}}</a>'
},
{
"label": "Description",
"map": "description"
},
{
"label": "Snippet",
"map": "snippet"
},
{
'label': 'Created By',
'map': 'user.name'
},
{
'label': 'Updated At',
'cellTemplate': '<span am-time-ago="dataRow.created_at"></span>'
}
];
$scope.snippets = [];
QuerySnippet.query(function(snippets) {
$scope.snippets = snippets;
});
};
var SnippetCtrl = function ($scope, $routeParams, $http, $location, growl, Events, QuerySnippet) {
$scope.$parent.pageTitle = "Query Snippets";
$scope.snippetId = $routeParams.snippetId;
Events.record(currentUser, "view", "query_snippet", $scope.snippetId);
$scope.editorOptions = {
mode: 'snippets',
advanced: {
behavioursEnabled: true,
enableSnippets: false,
autoScrollEditorIntoView: true,
},
onLoad: function(editor) {
editor.$blockScrolling = Infinity;
editor.getSession().setUseWrapMode(true);
editor.setShowPrintMargin(false);
}
};
$scope.saveChanges = function() {
$scope.snippet.$save(function(snippet) {
growl.addSuccessMessage("Saved.");
if ($scope.snippetId === "new") {
$location.path('/query_snippets/' + snippet.id).replace();
}
}, function() {
growl.addErrorMessage("Failed saving snippet.");
});
}
$scope.delete = function() {
$scope.snippet.$delete(function() {
$location.path('/query_snippets');
growl.addSuccessMessage("Query snippet deleted.");
}, function() {
growl.addErrorMessage("Failed deleting query snippet.");
});
}
if ($scope.snippetId == 'new') {
$scope.snippet = new QuerySnippet({description: ""});
$scope.canEdit = true;
} else {
$scope.snippet = QuerySnippet.get({id: $scope.snippetId}, function(snippet) {
$scope.canEdit = currentUser.canEdit(snippet);
});
}
};
angular.module('redash.controllers')
.controller('SnippetsCtrl', ['$scope', '$location', 'growl', 'Events', 'QuerySnippet', SnippetsCtrl])
.controller('SnippetCtrl', ['$scope', '$routeParams', '$http', '$location', 'growl', 'Events', 'QuerySnippet', SnippetCtrl])
})();

View File

@@ -68,7 +68,7 @@
},
transclude: true,
template:
'<h2>'+
'<h2 class="p-l-5">'+
'<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>',

View File

@@ -3,8 +3,8 @@
var directives = angular.module('redash.directives');
directives.directive('editDashboardForm', ['Events', '$http', '$location', '$timeout', 'Dashboard',
function(Events, $http, $location, $timeout, Dashboard) {
directives.directive('editDashboardForm', ['Events', '$http', '$location', '$timeout', 'Dashboard', 'growl',
function(Events, $http, $location, $timeout, Dashboard, growl) {
return {
restrict: 'E',
scope: {
@@ -81,10 +81,19 @@
$scope.dashboard.layout = layout;
layout = JSON.stringify(layout);
Dashboard.save({slug: $scope.dashboard.id, name: $scope.dashboard.name, layout: layout}, function(dashboard) {
Dashboard.save({slug: $scope.dashboard.id, name: $scope.dashboard.name,
version: $scope.dashboard.version, layout: layout}, function(dashboard) {
$scope.dashboard = dashboard;
$scope.saveInProgress = false;
$(element).modal('hide');
}, function(error) {
$scope.saveInProgress = false;
if(error.status == 403) {
growl.addErrorMessage("Unable to save dashboard: Permission denied.");
} else if(error.status == 409) {
growl.addErrorMessage('It seems like the dashboard has been modified by another user. ' +
'Please copy/backup your changes and reload this page.', {ttl: -1});
}
});
Events.record(currentUser, 'edit', 'dashboard', $scope.dashboard.id);
} else {
@@ -199,6 +208,7 @@
widget.$save().then(function(response) {
// update dashboard layout
$scope.dashboard.layout = response['layout'];
$scope.dashboard.version = response['version'];
var newWidget = new Widget(response['widget']);
if (response['new_row']) {
$scope.dashboard.widgets.push([newWidget]);

View File

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

View File

@@ -58,7 +58,7 @@
}
$scope.$on('$locationChangeStart', function (event, next, current) {
if (next.split("#")[0] == current.split("#")[0]) {
if (next.split("?")[0] == current.split("?")[0] || next.split("#")[0] == current.split("#")[0]) {
return;
}
@@ -92,13 +92,14 @@
restrict: 'E',
scope: {
'tabId': '@',
'name': '@'
'name': '@',
'basePath': '=?'
},
transclude: true,
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.basePath = scope.basePath || $location.path().substring(1);
scope.$watch(function () {
return scope.$parent.selectedTab
}, function (tab) {
@@ -119,36 +120,6 @@
}
});
directives.directive('rdTabs', ['$location', function ($location) {
return {
restrict: 'E',
scope: {
tabsCollection: '=',
selectedTab: '='
},
template: '<ul class="tab-nav bg-white"><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;
});
}
$scope.$watch(function () {
return $location.hash()
}, function (hash) {
if (hash) {
$scope.selectTab($location.hash());
} else {
$scope.selectTab($scope.tabsCollection[0].key);
}
});
}
}
}]);
// From: http://jsfiddle.net/joshdmiller/NDFHg/
directives.directive('editInPlace', function () {
return {
@@ -383,7 +354,109 @@
'</div>' +
'</div>'
}
})
});
directives.directive('dynamicForm', ['$http', 'growl', '$q', function ($http, growl, $q) {
return {
restrict: 'E',
replace: 'true',
transclude: true,
templateUrl: '/views/directives/dynamic_form.html',
scope: {
'target': '=',
'type': '@type',
'actions': '='
},
link: function ($scope) {
var setType = function(types) {
if ($scope.target.type === undefined) {
$scope.target.type = types[0].type;
return types[0];
}
$scope.type = _.find(types, function (t) {
return t.type == $scope.target.type;
});
};
$scope.inProgressActions = {};
_.each($scope.actions, function(action) {
var originalCallback = action.callback;
var name = action.name;
action.callback = function() {
action.name = '<i class="zmdi zmdi-spinner zmdi-hc-spin"></i> ' + name;
$scope.inProgressActions[action.name] = true;
function release() {
$scope.inProgressActions[action.name] = false;
action.name = name;
}
originalCallback(release);
}
});
$scope.files = {};
$scope.$watchCollection('files', function() {
_.each($scope.files, function(v, k) {
// THis is needed because angular-base64-upload sets the value to null at initialization, causing the field
// to be marked as dirty even if it wasn't changed.
if (!v && $scope.target.options[k]) {
$scope.dataSourceForm.$setPristine();
}
if (v) {
$scope.target.options[k] = v.base64;
}
});
});
var typesPromise = $http.get('api/' + $scope.type + '/types');
$q.all([typesPromise, $scope.target.$promise]).then(function(responses) {
var types = responses[0].data;
setType(types);
$scope.types = types;
_.each(types, function (type) {
_.each(type.configuration_schema.properties, function (prop, name) {
if (name == 'password' || name == 'passwd') {
prop.type = 'password';
}
if (_.string.endsWith(name, "File")) {
prop.type = 'file';
}
if (prop.type == 'boolean') {
prop.type = 'checkbox';
}
prop.required = _.contains(type.configuration_schema.required, name);
});
});
});
$scope.$watch('target.type', function(current, prev) {
if (prev !== current) {
if (prev !== undefined) {
$scope.target.options = {};
}
setType($scope.types);
}
});
$scope.saveChanges = function() {
$scope.target.$save(function() {
growl.addSuccessMessage("Saved.");
$scope.dataSourceForm.$setPristine()
}, function() {
growl.addErrorMessage("Failed saving.");
});
}
}
}
}]);
directives.directive('pageHeader', function() {
return {
@@ -403,14 +476,148 @@
restrict: 'E',
transclude: true,
templateUrl: '/views/directives/settings_screen.html',
link: function(scope, elem, attrs) {
controller: ['$scope', function(scope) {
scope.usersPage = _.string.startsWith($location.path(), '/users');
scope.groupsPage = _.string.startsWith($location.path(), '/groups');
scope.dsPage = _.string.startsWith($location.path(), '/data_sources');
scope.destinationsPage = _.string.startsWith($location.path(), '/destinations');
scope.snippetsPage = _.string.startsWith($location.path(), '/query_snippets');
scope.showGroupsLink = currentUser.hasPermission('list_users');
scope.showUsersLink = currentUser.hasPermission('list_users');
scope.showDsLink = currentUser.hasPermission('admin');
scope.showDestinationsLink = currentUser.hasPermission('admin');
}]
}
}]);
directives.directive('tabNav', ['$location', function($location) {
return {
restrict: 'E',
transclude: true,
scope: {
tabs: '='
},
template: '<ul class="tab-nav bg-white">' +
'<li ng-repeat="tab in tabs" ng-class="{\'active\': tab.active }"><a ng-href="{{tab.path}}">{{tab.name}}</a></li>' +
'</ul>',
link: function($scope) {
_.each($scope.tabs, function(tab) {
if (tab.isActive) {
tab.active = tab.isActive($location.path());
} else {
tab.active = _.string.startsWith($location.path(), "/" + tab.path);
}
});
}
}
}]);
directives.directive('queriesList', [function () {
return {
restrict: 'E',
replace: true,
scope: {
queries: '=',
total: '=',
selectPage: '=',
page: '=',
pageSize: '='
},
templateUrl: '/views/directives/queries_list.html',
link: function ($scope) {
function hasNext() {
return !($scope.page * $scope.pageSize >= $scope.total);
}
function hasPrevious() {
return $scope.page !== 1;
}
function updatePages() {
if ($scope.total === undefined) {
return;
}
var maxSize = 5;
var pageCount = Math.ceil($scope.total/$scope.pageSize);
var pages = [];
function makePage(title, page, disabled) {
return {title: title, page: page, active: page == $scope.page, disabled: disabled};
}
// Default page limits
var startPage = 1, endPage = pageCount;
// recompute if maxSize
if (maxSize && maxSize < pageCount) {
startPage = Math.max($scope.page - Math.floor(maxSize / 2), 1);
endPage = startPage + maxSize - 1;
// Adjust if limit is exceeded
if (endPage > pageCount) {
endPage = pageCount;
startPage = endPage - maxSize + 1;
}
}
// Add page number links
for (var number = startPage; number <= endPage; number++) {
var page = makePage(number, number, false);
pages.push(page);
}
// Add previous & next links
var previousPage = makePage('<', $scope.page - 1, !hasPrevious());
pages.unshift(previousPage);
var nextPage = makePage('>', $scope.page + 1, !hasNext());
pages.push(nextPage);
$scope.pages = pages;
}
$scope.$watch('total', updatePages);
$scope.$watch('page', updatePages);
}
}
}]);
directives.directive('parameters', ['$location', '$modal', function($location, $modal) {
return {
restrict: 'E',
transclude: true,
scope: {
'parameters': '=',
'syncValues': '=?',
'editable': '=?'
},
templateUrl: '/views/directives/parameters.html',
link: function(scope, elem, attrs) {
// is this the correct location for this logic?
if (scope.syncValues !== false) {
scope.$watch('parameters', function() {
_.each(scope.parameters, function(param) {
if (param.value !== null || param.value !== '') {
$location.search('p_' + param.name, param.value);
}
})
}, true);
}
scope.showParameterSettings = function(param) {
$modal.open({
templateUrl: '/views/dialogs/parameter_settings.html',
controller: ['$scope', '$modalInstance', function($scope, $modalInstance) {
$scope.close = function() {
$modalInstance.close();
};
$scope.parameter = param;
}]
})
}
}
}
}]);

View File

@@ -45,59 +45,82 @@
});
};
var normalAreaStacking = function(seriesList) {
fillXValues(seriesList);
var storeOriginalHeightForEachSeries = function(seriesList) {
_.each(seriesList, function(series) {
if(!_.has(series,'visible')){
series.visible = true;
series.original_y = series.y.slice();
}
});
};
var getEnabledSeries = function(seriesList){
return _.filter(seriesList, function(series) {
return series.visible === true;
});
};
var initializeTextAndHover = function(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 normalAreaStacking = function(seriesList) {
fillXValues(seriesList);
storeOriginalHeightForEachSeries(seriesList);
initializeTextAndHover(seriesList);
seriesList = getEnabledSeries(seriesList);
_.each(seriesList, function(series, seriesIndex, list){
_.each(series.y, function(undefined, yIndex, undefined2){
var cumulativeHeightOfPreviousSeries = seriesIndex > 0 ? list[seriesIndex-1].y[yIndex] : 0;
var cumulativeHeightWithThisSeries = cumulativeHeightOfPreviousSeries + series.original_y[yIndex];
series.y[yIndex] = cumulativeHeightWithThisSeries;
series.text.push('Value: ' + series.original_y[yIndex] + '<br>Sum: ' + cumulativeHeightWithThisSeries);
});
});
};
var lastVisibleY = function(seriesList, lastSeriesIndex, yIndex){
for(; lastSeriesIndex >= 0; lastSeriesIndex--){
if(seriesList[lastSeriesIndex].visible === true){
return seriesList[lastSeriesIndex].y[yIndex];
}
}
};
return 0;
}
var percentAreaStacking = function(seriesList) {
if (seriesList.length === 0) {
return;
}
fillXValues(seriesList);
_.each(seriesList, function(series) {
series.text = [];
series.hoverinfo = 'text+name';
storeOriginalHeightForEachSeries(seriesList);
initializeTextAndHover(seriesList);
_.each(seriesList[0].y, function(seriesY, yIndex, undefined){
var sumOfCorrespondingDataPoints = _.reduce(seriesList, function(total, series){
return total + series.original_y[yIndex];
}, 0);
_.each(seriesList, function(series, seriesIndex, list){
var percentage = (series.original_y[yIndex] / sumOfCorrespondingDataPoints ) * 100;
var previousVisiblePercentage = lastVisibleY(seriesList, seriesIndex-1, yIndex);
series.y[yIndex] = percentage + previousVisiblePercentage;
series.text.push('Value: ' + series.original_y[yIndex] + '<br>Relative: ' + percentage.toFixed(2) + '%');
});
});
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';
});
initializeTextAndHover(seriesList);
for (var i = 0; i < seriesList[0].y.length; i++) {
var sum = 0;
for(var j = 0; j < seriesList.length; j++) {
@@ -118,9 +141,26 @@
return value;
}
function seriesMinValue(series) {
return _.min(_.map(series, function(s) { return _.min(series.y) }));
}
function seriesMaxValue(series) {
return _.max(_.map(series, function(s) { return _.max(series.y) }));
}
function leftAxisSeries(series) {
return _.filter(series, function(s) { return s.yaxis !== 'y2' });
}
function rightAxisSeries(series) {
return _.filter(series, function(s) { return s.yaxis === 'y2' });
}
angular.module('plotly', [])
.constant('ColorPalette', ColorPalette)
.directive('plotlyChart', function () {
var bottomMargin = 50;
return {
restrict: 'E',
template: '<div></div>',
@@ -158,9 +198,18 @@
return ColorPaletteArray[index % ColorPaletteArray.length];
};
var calculateHeight = function() {
var height = Math.max(scope.height, (scope.height - 50) + bottomMargin);
return height;
}
var recalculateOptions = function() {
scope.data.length = 0;
scope.layout.showlegend = _.has(scope.options, 'legend') ? scope.options.legend.enabled : true;
if(_.has(scope.options, 'bottomMargin')) {
bottomMargin = parseInt(scope.options.bottomMargin);
scope.layout.margin.b = bottomMargin;
}
delete scope.layout.barmode;
delete scope.layout.xaxis;
delete scope.layout.yaxis;
@@ -245,6 +294,7 @@
return null;
};
scope.layout.xaxis = {title: getTitle(scope.options.xAxis),
type: getScaleType(scope.options.xAxis.type)};
if (angular.isDefined(scope.options.xAxis.labels)) {
@@ -253,12 +303,26 @@
if (angular.isArray(scope.options.yAxis)) {
scope.layout.yaxis = {title: getTitle(scope.options.yAxis[0]),
type: getScaleType(scope.options.yAxis[0].type)};
if (angular.isNumber(scope.options.yAxis[0].rangeMin) || angular.isNumber(scope.options.yAxis[0].rangeMax)) {
var min = scope.options.yAxis[0].rangeMin || Math.min(0, seriesMinValue(leftAxisSeries(scope.data)));
var max = scope.options.yAxis[0].rangeMax || seriesMaxValue(leftAxisSeries(scope.data));
scope.layout.yaxis.range = [min, max];
}
}
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'};
if (angular.isNumber(scope.options.yAxis[1].rangeMin) || angular.isNumber(scope.options.yAxis[1].rangeMax)) {
var min = scope.options.yAxis[1].rangeMin || Math.min(0, seriesMinValue(rightAxisSeries(scope.data)));
var max = scope.options.yAxis[1].rangeMax || seriesMaxValue(rightAxisSeries(scope.data));
scope.layout.yaxis2.range = [min, max];
}
} else {
delete scope.layout.yaxis2;
}
@@ -276,18 +340,39 @@
percentBarStacking(scope.data);
}
}
scope.layout.margin.b = bottomMargin;
scope.layout.height = calculateHeight();
};
scope.$watch('series', recalculateOptions);
scope.$watch('options', recalculateOptions, true);
scope.layout = {margin: {l: 50, r: 50, b: 50, t: 20, pad: 4}, height: scope.height, autosize: true, hovermode: 'closest'};
scope.layout = {margin: {l: 50, r: 50, b: bottomMargin, t: 20, pad: 4}, height: calculateHeight(), autosize: true, hovermode: 'closest'};
scope.plotlyOptions = {showLink: false, displaylogo: false};
scope.data = [];
var element = element[0].children[0];
Plotly.newPlot(element, scope.data, scope.layout, scope.plotlyOptions);
element.on('plotly_afterplot', function(d) {
if(scope.options.globalSeriesType === 'area' && (scope.options.series.stacking === 'normal' || scope.options.series.stacking === 'percent')){
$(element).find(".legendtoggle").each(function(i, rectDiv) {
d3.select(rectDiv).on('click', function () {
var maxIndex = scope.data.length - 1;
var itemClicked = scope.data[maxIndex - i];
itemClicked.visible = (itemClicked.visible === true) ? 'legendonly' : true;
if (scope.options.series.stacking === 'normal') {
normalAreaStacking(scope.data);
} else if (scope.options.series.stacking === 'percent') {
percentAreaStacking(scope.data);
}
Plotly.redraw(element);
});
});
}
});
scope.$watch('layout', function (layout, old) {
if (angular.equals(layout, old)) {
return;

View File

@@ -10,29 +10,29 @@
},
template: '<a ng-href="{{link}}" class="query-link">{{query.name}}</a>',
link: function(scope, element) {
scope.link = 'queries/' + scope.query.id;
var hash = null;
if (scope.visualization) {
if (scope.visualization.type === 'TABLE') {
// link to hard-coded table tab instead of the (hidden) visualization tab
scope.link += '#table';
hash = 'table';
} else {
scope.link += '#' + scope.visualization.id;
hash = scope.visualization.id;
}
}
// element.find('a').attr('href', link);
scope.link = scope.query.getUrl(false, hash);
}
}
}
function querySourceLink() {
function querySourceLink($location) {
return {
restrict: 'E',
template: '<span ng-show="query.id && canViewSource">\
<a ng-show="!sourceMode"\
ng-href="queries/{{query.id}}/source#{{selectedTab}}" class="btn btn-default">Show Source\
ng-href="{{query.getUrl(true, selectedTab)}}" class="btn btn-default">Show Source\
</a>\
<a ng-show="sourceMode"\
ng-href="queries/{{query.id}}#{{selectedTab}}" class="btn btn-default">Hide Source\
ng-href="{{query.getUrl(false, selectedTab)}}" class="btn btn-default">Hide Source\
</a>\
</span>'
}
@@ -52,7 +52,7 @@
if (scope.queryResult.getId() == null) {
element.attr('href', '');
} else {
element.attr('href', 'api/queries/' + scope.query.id + '/results/' + scope.queryResult.getId() + '.' + fileType);
element.attr('href', 'api/queries/' + scope.query.id + '/results/' + scope.queryResult.getId() + '.' + fileType + (scope.embed ? '?api_key=' + scope.apiKey : ''));
element.attr('download', scope.query.name.replace(" ", "_") + moment(scope.queryResult.getUpdatedAt()).format("_YYYY_MM_DD") + "." + fileType);
}
});
@@ -60,7 +60,22 @@
}
}
function queryEditor() {
// By default Ace will try to load snippet files for the different modes and fail. We don't need them, so we use these
// placeholders until we define our own.
function defineDummySnippets(mode) {
ace.define("ace/snippets/" + mode, ["require", "exports", "module"], function(require, exports, module) {
"use strict";
exports.snippetText = "";
exports.scope = mode;
});
};
defineDummySnippets("python");
defineDummySnippets("sql");
defineDummySnippets("json");
function queryEditor(QuerySnippet) {
return {
restrict: 'E',
scope: {
@@ -69,94 +84,106 @@
'schema': '=',
'syntax': '='
},
template: '<textarea></textarea>',
template: '<div ui-ace="editorOptions" ng-model="query.query"></div>',
link: {
pre: function ($scope, element) {
$scope.syntax = $scope.syntax || 'sql';
var modes = {
'sql': 'text/x-sql',
'python': 'text/x-python',
'json': 'application/json'
};
var textarea = element.children()[0];
var editorOptions = {
mode: modes[$scope.syntax],
lineWrapping: true,
lineNumbers: true,
readOnly: false,
matchBrackets: true,
autoCloseBrackets: true,
extraKeys: {"Ctrl-Space": "autocomplete"}
};
var additionalHints = [];
CodeMirror.commands.autocomplete = function(cm) {
var hinter = function(editor, options) {
var hints = CodeMirror.hint.anyword(editor, options);
var cur = editor.getCursor(), token = editor.getTokenAt(cur).string;
hints.list = _.union(hints.list, _.filter(additionalHints, function (h) {
return h.search(token) === 0;
}));
return hints;
};
// CodeMirror.showHint(cm, CodeMirror.hint.anyword);
CodeMirror.showHint(cm, hinter);
};
var codemirror = CodeMirror.fromTextArea(textarea, editorOptions);
codemirror.on('change', function(instance) {
var newValue = instance.getValue();
if (newValue !== $scope.query.query) {
$scope.$evalAsync(function() {
$scope.query.query = newValue;
});
}
});
$scope.$watch('query.query', function () {
if ($scope.query.query !== codemirror.getValue()) {
codemirror.setValue($scope.query.query);
}
});
$scope.$watch('schema', function (schema) {
if (schema) {
var keywords = [];
_.each(schema, function (table) {
keywords.push(table.name);
_.each(table.columns, function (c) {
keywords.push(c);
$scope.editorOptions = {
mode: 'json',
require: ['ace/ext/language_tools'],
advanced: {
behavioursEnabled: true,
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
autoScrollEditorIntoView: true,
},
onLoad: function(editor) {
QuerySnippet.query(function(snippets) {
var snippetManager = ace.require("ace/snippets").snippetManager;
var m = {
snippetText: ''
};
m.snippets = snippetManager.parseSnippetFile(m.snippetText);
_.each(snippets, function(snippet) {
m.snippets.push(snippet.getSnippet());
});
snippetManager.register(m.snippets || [], m.scope);
});
additionalHints = _.unique(keywords);
editor.$blockScrolling = Infinity;
editor.getSession().setUseWrapMode(true);
editor.setShowPrintMargin(false);
$scope.$watch('syntax', function(syntax) {
var newMode = 'ace/mode/' + syntax;
editor.getSession().setMode(newMode);
});
$scope.$watch('schema', function(newSchema, oldSchema) {
if (newSchema !== oldSchema) {
var tokensCount = _.reduce(newSchema, function(totalLength, table) { return totalLength + table.columns.length }, 0);
// If there are too many tokens we disable live autocomplete, as it makes typing slower.
if (tokensCount > 5000) {
editor.setOption('enableLiveAutocompletion', false);
} else {
editor.setOption('enableLiveAutocompletion', true);
}
}
});
$scope.$parent.$on("angular-resizable.resizing", function (event, args) {
editor.resize();
});
editor.focus();
}
};
codemirror.refresh();
});
var langTools = ace.require("ace/ext/language_tools");
$scope.$watch('syntax', function(syntax) {
codemirror.setOption('mode', modes[syntax]);
});
var schemaCompleter = {
getCompletions: function(state, session, pos, prefix, callback) {
if (prefix.length === 0 || !$scope.schema) {
callback(null, []);
return;
}
$scope.$watch('lock', function (locked) {
var readOnly = locked ? 'nocursor' : false;
codemirror.setOption('readOnly', readOnly);
});
if (!$scope.schema.keywords) {
var keywords = {};
_.each($scope.schema, function (table) {
keywords[table.name] = 'Table';
_.each(table.columns, function (c) {
keywords[c] = 'Column';
keywords[table.name + "." + c] = 'Column';
});
});
$scope.schema.keywords = _.map(keywords, function(v, k) {
return {
name: k,
value: k,
score: 0,
meta: v
};
});
}
callback(null, $scope.schema.keywords);
}
};
langTools.addCompleter(schemaCompleter);
}
}
};
}
function queryFormatter($http) {
function queryFormatter($http, growl) {
return {
restrict: 'E',
// don't create new scope to avoid ui-codemirror bug
@@ -165,23 +192,60 @@
template: '<button type="button" class="btn btn-default btn-s"\
ng-click="formatQuery()">\
<span class="zmdi zmdi-format-indent-increase"></span>\
Format SQL\
Format Query\
</button>',
link: function($scope) {
$scope.formatQuery = function formatQuery() {
if ($scope.dataSource.syntax == 'json') {
try {
$scope.query.query = JSON.stringify(JSON.parse($scope.query.query), ' ', 4);
} catch(err) {
growl.addErrorMessage(err);
}
} else if ($scope.dataSource.syntax =='sql') {
$scope.queryFormatting = true;
$http.post('api/queries/format', {
'query': $scope.query.query
'query': $scope.query.query
}).success(function (response) {
$scope.query.query = response;
$scope.query.query = response;
}).finally(function () {
$scope.queryFormatting = false;
});
} else {
growl.addInfoMessage("Query formatting is not supported for your data source syntax.");
}
};
}
}
}
function schemaBrowser() {
return {
restrict: 'E',
scope: {
schema: '='
},
templateUrl: '/views/directives/schema_browser.html',
link: function ($scope) {
$scope.showTable = function(table) {
table.collapsed = !table.collapsed;
$scope.$broadcast('vsRepeatTrigger');
}
$scope.getSize = function(table) {
var size = 18;
if (!table.collapsed) {
size += 18 * table.columns.length;
}
return size;
}
}
}
}
function queryTimePicker() {
return {
restrict: 'E',
@@ -263,11 +327,15 @@
});
$scope.refreshOptions.push({
value: String(7 * 24 * 3600),
name: 'Once a week'
name: 'Every 7 days'
});
$scope.refreshOptions.push({
value: String(14 * 24 * 3600),
name: 'Every 14 days'
});
$scope.refreshOptions.push({
value: String(30 * 24 * 3600),
name: 'Every 30d'
name: 'Every 30 days'
});
$scope.$watch('refreshType', function() {
@@ -285,10 +353,11 @@
angular.module('redash.directives')
.directive('queryLink', queryLink)
.directive('querySourceLink', querySourceLink)
.directive('querySourceLink', ['$location', querySourceLink])
.directive('queryResultLink', queryResultLink)
.directive('queryEditor', queryEditor)
.directive('queryEditor', ['QuerySnippet', queryEditor])
.directive('queryRefreshSelect', queryRefreshSelect)
.directive('queryTimePicker', queryTimePicker)
.directive('queryFormatter', ['$http', queryFormatter]);
.directive('schemaBrowser', schemaBrowser)
.directive('queryFormatter', ['$http', 'growl', queryFormatter]);
})();

View File

@@ -49,6 +49,7 @@ angular.module('redash', [
$scope.embed = true;
$scope.visualization = visualization;
$scope.query = visualization.query;
$scope.apiKey = $location.search()['api_key'];
query = new Query(visualization.query);
$scope.queryResult = new QueryResult({query_result: query_result});
}])

View File

@@ -48,14 +48,15 @@ angular.module('redash.filters', []).
.filter('colWidth', function () {
return function (widgetWidth) {
if (widgetWidth == 0) {
if (widgetWidth === 0) {
return 0;
}
if (widgetWidth == 1) {
} else if (widgetWidth === 1) {
return 6;
} else if (widgetWidth === 2) {
return 12;
}
return 12;
}
return widgetWidth;
};
})
.filter('capitalize', function () {
@@ -71,6 +72,9 @@ angular.module('redash.filters', []).
.filter('dateTime', function() {
return function(value) {
if (!value) {
return '-';
}
return moment(value).format(clientConfig.dateTimeFormat);
}
})
@@ -120,4 +124,10 @@ angular.module('redash.filters', []).
filtered.push(items[i])
return filtered;
};
})
.filter('notEmpty', function() {
return function(collection) {
return !_.isEmpty(collection);
}
});

View File

@@ -1,5 +1,6 @@
(function () {
var Dashboard = function($resource, $http, Widget) {
var transformSingle = function(dashboard) {
dashboard.widgets = _.map(dashboard.widgets, function (row) {
return _.map(row, function (widget) {
@@ -27,13 +28,13 @@
isArray: true,
url: "api/dashboards/recent",
transformResponse: transform
}});
}});
resource.prototype.canEdit = function() {
return currentUser.canEdit(this) || this.can_edit;
};
resource.prototype.canEdit = function() {
return currentUser.hasPermission('admin') || currentUser.canEdit(this);
}
return resource;
return resource;
}
angular.module('redash.services')

View File

@@ -42,7 +42,7 @@
return;
}
if (Notification.permission !== "granted") {
if (Notification.permission === "default") {
Notification.requestPermission(function (status) {
if (Notification.permission !== status) {
Notification.permission = status;

View File

@@ -174,9 +174,14 @@
};
return (memo && _.some(filter.current, function(v) {
// We compare with either the value or the String representation of the value,
// because Select2 casts true/false to "true"/"false".
return v == row[filter.name] || String(row[filter.name]) == v
var value = row[filter.name];
if (moment.isMoment(value)) {
return value.isSame(v);
} else {
// We compare with either the value or the String representation of the value,
// because Select2 casts true/false to "true"/"false".
return (v == value || String(value) == v);
}
}));
}, true);
});
@@ -279,7 +284,7 @@
var typeSplit;
if (column.indexOf("::") != -1) {
typeSplit = "::";
} else if (column.indexOf("__" != -1)) {
} else if (column.indexOf("__") != -1) {
typeSplit = "__";
} else {
return column;
@@ -353,7 +358,13 @@
});
_.each(filters, function(filter) {
filter.values = _.uniq(filter.values);
filter.values = _.uniq(filter.values, function(v) {
if (moment.isMoment(v)) {
return v.unix();
} else {
return v;
}
});
});
this.filters = filters;
@@ -372,7 +383,10 @@
refreshStatus(queryResult, query);
}, 3000);
}
})
}, function(error) {
console.log("Connection error", error);
queryResult.update({job: {error: 'failed communicating with server. Please check your Internet connection and try again.', status: 4}})
});
}
QueryResult.getById = function (id) {
@@ -406,6 +420,11 @@
}, function(error) {
if (error.status === 403) {
queryResult.update(error.data);
} else if (error.status === 400 && 'job' in error.data) {
queryResult.update(error.data);
} else {
console.log("Unknown error", error);
queryResult.update({job: {error: 'unknown error occurred. Please try again later.', status: 4}})
}
});
@@ -415,7 +434,7 @@
return QueryResult;
};
var Query = function ($resource, QueryResult, DataSource) {
var Query = function ($resource, $location, QueryResult) {
var Query = $resource('api/queries/:id', {id: '@id'},
{
search: {
@@ -427,32 +446,27 @@
method: 'get',
isArray: true,
url: "api/queries/recent"
}});
},
query: {
isArray: false
},
myQueries: {
method: 'get',
isArray: false,
url: "api/queries/my"
}
});
Query.newQuery = function () {
return new Query({
query: "",
name: "New Query",
schedule: null,
user: currentUser
user: currentUser,
options: {}
});
};
Query.collectParamsFromQueryString = function($location, query) {
var parameterNames = query.getParameters();
var parameters = {};
var queryString = $location.search();
_.each(parameterNames, function(param, i) {
var qsName = "p_" + param;
if (qsName in queryString) {
parameters[param] = queryString[qsName];
}
});
return parameters;
};
Query.prototype.getSourceLink = function () {
return '/queries/' + this.id + '/source';
};
@@ -475,32 +489,31 @@
};
Query.prototype.paramsRequired = function() {
var queryParameters = this.getParameters();
return !_.isEmpty(queryParameters);
return this.getParameters().isRequired();
};
Query.prototype.getQueryResult = function (maxAge, parameters) {
Query.prototype.getQueryResult = function (maxAge) {
if (!this.query) {
return;
}
var queryText = this.query;
var queryParameters = this.getParameters();
var paramsRequired = !_.isEmpty(queryParameters);
var parameters = this.getParameters();
var missingParams = parameters.getMissing();
var missingParams = parameters === undefined ? queryParameters : _.difference(queryParameters, _.keys(parameters));
if (paramsRequired && missingParams.length > 0) {
if (missingParams.length > 0) {
var paramsWord = "parameter";
var valuesWord = "value";
if (missingParams.length > 1) {
paramsWord = "parameters";
valuesWord = "values";
}
return new QueryResult({job: {error: "Missing values for " + missingParams.join(', ') + " "+paramsWord+".", status: 4}});
return new QueryResult({job: {error: "missing " + valuesWord + " for " + missingParams.join(', ') + " "+paramsWord+".", status: 4}});
}
if (paramsRequired) {
queryText = Mustache.render(queryText, parameters);
if (parameters.isRequired()) {
queryText = Mustache.render(queryText, parameters.getValues());
// Need to clear latest results, to make sure we don't use results for different params.
this.latest_query_data = null;
@@ -524,39 +537,148 @@
return this.queryResult;
};
Query.prototype.getUrl = function(source, hash) {
var url = "queries/" + this.id;
if (source) {
url += '/source';
}
var params = "";
if (this.getParameters().isRequired()) {
_.each(this.getParameters().getValues(), function(value, name) {
if (value === null) {
return;
}
if (params !== "") {
params += "&";
}
params += 'p_' + encodeURIComponent(name) + "=" + encodeURIComponent(value);
});
}
if (params !== "") {
url += "?" + params;
}
if (hash) {
url += "#" + hash;
}
return url;
}
Query.prototype.getQueryResultPromise = function() {
return this.getQueryResult().toPromise();
};
Query.prototype.getParameters = function() {
var parts = Mustache.parse(this.query);
var parameters = [];
var collectParams = function(parts) {
parameters = [];
_.each(parts, function(part) {
if (part[0] == 'name' || part[0] == '&') {
parameters.push(part[1]);
} else if (part[0] == '#') {
parameters = _.union(parameters, collectParams(part[4]));
var Parameters = function(query) {
this.query = query;
this.parseQuery = function() {
var parts = Mustache.parse(this.query.query);
var parameters = [];
var collectParams = function(parts) {
parameters = [];
_.each(parts, function(part) {
if (part[0] == 'name' || part[0] == '&') {
parameters.push(part[1]);
} else if (part[0] == '#') {
parameters = _.union(parameters, collectParams(part[4]));
}
});
return parameters;
};
parameters = _.uniq(collectParams(parts));
return parameters;
}
this.updateParameters = function() {
if (this.query.query === this.cachedQueryText) {
return;
}
this.cachedQueryText = this.query.query;
var parameterNames = this.parseQuery();
this.query.options.parameters = this.query.options.parameters || [];
var parametersMap = {};
_.each(this.query.options.parameters, function(param) {
parametersMap[param.name] = param;
});
_.each(parameterNames, function(param) {
if (!_.has(parametersMap, param)) {
this.query.options.parameters.push({
'title': param,
'name': param,
'type': 'text',
'value': null
});
}
}.bind(this));
this.query.options.parameters = _.filter(this.query.options.parameters, function(p) { return _.indexOf(parameterNames, p.name) !== -1});
}
this.initFromQueryString = function() {
var queryString = $location.search();
_.each(this.get(), function(param) {
var queryStringName = 'p_' + param.name;
if (_.has(queryString, queryStringName)) {
param.value = queryString[queryStringName];
}
});
return parameters;
};
}
parameters = collectParams(parts);
this.updateParameters();
this.initFromQueryString();
}
return parameters;
Parameters.prototype.get = function() {
this.updateParameters();
return this.query.options.parameters;
};
Parameters.prototype.getMissing = function() {
return _.pluck(_.filter(this.get(), function(p) { return p.value === null || p.value === ''; }), 'title');
}
Parameters.prototype.isRequired = function() {
return !_.isEmpty(this.get());
}
Parameters.prototype.getValues = function() {
var params = this.get();
return _.object(_.pluck(params, 'name'), _.pluck(params, 'value'));
}
Query.prototype.getParameters = function() {
if (!this.$parameters) {
this.$parameters = new Parameters(this);
}
return this.$parameters;
}
Query.prototype.getParametersDefs = function() {
return this.getParameters().get();
}
return Query;
};
var DataSource = function ($resource) {
var actions = {
'get': {'method': 'GET', 'cache': false, 'isArray': false},
'query': {'method': 'GET', 'cache': false, 'isArray': true},
'test': {'method': 'POST', 'cache': false, 'isArray': false, 'url': 'api/data_sources/:id/test'},
'getSchema': {'method': 'GET', 'cache': true, 'isArray': true, 'url': 'api/data_sources/:id/schema'}
};
@@ -565,6 +687,17 @@
return DataSourceResource;
};
var Destination = function ($resource) {
var actions = {
'get': {'method': 'GET', 'cache': false, 'isArray': false},
'query': {'method': 'GET', 'cache': false, 'isArray': true}
};
var DestinationResource = $resource('api/destinations/:id', {id: '@id'}, actions);
return DestinationResource;
};
var User = function ($resource, $http) {
var transformSingle = function(user) {
if (user.groups !== undefined) {
@@ -605,7 +738,7 @@
};
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/:subscriberId', {alertId: '@alert_id', subscriberId: '@id'});
return resource;
};
@@ -617,7 +750,9 @@
var newData = _.extend({}, data);
if (newData.query_id === undefined) {
newData.query_id = newData.query.id;
newData.destination_id = newData.destinations;
delete newData.query;
delete newData.destinations;
}
return newData;
@@ -629,6 +764,24 @@
return resource;
};
var QuerySnippet = function ($resource) {
var resource = $resource('api/query_snippets/:id', {id: '@id'});
resource.prototype.getSnippet = function() {
var name = this.trigger;
if (this.description !== "") {
name = this.trigger + ": " + this.description;
}
return {
"name": name,
"content": this.snippet,
"tabTrigger": this.trigger
};
}
return resource;
};
var Widget = function ($resource, Query) {
var WidgetResource = $resource('api/widgets/:id', {id: '@id'});
@@ -652,11 +805,13 @@
angular.module('redash.services')
.factory('QueryResult', ['$resource', '$timeout', '$q', QueryResult])
.factory('Query', ['$resource', 'QueryResult', 'DataSource', Query])
.factory('Query', ['$resource', '$location', 'QueryResult', Query])
.factory('DataSource', ['$resource', DataSource])
.factory('Destination', ['$resource', Destination])
.factory('Alert', ['$resource', '$http', Alert])
.factory('AlertSubscription', ['$resource', AlertSubscription])
.factory('Widget', ['$resource', 'Query', Widget])
.factory('User', ['$resource', '$http', User])
.factory('Group', ['$resource', Group]);
.factory('Group', ['$resource', Group])
.factory('QuerySnippet', ['$resource', QuerySnippet]);
})();

505
rd_ui/app/scripts/vendor/cloud.js vendored Normal file
View File

@@ -0,0 +1,505 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.d3||(g.d3 = {}));g=(g.layout||(g.layout = {}));g.cloud = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
// Word cloud layout by Jason Davies, https://www.jasondavies.com/wordcloud/
// Algorithm due to Jonathan Feinberg, http://static.mrfeinberg.com/bv_ch03.pdf
var dispatch = require("d3-dispatch").dispatch;
var cloudRadians = Math.PI / 180,
cw = 1 << 11 >> 5,
ch = 1 << 11;
d3.cloud = function() {
var size = [256, 256],
text = cloudText,
font = cloudFont,
fontSize = cloudFontSize,
fontStyle = cloudFontNormal,
fontWeight = cloudFontNormal,
rotate = cloudRotate,
padding = cloudPadding,
spiral = archimedeanSpiral,
words = [],
timeInterval = Infinity,
event = dispatch("word", "end"),
timer = null,
random = Math.random,
cloud = {},
canvas = cloudCanvas;
cloud.canvas = function(_) {
return arguments.length ? (canvas = functor(_), cloud) : canvas;
};
cloud.start = function() {
var contextAndRatio = getContext(canvas()),
board = zeroArray((size[0] >> 5) * size[1]),
bounds = null,
n = words.length,
i = -1,
tags = [],
data = words.map(function(d, i) {
d.text = text.call(this, d, i);
d.font = font.call(this, d, i);
d.style = fontStyle.call(this, d, i);
d.weight = fontWeight.call(this, d, i);
d.rotate = rotate.call(this, d, i);
d.size = ~~fontSize.call(this, d, i);
d.padding = padding.call(this, d, i);
return d;
}).sort(function(a, b) { return b.size - a.size; });
if (timer) clearInterval(timer);
timer = setInterval(step, 0);
step();
return cloud;
function step() {
var start = Date.now();
while (Date.now() - start < timeInterval && ++i < n && timer) {
var d = data[i];
d.x = (size[0] * (random() + .5)) >> 1;
d.y = (size[1] * (random() + .5)) >> 1;
cloudSprite(contextAndRatio, d, data, i);
if (d.hasText && place(board, d, bounds)) {
tags.push(d);
event.word(d);
if (bounds) cloudBounds(bounds, d);
else bounds = [{x: d.x + d.x0, y: d.y + d.y0}, {x: d.x + d.x1, y: d.y + d.y1}];
// Temporary hack
d.x -= size[0] >> 1;
d.y -= size[1] >> 1;
}
}
if (i >= n) {
cloud.stop();
event.end(tags, bounds);
}
}
}
cloud.stop = function() {
if (timer) {
clearInterval(timer);
timer = null;
}
return cloud;
};
function getContext(canvas) {
canvas.width = canvas.height = 1;
var ratio = Math.sqrt(canvas.getContext("2d").getImageData(0, 0, 1, 1).data.length >> 2);
canvas.width = (cw << 5) / ratio;
canvas.height = ch / ratio;
var context = canvas.getContext("2d");
context.fillStyle = context.strokeStyle = "red";
context.textAlign = "center";
return {context: context, ratio: ratio};
}
function place(board, tag, bounds) {
var perimeter = [{x: 0, y: 0}, {x: size[0], y: size[1]}],
startX = tag.x,
startY = tag.y,
maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]),
s = spiral(size),
dt = random() < .5 ? 1 : -1,
t = -dt,
dxdy,
dx,
dy;
while (dxdy = s(t += dt)) {
dx = ~~dxdy[0];
dy = ~~dxdy[1];
if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break;
tag.x = startX + dx;
tag.y = startY + dy;
if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 ||
tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) continue;
// TODO only check for collisions within current bounds.
if (!bounds || !cloudCollide(tag, board, size[0])) {
if (!bounds || collideRects(tag, bounds)) {
var sprite = tag.sprite,
w = tag.width >> 5,
sw = size[0] >> 5,
lx = tag.x - (w << 4),
sx = lx & 0x7f,
msx = 32 - sx,
h = tag.y1 - tag.y0,
x = (tag.y + tag.y0) * sw + (lx >> 5),
last;
for (var j = 0; j < h; j++) {
last = 0;
for (var i = 0; i <= w; i++) {
board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0);
}
x += sw;
}
delete tag.sprite;
return true;
}
}
}
return false;
}
cloud.timeInterval = function(_) {
return arguments.length ? (timeInterval = _ == null ? Infinity : _, cloud) : timeInterval;
};
cloud.words = function(_) {
return arguments.length ? (words = _, cloud) : words;
};
cloud.size = function(_) {
return arguments.length ? (size = [+_[0], +_[1]], cloud) : size;
};
cloud.font = function(_) {
return arguments.length ? (font = functor(_), cloud) : font;
};
cloud.fontStyle = function(_) {
return arguments.length ? (fontStyle = functor(_), cloud) : fontStyle;
};
cloud.fontWeight = function(_) {
return arguments.length ? (fontWeight = functor(_), cloud) : fontWeight;
};
cloud.rotate = function(_) {
return arguments.length ? (rotate = functor(_), cloud) : rotate;
};
cloud.text = function(_) {
return arguments.length ? (text = functor(_), cloud) : text;
};
cloud.spiral = function(_) {
return arguments.length ? (spiral = spirals[_] || _, cloud) : spiral;
};
cloud.fontSize = function(_) {
return arguments.length ? (fontSize = functor(_), cloud) : fontSize;
};
cloud.padding = function(_) {
return arguments.length ? (padding = functor(_), cloud) : padding;
};
cloud.random = function(_) {
return arguments.length ? (random = _, cloud) : random;
};
cloud.on = function() {
var value = event.on.apply(event, arguments);
return value === event ? cloud : value;
};
return cloud;
};
function cloudText(d) {
return d.text;
}
function cloudFont() {
return "serif";
}
function cloudFontNormal() {
return "normal";
}
function cloudFontSize(d) {
return Math.sqrt(d.value);
}
function cloudRotate() {
return (~~(Math.random() * 6) - 3) * 30;
}
function cloudPadding() {
return 1;
}
// Fetches a monochrome sprite bitmap for the specified text.
// Load in batches for speed.
function cloudSprite(contextAndRatio, d, data, di) {
if (d.sprite) return;
var c = contextAndRatio.context,
ratio = contextAndRatio.ratio;
c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio);
var x = 0,
y = 0,
maxh = 0,
n = data.length;
--di;
while (++di < n) {
d = data[di];
c.save();
c.font = d.style + " " + d.weight + " " + ~~((d.size + 1) / ratio) + "px " + d.font;
var w = c.measureText(d.text + "m").width * ratio,
h = d.size << 1;
if (d.rotate) {
var sr = Math.sin(d.rotate * cloudRadians),
cr = Math.cos(d.rotate * cloudRadians),
wcr = w * cr,
wsr = w * sr,
hcr = h * cr,
hsr = h * sr;
w = (Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f) >> 5 << 5;
h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr));
} else {
w = (w + 0x1f) >> 5 << 5;
}
if (h > maxh) maxh = h;
if (x + w >= (cw << 5)) {
x = 0;
y += maxh;
maxh = 0;
}
if (y + h >= ch) break;
c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio);
if (d.rotate) c.rotate(d.rotate * cloudRadians);
c.fillText(d.text, 0, 0);
if (d.padding) c.lineWidth = 2 * d.padding, c.strokeText(d.text, 0, 0);
c.restore();
d.width = w;
d.height = h;
d.xoff = x;
d.yoff = y;
d.x1 = w >> 1;
d.y1 = h >> 1;
d.x0 = -d.x1;
d.y0 = -d.y1;
d.hasText = true;
x += w;
}
var pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data,
sprite = [];
while (--di >= 0) {
d = data[di];
if (!d.hasText) continue;
var w = d.width,
w32 = w >> 5,
h = d.y1 - d.y0;
// Zero the buffer
for (var i = 0; i < h * w32; i++) sprite[i] = 0;
x = d.xoff;
if (x == null) return;
y = d.yoff;
var seen = 0,
seenRow = -1;
for (var j = 0; j < h; j++) {
for (var i = 0; i < w; i++) {
var k = w32 * j + (i >> 5),
m = pixels[((y + j) * (cw << 5) + (x + i)) << 2] ? 1 << (31 - (i % 32)) : 0;
sprite[k] |= m;
seen |= m;
}
if (seen) seenRow = j;
else {
d.y0++;
h--;
j--;
y++;
}
}
d.y1 = d.y0 + seenRow;
d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32);
}
}
// Use mask-based collision detection.
function cloudCollide(tag, board, sw) {
sw >>= 5;
var sprite = tag.sprite,
w = tag.width >> 5,
lx = tag.x - (w << 4),
sx = lx & 0x7f,
msx = 32 - sx,
h = tag.y1 - tag.y0,
x = (tag.y + tag.y0) * sw + (lx >> 5),
last;
for (var j = 0; j < h; j++) {
last = 0;
for (var i = 0; i <= w; i++) {
if (((last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0))
& board[x + i]) return true;
}
x += sw;
}
return false;
}
function cloudBounds(bounds, d) {
var b0 = bounds[0],
b1 = bounds[1];
if (d.x + d.x0 < b0.x) b0.x = d.x + d.x0;
if (d.y + d.y0 < b0.y) b0.y = d.y + d.y0;
if (d.x + d.x1 > b1.x) b1.x = d.x + d.x1;
if (d.y + d.y1 > b1.y) b1.y = d.y + d.y1;
}
function collideRects(a, b) {
return a.x + a.x1 > b[0].x && a.x + a.x0 < b[1].x && a.y + a.y1 > b[0].y && a.y + a.y0 < b[1].y;
}
function archimedeanSpiral(size) {
var e = size[0] / size[1];
return function(t) {
return [e * (t *= .1) * Math.cos(t), t * Math.sin(t)];
};
}
function rectangularSpiral(size) {
var dy = 4,
dx = dy * size[0] / size[1],
x = 0,
y = 0;
return function(t) {
var sign = t < 0 ? -1 : 1;
// See triangular numbers: T_n = n * (n + 1) / 2.
switch ((Math.sqrt(1 + 4 * sign * t) - sign) & 3) {
case 0: x += dx; break;
case 1: y += dy; break;
case 2: x -= dx; break;
default: y -= dy; break;
}
return [x, y];
};
}
// TODO reuse arrays?
function zeroArray(n) {
var a = [],
i = -1;
while (++i < n) a[i] = 0;
return a;
}
function cloudCanvas() {
return document.createElement("canvas");
}
function functor(d) {
return typeof d === "function" ? d : function() { return d; };
}
var spirals = {
archimedean: archimedeanSpiral,
rectangular: rectangularSpiral
};
},{"d3-dispatch":2}],2:[function(require,module,exports){
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
factory((global.dispatch = {}));
}(this, function (exports) { 'use strict';
function Dispatch(types) {
var i = -1,
n = types.length,
callbacksByType = {},
callbackByName = {},
type,
that = this;
that.on = function(type, callback) {
type = parseType(type);
// Return the current callback, if any.
if (arguments.length < 2) {
return (callback = callbackByName[type.name]) && callback.value;
}
// If a type was specified…
if (type.type) {
var callbacks = callbacksByType[type.type],
callback0 = callbackByName[type.name],
i;
// Remove the current callback, if any, using copy-on-remove.
if (callback0) {
callback0.value = null;
i = callbacks.indexOf(callback0);
callbacksByType[type.type] = callbacks = callbacks.slice(0, i).concat(callbacks.slice(i + 1));
delete callbackByName[type.name];
}
// Add the new callback, if any.
if (callback) {
callback = {value: callback};
callbackByName[type.name] = callback;
callbacks.push(callback);
}
}
// Otherwise, if a null callback was specified, remove all callbacks with the given name.
else if (callback == null) {
for (var otherType in callbacksByType) {
if (callback = callbackByName[otherType + type.name]) {
callback.value = null;
var callbacks = callbacksByType[otherType], i = callbacks.indexOf(callback);
callbacksByType[otherType] = callbacks.slice(0, i).concat(callbacks.slice(i + 1));
delete callbackByName[callback.name];
}
}
}
return that;
};
while (++i < n) {
type = types[i] + "";
if (!type || (type in that)) throw new Error("illegal or duplicate type: " + type);
callbacksByType[type] = [];
that[type] = applier(type);
}
function parseType(type) {
var i = (type += "").indexOf("."), name = type;
if (i >= 0) type = type.slice(0, i); else name += ".";
if (type && !callbacksByType.hasOwnProperty(type)) throw new Error("unknown type: " + type);
return {type: type, name: name};
}
function applier(type) {
return function() {
var callbacks = callbacksByType[type], // Defensive reference; copy-on-remove.
callback,
callbackValue,
i = -1,
n = callbacks.length;
while (++i < n) {
if (callbackValue = (callback = callbacks[i]).value) {
callbackValue.apply(this, arguments);
}
}
return that;
};
}
}
function dispatch() {
return new Dispatch(arguments);
}
dispatch.prototype = Dispatch.prototype; // allow instanceof
exports.dispatch = dispatch;
}));
},{}]},{},[1])(1)
});

294
rd_ui/app/scripts/vendor/d3.sankey.js vendored Normal file
View File

@@ -0,0 +1,294 @@
d3.sankey = function() {
var sankey = {},
nodeWidth = 24,
nodePadding = 8,
size = [1, 1],
nodes = [],
links = [];
sankey.nodeWidth = function(_) {
if (!arguments.length) return nodeWidth;
nodeWidth = +_;
return sankey;
};
sankey.nodePadding = function(_) {
if (!arguments.length) return nodePadding;
nodePadding = +_;
return sankey;
};
sankey.nodes = function(_) {
if (!arguments.length) return nodes;
nodes = _;
return sankey;
};
sankey.links = function(_) {
if (!arguments.length) return links;
links = _;
return sankey;
};
sankey.size = function(_) {
if (!arguments.length) return size;
size = _;
return sankey;
};
sankey.layout = function(iterations) {
computeNodeLinks();
computeNodeValues();
computeNodeBreadths();
computeNodeDepths(iterations);
computeLinkDepths();
return sankey;
};
sankey.relayout = function() {
computeLinkDepths();
return sankey;
};
sankey.link = function() {
var curvature = .5;
function link(d) {
var x0 = d.source.x + d.source.dx,
x1 = d.target.x,
xi = d3.interpolateNumber(x0, x1),
x2 = xi(curvature),
x3 = xi(1 - curvature),
y0 = d.source.y + d.sy + d.dy / 2,
y1 = d.target.y + d.ty + d.dy / 2;
return "M" + x0 + "," + y0
+ "C" + x2 + "," + y0
+ " " + x3 + "," + y1
+ " " + x1 + "," + y1;
}
link.curvature = function(_) {
if (!arguments.length) return curvature;
curvature = +_;
return link;
};
return link;
};
// Populate the sourceLinks and targetLinks for each node.
// Also, if the source and target are not objects, assume they are indices.
function computeNodeLinks() {
nodes.forEach(function(node) {
node.sourceLinks = [];
node.targetLinks = [];
});
links.forEach(function(link) {
var source = link.source,
target = link.target;
if (typeof source === "number") source = link.source = nodes[link.source];
if (typeof target === "number") target = link.target = nodes[link.target];
source.sourceLinks.push(link);
target.targetLinks.push(link);
});
}
// Compute the value (size) of each node by summing the associated links.
function computeNodeValues() {
nodes.forEach(function(node) {
node.value = Math.max(
d3.sum(node.sourceLinks, value),
d3.sum(node.targetLinks, value)
);
});
}
// Iteratively assign the breadth (x-position) for each node.
// Nodes are assigned the maximum breadth of incoming neighbors plus one;
// nodes with no incoming links are assigned breadth zero, while
// nodes with no outgoing links are assigned the maximum breadth.
function computeNodeBreadths() {
var remainingNodes = nodes,
nextNodes,
x = 0;
while (remainingNodes.length) {
nextNodes = [];
remainingNodes.forEach(function(node) {
node.x = x;
node.dx = nodeWidth;
node.sourceLinks.forEach(function(link) {
if (nextNodes.indexOf(link.target) < 0) {
nextNodes.push(link.target);
}
});
});
remainingNodes = nextNodes;
++x;
}
//
moveSinksRight(x);
scaleNodeBreadths((size[0] - nodeWidth) / (x - 1));
}
function moveSourcesRight() {
nodes.forEach(function(node) {
if (!node.targetLinks.length) {
node.x = d3.min(node.sourceLinks, function(d) { return d.target.x; }) - 1;
}
});
}
function moveSinksRight(x) {
nodes.forEach(function(node) {
if (!node.sourceLinks.length) {
node.x = x - 1;
}
});
}
function scaleNodeBreadths(kx) {
nodes.forEach(function(node) {
node.x *= kx;
});
}
function computeNodeDepths(iterations) {
var nodesByBreadth = d3.nest()
.key(function(d) { return d.x; })
.sortKeys(d3.ascending)
.entries(nodes)
.map(function(d) { return d.values; });
//
initializeNodeDepth();
resolveCollisions();
for (var alpha = 1; iterations > 0; --iterations) {
relaxRightToLeft(alpha *= .99);
resolveCollisions();
relaxLeftToRight(alpha);
resolveCollisions();
}
function initializeNodeDepth() {
var ky = d3.min(nodesByBreadth, function(nodes) {
return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value);
});
nodesByBreadth.forEach(function(nodes) {
nodes.forEach(function(node, i) {
node.y = i;
node.dy = node.value * ky;
});
});
links.forEach(function(link) {
link.dy = link.value * ky;
});
}
function relaxLeftToRight(alpha) {
nodesByBreadth.forEach(function(nodes, breadth) {
nodes.forEach(function(node) {
if (node.targetLinks.length) {
var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value);
node.y += (y - center(node)) * alpha;
}
});
});
function weightedSource(link) {
return center(link.source) * link.value;
}
}
function relaxRightToLeft(alpha) {
nodesByBreadth.slice().reverse().forEach(function(nodes) {
nodes.forEach(function(node) {
if (node.sourceLinks.length) {
var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
node.y += (y - center(node)) * alpha;
}
});
});
function weightedTarget(link) {
return center(link.target) * link.value;
}
}
function resolveCollisions() {
nodesByBreadth.forEach(function(nodes) {
var node,
dy,
y0 = 0,
n = nodes.length,
i;
// Push any overlapping nodes down.
nodes.sort(ascendingDepth);
for (i = 0; i < n; ++i) {
node = nodes[i];
dy = y0 - node.y;
if (dy > 0) node.y += dy;
y0 = node.y + node.dy + nodePadding;
}
// If the bottommost node goes outside the bounds, push it back up.
dy = y0 - nodePadding - size[1];
if (dy > 0) {
y0 = node.y -= dy;
// Push any overlapping nodes back up.
for (i = n - 2; i >= 0; --i) {
node = nodes[i];
dy = node.y + node.dy + nodePadding - y0;
if (dy > 0) node.y -= dy;
y0 = node.y;
}
}
});
}
function ascendingDepth(a, b) {
return a.y - b.y;
}
}
function computeLinkDepths() {
nodes.forEach(function(node) {
node.sourceLinks.sort(ascendingTargetDepth);
node.targetLinks.sort(ascendingSourceDepth);
});
nodes.forEach(function(node) {
var sy = 0, ty = 0;
node.sourceLinks.forEach(function(link) {
link.sy = sy;
sy += link.dy;
});
node.targetLinks.forEach(function(link) {
link.ty = ty;
ty += link.dy;
});
});
function ascendingSourceDepth(a, b) {
return a.source.y - b.source.y;
}
function ascendingTargetDepth(a, b) {
return a.target.y - b.target.y;
}
}
function center(node) {
return node.y + node.dy / 2;
}
function value(link) {
return link.value;
}
return sankey;
};

View File

@@ -167,7 +167,7 @@
scope.$watch('visualization.type', function (type, oldType) {
// if not edited by user, set name to match type
if (type && oldType !== type && scope.visualization && !scope.visForm.name.$dirty) {
scope.visualization.name = _.string.titleize(scope.visualization.type);
scope.visualization.name = Visualization.visualizations[scope.visualization.type].name;
}
if (type && oldType !== type && scope.visualization) {

View File

@@ -8,7 +8,7 @@
'</boxplot-renderer>';
var editTemplate = '<boxplot-editor></boxplot-editor>';
VisualizationProvider.registerVisualization({
type: 'BOXPLOT',
name: 'Boxplot',
@@ -37,7 +37,10 @@
};
$scope.$watch('[queryResult && queryResult.getData(), visualization.options]', function () {
if ($scope.queryResult.getData() === null) {
return;
}
var data = $scope.queryResult.getData();
var parentWidth = d3.select(elm[0].parentNode).node().getBoundingClientRect().width;
var margin = {top: 10, right: 50, bottom: 40, left: 50, inner: 25},
@@ -52,7 +55,7 @@
var xAxisLabel = $scope.visualization.options.xAxisLabel;
var yAxisLabel = $scope.visualization.options.yAxisLabel;
var columns = $scope.queryResult.columnNames;
var columns = $scope.queryResult.getColumnNames();
var xscale = d3.scale.ordinal()
.domain(columns)
.rangeBands([0, parentWidth-margin.left-margin.right]);
@@ -82,7 +85,7 @@
.whiskers(iqr(1.5))
.width(boxWidth-2*margin.inner)
.height(height)
.domain([min*0.99,max*1.01]);
.domain([min*0.99,max*1.01]);
var xAxis = d3.svg.axis()
.scale(xscale)
.orient("bottom");
@@ -132,7 +135,7 @@
plot.append("rect")
.attr("class", "grid-background")
.attr("width", width)
.attr("height", height);
.attr("height", height);
plot.append("g")
.attr("class","grid")
@@ -157,7 +160,7 @@
.attr("width", boxWidth)
.attr("height", height)
.attr("transform", function(d,i) { return "translate(" + barOffset(i) + "," + 0 + ")"; } )
.call(chart);
.call(chart);
}, true);
}
}

View File

@@ -13,7 +13,8 @@
xAxis: {type: 'datetime', labels: {enabled: true}},
series: {stacking: null},
seriesOptions: {},
columnMapping: {}
columnMapping: {},
bottomMargin: 50
};
VisualizationProvider.registerVisualization({
@@ -93,7 +94,7 @@
};
scope.xAxisScales = ['datetime', 'linear', 'logarithmic', 'category'];
scope.yAxisScales = ['linear', 'logarithmic'];
scope.yAxisScales = ['linear', 'logarithmic', 'datetime'];
var refreshColumns = function() {
scope.columns = scope.queryResult.getColumns();
@@ -194,6 +195,10 @@
scope.options.legend = {enabled: true};
}
if (!_.has(scope.options, 'bottomMargin')) {
scope.options.bottomMargin = 50;
}
if (scope.columnNames)
_.each(scope.options.columnMapping, function(value, key) {
if (scope.columnNames.length > 0 && !_.contains(scope.columnNames, key))

View File

@@ -1,43 +0,0 @@
(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

@@ -11,9 +11,9 @@
var editTemplate = '<map-editor></map-editor>';
var defaultOptions = {
'height': 500,
'draw': 'Marker',
'classify':'none'
height: 500,
classify: 'none',
clusterMarkers: true
};
VisualizationProvider.registerVisualization({
@@ -31,195 +31,200 @@
restrict: 'E',
templateUrl: '/views/visualizations/map.html',
link: function($scope, elm, attrs) {
$scope.$watch('queryResult && queryResult.getData()', render, true);
$scope.$watch('visualization.options', render, true);
angular.element(window).on("resize", resize);
$scope.$watch('visualization.options.height', resize);
var setBounds = function(){
var color = d3.scale.category10();
var map = L.map(elm[0].children[0].children[0], {scrollWheelZoom: false});
var mapControls = L.control.layers().addTo(map);
var layers = {};
var tileLayer = L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
map.on('focus',function(){
map.on('moveend', getBounds);
});
map.on('blur',function(){
map.off('moveend', getBounds);
});
// Following line is used to avoid "Couldn't autodetect L.Icon.Default.imagePath" error
// https://github.com/Leaflet/Leaflet/issues/766#issuecomment-7741039
L.Icon.Default.imagePath = L.Icon.Default.imagePath || "//api.tiles.mapbox.com/mapbox.js/v2.2.1/images";
function resize() {
if (!map) return;
map.invalidateSize(false);
setBounds();
}
function setBounds (){
var b = $scope.visualization.options.bounds;
if(b){
$scope.map.fitBounds([[b._southWest.lat, b._southWest.lng],[b._northEast.lat, b._northEast.lng]]);
} else if ($scope.features.length > 0){
var group= new L.featureGroup($scope.features);
$scope.map.fitBounds(group.getBounds());
map.fitBounds([[b._southWest.lat, b._southWest.lng],[b._northEast.lat, b._northEast.lng]]);
} else if (layers){
var allMarkers = _.flatten(_.map(_.values(layers), function(l) { return l.getLayers() }));
var group = new L.featureGroup(allMarkers);
map.fitBounds(group.getBounds());
}
};
$scope.$watch('[queryResult && queryResult.getData(), visualization.options.draw,visualization.options.latColName,'+
'visualization.options.lonColName,visualization.options.classify,visualization.options.classify]',
function() {
var marker = function(lat,lon){
if (lat == null || lon == null) return;
var createMarker = function(lat,lon){
if (lat == null || lon == null) return;
return L.marker([lat, lon]);
};
return L.marker([lat, lon]);
};
var heatpoint = function(lat,lon,obj){
if (lat == null || lon == null) return;
var heatpoint = function(lat, lon, color){
if (lat == null || lon == null) return;
var color = 'red';
var style = {
fillColor:color,
fillOpacity:0.9,
stroke:false
};
if (obj &&
obj[$scope.visualization.options.classify] &&
$scope.visualization.options.classification){
var v = $.grep($scope.visualization.options.classification,function(e){
return e.value == obj[$scope.visualization.options.classify];
});
if (v.length >0) color = v[0].color;
}
return L.circleMarker([lat,lon],style)
};
var style = {
fillColor:color,
fillOpacity:0.5,
stroke:false
};
function getBounds() {
$scope.visualization.options.bounds = map.getBounds();
}
return L.circleMarker([lat,lon],style)
};
function createDescription(latCol, lonCol, row) {
var lat = row[latCol];
var lon = row[lonCol];
var color = function(val){
// taken from http://jsfiddle.net/xgJ2e/2/
var description = '<ul style="list-style-type: none;padding-left: 0">';
description += "<li><strong>"+lat+ ", " + lon + "</strong>";
var h= Math.floor((100 - val) * 120 / 100);
var s = Math.abs(val - 50)/50;
var v = 1;
for (var k in row){
if (!(k == latCol || k == lonCol)) {
description += "<li>" + k + ": " + row[k] + "</li>";
}
}
var rgb, i, data = [];
if (s === 0) {
rgb = [v,v,v];
} else {
h = h / 60;
i = Math.floor(h);
data = [v*(1-s), v*(1-s*(h-i)), v*(1-s*(1-(h-i)))];
switch(i) {
case 0:
rgb = [v, data[2], data[0]];
break;
case 1:
rgb = [data[1], v, data[0]];
break;
case 2:
rgb = [data[0], v, data[2]];
break;
case 3:
rgb = [data[0], data[1], v];
break;
case 4:
rgb = [data[2], data[0], v];
break;
default:
rgb = [v, data[0], data[1]];
break;
return description;
}
function removeLayer(layer) {
if (layer) {
mapControls.removeLayer(layer);
map.removeLayer(layer);
}
}
function addLayer(name, points) {
var latCol = $scope.visualization.options.latColName || 'lat';
var lonCol = $scope.visualization.options.lonColName || 'lon';
var classify = $scope.visualization.options.classify;
var markers;
if ($scope.visualization.options.clusterMarkers) {
var color = $scope.visualization.options.groups[name].color;
var options = {};
if (classify) {
options.iconCreateFunction = function (cluster) {
var childCount = cluster.getChildCount();
var c = ' marker-cluster-';
if (childCount < 10) {
c += 'small';
} else if (childCount < 100) {
c += 'medium';
} else {
c += 'large';
}
c = '';
var style = 'color: white; background-color: '+color+';';
return L.divIcon({ html: '<div style="'+style+'"><span>' + childCount + '</span></div>', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) });
}
return '#' + rgb.map(function(x){
return ("0" + Math.round(x*255).toString(16)).slice(-2);
}).join('');
};
// Following line is used to avoid "Couldn't autodetect L.Icon.Default.imagePath" error
// https://github.com/Leaflet/Leaflet/issues/766#issuecomment-7741039
L.Icon.Default.imagePath = L.Icon.Default.imagePath || "//api.tiles.mapbox.com/mapbox.js/v2.2.1/images";
function getBounds(e) {
$scope.visualization.options.bounds = $scope.map.getBounds();
}
var queryData = $scope.queryResult.getData();
var classify = $scope.visualization.options.classify;
markers = L.markerClusterGroup(options);
} else {
markers = L.layerGroup();
}
if (queryData) {
$scope.visualization.options.classification = [];
// create markers
_.each(points, function(row) {
var marker;
for (var row in queryData) {
if (queryData[row][classify] &&
$.grep($scope.visualization.options.classification, function (e) {
return e.value == queryData[row][classify]
}).length == 0) {
$scope.visualization.options.classification.push({value: queryData[row][classify], color: null});
}
}
$.each($scope.visualization.options.classification, function (i, c) {
c.color = color(parseInt((i / $scope.visualization.options.classification.length) * 100));
});
if (!$scope.map) {
$scope.map = L.map(elm[0].children[0].children[0])
}
L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo($scope.map);
$scope.features = $scope.features || [];
var tmp_features = [];
var lat_col = $scope.visualization.options.latColName || 'lat';
var lon_col = $scope.visualization.options.lonColName || 'lon';
for (var row in queryData) {
var feature;
if ($scope.visualization.options.draw == 'Marker') {
feature = marker(queryData[row][lat_col], queryData[row][lon_col])
} else if ($scope.visualization.options.draw == 'Color') {
feature = heatpoint(queryData[row][lat_col], queryData[row][lon_col], queryData[row])
}
if (!feature) continue;
var obj_description = '<ul style="list-style-type: none;padding-left: 0">';
for (var k in queryData[row]){
obj_description += "<li>" + k + ": " + queryData[row][k] + "</li>";
}
obj_description += '</ul>';
feature.bindPopup(obj_description);
tmp_features.push(feature);
}
$.each($scope.features, function (i, f) {
$scope.map.removeLayer(f);
});
$scope.features = tmp_features;
$.each($scope.features, function (i, f) {
f.addTo($scope.map)
});
setBounds();
$scope.map.on('focus',function(){
$scope.map.on('moveend', getBounds);
});
$scope.map.on('blur',function(){
$scope.map.off('moveend', getBounds);
});
// We redraw the map if it was loaded in a hidden tab
if ($('a[href="#'+$scope.visualization.id+'"]').length > 0) {
$('a[href="#'+$scope.visualization.id+'"]').on('click', function () {
setTimeout(function() {
$scope.map.invalidateSize(false);
setBounds();
},500);
});
}
var lat = row[latCol];
var lon = row[lonCol];
if (classify && classify != 'none') {
var color = $scope.visualization.options.groups[name].color;
marker = heatpoint(lat, lon, color);
} else {
marker = createMarker(lat, lon);
}
}, true);
$scope.$watch('visualization.options.height', function() {
if (!marker) return;
if (!$scope.map) return;
$scope.map.invalidateSize(false);
setBounds();
marker.bindPopup(createDescription(latCol, lonCol, row));
markers.addLayer(marker);
});
markers.addTo(map);
layers[name] = markers;
mapControls.addOverlay(markers, name);
}
function render() {
var queryData = $scope.queryResult.getData();
var classify = $scope.visualization.options.classify;
$scope.visualization.options.mapTileUrl = $scope.visualization.options.mapTileUrl || '//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
tileLayer.setUrl($scope.visualization.options.mapTileUrl);
if ($scope.visualization.options.clusterMarkers === undefined) {
$scope.visualization.options.clusterMarkers = true;
}
if (queryData) {
var pointGroups;
if (classify && classify != 'none') {
pointGroups = _.groupBy(queryData, classify);
} else {
pointGroups = {'All': queryData};
}
var groupNames = _.keys(pointGroups);
var options = _.map(groupNames, function(group) {
if ($scope.visualization.options.groups && $scope.visualization.options.groups[group]) {
return $scope.visualization.options.groups[group];
}
return {color: color(group)};
});
$scope.visualization.options.groups = _.object(groupNames, options);
_.each(layers, function(v, k) {
removeLayer(v);
});
_.each(pointGroups, function(v, k) {
addLayer(k, v);
});
setBounds();
}
}
});
}
}
});
@@ -229,8 +234,58 @@
restrict: 'E',
templateUrl: '/views/visualizations/map_editor.html',
link: function($scope, elm, attrs) {
$scope.draw_options = ['Marker','Color'];
$scope.currentTab = 'general';
$scope.classify_columns = $scope.queryResult.columnNames.concat('none');
$scope.mapTiles = [
{
name: 'OpenStreetMap',
url: '//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
},
{
name: 'OpenStreetMap BW',
url: '//{s}.tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png'
},
{
name: 'OpenStreetMap DE',
url: '//{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png'
},
{
name: 'OpenStreetMap FR',
url: '//{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png'
},
{
name: 'OpenStreetMap Hot',
url: '//{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png'
},
{
name: 'Thunderforest',
url: '//{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png'
},
{
name: 'Thunderforest Spinal',
url: '//{s}.tile.thunderforest.com/spinal-map/{z}/{x}/{y}.png'
},
{
name: 'OpenMapSurfer',
url: '//korona.geog.uni-heidelberg.de/tiles/roads/x={x}&y={y}&z={z}'
},
{
name: 'Stamen Toner',
url: '//stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}.png'
},
{
name: 'Stamen Toner Background',
url: '//stamen-tiles-{s}.a.ssl.fastly.net/toner-background/{z}/{x}/{y}.png'
},
{
name: 'Stamen Toner Lite',
url: '//stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png'
},
{
name: 'OpenTopoMap',
url: '//{s}.tile.opentopomap.org/{z}/{x}/{y}.png'
}
];
}
}
});

View File

@@ -28,6 +28,7 @@
//delete some values which are functions
delete configCopy.aggregators;
delete configCopy.renderers;
delete configCopy.onRefresh;
//delete some bulky default values
delete configCopy.rendererOptions;
delete configCopy.localeStrings;

View File

@@ -0,0 +1,268 @@
(function() {
'use strict';
var module = angular.module('redash.visualization');
module.directive('sankeyRenderer', function() {
return {
restrict: 'E',
link: function(scope, element) {
var refreshData = function() {
var queryData = scope.queryResult.getData();
if (queryData) {
// do the render logic.
angular.element(element[0]).empty();
createSankey(element[0], scope.visualization.options.height, queryData);
}
};
angular.element(window).on("resize", refreshData);
scope.$watch("queryResult && queryResult.getData()", refreshData);
scope.$watch('visualization.options.height', function(oldValue, newValue) {
if (oldValue !== newValue) {
refreshData();
}
});
}
}
});
module.directive('sankeyEditor', function() {
return {
restrict: 'E',
templateUrl: '/views/visualizations/sankey_editor.html'
}
});
module.config(['VisualizationProvider', function(VisualizationProvider) {
var renderTemplate =
'<sankey-renderer options="visualization.options" query-result="queryResult"></sankey-renderer>';
var editTemplate = '<sankey-editor></sankey-editor>';
var defaultOptions = {
height: 300
};
VisualizationProvider.registerVisualization({
type: 'SANKEY',
name: 'Sankey',
renderTemplate: renderTemplate,
editorTemplate: editTemplate,
defaultOptions: defaultOptions
});
}
]);
function createSankey(element, height, data) {
var margin = {top: 10, right: 10, bottom: 10, left: 10},
width = $(element).parent().width() - margin.left - margin.right,
height = height - margin.top - margin.bottom;
data = graph(data);
var formatNumber = d3.format(",.0f"); // zero decimal places
var format = function(d) { return formatNumber(d); };
var color = d3.scale.category20();
// append the svg canvas to the page
var svg = d3.select(element).append("svg")
.attr("class", "sankey")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Set the sankey diagram properties
var sankey = d3.sankey()
.nodeWidth(15)
.nodePadding(10)
.size([width, height]);
var path = sankey.link();
sankey
.nodes(data.nodes)
.links(data.links)
.layout(0);
spreadNodes(height, data);
sankey.relayout();
// add in the links
var link = svg.append("g").selectAll(".link")
.data(data.links)
.enter().append("path")
.filter(function(link) {
return link.target.name != 'Exit';
})
.attr("class", "link")
.attr("d", path)
.style("stroke-width", function(d) { return Math.max(1, d.dy); })
.sort(function(a, b) { return b.dy - a.dy; });
// add the link titles
link.append("title")
.text(function(d) {
return d.source.name + " → " + d.target.name + "\n" + format(d.value);
});
// add in the nodes
var node = svg.append("g").selectAll(".node")
.data(data.nodes)
.enter().append("g")
.filter(function(node) {
return node.name != 'Exit';
})
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.on("mouseover", nodeMouseOver)
.on("mouseout", nodeMouseOut);
// add the rectangles for the nodes
node.append("rect")
.attr("height", function(d) { return d.dy; })
.attr("width", sankey.nodeWidth())
.style("fill", function(d) {
return d.color = color(d.name.replace(/ .*/, ""));
})
.style("stroke", function(d) {
return d3.rgb(d.color).darker(2);
})
.append("title").text(function(d) {
return d.name + "\n" + format(d.value);
});
// add in the title for the nodes
node.append("text")
.attr("x", -6)
.attr("y", function(d) { return d.dy / 2; })
.attr("dy", ".35em")
.attr("text-anchor", "end")
.attr("transform", null)
.text(function(d) { return d.name; })
.filter(function(d) { return d.x < width / 2; })
.attr("x", 6 + sankey.nodeWidth())
.attr("text-anchor", "start");
function nodeMouseOver(currentNode) {
var nodes = getConnectedNodes(currentNode);
nodes = _.pluck(nodes, 'id');
node.filter(function(d) {
if (d === currentNode) {
return false;
}
if (_.contains(nodes, d.id)) {
return false;
}
return true;
}).style('opacity', 0.2);
link.filter(function(l) {
return !(_.include(currentNode.sourceLinks, l) || _.include(currentNode.targetLinks, l));
}).style('opacity', 0.2);
}
function nodeMouseOut(currentNode) {
node.style('opacity', 1);
link.style('opacity', 1);
}
function spreadNodes(height, data) {
var nodesByBreadth = d3.nest()
.key(function(d) { return d.x; })
.entries(data.nodes)
.map(function(d) { return d.values; });
nodesByBreadth.forEach(function(nodes) {
nodes = _.filter(_.sortBy(nodes, function(node) { return -node.value; }), function(node) {
return node.name !== 'Exit';
});
var sum = d3.sum(nodes, function(o) { return o.dy; });
var padding = (height - sum) / nodes.length;
_.reduce(nodes, function(y0, node) {
node.y = y0;
return y0 + node.dy + padding;
}, 0);
});
}
function getConnectedNodes(node) {
// source link = this node is the source, I need the targets
var nodes = [];
_.each(node.sourceLinks, function(link) {
nodes.push(link.target);
});
_.each(node.targetLinks, function(link) {
nodes.push(link.source);
});
return nodes;
}
function graph(data) {
var nodesDict = {};
var links = {};
var nodes = [];
var keys = _.sortBy(_.without(_.keys(data[0]), 'value'), _.identity);
data.forEach(function(row) {
addLink(row[keys[0]], row[keys[1]], row.value, 1);
addLink(row[keys[1]], row[keys[2]], row.value, 2);
addLink(row[keys[2]], row[keys[3]], row.value, 3);
addLink(row[keys[3]], row[keys[4]], row.value, 4);
});
return {nodes: nodes, links: _.values(links)};
function normalizeName(name) {
if (name) {
return name;
}
return 'Exit';
}
function getNode(name, level) {
name = normalizeName(name);
var key = name + ":" + String(level);
var node = nodesDict[key];
if (!node) {
node = {name: name};
var id = nodes.push(node) - 1;
node.id = id;
nodesDict[key] = node;
}
return node;
}
function getLink(source, target) {
var link = links[[source, target]];
if (!link) {
link = {target: target, source: source, value: 0};
links[[source, target]] = link;
}
return link;
}
function addLink(sourceName, targetName, value, depth) {
if ((sourceName === '' || !sourceName) && depth > 1) {
return;
}
var source = getNode(sourceName, depth);
var target = getNode(targetName, depth+1);
var link = getLink(source.id, target.id);
link.value += parseInt(value);
}
}
}
})();

View File

@@ -0,0 +1,495 @@
'use strict';
(function () {
var module = angular.module('redash.visualization');
module.directive('sunburstSequenceRenderer', function () {
return {
restrict: 'E',
link: function(scope, element) {
var sunburst = new Sunburst(scope, element);
function resize() {
sunburst.remove();
sunburst = new Sunburst(scope, element);
}
angular.element(window).on("resize", resize);
scope.$watch('visualization.options.height', function(oldValue, newValue) {
if (oldValue !== newValue) {
resize();
}
});
}
}
});
module.directive('sunburstSequenceEditor', function () {
return {
restrict: 'E',
templateUrl: '/views/visualizations/sunburst_sequence_editor.html'
}
});
module.config(['VisualizationProvider', function (VisualizationProvider) {
var renderTemplate =
'<sunburst-sequence-renderer options="visualization.options" query-result="queryResult"></sunburst-sequence-renderer>';
var editTemplate = '<sunburst-sequence-editor></sunburst-sequence-editor>';
var defaultOptions = {
height: 300,
//
};
VisualizationProvider.registerVisualization({
type: 'SUNBURST_SEQUENCE',
name: 'Sunburst Sequence',
renderTemplate: renderTemplate,
editorTemplate: editTemplate,
defaultOptions: defaultOptions
});
}
]);
// The following is based on @chrisrzhou's example from: http://bl.ocks.org/chrisrzhou/d5bdd8546f64ca0e4366.
function Sunburst(scope, element) {
this.element = element;
var refreshData = function () {
var queryData = scope.queryResult.getData();
if (queryData) {
render(queryData);
}
};
this.watches = [];
this.watches.push(scope.$watch("visualization.options", refreshData, true));
this.watches.push(scope.$watch("queryResult && queryResult.getData()", refreshData));
var exitNode = "<<<Exit>>>";
// svg dimensions
var width = element[0].parentElement.clientWidth;
var height = scope.visualization.options.height;
var radius = Math.min(width, height) / 2;
// Breadcrumb dimensions: width, height, spacing, width of tip/tail.
var b = {
w: width / 6,
h: 30,
s: 3,
t: 10
};
// Legend dimensions: width, height, spacing, radius of rounded rect.
var li = {
w: 75,
h: 30,
s: 3,
r: 3
};
// margins
var margin = {
top: radius,
bottom: 50,
left: radius,
right: 0
};
/**
* Drawing variables:
*
* e.g. colors, totalSize, partitions, arcs
*/
// Mapping of nodes to colorscale.
var colors = d3.scale.category10();
// Total size of all nodes, to be used later when data is loaded
var totalSize = 0;
// create d3.layout.partition
var partition = d3.layout.partition()
.size([2 * Math.PI, radius * radius])
.value(function (d) {
return d.size;
});
// create arcs for drawing D3 paths
var arc = d3.svg.arc()
.startAngle(function (d) {
return d.x;
})
.endAngle(function (d) {
return d.x + d.dx;
})
.innerRadius(function (d) {
return Math.sqrt(d.y);
})
.outerRadius(function (d) {
return Math.sqrt(d.y + d.dy);
});
/**
* Define and initialize D3 select references and div-containers
*
* e.g. vis, breadcrumbs, lastCrumb, summary, sunburst, legend
*/
// create main vis selection
var vis = d3.select(element[0])
.append("div").classed("vis-container", true)
.style("position", "relative")
.style("margin-top", "5px")
.style("height", height + 2 * b.h + "px");
// create and position breadcrumbs container and svg
var breadcrumbs = vis
.append("div").classed("breadcrumbs-container", true)
.append("svg")
.attr("width", width)
.attr("height", b.h)
.attr("fill", "white")
.attr("font-weight", 600);
var marginLeft = (width - radius * 2) / 2;
// create and position SVG
var sunburst = vis
.append("div").classed("sunburst-container", true)
.style('z-index', '2')
// .style("margin-left", marginLeft + "px")
.style("left", marginLeft + "px")
.style('position', 'absolute')
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// create last breadcrumb element
var lastCrumb = breadcrumbs.append("text").classed("lastCrumb", true);
// create and position summary container
var summary = vis
.append("div").classed("summary-container", true)
.style("position", "absolute")
.style("top", b.h + radius * 0.80 + "px")
.style("left", marginLeft + radius / 2 + "px")
.style("width", radius + "px")
.style("height", radius + "px")
.style("text-align", "center")
.style("font-size", "11px")
.style("color", "#666")
.style('z-index', '1');
refreshData();
/**
* Render process:
*
* 1) Load data
* 2) Build Tree
* 3) Draw visualization
*/
// render visualization
function render(data) {
var json = buildHierarchy(data); // build json tree
removeVisualization(); // remove existing visualization if any
createVisualization(json); // visualize json tree
}
/**
* Helper functions:
*
* @function removeVisualization(): removes existing SVG components
* @function createVisualization(json): create visualization from json tree structure
* @function colorMap(d): color nodes with colors mapping
* @function mouseover(d): mouseover function
* @function mouseleave(d): mouseleave function
* @function getAncestors(node): get ancestors of a specified node
* @function buildHierarchy(data): generate json nested structure from csv data input
*/
// removes existing SVG components
function removeVisualization() {
sunburst.selectAll(".nodePath").remove();
// legend.selectAll("g").remove();
}
// visualize json tree structure
function createVisualization(json) {
drawSunburst(json); // draw sunburst
// drawLegend(); // draw legend
};
// helper function colorMap - color gray if "end" is detected
function colorMap(d) {
return colors(d.name);
}
// helper function to draw the sunburst and breadcrumbs
function drawSunburst(json) {
// Build only nodes of a threshold "visible" sizes to improve efficiency
var nodes = partition.nodes(json)
.filter(function (d) {
return (d.dx > 0.005) && d.name !== exitNode; // 0.005 radians = 0.29 degrees
});
// this section is required to update the colors.domain() every time the data updates
var uniqueNames = (function (a) {
var output = [];
a.forEach(function (d) {
if (output.indexOf(d.name) === -1) output.push(d.name);
});
return output;
})(nodes);
colors.domain(uniqueNames); // update domain colors
// create path based on nodes
var path = sunburst.data([json]).selectAll("path")
.data(nodes).enter()
.append("path").classed("nodePath", true)
.attr("display", function (d) {
return d.depth ? null : "none";
})
.attr("d", arc)
.attr("fill", colorMap)
.attr("opacity", 1)
.attr("stroke", "white")
.on("mouseover", mouseover);
// // trigger mouse click over sunburst to reset visualization summary
vis.on("click", click);
// Update totalSize of the tree = value of root node from partition.
totalSize = path.node().__data__.value;
}
// helper function mouseover to handle mouseover events/animations and calculation of ancestor nodes etc
function mouseover(d) {
// build percentage string
var percentage = (100 * d.value / totalSize).toPrecision(3);
var percentageString = percentage + "%";
if (percentage < 1) {
percentageString = "< 1.0%";
}
// update breadcrumbs (get all ancestors)
var ancestors = getAncestors(d);
updateBreadcrumbs(ancestors, percentageString);
// update sunburst (Fade all the segments and highlight only ancestors of current segment)
sunburst.selectAll("path")
.attr("opacity", 0.3);
sunburst.selectAll("path")
.filter(function (node) {
return (ancestors.indexOf(node) >= 0);
})
.attr("opacity", 1);
// update summary
summary.html(
"Stage: " + d.depth + "<br />" +
"<span class='percentage' style='font-size: 2em;'>" + percentageString + "</span><br />" +
d.value + " of " + totalSize + "<br />"
);
// display summary and breadcrumbs if hidden
summary.style("visibility", "");
breadcrumbs.style("visibility", "");
}
// helper function click to handle mouseleave events/animations
function click(d) {
// Deactivate all segments then retransition each segment to full opacity.
sunburst.selectAll("path").on("mouseover", null);
sunburst.selectAll("path")
.transition()
.duration(1000)
.attr("opacity", 1)
.each("end", function () {
d3.select(this).on("mouseover", mouseover);
});
// hide summary and breadcrumbs if visible
breadcrumbs.style("visibility", "hidden");
summary.style("visibility", "hidden");
}
// Return array of ancestors of nodes, highest first, but excluding the root.
function getAncestors(node) {
var path = [];
var current = node;
while (current.parent) {
path.unshift(current);
current = current.parent;
}
return path;
}
// Generate a string representation for drawing a breadcrumb polygon.
function breadcrumbPoints(d, i) {
var points = [];
points.push("0,0");
points.push(b.w + ",0");
points.push(b.w + b.t + "," + (b.h / 2));
points.push(b.w + "," + b.h);
points.push("0," + b.h);
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
points.push(b.t + "," + (b.h / 2));
}
return points.join(" ");
}
// Update the breadcrumb breadcrumbs to show the current sequence and percentage.
function updateBreadcrumbs(ancestors, percentageString) {
// Data join, where primary key = name + depth.
var g = breadcrumbs.selectAll("g")
.data(ancestors, function (d) {
return d.name + d.depth;
});
// Add breadcrumb and label for entering nodes.
var breadcrumb = g.enter().append("g");
breadcrumb
.append("polygon").classed("breadcrumbs-shape", true)
.attr("points", breadcrumbPoints)
.attr("fill", colorMap);
breadcrumb
.append("text").classed("breadcrumbs-text", true)
.attr("x", (b.w + b.t) / 2)
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("font-size", "10px")
.attr("text-anchor", "middle")
.text(function (d) {
return d.name;
});
// Set position for entering and updating nodes.
g.attr("transform", function (d, i) {
return "translate(" + i * (b.w + b.s) + ", 0)";
});
// Remove exiting nodes.
g.exit().remove();
// Update percentage at the lastCrumb.
lastCrumb
.attr("x", (ancestors.length + 0.5) * (b.w + b.s))
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.attr("fill", "black")
.attr("font-weight", 600)
.text(percentageString);
}
function buildHierarchy(csv) {
var data = buildNodes(csv);
// build tree
var root = {
name: "root",
children: []
};
data.forEach(function (d) {
var nodes = d.nodes;
var size = parseInt(d.size);
// build graph, nodes, and child nodes
var currentNode = root;
for (var j = 0; j < nodes.length; j++) {
var children = currentNode.children;
var nodeName = nodes[j];
var isLeaf = j + 1 === nodes.length;
if (!children) {
currentNode.children = children = [];
children.push({
name: exitNode,
size: currentNode.size
})
}
var childNode = _.find(children, function(child) { return child.name == nodeName });
if (isLeaf && childNode) {
childNode.children.push({
name: exitNode,
size: size
})
} else if (isLeaf) {
children.push({
name: nodeName,
size: size
})
} else {
if (!childNode) {
childNode = {
name: nodeName,
children: []
};
children.push(childNode);
}
currentNode = childNode;
}
}
});
return root;
}
function buildNodes(raw) {
var values;
if (_.has(raw[0], 'sequence') && _.has(raw[0], 'stage') && _.has(raw[0], 'node') && _.has(raw[0], 'value')) {
var grouped = _.groupBy(raw, 'sequence');
var values = _.map(grouped, function(value, key) {
var sorted = _.sortBy(value, 'stage');
return {
size: value[0].value,
sequence: value[0].sequence,
nodes: _.pluck(sorted, 'node')
}
});
} else {
var keys = _.sortBy(_.without(_.keys(raw[0]), 'value'), _.identity);
values = _.map(raw, function(row, sequence) {
return {
size: row.value,
sequence: sequence,
nodes: _.compact(_.map(keys, function(key) { return row[key] }))
}
})
}
return values;
}
}
Sunburst.prototype.remove = function() {
_.each(this.watches, function(unregister) { unregister() });
angular.element(this.element[0]).empty('.vis-container');
}
})();

View File

@@ -0,0 +1,97 @@
(function () {
var wordCloudVisualization = angular.module('redash.visualization');
wordCloudVisualization.config(['VisualizationProvider', function (VisualizationProvider) {
VisualizationProvider.registerVisualization({
type: 'WORD_CLOUD',
name: 'Word Cloud',
renderTemplate: '<word-cloud-renderer options="visualization.options" query-result="queryResult"></word-cloud-renderer>',
editorTemplate: '<word-cloud-editor></word-cloud-editor>'
});
}]);
wordCloudVisualization.directive('wordCloudRenderer', function () {
return {
restrict: 'E',
link: function($scope, elem, attrs) {
reloadCloud = function () {
if (!angular.isDefined($scope.queryResult)) retun;
data = $scope.queryResult.getData();
cloud = d3.cloud;
wordsHash = {};
if($scope.visualization.options.column){
data.map(function(d) {
d[$scope.visualization.options.column]
.toString()
.split(' ')
.map(function(d) {
if (d in wordsHash) {
wordsHash[d]+=1;
} else {
wordsHash[d]=1;
}
})
})
}
wordList = [];
for(var key in wordsHash) {
wordList.push({text: key, size: 10 + Math.pow(wordsHash[key],2)});
}
var fill = d3.scale.category20();
var layout = cloud()
.size([500, 500])
.words(wordList)
.padding(5)
.rotate(function() { return ~~(Math.random() * 2) * 90; })
.font("Impact")
.fontSize(function(d) { return d.size; })
.on("end", draw);
layout.start();
function draw(words) {
d3.select(elem[0].parentNode)
.select("svg")
.remove();
d3.select(elem[0].parentNode)
.append("svg")
.attr("width", layout.size()[0])
.attr("height", layout.size()[1])
.append("g")
.attr("transform", "translate(" + layout.size()[0] / 2 + "," + layout.size()[1] / 2 + ")")
.selectAll("text")
.data(words)
.enter().append("text")
.style("font-size", function(d) { return d.size + "px"; })
.style("font-family", "Impact")
.style("fill", function(d, i) { return fill(i); })
.attr("text-anchor", "middle")
.attr("transform", function(d) {
return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
})
.text(function(d) { return d.text; });
}
}
$scope.$watch('queryResult && queryResult.getData()', reloadCloud);
$scope.$watch('visualization.options.column', reloadCloud);
}
}
});
wordCloudVisualization.directive('wordCloudEditor', function() {
return {
restrict: 'E',
templateUrl: '/views/visualizations/word_cloud_editor.html'
};
});
})();

View File

@@ -40,6 +40,8 @@
<!-- build:js /scripts/layout_vendor.js -->
<script src="/bower_components/jquery/jquery.js"></script>
<!-- endbuild -->
{% block scripts %}
{% endblock %}
{% include '_includes/signed_out_tail.html' %}

View File

@@ -158,23 +158,6 @@ a.navbar-brand img {
}
/* Visualization Filters */
.filters-container {
display: flex;
flex-wrap: wrap;
}
.filter {
width: 33%;
padding-left: 5px;
padding-bottom: 5px;
}
.filter > div {
width: 100%;
}
/* Gridster */
.gridster ul {
@@ -205,18 +188,14 @@ li.widget:hover {
background: rgba(0, 0, 0, 0.5) !important;
}
/* CodeMirror */
.CodeMirror {
/* Editor */
.ace_editor {
border: 1px solid #eee;
height: 100%;
margin-bottom: 10px;
}
.CodeMirror-scroll {
overflow-y: hidden;
overflow-x: auto;
}
/* Support for Font-Awesome in btn-xs */
.btn-xs > .fa {
@@ -509,6 +488,10 @@ div.table-name:hover {
margin-top: 10px;
}
.smart-table .smart-table-header-row .header-content {
cursor: pointer;
}
.voffset {
margin-top: 2px;
}
@@ -657,3 +640,42 @@ div.table-name:hover {
.t-body a.actions.open > a {
background-color: rgba(0, 0, 0, 0.1);
}
/* ui-select adjustments for SuperFlat */
/* Same definition as .form-control */
.ui-select-toggle.btn-default {
height: 35px;
padding: 6px 12px;
font-size: 13px;
line-height: 1.42857143;
color: #9E9E9E;
background: #fff none;
border: 1px solid #e8e8e8;
border-radius: 5px;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
}
.t-header.widget {
padding: 5px;
}
/* Sankey Visualization */
.sankey .node rect {
fill-opacity: .9;
shape-rendering: crispEdges;
stroke-width: 0;
}
.sankey .node text {
text-shadow: 0 1px 0 #fff;
}
.sankey .link {
fill: none;
stroke: #000;
stroke-opacity: .2;
}

View File

@@ -1,5 +1,11 @@
<!-- build:js /scripts/plugins.js -->
<script src="/bower_components/jquery/jquery.js"></script>
<script src="/bower_components/ace-builds/src-min-noconflict/ace.js"></script>
<script src="/bower_components/ace-builds/src-min-noconflict/mode-sql.js"></script>
<script src="/bower_components/ace-builds/src-min-noconflict/mode-json.js"></script>
<script src="/bower_components/ace-builds/src-min-noconflict/mode-python.js"></script>
<script src="/bower_components/ace-builds/src-min-noconflict/mode-snippets.js"></script>
<script src="/bower_components/ace-builds/src-min-noconflict/ext-language_tools.js"></script>
<script src="/bower_components/angular/angular.js"></script>
<script src="/bower_components/angular-sanitize/angular-sanitize.js"></script>
<script src="/bower_components/angular-resizable/src/angular-resizable.js"></script>
@@ -11,14 +17,6 @@
<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>
@@ -29,7 +27,7 @@
<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.js/dist/plotly.js"></script>
<script src="/bower_components/plotly.js/dist/plotly-basic.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>
@@ -42,4 +40,7 @@
<script src="/bower_components/angular-bootstrap-show-errors/src/showErrors.js"></script>
<script src="/bower_components/d3/d3.js"></script>
<script src="/bower_components/angular-ui-sortable/sortable.js"></script>
<script src="/bower_components/angular-ui-ace/ui-ace.js"></script>
<script src="/bower_components/angular-vs-repeat/src/angular-vs-repeat.js"></script>
<script src="/bower_components/leaflet.markercluster/dist/leaflet.markercluster-src.js"></script>
<!-- endbuild -->

View File

@@ -15,5 +15,6 @@
<div class="badge">
Last update: <span am-time-ago="updatedAt"></span>
</div>
(<label><input type="checkbox" ng-model="autoUpdate"> Auto Update</label>)
</div>
</div>

View File

@@ -12,12 +12,14 @@
<ul class="tab-nav">
<rd-tab tab-id="in_progress" name="In Progress ({{tasks.in_progress.length}})" ng-click="setTab('in_progress')"></rd-tab>
<rd-tab tab-id="waiting" name="Waiting ({{tasks.waiting.length}})" ng-click="setTab('waiting')"></rd-tab>
<rd-tab tab-id="done" name="Done ({{tasks.done.length}})" ng-click="setTab('done')"></rd-tab>
<rd-tab tab-id="done" name="Done" ng-click="setTab('done')"></rd-tab>
</ul>
<smart-table rows="showingTasks" columns="gridColumns"
config="gridConfig"
class="table table-condensed table-hover"></smart-table>
<label><input type="checkbox" ng-model="autoUpdate"> Auto Update</label>
</div>
</div>

View File

@@ -39,7 +39,7 @@
<li class="list-group-item active">Queues</li>
<li class="list-group-item" ng-repeat="(name, value) in manager.queues">
<span class="badge">{{value.size}}</span>
{{name}} ({{value.data_sources}})
{{name}} <span popover="{{value.data_sources}}" popover-trigger="mouseenter"><i class="fa fa-question-circle"></i></span>
</li>
</ul>
</div>

View File

@@ -0,0 +1,27 @@
<div class="p-5">
<h4>Notifications</h4>
<div>
<ui-select ng-model="newSubscription.destination" ng-disabled="destinations.length == 0">
<ui-select-match><span ng-bind-html="destinationsDisplay($select.selected)"></span></ui-select-match>
<ui-select-choices repeat="d in destinations">
<span ng-bind-html="destinationsDisplay(d)"></span>
</ui-select-choices>
</ui-select>
</div>
<div class="m-t-5">
<button class="btn btn-default" ng-click="saveSubscriber()" ng-disabled="destinations.length == 0" style="width:50%;">Add</button>
<span class="pull-right m-t-5">
<a href="destinations/new" ng-if="currentUser.isAdmin">Create New Destination</a>
</span>
</div>
<hr/>
<div>
<div class="list-group-item" ng-repeat="subscriber in subscribers">
<span ng-bind-html="destinationsDisplay(subscriber)"></span>
<button class="btn btn-xs btn-danger pull-right" ng-click="unsubscribe(subscriber)" ng-if="currentUser.isAdmin || currentUser.id == subscriber.user.id">Remove</button>
</div>
</div>
</div>

View File

@@ -7,10 +7,10 @@
<div class="container">
<div class="row bg-white p-10">
<div class="col-md-8">
<form name="alertForm" ng-submit="saveChanges()" class="form">
<form name="alertForm" class="form">
<div class="form-group">
<label>Query</label>
<ui-select ng-model="alert.query" reset-search-input="false" on-select="onQuerySelected($item)">
<ui-select ng-model="alert.query" reset-search-input="false" on-select="onQuerySelected($item)" ng-disabled="!canEdit">
<ui-select-match placeholder="Search a query by name">{{$select.selected.name}}</ui-select-match>
<ui-select-choices repeat="q in queries"
refresh="searchQueries($select.search)"
@@ -22,7 +22,7 @@
<div class="form-group" ng-show="selectedQuery">
<label>Name</label>
<input type="string" placeholder="{{getDefaultName()}}" class="form-control" ng-model="alert.name">
<input type="string" placeholder="{{getDefaultName()}}" class="form-control" ng-model="alert.name" ng-disabled="!canEdit">
</div>
<div ng-show="queryResult" class="form-horizontal">
@@ -30,7 +30,7 @@
<label class="control-label col-md-2">Value column</label>
<div class="col-md-4">
<select ng-options="name for name in queryResult.getColumnNames()" ng-model="alert.options.column"
class="form-control"></select>
class="form-control" ng-disabled="!canEdit"></select>
</div>
<label class="control-label col-md-2">Value</label>
<div class="col-md-4">
@@ -40,29 +40,30 @@
<div class="form-group">
<label class="control-label col-md-2">Op</label>
<div class="col-md-4">
<select ng-options="name for name in ops" ng-model="alert.options.op" class="form-control"></select>
<select ng-options="name for name in ops" ng-model="alert.options.op" class="form-control" ng-disabled="!canEdit"></select>
</div>
<label class="control-label col-md-2">Reference</label>
<div class="col-md-4">
<input type="number" class="form-control" ng-model="alert.options.value" placeholder="reference value"
<input type="number" step="any" class="form-control" ng-model="alert.options.value" placeholder="reference value" ng-disabled="!canEdit"
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"/>
<input type="number" class="form-control" ng-model="alert.rearm" ng-disabled="!canEdit"/>
</div>
</div>
</div>
<div class="form-group">
<button class="btn btn-primary" ng-disabled="!alertForm.$valid">Save</button>
<div class="form-group" ng-if="canEdit">
<button class="btn btn-primary" ng-disabled="!alertForm.$valid" ng-click="saveChanges()">Save</button>
<button class="btn btn-danger" ng-if="alert.id" ng-click="delete()">Delete</button>
</div>
</form>
</div>
<div class="col-md-4" ng-if="alert.id">
<alert-subscribers alert-id="alert.id"></alert-subscribers>
<alert-subscriptions alert-id="alert.id"></alert-subscriptions>
</div>
</div>
</div>

View File

@@ -1,4 +0,0 @@
<div>
<strong>Subscribers</strong> <subscribe-button alert-id="alertId" subscribers="subscribers"></subscribe-button><br/>
<img ng-src="{{s.user.gravatar_url}}" class="img-circle" alt="{{s.user.name}}" ng-repeat="s in subscribers"/>
</div>

View File

@@ -60,7 +60,7 @@
<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="Settings"><i class="fa fa-cog"></i></a>
</li>
<li class="dropdown" dropdown>
<a href="#" class="dropdown-toggle" dropdown-toggle><span ng-bind="currentUser.name"></span> <span

View File

@@ -5,7 +5,7 @@
<page-header title="{{dashboard.name}}">
<span ng-if="!dashboard.is_archived && !public" class="hidden-print">
<button type="button" class="btn btn-sm" ng-class="{'btn-default': !refreshEnabled, 'btn-primary': refreshEnabled}" tooltip="Enable/Disable Auto Refresh" ng-click="triggerRefresh()">
<span class="zmdi zmdi-refresh-sync"></span>
<span class="zmdi zmdi-refresh"></span>
</button>
<button type="button" class="btn btn-sm" ng-class="{'btn-default': !isFullscreen, 'btn-primary': isFullscreen}" tooltip="Enable/Disable Fullscreen display" ng-click="toggleFullscreen()">
<span class="zmdi zmdi-fullscreen"></span>
@@ -21,6 +21,7 @@
<ul class="dropdown-menu pull-right" dropdown-menu>
<li><a data-toggle="modal" hash-link hash="edit_dashboard_dialog">Edit Dashboard</a></li>
<li><a data-toggle="modal" hash-link hash="add_query_dialog">Add Widget</a></li>
<li ng-if="showPermissionsControl"><a ng-click="showManagePermissionsModal()">Manage Permissions</a></li>
<li ng-if="!dashboard.is_archived"><a ng-click="archiveDashboard()">Archive Dashboard</a></li>
</ul>
</div>
@@ -29,11 +30,14 @@
This dashboard is archived and won't appear in the dashboards list or search results.
</div>
<div class="m-b-5">
<filters ng-if="dashboard.dashboard_filters_enabled"></filters>
</div>
<div ng-repeat="row in dashboard.widgets" class="row">
<div ng-repeat="widget in row" class="col-lg-{{widget.width | colWidth}}" ng-controller='WidgetCtrl'>
<div class="tile" ng-if="type=='visualization'">
<div class="t-header">
<div class="t-header widget">
<div class="th-title">
<p class="hidden-print">
<span ng-hide="currentUser.hasPermission('view_query')">{{query.name}}</span>
@@ -53,18 +57,31 @@
<li><a ng-disabled="!queryResult.getData()" query-result-link target="_self">Download as CSV File</a></li>
<li><a ng-disabled="!queryResult.getData()" file-type="xlsx" query-result-link target="_self" >Download as Excel File</a></li>
<li><a ng-href="queries/{{query.id}}#{{widget.visualization.id}}" ng-show="currentUser.hasPermission('view_query')">View Query</a></li>
<li><a ng-show="dashboard.canEdit()" ng-click="deleteWidget()">Remove From Dashbaord</a></li>
<li><a ng-show="dashboard.canEdit()" ng-click="deleteWidget()">Remove From Dashboard</a></li>
</ul>
</div>
</div>
<visualization-renderer visualization="widget.visualization" query-result="queryResult" class="t-body"></visualization-renderer>
<parameters parameters="widget.query.getParametersDefs()"></parameters>
<div class="panel-footer">
<span class="label label-default hidden-print">Updated: <span am-time-ago="queryResult.getUpdatedAt()"></span></span>
<div ng-switch="queryResult.getStatus()">
<div ng-switch-when="failed">
<div class="alert alert-danger m-5" ng-show="queryResult.getError()">Error running query: <strong>{{queryResult.getError()}}</strong></div>
</div>
<div ng-switch-when="done">
<visualization-renderer visualization="widget.visualization" query-result="queryResult" class="t-body"></visualization-renderer>
</div>
<div ng-switch-default class="text-center">
<i class="zmdi zmdi-refresh zmdi-hc-spin zmdi-hc-5x"></i>
</div>
</div>
<div class="p-5 clearfix" style="line-height:28px;">
<span class="small hidden-print">Updated: <span am-time-ago="queryResult.getUpdatedAt()"></span></span>
<span class="visible-print">
Updated: {{queryResult.getUpdatedAt() | dateTime}}
</span>
<button class="btn btn-sm btn-default pull-right hidden-print" ng-click="reload(true)" ng-if="!public"><i class="zmdi zmdi-refresh"></i></button>
</div>
</div>
@@ -87,7 +104,8 @@
</div>
<ul class="dropdown-menu pull-right" dropdown-menu style="z-index:1000000">
<li><a ng-show="dashboard.canEdit()" ng-click="deleteWidget()">Remove From Dashbaord</a></li>
<li><a ng-show="dashboard.canEdit()" ng-click="editTextBox()">Edit</a></li>
<li><a ng-show="dashboard.canEdit()" ng-click="deleteWidget()">Remove From Dashboard</a></li>
</ul>
</div>
<p ng-bind-html="widget.text | markdown" class="p-5"></p>

View File

@@ -1,7 +1,8 @@
<settings-screen>
<div class="row">
<div class="col-md-8">
<data-source-form data-data-source="dataSource" />
<dynamic-form target="dataSource" type="data_sources" actions="actions">
</dynamic-form>
</div>
</div>
</settings-screen>

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